Increment 4: the exported C surface for host callbacks, plus an end-to-end
test that the host can answer from a different thread than the FFI loop.
- declareLibrary now emits two exportc/cdecl procs on every library's
FFIContext (like the event ABI):
<lib>_register_host_fn(ctx, name, fn, userData)
<lib>_host_complete(ctx, token, ret, msg, len)
(the `name` param is spelled `hostFnName` to dodge the macros.name capture
under quote, same class as the existing id/ret collisions.)
- c.nim emits the FFIHostFn typedef + both declarations into <lib>.h
(guarded, format-agnostic), and the timer header is regenerated.
- Verified: the built timer lib exports both symbols.
The e2e (test_ffi_host_e2e) drives the real bridge: a {.ffi.} handler awaits a
{.ffiHost.} call; the host fn (invoked on the FFI thread, non-blocking) hands
the work to a worker thread, which answers via the completion path. The result
resolves on the loop thread and round-trips correctly (orc+refc). It calls the
underlying registerHostFn/completeHostCall directly, since the exported shims
need an --app:lib build; those shims are verified by the symbol check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C bindings — native (same-process) example
Generated C headers for the timer library plus a small driver that links the library directly and calls the native (zero-serialization) ABI.
Which ABI? The library exports both ABIs from the same shared object, side by side: the native
<name>symbols and the CBOR<name>_cborsymbols. Use the native (pure-C) ABI for same-process / local calls — it passes flat C structs with zero serialization. Use the CBOR ABI only for inter-process communication (a different process, or a different machine), where the data has to be serialized to cross the boundary anyway. In one address space, CBOR is pure overhead — prefer native. See../ipcfor the CBOR/IPC path.
Files
| File | Description |
|---|---|
my_timer.h |
Native ABI: each {.ffi.} type is a plain C struct, passed by value to int <name>(ctx, cb, ud, <args…>). Results arrive on the callback. Best for same-process callers — no serialization. |
my_timer_cbor.h |
CBOR ABI (<name>_cbor): request/response as CBOR bytes. Use this when the call crosses a process or machine boundary. See ../ipc. |
example.c |
Native same-process driver: create → version → echo → complex → destroy. |
Makefile |
Builds the Nim dylib (from the repo root) and the driver. |
The headers are regenerated by nimble genbindings_c (run from the repo root)
and overwritten each time — don't edit them by hand.
Build & run
cd examples/timer/c_bindings
make run
This compiles libmy_timer.{dylib,so} and runs ./example, which prints the
library version and the round-tripped echo/complex responses. Every call is
dispatched on the library's FFI thread, so the driver blocks on a condvar-backed
callback for each result.
Native vs CBOR
The native path passes {.ffi.} structs as flat C-POD values (const char* for
strings, { T* ptr; size_t len } for sequences, { int present; T } for
options). Arguments are deep-copied across the FFI-thread boundary, so the C
caller's buffers can be freed immediately after the call returns. String returns
arrive as raw bytes; struct returns arrive as a typed const <Type>* in the
callback (cast and read it there — it is valid only for the callback's lifetime,
and the library deep-frees it afterwards, so copy out anything you need).
For the cross-process / cross-machine path, the same library is reached over a
socket using the CBOR ABI — see ../ipc.