feat(ffi): per-interaction ABI-format annotations + c codec (ffiRaw) (#85)

This commit is contained in:
Gabriel Cruz 2026-06-23 11:18:03 -03:00 committed by GitHub
parent 021f469041
commit a5ccadd62d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 354 additions and 33 deletions

View File

@ -68,6 +68,18 @@ jobs:
nim-versions: ${{ needs.versions.outputs.nim-versions }}
nimble-version: ${{ needs.versions.outputs.nimble }}
abi-format:
# Runs cross-OS to also guard declareLibrary's --app:lib linker-flag check:
# this test compiles FFI code as a plain executable, which would fail to
# link on macOS if the soname/install_name flags leaked into a non-lib build.
name: ABI Format
needs: versions
uses: ./.github/workflows/test.yml
with:
test: test_abi_format
nim-versions: ${{ needs.versions.outputs.nim-versions }}
nimble-version: ${{ needs.versions.outputs.nimble }}
cpp-e2e:
# Codegen output doesn't vary with mm, so we matrix over OS and Nim only.
# Windows runs MSVC by default and may surface codegen tweaks needed in

View File

@ -14,8 +14,20 @@ All notable changes to this project are documented in this file.
runs on the event thread. The FFI thread advances an atomic heartbeat
each loop iteration; if it stalls for more than 1s past the start-up
grace window, the event thread emits the `not_responding` event.
- `declareLibrary` no longer emits the shared-library `soname` /
`install_name` linker flags when building as an executable (`--app:lib`
guard), so FFI code can be unit-tested as a plain binary — fatal on macOS,
where `-install_name` requires `-dynamiclib`.
### Added
- Per-interaction ABI-format annotations: `declareLibrary` now takes an
optional `defaultABIFormat` (`"cbor"` default, or `"c"`) that every
`{.ffi.}` / `{.ffiCtor.}` / `{.ffiDtor.}` / `{.ffiRaw.}` / `{.ffiEvent.}`
inherits, and each annotation can override it with an `"abi = c"` /
`"abi = cbor"` spec (e.g. `{.ffi: "abi = cbor".}`). `declareLibrary` is now
required before any FFI annotation. `c` is parsed and recorded but gated
until its codec lands; only `cbor` currently generates working bindings
([#78](https://github.com/logos-messaging/nim-ffi/issues/78)).
- Queue-overflow handling: when the bounded event queue is full, the
library sets a sticky "stuck" flag, logs an error, fires
`not_responding` from the event thread, and rejects subsequent

View File

@ -10,7 +10,10 @@ type Maybe[T] = Option[T]
type MyTimer = object
name: string # set at creation time, read back in each response
declareLibrary("my_timer", MyTimer)
# `defaultABIFormat` selects the wire format every {.ffi.} / {.ffiEvent.} / ...
# in this library inherits; "cbor" is the default and can be overridden per
# annotation with an `"abi = ..."` spec.
declareLibrary("my_timer", MyTimer, defaultABIFormat = "cbor")
type TimerConfig {.ffi.} = object
name: string

View File

@ -1,7 +1,15 @@
## Compile-time metadata types for FFI binding generation.
## Populated by the {.ffiCtor.} and {.ffi.} macros and consumed by codegen.
import std/strutils
type
ABIFormat* {.pure.} = enum
## Wire format for an FFI payload. `Cbor` is wired end-to-end; `C` (flat
## C-struct) is recognized but gated by the macros until its codegen lands.
Cbor = "cbor"
C = "c"
FFIParamMeta* = object
name*: string # Nim param name, e.g. "req"
typeName*: string # Nim type name, e.g. "EchoRequest"
@ -22,6 +30,7 @@ type
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
returnIsPtr*: bool # true if return type is ptr T
returnIsHandle*: bool # true if return type is an {.ffiHandle.} type
abiFormat*: ABIFormat # wire format for this interaction (default Cbor)
FFIFieldMeta* = object
name*: string # e.g. "delayMs"
@ -30,17 +39,16 @@ type
FFITypeMeta* = object
name*: string
fields*: seq[FFIFieldMeta]
abiFormat*: ABIFormat # wire format for this type (default Cbor)
FFIEventMeta* = object
## Library-initiated event declared with `{.ffiEvent: "wire_name".}`.
## `wireName` is the literal string the foreign side dispatches on
## (it appears in the CBOR `eventType` field, verbatim — no case
## conversion). `payloadTypeName` is the Nim type of the single
## payload parameter.
## Library-initiated event from `{.ffiEvent: "wire_name".}`. `wireName` is
## the verbatim CBOR `eventType` string the foreign side dispatches on.
wireName*: string
nimProcName*: string
libName*: string
payloadTypeName*: string
abiFormat*: ABIFormat # wire format for this event (default Cbor)
# Compile-time registries populated by the macros
var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
@ -48,6 +56,53 @@ var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
var ffiEventRegistry* {.compileTime.}: seq[FFIEventMeta]
var currentLibName* {.compileTime.}: string
# Set by `declareLibrary`; the FFI annotations require it (name/type/default ABI).
var libraryDeclared* {.compileTime.}: bool = false
# Library-wide default ABI, inherited by each annotation unless it overrides.
var currentDefaultABIFormat* {.compileTime.}: ABIFormat = ABIFormat.Cbor
proc abiCodegenImplemented*(fmt: ABIFormat): bool =
## Whether `fmt` has a working end-to-end proc-dispatch path. Only `Cbor` does
## today; this is the single seam a future PR flips when `c` dispatch lands.
fmt == ABIFormat.Cbor
proc parseABIFormatName*(name: string): tuple[ok: bool, fmt: ABIFormat] =
## Bare format name (`"c"`/`"cbor"`, case-insensitive) → `ABIFormat`;
## `ok` is false otherwise.
case name.strip().toLowerAscii()
of "cbor":
(true, ABIFormat.Cbor)
of "c":
(true, ABIFormat.C)
else:
(false, ABIFormat.Cbor)
proc parseAbiSpec*(spec: string): tuple[ok: bool, fmt: ABIFormat, err: string] =
## Parse an `"abi = <format>"` override (whitespace/case tolerant). On bad
## grammar or format, returns `ok = false` with a human-readable `err`.
let parts = spec.split('=')
if parts.len != 2:
return (
false,
ABIFormat.Cbor,
"invalid ABI override '" & spec & "'; expected `abi = c` or `abi = cbor`",
)
if parts[0].strip().toLowerAscii() != "abi":
return (
false,
ABIFormat.Cbor,
"invalid ABI override '" & spec & "'; expected `abi = c` or `abi = cbor`",
)
let (ok, fmt) = parseABIFormatName(parts[1])
if not ok:
return (
false,
ABIFormat.Cbor,
"unknown ABI format '" & parts[1].strip() & "'; valid values are `c` and `cbor`",
)
(true, fmt, "")
# Lib type name (set by declareLibrary) so handle-receiver procs resolve the pool.
var currentLibType* {.compileTime.}: string

View File

@ -26,22 +26,27 @@ macro declareLibraryBase*(libraryName: static[string]): untyped =
## Generate {.passc: "-fPIC".}
res.add nnkPragma.newTree(nnkExprColonExpr.newTree(ident"passc", newLit("-fPIC")))
when defined(linux):
## Generates {.passl: "-Wl,-soname,libwaku.so".} (considering libraryName=="waku", for example)
let soName = fmt"-Wl,-soname,lib{libraryName}.so"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName))
# The soname / install_name only make sense for an actual shared library and
# break a plain-executable link (fatally so on macOS: `-install_name` requires
# `-dynamiclib`). Emit them only when building `--app:lib` so that declaring a
# library is safe in unit tests that compile the FFI code as an executable.
if compileOption("app", "lib"):
when defined(linux):
## Generates {.passl: "-Wl,-soname,libwaku.so".} (considering libraryName=="waku", for example)
let soName = fmt"-Wl,-soname,lib{libraryName}.so"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName))
)
)
)
elif defined(macosx):
## Generates {.passl: "-install_name @rpath/libwaku.dylib".}
let installName = fmt"-install_name @rpath/lib{libraryName}.dylib"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(installName))
elif defined(macosx):
## Generates {.passl: "-install_name @rpath/libwaku.dylib".}
let installName = fmt"-install_name @rpath/lib{libraryName}.dylib"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(installName))
)
)
)
## proc lib{libraryName}NimMain() {.importc.}
let libNimMainName = ident(fmt"lib{libraryName}NimMain")
let importcPragma = nnkPragma.newTree(ident"importc")
@ -107,7 +112,11 @@ macro declareLibraryBase*(libraryName: static[string]): untyped =
return res
macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
macro declareLibrary*(
libraryName: static[string],
libType: untyped,
defaultABIFormat: static[string] = "cbor",
): untyped =
## Declares a library with the given name and emits the C-exported event
## ABI on its `FFIContext`:
##
@ -121,8 +130,21 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
## `libType` is the Nim type of the main library object, used to type
## the `ctx: ptr FFIContext[libType]` parameter. See
## `examples/timer/timer.nim` for a working call site.
##
## `defaultABIFormat` selects the wire format inherited by every `{.ffi.}`,
## `{.ffiEvent.}`, `{.ffiCtor.}`, ... in this library — `"cbor"` (default) or
## `"c"`. Individual annotations override it with an `"abi = ..."` spec.
currentLibType = $libType # so handle-receiver `.ffi.` procs can resolve the pool
let (abiOk, abiFmt) = parseABIFormatName(defaultABIFormat)
if not abiOk:
error(
"declareLibrary: unknown defaultABIFormat '" & defaultABIFormat &
"'; valid values are \"c\" and \"cbor\""
)
currentDefaultABIFormat = abiFmt
libraryDeclared = true
var stmts = newStmtList()
# Emit the base bootstrap (pragmas, linker flags, NimMain, initializeLibrary)

View File

@ -7,6 +7,40 @@ when defined(ffiGenBindings):
import ../codegen/cpp
import ../codegen/cddl
proc requireLibraryDeclared(where: string) {.compileTime.} =
## Enforce that `declareLibrary(...)` (which records name/type/default-ABI)
## ran before this annotation.
if not libraryDeclared:
error(
where &
": declareLibrary(name, LibType[, defaultABIFormat]) must be called before any FFI annotation"
)
proc resolveABIFormat(abiSpecs: seq[NimNode]): ABIFormat {.compileTime.} =
## Resolve one annotation's ABI from its optional `"abi = ..."` string specs
## (last wins), inheriting the library default when absent.
var fmt = currentDefaultABIFormat
for spec in abiSpecs:
if spec.kind notin {nnkStrLit, nnkRStrLit, nnkTripleStrLit}:
error(
"FFI ABI override must be a string literal like \"abi = c\", got: " & spec.repr
)
let parsed = parseAbiSpec($spec)
if not parsed.ok:
error(parsed.err)
fmt = parsed.fmt
fmt
proc gateABIFormat(fmt: ABIFormat, where: string) {.compileTime.} =
## Abort if the selected ABI's codegen isn't wired yet (only `Cbor` is), so a
## `c` request fails loudly instead of emitting CBOR mislabeled as C.
if not abiCodegenImplemented(fmt):
error(
where &
": ABI format is recognized but not yet implemented (only 'cbor' currently generates working bindings): " &
$fmt
)
proc isPtr(typ: NimNode): bool =
## True iff `typ` is a `ptr T` type expression — i.e. an `nnkPtrTy` AST node.
## Used by the binding-generator metadata path to flag pointer-typed params
@ -37,7 +71,9 @@ proc rejectRawPtrType(typ: NimNode, where: string) =
"(only the ctx handle, managed by the framework, may be a pointer)"
)
proc registerFFITypeInfo(typeDef: NimNode): NimNode {.compileTime.} =
proc registerFFITypeInfo(
typeDef: NimNode, abiFormat: ABIFormat
): NimNode {.compileTime.} =
## Registers the type in ffiTypeRegistry for binding generation and returns
## the clean typeDef. Serialization is handled by the generic overloads in
## cbor_serial.nim.
@ -68,7 +104,9 @@ proc registerFFITypeInfo(typeDef: NimNode): NimNode {.compileTime.} =
for i in 0 ..< identDef.len - 2:
fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName))
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
ffiTypeRegistry.add(
FFITypeMeta(name: typeNameStr, fields: fieldMetas, abiFormat: abiFormat)
)
return typeDef
proc nimTypeNameRepr(typ: NimNode): string =
@ -529,7 +567,7 @@ macro processReq*(
echo blockExpr.repr
return blockExpr
macro ffiRaw*(prc: untyped): untyped =
macro ffiRaw*(args: varargs[untyped]): untyped =
## Defines an FFI-exported proc that registers a request handler to be executed
## asynchronously in the FFI thread.
##
@ -547,12 +585,19 @@ macro ffiRaw*(prc: untyped): untyped =
## Then, additional parameters may be defined as needed, after these first
## three, always considering that only no-GC'ed (or C-like) types are allowed.
##
## The wire format follows the library default and can be overridden with
## `{.ffiRaw: "abi = c".}` / `{.ffiRaw: "abi = cbor".}`.
##
## e.g.:
## proc waku_version(
## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
## ) {.ffiRaw.} =
## return ok(WakuNodeVersionString)
requireLibraryDeclared("`.ffiRaw.`")
let prc = args[^1]
gateABIFormat(resolveABIFormat(args[0 ..^ 2]), "`.ffiRaw.` proc")
let procName = prc[0]
let formalParams = prc[3]
let bodyNode = prc[^1]
@ -630,12 +675,18 @@ macro ffiRaw*(prc: untyped): untyped =
echo stmts.repr
return stmts
macro ffiHandle*(prc: untyped): untyped =
macro ffiHandle*(args: varargs[untyped]): untyped =
## Marks a `ref object` as an opaque FFI handle. Its wire form is a `uint64`
## id; the live object stays in the per-ctx handle registry and never crosses.
##
## type Kernel {.ffiHandle.} = ref object
## ...
##
## An optional `"abi = ..."` spec is accepted for surface parity but only
## validated — a handle always rides as an abi-agnostic `uint64` id.
requireLibraryDeclared("`.ffiHandle.`")
let prc = args[^1]
discard resolveABIFormat(args[0 ..^ 2])
if prc.kind != nnkTypeDef:
error("`.ffiHandle.` must be applied to a type definition")
@ -664,7 +715,7 @@ macro ffiHandle*(prc: untyped): untyped =
echo clean.repr
return clean
macro ffi*(prc: untyped): untyped =
macro ffi*(args: varargs[untyped]): untyped =
## Simplified FFI macro — applies to procs or types.
##
## On a type: `type Foo {.ffi.} = object` registers Foo for binding generation
@ -676,6 +727,9 @@ macro ffi*(prc: untyped): untyped =
## userData in its signature — the macro generates a C-exported wrapper that
## takes one CBOR-encoded buffer as the call payload and fires the callback.
##
## The wire format defaults to the library's `defaultABIFormat` and can be
## overridden per annotation with `{.ffi: "abi = c".}` / `{.ffi: "abi = cbor".}`.
##
## Example (type):
## type EchoRequest {.ffi.} = object
## message: string
@ -685,11 +739,23 @@ macro ffi*(prc: untyped): untyped =
## proc mylib_send*(w: MyLib, cfg: SendConfig): Future[Result[string, string]] {.ffi.} =
## return ok("done")
# The annotated node is always the last vararg; any leading args are ABI
# override specs (`"abi = ..."`).
let prc = args[^1]
let abiFormat = resolveABIFormat(args[0 ..^ 2])
# A `{.ffi.}` value type is a passive data definition (it can stand alone, so
# it does not require a declared library). `cbor` serialization rides the
# generic overloads; `c` is recognized but gated until its codec lands.
if prc.kind == nnkTypeDef:
gateABIFormat(abiFormat, "`.ffi.` type")
var cleanTypeDef = prc.copyNimTree()
if cleanTypeDef[0].kind == nnkPragmaExpr:
cleanTypeDef[0] = cleanTypeDef[0][0]
return registerFFITypeInfo(cleanTypeDef)
return registerFFITypeInfo(cleanTypeDef, abiFormat)
requireLibraryDeclared("`.ffi.`")
gateABIFormat(abiFormat, "`.ffi.` proc")
let procName = prc[0]
let formalParams = prc[3]
@ -914,6 +980,7 @@ macro ffi*(prc: untyped): untyped =
returnTypeName: retTn,
returnIsPtr: retIsPtr,
returnIsHandle: retIsHandle,
abiFormat: abiFormat,
)
)
@ -1139,7 +1206,7 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode =
echo regAssign.repr
return regAssign
macro ffiCtor*(prc: untyped): untyped =
macro ffiCtor*(args: varargs[untyped]): untyped =
## Defines a C-exported constructor that creates an FFIContext and populates
## ctx.myLib asynchronously in the FFI thread.
##
@ -1148,6 +1215,9 @@ macro ffiCtor*(prc: untyped): untyped =
## - Return Future[Result[LibType, string]]
## - NOT include ctx, callback, or userData in its signature
##
## The wire format follows the library default and can be overridden with
## `{.ffiCtor: "abi = c".}` / `{.ffiCtor: "abi = cbor".}`.
##
## Example:
## proc mylib_create*(config: SimpleConfig): Future[Result[SimpleLib, string]] {.ffiCtor.} =
## return ok(SimpleLib(value: config.initialValue))
@ -1162,6 +1232,11 @@ macro ffiCtor*(prc: untyped): untyped =
## a decimal string on success. The caller should hold the returned pointer
## and pass it to subsequent .ffi. calls.
requireLibraryDeclared("`.ffiCtor.`")
let prc = args[^1]
let abiFormat = resolveABIFormat(args[0 ..^ 2])
gateABIFormat(abiFormat, "`.ffiCtor.` proc")
let procName = prc[0]
let formalParams = prc[3]
let bodyNode = prc[^1]
@ -1320,6 +1395,7 @@ macro ffiCtor*(prc: untyped): untyped =
extraParams: ctorExtraParams,
returnTypeName: $libTypeName,
returnIsPtr: false,
abiFormat: abiFormat,
)
)
@ -1335,13 +1411,16 @@ macro ffiCtor*(prc: untyped): untyped =
echo stmts.repr
return stmts
macro ffiDtor*(prc: untyped): untyped =
macro ffiDtor*(args: varargs[untyped]): untyped =
## Defines a C-exported destructor that tears down the FFIContext after the
## body runs.
##
## The annotated proc must have exactly one parameter of the library type.
## The body contains any library-level cleanup to run before context teardown.
##
## The wire format follows the library default and can be overridden with
## `{.ffiDtor: "abi = c".}` / `{.ffiDtor: "abi = cbor".}`.
##
## Example:
## proc waku_destroy*(w: Waku) {.ffiDtor.} =
## w.cleanup()
@ -1354,6 +1433,11 @@ macro ffiDtor*(prc: untyped): untyped =
## Returns RET_OK on success, RET_ERR on failure (null/invalid ctx, or
## destroyFFIContext failure).
requireLibraryDeclared("`.ffiDtor.`")
let prc = args[^1]
let abiFormat = resolveABIFormat(args[0 ..^ 2])
gateABIFormat(abiFormat, "`.ffiDtor.` proc")
let procName = prc[0]
let formalParams = prc[3]
let bodyNode = prc[^1]
@ -1434,6 +1518,7 @@ macro ffiDtor*(prc: untyped): untyped =
extraParams: @[],
returnTypeName: "",
returnIsPtr: false,
abiFormat: abiFormat,
)
)
@ -1447,16 +1532,20 @@ macro ffiDtor*(prc: untyped): untyped =
echo stmts.repr
return stmts
macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
macro ffiEvent*(args: varargs[untyped]): untyped =
## Declares a library-initiated event. The annotated proc has an empty
## body — the macro fills it with a `dispatchFFIEventCbor` call so the
## Nim author dispatches the event by calling the proc with a typed
## payload, and the per-target codegens emit a typed handler dispatcher
## on the foreign side.
##
## The pragma takes the wire-format event name verbatim (no case
## conversion). That string appears in the CBOR `eventType` field and is
## the single source of truth across Nim / C++ / Rust bindings.
## The first pragma argument is the wire-format event name, taken verbatim
## (no case conversion). That string appears in the CBOR `eventType` field
## and is the single source of truth across Nim / C++ / Rust bindings.
##
## The wire format follows the library default and can be overridden by
## passing an `"abi = ..."` spec after the event name, e.g.
## `{.ffiEvent("on_peer_connected", "abi = cbor").}`.
##
## Example:
## type PeerInfo {.ffi.} = object
@ -1471,9 +1560,21 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
## Restriction (first pass): exactly one parameter. Multi-param events
## need a synthesised envelope struct; planned for a follow-up.
requireLibraryDeclared("`.ffiEvent.`")
if args.len < 2:
error("ffiEvent requires a wire-name string and a proc declaration")
let prc = args[^1]
if prc.kind notin {nnkProcDef, nnkFuncDef}:
error("ffiEvent must be applied to a proc declaration")
if args[0].kind notin {nnkStrLit, nnkRStrLit, nnkTripleStrLit}:
error("ffiEvent: the first argument must be the wire-name string literal")
let wireName = $args[0]
# Args between the wire name and the proc are ABI override specs.
let abiFormat = resolveABIFormat(args[1 ..^ 2])
gateABIFormat(abiFormat, "`.ffiEvent.` proc")
let procName = prc[0]
let formalParams = prc[3]
@ -1527,6 +1628,7 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
nimProcName: $userProcName,
libName: currentLibName,
payloadTypeName: payloadTypeNameStr,
abiFormat: abiFormat,
)
)

View File

@ -0,0 +1,100 @@
## ABI-format annotation mechanism (issue #78): inherited default + per-spec
## override, recorded on the registry. `static:` blocks assert the registry at
## compile time; the parsing helpers run at runtime with unittest2.
import std/strutils
import unittest2
import results
import ffi
import ffi/codegen/meta
type AbiLib = object
# Stub the dylib NimMain importc that declareLibrary emits (links as a plain exe).
{.emit: "void libabitestNimMain(void) {}".}
declareLibrary("abitest", AbiLib, defaultABIFormat = "cbor")
# declareLibrary must wire its parameter into the library-wide default.
static:
doAssert currentDefaultABIFormat == ABIFormat.Cbor
type AbiConfig {.ffi.} = object
v: int
type Pinged {.ffi.} = object
n: int
# Plain annotations inherit the library default (cbor).
proc abitest_create*(c: AbiConfig): Future[Result[AbiLib, string]] {.ffiCtor.} =
return ok(AbiLib())
proc abitest_ping*(lib: AbiLib): Future[Result[string, string]] {.ffi.} =
return ok("pong")
# Explicit override — same value, but exercises the spec parser end-to-end.
proc abitest_echo*(
lib: AbiLib, n: int
): Future[Result[int, string]] {.ffi: "abi = cbor".} =
return ok(n)
# Event with an explicit ABI override passed after the wire name.
proc abitest_pinged*(p: Pinged) {.ffiEvent("on_pinged", "abi = cbor").}
# Handles accept the spec for surface parity; the wire form stays uint64.
type PlainHandle {.ffiHandle.} = ref object
v: int
type AbiHandle {.ffiHandle: "abi = cbor".} = ref object
v: int
# Both the inherited-default and the explicit-override annotations must record
# the resolved format on their registry entries.
static:
for name in ["abitest_create", "abitest_ping", "abitest_echo"]:
var found = false
for p in ffiProcRegistry:
if p.procName == name:
doAssert p.abiFormat == ABIFormat.Cbor, name & ": unexpected abiFormat"
found = true
doAssert found, "proc not registered: " & name
block:
var found = false
for e in ffiEventRegistry:
if e.wireName == "on_pinged":
doAssert e.abiFormat == ABIFormat.Cbor, "event: unexpected abiFormat"
found = true
doAssert found, "event not registered: on_pinged"
suite "ABI format parsing":
test "parseABIFormatName maps both formats, case-insensitive and trimmed":
check parseABIFormatName("cbor") == (true, ABIFormat.Cbor)
check parseABIFormatName("c") == (true, ABIFormat.C)
check parseABIFormatName("CBOR") == (true, ABIFormat.Cbor)
check parseABIFormatName(" C ") == (true, ABIFormat.C)
check parseABIFormatName("bson").ok == false
test "parseAbiSpec accepts `abi = <fmt>` for both formats, flexible spacing":
check parseAbiSpec("abi = c").fmt == ABIFormat.C
check parseAbiSpec("abi = cbor").fmt == ABIFormat.Cbor
check parseAbiSpec("abi=cbor").fmt == ABIFormat.Cbor
check parseAbiSpec("ABI = C").fmt == ABIFormat.C
check parseAbiSpec(" abi = cbor ").fmt == ABIFormat.Cbor
check parseAbiSpec("abi = c").ok
check parseAbiSpec("abi = cbor").ok
test "parseAbiSpec rejects malformed specs and unknown formats":
check parseAbiSpec("c").ok == false # missing `abi =`
check parseAbiSpec("mode = c").ok == false # wrong key
check parseAbiSpec("abi = c = x").ok == false # too many `=`
check parseAbiSpec("abi = bson").ok == false # unknown format
check "bson" in parseAbiSpec("abi = bson").err
suite "ABI proc-dispatch readiness (why c is still gated on procs)":
test "cbor proc-dispatch is wired; c proc-dispatch is gated":
# This predicate is what the proc-form macros consult: `cbor` is wired
# end-to-end, while `c` is recognized but gated pending its codec. It is the
# single seam a future PR flips when the c codec and dispatch path land.
check abiCodegenImplemented(ABIFormat.Cbor)
check not abiCodegenImplemented(ABIFormat.C)

View File

@ -5,6 +5,11 @@ import ffi
type TestLib = object
# Stub the dylib NimMain importc that declareLibrary emits (this links as a plain exe).
{.emit: "void libctxvaltestNimMain(void) {}".}
declareLibrary("ctxvaltest", TestLib)
type CtxValidationConfig {.ffi.} = object
initialValue: int

View File

@ -327,6 +327,11 @@ suite "sendRequestToFFIThread":
type SimpleLib = object
value: int
# Stub the dylib NimMain importc that declareLibrary emits (this links as a plain exe).
{.emit: "void libtestlibNimMain(void) {}".}
declareLibrary("testlib", SimpleLib)
type SimpleConfig {.ffi.} = object
initialValue: int

View File

@ -12,6 +12,11 @@ import ffi
type Counter = object
start: int
# Stub the dylib NimMain importc that declareLibrary emits (this links as a plain exe).
{.emit: "void libcounterlibNimMain(void) {}".}
declareLibrary("counterlib", Counter)
type CounterConfig {.ffi.} = object
initial: int