nim-ffi/docs/design-abi-format.md
Ivan FB 50b56c0cad
docs: ABI format selection decision (raw vs cbor)
Records the user-chosen direction for telling the compiler a proc's wire
format, applicable across the whole pragma family ({.ffi.}, {.ffiHost.}, …):

- per-proc pragma argument: {.ffi: raw.} / {.ffi: cbor.} (A1), default native;
- library-wide override declareLibrary(defaultAbiFormat = raw) (C2);
- the global -d:ffiMode compile flag is discarded.

'raw' (not 'native') names the zero-serialization C-POD format. Design only;
captures open questions (symbol emission, resolution order, codegen impact) for
further discussion before implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:01:16 +02:00

3.3 KiB

Design: ABI format selection (raw vs cbor)

Status: direction decided, open for refinement. How a library author tells the compiler which wire format a boundary-crossing proc uses. Applies to the whole pragma family ({.ffi.}, {.ffiHost.}, {.ffiCtor.}, {.ffiDtor.}).

The two formats

  • raw — native, zero-serialization C-POD ABI. Same-process; the request/ result is a C struct passed/cast directly under the deep-copy + callback-lifetime ownership rule. The common path.
  • cbor — CBOR-encoded buffer. For IPC / generic / cross-language callers where serialization is required anyway.

Decision

A1 — per-proc pragma argument (chosen). The format is an argument on the pragma, read by the macro at compile time:

proc echo(req: EchoReq): Future[Result[EchoResp, string]] {.ffi: raw.}
proc echo(req: EchoReq): Future[Result[EchoResp, string]] {.ffi: cbor.}

proc fetchProfile(id: string): Future[Result[Profile, string]] {.ffiHost: cbor.}

Matches the existing {.ffiEvent: "wire_name".} precedent (pragmas already take args in this codebase). Local, granular, lets one library mix both formats.

C2 — library-wide default (chosen, layered on top). Set the default once at declareLibrary; a per-proc pragma arg overrides it:

declareLibrary("my_app", MyApp, defaultAbiFormat = raw)
# every {.ffi.}/{.ffiHost.}/… is `raw` unless it says otherwise

So the common case stays terse, with per-proc control when needed.

Rejected:

  • Global compile flag (-d:ffiFormat=…) — discarded. All-or-nothing, action-at-a-distance, can't mix formats in one library. defaultAbiFormat replaces its only virtue (a single default) without the downsides.
  • Distinct pragma names ({.ffiCbor.}, {.ffiHostCbor.}) — combinatorial explosion across the pragma family.
  • Format on the data type (type T {.ffi: cbor.}) — misattributes a transport property to the data; breaks when one type is used by both a raw and a cbor proc.

Sketch

type AbiFormat* = enum
  raw    # native zero-serialization C-POD (same-process)
  cbor   # CBOR buffer (IPC / generic)

# two overloads, like ffiEvent: no-arg form defaults to the library default
macro ffi*(prc: untyped): untyped = ...                       # uses defaultAbiFormat
macro ffi*(fmt: static[AbiFormat], prc: untyped): untyped = ...  # explicit override

Open questions (for further discussion)

  • Enum name / spelling. AbiFormat with values raw / cbor? (raw over native per current preference.)
  • Resolution order. proc pragma arg → defaultAbiFormat → built-in fallback (raw?). Confirm the fallback when declareLibrary sets no default.
  • Symbol emission. Today {.ffi.} emits BOTH <name> and <name>_cbor. Does a per-proc format mean we emit only the chosen symbol, or keep emitting both and let the format arg pick the primary? (Leaning: emit only the chosen one; <name>_cbor suffix stays only when both are explicitly wanted.)
  • Codegen impact. Each generator (c/go/cpp/rust/swift/kotlin) must honour the per-proc format when emitting wrappers + registration symbols.
  • Future A3. Whether to later expose an orthogonal {.ffi, wire: cbor.} transport pragma that composes across the family (deferred; A1+C2 first).