# nim-ffi Expose a Nim library to C, C++ and Rust by annotating ordinary Nim procs. You write async Nim; `nim-ffi` provides the whole FFI runtime — a dedicated worker thread, a request channel, CBOR (de)serialization, an event queue and a context/handle registry — and generates the foreign-language bindings for you. No hand-written `.h` files, no manual request enums, no shared-memory plumbing. ## Install Add nim-ffi to your library's `.nimble`, then `import ffi`: ```nim requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0" ``` ## Mental model - You **declare a library** once with `declareLibrary(name, LibType)`. - You **annotate procs and types** with pragmas (`{.ffi.}`, `{.ffiCtor.}`, `{.ffiDtor.}`, `{.ffiEvent.}`). - You **call `genBindings()` last**, which emits the foreign bindings. By default, every request/response crosses the boundary as a single CBOR blob (the wire format is configurable per library or per annotation — see [ABI format](#abi-format)); the ctx handle returned by the constructor is the only pointer that crosses. Each `{.ffi.}` proc runs on the library's own chronos event loop, so bodies can `await` freely. ## Minimal example ```nim import ffi, chronos # 1. The library's main state. The FFI context owns one instance. type Counter = object value: int declareLibrary("counter", Counter) # 2. Request/response shapes. Any {.ffi.} object type becomes a first-class # struct/class in the generated bindings and rides the wire in the library's # ABI format (CBOR by default). type BumpRequest {.ffi.} = object by: int type BumpResponse {.ffi.} = object newValue: int # 3. Constructor: returns Future[Result[LibType, string]]. proc counterNew*(): Future[Result[Counter, string]] {.ffiCtor.} = return ok(Counter(value: 0)) # 4. A method: first param is the library value, then any typed params. # Return type is always Future[Result[T, string]]. proc counterBump*( c: Counter, req: BumpRequest ): Future[Result[BumpResponse, string]] {.ffi.} = await sleepAsync(1.milliseconds) return ok(BumpResponse(newValue: c.value + req.by)) # 5. Destructor: exactly one param (the library value). proc counterDestroy*(c: Counter) {.ffiDtor.} = discard # 6. genBindings() must be the LAST FFI call in the file (see below). genBindings() ``` The generated C export names are the snake_case form of the proc names, e.g. `counterBump` → `counter_bump`. ## Pragma reference | Pragma | Applies to | Purpose | | --- | --- | --- | | `declareLibrary(name, LibType[, defaultABIFormat])` | call | Registers the library, its state type, and the default wire format. Must run before any annotation. | | `{.ffi.}` on a `type` | `object` | Registers the type for binding generation; it serializes via the library's ABI format (CBOR by default). | | `{.ffi.}` on a `proc` | proc | Exposes a method. First param is the library value, then typed params; returns `Future[Result[T, string]]`. | | `{.ffiCtor.}` | proc | The constructor. Returns `Future[Result[LibType, string]]`; creates the FFI context. | | `{.ffiDtor.}` | proc | The destructor. Exactly one param `(x: LibType)`; tears the context down. | | `{.ffiEvent[: "wire_name"].}` | proc (empty body) | A library-initiated callback. Call the proc from any `{.ffi.}` handler to fire it. The wire name is optional — see below. | | `{.ffiHandle.}` | `ref object` | Marks a type as an opaque handle: it stays server-side and crosses the wire as a `uint64` id. | | `genBindings()` | call | Emits the bindings. Must be the **last** FFI call in the compilation root. | ### The return-type contract Every `{.ffi.}` / `{.ffiCtor.}` proc must have an explicit `Future[Result[T, string]]` return type — even for synchronous logic (just `return ok(...)` without awaiting). The `Result`'s error string is delivered to the foreign caller as the failure message. ### Request timeouts Every handler runs under a deadline. The default is `DefaultRequestTimeout` (5s, `ffi/ffi_context.nim`), applied to every proc so a wedged handler can't hang a foreign caller forever. On trip the caller is unblocked with an `ffi request timed out after ms` error; the handler is **not** cancelled — a hard cancel mid-call into the underlying library can leave it half-applied — so it keeps running, and the caller's callback still fires exactly once. Raise or lower the deadline per proc with a `"timeout = "` spec, parsed like the `abi = ...` spec below: ```nim proc slowOp*( c: Counter, req: BumpRequest ): Future[Result[BumpResponse, string]] {.ffi: "timeout = 30000".} = ... ``` The timeout is runtime-only; binding codegen ignores it. ### Events An event is a proc with an empty body annotated `{.ffiEvent.}`. You fire it by calling it with a typed payload from inside any `{.ffi.}` handler; the foreign side receives it through a registered callback. ```nim type PeerConnected {.ffi.} = object id: string proc onPeerConnected*(peer: PeerConnected) {.ffiEvent.} # wire name: "on_peer_connected" proc counterBump*(c: Counter, req: BumpRequest): Future[Result[BumpResponse, string]] {.ffi.} = onPeerConnected(PeerConnected(id: "p-1")) return ok(BumpResponse(newValue: c.value + req.by)) ``` The wire name is **optional**: when omitted it is derived from the proc name (`onPeerConnected` → `on_peer_connected`), matching how `{.ffi.}` derives its C export symbol. Pass a string literal (`{.ffiEvent: "custom_name".}`) only when you need a name that differs from the proc. ### ABI format The wire format is chosen **in code**, never by a compile flag. Override the library default with `declareLibrary("lib", Lib, defaultABIFormat = "c")`, or per annotation with an `"abi = ..."` spec, e.g. `{.ffi: "abi = c".}`. The `-d:targetLang` flag (below) picks which *language* the bindings are emitted for; it does not change the wire. `cbor` is the default and fully-supported format: every proc, ctor, dtor and event serializes through the generic CBOR path, and all binding generators emit working callers for it. `abi = c` is a newer, flat C-struct wire (no CBOR round-trip). Callers for it are emitted only by the dedicated `c_abi` generator (`-d:targetLang=c_abi`). It carries two honest limits today: - **Events are CBOR-only.** Applying `abi = c` to an `{.ffiEvent.}` proc is a hard compile error; declare events with `abi = cbor` (they ride CBOR internally regardless of the library default). - **All-scalar `abi = c` procs are dropped from the foreign bindings.** A `{.ffi: "abi = c".}` method whose every param and return is a plain scalar takes the CBOR-free scalar fast path at runtime, but the foreign codegen for that inline-args shape is a follow-up (tracked in #120) — such procs are omitted from the generated `.h`. Give a proc at least one non-scalar (struct / `seq` / `Option`) param or return, or use `abi = cbor`, if you need it in the bindings. An `abi = c` proc whose whole signature is scalar — fixed-width integer, float, or bool params (a `string` return is fine, a `string` param is not) and no structs, handles, or pointers — dispatches through a CBOR-free scalar fast path. Foreign-binding codegen for that shape isn't implemented yet, so under `-d:ffiGenBindings` such a proc would be omitted from the generated bindings — and `genBindings()` fails with an error naming the affected procs. Resolve it by switching the proc to `abi = cbor`, adding a non-scalar param so it takes the CBOR wire shape, or passing `-d:ffiAllowScalarSkip` to accept the omission (the proc still works over the scalar fast path; it's just absent from the generated foreign bindings). ## Placement of `genBindings()` `genBindings()` reads the compile-time registries that the pragmas populate as the compiler expands them, so **it must come after every annotation**. Since Nim resolves `import`s before running the importing module's body, a multi-file library keeps its annotations in imported sub-modules and calls `genBindings()` once at the bottom of the top-level root file. An annotation that expands *after* `genBindings()` is now a **compile error** (previously it was silently dropped from the bindings). ## Building — the two-compile model A nim-ffi library ships from **two separate compiles of the same source**, because binding emission is gated behind `-d:ffiGenBindings`: without that define `genBindings()` is a no-op, so the normal build just produces the shared library and nothing else. **1. Build the shared library** (the artifact your host loads): ```sh nim c --app:lib --noMain --nimMainPrefix:libmylib mylib.nim ``` **2. Emit the foreign bindings** — add the binding defines and `--compileOnly`, which stops after codegen: the binding files are written during macro expansion, so there's no library to link (no `--app:lib`/`-o:/dev/null` needed). The generated files (for `targetLang=c`/`c_abi`: the `.h` header your host includes, plus a `CMakeLists.txt`) land in `-d:ffiOutputDir`: ```sh nim c -d:ffiGenBindings -d:targetLang=rust,cpp,c --compileOnly mylib.nim ``` - `-d:targetLang` — which generator(s) run; pass a comma-separated list to emit several from one compile. Two kinds: - **Language bindings over the CBOR wire:** `rust` (default), `cpp`, `c`. - **Non-peer generators:** `c_abi` — C bindings that speak the flat `abi = c` wire instead of CBOR; `cddl` — a CDDL schema of the CBOR wire, not a language binding at all. - `-d:ffiOutputDir` — override where the generated files land. Defaults to `_bindings/` next to the compiled source. - `-d:ffiSrcPath` — override the Nim source path embedded in the generated build files. Defaults to the compiled source made relative to the output dir. ### The `--nimMainPrefix:lib` rule `--app:lib` builds a shared library; `--noMain` hands program entry to the foreign host rather than Nim's own `main`. To initialize the Nim runtime, `declareLibrary("", …)` emits an `initializeLibrary()` export that calls `libNimMain()` — the symbol Nim's `NimMain` is renamed to by `--nimMainPrefix`. So the prefix **must be exactly `lib` + the `declareLibrary` name**, on *both* compiles above, or the library fails to link. For `declareLibrary("my_timer", …)` that is `--nimMainPrefix:libmy_timer`. **Library-naming collisions.** When several nim-ffi libraries are loaded into one process (as the C++ end-to-end test does with `timer` + `echo`), each must use a **distinct** library name — and therefore a distinct `--nimMainPrefix` — so their exported `NimMain`, `initializeLibrary` and per-symbol names don't clash. The example libraries deliberately differ: `libmy_timer` vs `libecho`. ## Examples - `examples/timer` — a self-contained Nimble project covering the ctor, sync and async methods, multi-param methods, events, and C / C++ / Rust / CDDL bindings with runnable clients. Start here: ```sh cd examples/timer && nimble install -y ../.. && nimble build ``` - `examples/echo` — a second minimal library, loaded alongside `timer` in the C++ end-to-end test to prove two libraries coexist in one process.