mem: reject call-conv mismatches at bare-fn → fn-ptr coercion

Passing a default-conv sx function to a `callconv(.c)` fn-pointer slot
(e.g. pthread_create's start routine) used to silently mismatch ABIs:
the C-side caller didn't supply __sx_ctx, so the sx-side body read its
first user param as garbage. The bug surfaced as a SIGSEGV inside
ANativeWindow_setBuffersGeometry on Android during chess bringup.

Now the compiler rejects the coercion outright at the bare-fn name
lookup site:

  error: call-convention mismatch: 'sx_handler' is declared with
  default sx convention but the target type expects callconv(.c)

Also: `#foreign` declarations without an explicit `callconv` now default
to `.c` instead of `.default`. Every external C symbol is by definition
C-conv; the previous default silently typed `objc_msgSend` (et al.) as
default-conv, so the check would fire on the consumer side when the
user typed a fn-ptr as `callconv(.c)`. With the foreign-default fix,
the existing typed-msgSend casts in `std/objc.sx` and `gpu/metal.sx`
keep type-checking and the rule is "C-conv on both sides or neither."

Caught by the new check (fixed in the same commit):
- `ios_gl_proc` in `platform/uikit.sx` lacked callconv(.c) but was
  passed to `load_gl` whose `get_proc` slot expects it.
- `ffi_apply_callback` / `ffi_apply_callback2` in
  `examples/ffi-06-callback.sx` had default-conv fn-ptr params but
  the C bodies (in the companion .c) are unambiguously C-conv.

Regression test: `examples/131-callconv-mismatch-diagnostic.sx`
locks in the diagnostic shape (sx-conv fn → callconv(.c) slot).

153/153 example tests pass. Chess green on macOS / iOS sim / Android.
This commit is contained in:
agra
2026-05-25 09:50:37 +03:00
parent d4a342d0c1
commit f886d5f1be
17 changed files with 45 additions and 26 deletions

View File

@@ -796,7 +796,13 @@ pub const Lowering = struct {
}) catch unreachable;
}
const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default;
// `#foreign` declarations are external C symbols by definition —
// promote them to callconv(.c) when the user didn't write it
// explicitly. This keeps fn-ptr coercion type-safe: anything
// typed by name as `(args) -> ret` of a `#foreign` decl can be
// assigned to / passed as a `callconv(.c)` fn-pointer without a
// call-convention mismatch.
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign) .c else .default;
// For #foreign with C name override, declare under C name and map sx name → C name
if (is_foreign) {
@@ -1968,6 +1974,24 @@ pub const Lowering = struct {
const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure);
break :blk self.builder.closureCreate(tramp_id, Ref.none, tt);
}
// Coercing a bare fn name to a fn-pointer
// type — the call_conv must match. A
// default-conv sx fn assigned to a
// callconv(.c) slot (e.g. passed to
// pthread_create) would otherwise crash at
// runtime when the C caller doesn't supply
// the implicit __sx_ctx arg.
if (tt_info == .function) {
const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv;
if (func_cc != tt_info.function.call_conv) {
if (self.diagnostics) |d| {
const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention";
const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention";
d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc });
}
break :blk self.emitPlaceholder(eff_fn_name);
}
}
}
}
break :blk self.builder.emit(.{ .func_ref = fid }, .s64);