ffi block-string-arg ABI fix: split foreign-C-API collapse from callconv(.c)

`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: <hello>");
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.
This commit is contained in:
agra
2026-05-28 12:25:35 +03:00
parent 9e76a83f69
commit 9b7ffd70b2

View File

@@ -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).