mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-23 17:59:33 +00:00
feat(ffi): per-interaction ABI-format annotations + c codec (ffiRaw) (#85)
This commit is contained in:
parent
021f469041
commit
a5ccadd62d
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
12
CHANGELOG.md
12
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
100
tests/unit/test_abi_format.nim
Normal file
100
tests/unit/test_abi_format.nim
Normal 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)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user