fix: call callbacks via the executable trampoline address (#390)#427
Conversation
On libffi 3.4+ the executable trampoline is a separate memory mapping from the writable ffi_closure, so the closure pointer itself is not callable; passing it to C as the callback function pointer segfaults when the callback fires. This is reproducible on Ubuntu 26 (libffi 3.5) and matches the `node examples/glib-timeout.js` crash in the report. Pass g_callable_info_get_closure_native_address() instead of the raw closure. This re-applies #391, which was reverted in #393 because it broke startup — guarded here by falling back to the closure pointer when introspection returns NULL, so a callback pointer is never NULL at bootstrap. On platforms where the two addresses coincide (older libffi, or where the closure is already executable) the behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified on real Ubuntu 26.04 (rootless podman container)Built both
One caveat — a separate teardown crash (not this PR)With This is not the callback bug and not caused by this PR — master can't even reach it. It's the |
Summary
Addresses #390 — callback segfaults on Ubuntu 26 (
libffi 3.5), e.g.node examples/glib-timeout.jscrashing the moment the callback fires.Root cause
When a JS function is passed as a callback argument, node-gtk hands the C side the address of the
ffi_closure. On libffi 3.4+ the executable trampoline lives in a separate memory mapping from the writableffi_closurestruct (static trampolines / W^X), so theffi_closure*pointer is not itself callable — invoking it segfaults. The correct, callable address isg_callable_info_get_closure_native_address().History — why this isn't just "re-merge #391"
function.ccto pass the native address.masteris back to passing the raw (non-executable) closure — the bug is live again.The most likely cause of that startup break is
g_callable_info_get_closure_native_address()returningNULLon some GI builds, which then passed aNULLcallback at bootstrap. This PR re-applies the fix with a NULL-guard fallback to the closure pointer, so a callback pointer is never NULL.Why this is safe everywhere
The pointer handed to C changes only when
native_addressis non-NULL and differs fromclosure— i.e. precisely the libffi-3.4+ static-trampoline case that currently segfaults. In every configuration where the old code worked:closure→ identical behavior.So it cannot regress a setup that currently works; it only repairs the one that crashes.
Testing
closure == native_addresshere, so the change is a no-op locally — startup is fine,examples/glib-timeout.jsruns, andGst.Promise.newWithChangeFunc(...)+reply()fires its change func without crashing.require.jsGIRepository 3.0/2.0 bootstrap failure (reproduces onmaster).I could not reproduce the original #393 startup break locally (here the two addresses are equal, so #391 would have worked too). The NULL-guard is my best diagnosis of that break, but since your machine is where it manifested, please confirm
node examples/glib-timeout.jsand a normalrequire('node-gtk')still start cleanly for you. If it still breaks at startup, that points toget_closure_native_addressreturning a non-NULL-but-wrong value on your GI, and I'll dig further.🤖 Generated with Claude Code