feat(ffi): experimental reverse FFI — host-implemented plugin interfaces ({.ffiReverse.} / {.ffiReverseEvent.}) - #154
Draft
NagyZoltanPeter wants to merge 4 commits into
Draft
Conversation
…rseEvent.}) (#153) Host-implemented plugin interfaces, delivered on the event dispatch thread with an async any-thread reply ABI: - ffi/ffi_reverse.nim: impl registry (snapshot-dispatch semantics like the event registry), monotonic call ids, c_malloc reply mailbox, and the FFI-thread call/drain helpers (ffiReverseCall parks a chronos future under a deadline; the thread keeps processing requests). - Event ring records gain a kind (ekListener = 0 keeps zero-init paths) so reverse invocations ride the existing SPSC queue; a full ring fails the caller's future instead of tripping the sticky stuck flag. - Replies wake the FFI thread through the existing reqSignal (no 7th ThreadSignalPtr; refc cannot close them) and are decoded on its own heap — no Nim ref ever crosses threads, identical under refc and orc. - Recycle/shutdown fail pending reverse calls up front, so a parked handler cannot hold drainOngoing until its deadline and trip a DrainTimeout quarantine. - {.ffiReverse.}: bodyless proc -> async caller stub + generated <lib>_set_<wire>_impl export; declareLibrary emits <lib>_reverse_reply. - {.ffiReverseEvent.}: proc body becomes the handler; sugar over the one-way request path with a generated fire-and-forget <lib>_emit_<wire> export. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…153) - The CBOR C header declares the raw reverse exports (FFIReverseImpl typedef, <lib>_set_<wire>_impl, <lib>_reverse_reply, <lib>_emit_<wire>) plus typed ctx-wrapper sugar: _ctx_set_*_impl, _decode_*_args, _ctx_reverse_reply_* (payload-less for void replies), _ctx_reverse_reply_err and _ctx_emit_*, with buffer adapters for the reverse-direction types. - An `abi = c` library with reverse declarations fails C binding generation loudly instead of emitting an unusable header. - examples/timer gains a fetch_host_clock {.ffiReverse.} interface, an on_host_tick {.ffiReverseEvent.} handler and driver methods; all four binding sets regenerated (cpp/rust/cddl carry only the new forward methods). - The C e2e suite covers the full host side: typed args decode + inline reply on the dispatch thread, fail-fast on an unfulfilled interface, unregister, and a host-emitted reverse event observed through a normal method. - README (pragma table + "Reverse FFI" section with a semantics table) and CHANGELOG. Verified: nimble test (60/60 green, orc+refc), test_c_e2e, test_cpp_e2e, check_bindings; the three reverse unit test files are ASAN+UBSAN- and TSAN-clean under both orc and refc. Note: tests/unit/test_event_dispatch fails under ASAN+orc identically on master (pre-existing, not from this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stack (#153) The reverse-FFI ring record added kind (enum) + callId (uint64) fields, growing QueuedEvent 40 -> 56 bytes. The ring is embedded 1024x per context and 32x per pool, so FFIContextPool grew ~2.0 MB -> ~2.5 MB — past the 2 MB mingw-w64 stack reserve — and every Windows CI suite whose tests stack-allocate a pool (test_ffi_context, test_gc_compat, test_event_listener_reentrancy, the recycle suite) crashed at the first test frame with no output. Linux/macOS were safe behind 8 MB stacks, and suites using the module-global declareLibrary pool passed everywhere. Fix: kind packs into the existing padding next to the HeapOwned bools, and the call id rides as an 8-byte native-endian prefix of the ekReverse payload (ReverseCallIdPrefixLen) instead of a per-slot field. QueuedEvent lands at 32 bytes — 8 below master — and the pool at ~1.7 MB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nph 0.7.0 (the version the linters workflow pins), no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #153
Experimental reverse FFI: a library can declare interfaces the host fulfils at runtime (plugin/SPI direction), and event handlers the host emits into. Design and phase-by-phase plan:
REVERSE_FFI_PLAN.md(option B — delivery on the event dispatch thread, async any-thread reply ABI).What's in here
Pragmas
Runtime (
ffi/ffi_reverse.nim+ thread wiring)QueuedEventgains akind;ekListener = 0keeps zero-init paths intact). The FFI processing thread stays free while the call is parked.set_impl(NULL)unregisters and waits an in-flight invocation out).<lib>_reverse_reply: c_malloc'd CBOR into an intrusive mailbox, woken through the existingreqSignal(no extraThreadSignalPtr— refc cannot close them), decoded on the FFI thread's own heap. No Nim ref ever crosses a thread boundary; behavior is identical underrefcandorc.ReverseCallTimeoutMs, per-proc{.ffiReverse("wire", timeout = ms).}); unfulfilled interfaces fail fast; late/stale replies are dropped by call-id lookup.drainOngoinguntil its deadline and trip the fix(ffi): quarantine a context whose {.ffiDtor.} did not finish #151DrainTimeoutquarantine.{.ffiReverseEvent.}is sugar over the one-way request path: generated<lib>_emit_<wire>enqueues a fire-and-forget request; the return code reports the enqueue only.C bindings (CBOR ABI)
Raw exports plus typed ctx-wrapper sugar:
_ctx_set_<wire>_impl,_decode_<wire>_args,_ctx_reverse_reply_<wire>(payload-less forvoidreplies),_ctx_reverse_reply_err,_ctx_emit_<wire>. Anabi = clibrary with reverse declarations fails C binding generation loudly.Example + docs
examples/timergainsfetch_host_clock/on_host_tickwith driver methods; all four binding sets regenerated. README documents the pragmas and a semantics table; CHANGELOG entry added.Verification
nimble test(all unit tests, orc + refc)test_ffi_reverse_state,test_ffi_reverse,test_ffi_reverse_macro)test_c_codegennimble test_c_e2enimble test_cpp_e2e,nimble check_bindingsNote:
test_event_dispatchfails under ASAN+orc identically on master (cross-thread table dealloc indeinitEventRegistry; possibly the local mixed-toolchain setup) — pre-existing, untouched here.Out of scope (follow-ups)
cpp/rust/cddl reverse codegen,
abi = cwire shape for reverse,{.ffiHandle.}params in reverse calls, host-side cancellation, re-entrant reverse calls from inside an impl.🤖 Generated with Claude Code