diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9ec05f..a7f56fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 805b8fa..713eda8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/timer/timer.nim b/examples/timer/timer.nim index 5b119de..0ce0924 100644 --- a/examples/timer/timer.nim +++ b/examples/timer/timer.nim @@ -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 diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index c3844e6..c316724 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -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 = "` 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 diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index e0e00e8..b820f24 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -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) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 7ec84fe..137c4b5 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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, ) ) diff --git a/tests/unit/test_abi_format.nim b/tests/unit/test_abi_format.nim new file mode 100644 index 0000000..ec2ecac --- /dev/null +++ b/tests/unit/test_abi_format.nim @@ -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 = ` 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) diff --git a/tests/unit/test_ctx_validation.nim b/tests/unit/test_ctx_validation.nim index e7a4e10..dac3d04 100644 --- a/tests/unit/test_ctx_validation.nim +++ b/tests/unit/test_ctx_validation.nim @@ -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 diff --git a/tests/unit/test_ffi_context.nim b/tests/unit/test_ffi_context.nim index 2e28591..b8e8cb8 100644 --- a/tests/unit/test_ffi_context.nim +++ b/tests/unit/test_ffi_context.nim @@ -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 diff --git a/tests/unit/test_nim_native_api.nim b/tests/unit/test_nim_native_api.nim index 90c2906..cd1194b 100644 --- a/tests/unit/test_nim_native_api.nim +++ b/tests/unit/test_nim_native_api.nim @@ -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