From 9b7ffd70b29d499181a484a2f0ed1586f81d8e62 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 28 May 2026 12:25:35 +0300 Subject: [PATCH] ffi block-string-arg ABI fix: split foreign-C-API collapse from callconv(.c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `abiCoerceParamType` had a libc-friendly heuristic: sx `string` / `[]T` slice → `ptr` (drop the len, just pass the start pointer). The heuristic is right for `#foreign` decls that mirror libc signatures (`puts(const char *)`, `strlen(const char *)`); it's wrong for sx-internal `callconv(.c)` (e.g. block trampolines) where both sides see and exchange the full slice. Split via a new `abiCoerceParamTypeEx(ir_ty, llvm_ty, is_foreign_c_api)`. The old single-arg form forwards with `is_foreign_c_api = true` so every call site that already collapses keeps doing so. The function-decl emit at lines 1442 / 1454 now passes `func.is_extern` — sx-internal `callconv(.c)` declarations take the false path and preserve the slice as `{ptr, i64}` → `[2 x i64]` via the general struct-coerce branch (true C ABI for a 16-byte aggregate: passed in x0+x1 on AArch64). `examples/188-block-string-arg.sx` flips green ("got: "); suite stays at 222/222. Foreign-decl call sites (objc msg_send / JNI / direct extern calls) keep the libc collapse — they pass `is_foreign_c_api = true` via the legacy `abiCoerceParamType` shim. --- src/ir/emit_llvm.zig | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9f6b583..f177916 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1439,7 +1439,7 @@ pub const LLVMEmitter = struct { const uses_sret = needs_c_abi and !is_main and self.needsByval(func.ret, raw_ret_ty); const ret_ty = if (is_main) self.cached_i32 else if (uses_sret) self.cached_void - else if (needs_c_abi) self.abiCoerceParamType(func.ret, raw_ret_ty) + else if (needs_c_abi) self.abiCoerceParamTypeEx(func.ret, raw_ret_ty, func.is_extern) else raw_ret_ty; // Build parameter types — apply C ABI coercion for foreign/callconv(.c) functions. @@ -1451,7 +1451,7 @@ pub const LLVMEmitter = struct { if (uses_sret) param_types[0] = self.cached_ptr; for (func.params, 0..) |param, j| { const llvm_ty = self.toLLVMType(param.ty); - param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty; + param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamTypeEx(param.ty, llvm_ty, func.is_extern) else llvm_ty; } const is_var_arg: c_int = if (func.is_variadic) 1 else 0; @@ -4241,11 +4241,28 @@ pub const LLVMEmitter = struct { // - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it) fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef { - // String/slice → raw pointer (universal across all targets for foreign calls) - if (ir_ty == .string) return self.cached_ptr; - if (!ir_ty.isBuiltin()) { - const info = self.ir_mod.types.get(ir_ty); - if (info == .slice) return self.cached_ptr; + return self.abiCoerceParamTypeEx(ir_ty, llvm_ty, true); + } + + /// Same as `abiCoerceParamType` but with an explicit + /// `is_foreign_c_api` knob. When true, sx `string` / `[]T` slices + /// collapse to `ptr` — the libc convention where the user writes + /// `string` to mean `char *` and the length is dropped. When + /// false (sx-internal `callconv(.c)` like block trampolines), the + /// full slice shape is preserved and goes through the general + /// struct-coerce path (16-byte slice → `[2 x i64]`, lands in two + /// registers on AArch64 — the true C ABI for a 16-byte + /// aggregate). Without the split, sx-to-sx calls through a + /// `(*Block, string) -> void callconv(.c)` fn-pointer mismatched + /// the caller's `{ptr, i64}` value against the trampoline's + /// collapsed `ptr` param. + fn abiCoerceParamTypeEx(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef, is_foreign_c_api: bool) c.LLVMTypeRef { + if (is_foreign_c_api) { + if (ir_ty == .string) return self.cached_ptr; + if (!ir_ty.isBuiltin()) { + const info = self.ir_mod.types.get(ir_ty); + if (info == .slice) return self.cached_ptr; + } } // WASM32: usize/isize are pointer-sized (i32 on wasm32).