Files
sx/examples/ffi-06-callback.sx
agra f886d5f1be 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.
2026-05-25 09:50:37 +03:00

60 lines
2.1 KiB
Plaintext

// Phase 0 baseline (PLAN-FFI.md step 0.6): sx function passed to C
// as a function pointer; C invokes it; sx-side observable effect.
// Mirrors the `app->onInputEvent` install pattern in
// library/modules/platform/android.sx.
//
// Two arities covered:
// 1. (s32) -> s32 — single-arg callback
// 2. (*void, s32) -> s32 — pointer + value (onInputEvent shape)
//
// Plus a side-effect via a global so we can confirm the callback
// actually fired (return value + state mutation both observable).
#import "modules/std.sx";
#import c {
#source "ffi-06-callback.c";
};
ffi_apply_callback :: (cb: (s32) -> s32 callconv(.c), value: s32) -> s32 #foreign;
ffi_apply_callback2 :: (cb: (*void, s32) -> s32 callconv(.c), ctx: *void, v: s32) -> s32 #foreign;
g_callback_hits : s32 = 0;
g_callback_sum : s32 = 0;
double_it :: (x: s32) -> s32 callconv(.c) {
g_callback_hits += 1;
g_callback_sum += x;
x * 2;
}
add_with_ctx :: (ctx: *void, v: s32) -> s32 callconv(.c) {
g_callback_hits += 1;
// Pass a sentinel via ctx to prove the pointer arg also survives the
// round-trip — read it back as an s32 through *s32.
p : *s32 = xx ctx;
p.* + v;
}
main :: () -> s32 {
// ── Single-arg callback ────────────────────────────────────────
r1 := ffi_apply_callback(double_it, 21);
print("callback returned = {}\n", r1);
print("hits after first call = {}\n", g_callback_hits);
print("sum after first call = {}\n", g_callback_sum);
// Two more calls confirm the same fn pointer keeps working.
ffi_apply_callback(double_it, 7);
ffi_apply_callback(double_it, 11);
print("hits after three calls = {}\n", g_callback_hits);
print("sum after three calls = {}\n", g_callback_sum);
// ── Two-arg callback with opaque ctx pointer ───────────────────
ctx_val : s32 = 100;
r2 := ffi_apply_callback2(add_with_ctx, xx @ctx_val, 42);
print("ctx + value = {}\n", r2);
print("hits after ctx callback = {}\n", g_callback_hits);
0;
}