2026-05-11 23:28:17 +02:00
|
|
|
|
import std/[macros, tables, strutils]
|
2025-08-09 22:56:44 +02:00
|
|
|
|
import chronos
|
2025-09-02 23:47:08 +02:00
|
|
|
|
import ../ffi_types
|
2026-05-16 01:08:42 +02:00
|
|
|
|
import ../codegen/[meta, string_helpers]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
when defined(ffiGenBindings):
|
|
|
|
|
|
import ../codegen/rust
|
|
|
|
|
|
import ../codegen/cpp
|
2026-05-18 20:00:57 +02:00
|
|
|
|
import ../codegen/cddl
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
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
|
|
|
|
|
|
## and return types so the foreign side can render them as opaque addresses.
|
|
|
|
|
|
typ.kind == nnkPtrTy
|
|
|
|
|
|
|
|
|
|
|
|
proc rejectRawPtrType(typ: NimNode, where: string) =
|
|
|
|
|
|
## Errors out at macro-expansion time if `typ` is `pointer` or `ptr T`.
|
|
|
|
|
|
## Raw addresses must not cross the FFI boundary in user-declared fields,
|
|
|
|
|
|
## parameters, or return types: the only pointer that legitimately crosses
|
|
|
|
|
|
## the boundary is the opaque ctx handle returned by `.ffiCtor.` and passed
|
|
|
|
|
|
## back as the first C-ABI argument, which the framework validates via
|
|
|
|
|
|
## FFIContextPool.isValidCtx before dereferencing. Any other raw pointer
|
|
|
|
|
|
## would hand the foreign caller an address with no way to validate its
|
|
|
|
|
|
## memory state — see PR #23 review (discussion_r3236531712).
|
|
|
|
|
|
##
|
|
|
|
|
|
## `object` and `ref T` are not rejected: they flow as value copies through
|
|
|
|
|
|
## cbor_serialization (the library's default `ref T` writer dereferences
|
|
|
|
|
|
## and encodes the pointee, so no address crosses the boundary).
|
|
|
|
|
|
if typ.kind == nnkPtrTy:
|
|
|
|
|
|
error(
|
|
|
|
|
|
where & ": raw `ptr T` is not allowed across the FFI boundary " &
|
|
|
|
|
|
"(only the ctx handle, managed by the framework, may be a pointer)"
|
|
|
|
|
|
)
|
|
|
|
|
|
if typ.kind == nnkIdent and $typ == "pointer":
|
|
|
|
|
|
error(
|
|
|
|
|
|
where & ": raw `pointer` is not allowed across the FFI boundary " &
|
|
|
|
|
|
"(only the ctx handle, managed by the framework, may be a pointer)"
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc registerFFITypeInfo(typeDef: NimNode): NimNode {.compileTime.} =
|
2026-05-11 23:28:17 +02:00
|
|
|
|
## Registers the type in ffiTypeRegistry for binding generation and returns
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## the clean typeDef. Serialization is handled by the generic overloads in
|
|
|
|
|
|
## cbor_serial.nim.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let typeName =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
if typeDef[0].kind == nnkPostfix:
|
|
|
|
|
|
typeDef[0][1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
typeDef[0]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let typeNameStr = $typeName
|
|
|
|
|
|
|
|
|
|
|
|
var fieldMetas: seq[FFIFieldMeta] = @[]
|
|
|
|
|
|
let objTy = typeDef[2]
|
|
|
|
|
|
if objTy.kind == nnkObjectTy and objTy.len >= 3:
|
|
|
|
|
|
let recList = objTy[2]
|
|
|
|
|
|
if recList.kind == nnkRecList:
|
|
|
|
|
|
for identDef in recList:
|
|
|
|
|
|
if identDef.kind == nnkIdentDefs:
|
|
|
|
|
|
let fieldType = identDef[^2]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
for i in 0 ..< identDef.len - 2:
|
|
|
|
|
|
rejectRawPtrType(
|
|
|
|
|
|
fieldType, "{.ffi.} type " & typeNameStr & "." & $identDef[i]
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let fieldTypeName =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
if fieldType.kind == nnkIdent:
|
|
|
|
|
|
$fieldType
|
|
|
|
|
|
else:
|
|
|
|
|
|
fieldType.repr
|
2026-05-11 23:28:17 +02:00
|
|
|
|
for i in 0 ..< identDef.len - 2:
|
|
|
|
|
|
fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName))
|
|
|
|
|
|
|
|
|
|
|
|
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return typeDef
|
|
|
|
|
|
|
|
|
|
|
|
proc nimTypeNameRepr(typ: NimNode): string =
|
|
|
|
|
|
## Stringifies a parameter or field type for the binding-generator registry.
|
|
|
|
|
|
## `$ident` works for simple types; bracket/dot/expression types need `repr`.
|
|
|
|
|
|
case typ.kind
|
|
|
|
|
|
of nnkIdent:
|
|
|
|
|
|
$typ
|
|
|
|
|
|
of nnkPtrTy:
|
|
|
|
|
|
"ptr " & nimTypeNameRepr(typ[0])
|
2026-05-11 23:28:17 +02:00
|
|
|
|
else:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
typ.repr
|
|
|
|
|
|
|
|
|
|
|
|
proc storageType(typ: NimNode): NimNode =
|
|
|
|
|
|
## Returns the in-Req-struct storage type for a user-declared param type.
|
|
|
|
|
|
## `cstring` is stored as `string` for trivial CBOR transport; everything
|
|
|
|
|
|
## else is stored as the user typed it.
|
|
|
|
|
|
if typ.kind == nnkIdent and $typ == "cstring":
|
|
|
|
|
|
return ident("string")
|
|
|
|
|
|
return typ
|
|
|
|
|
|
|
|
|
|
|
|
proc unpackReqField*(fieldIdent, userType, decodedIdent: NimNode): NimNode =
|
|
|
|
|
|
## Emits AST for unpacking one field from a CBOR-decoded Req struct into a
|
|
|
|
|
|
## local typed as the user's original param type.
|
|
|
|
|
|
##
|
|
|
|
|
|
## `cstring` params are stored as `string` in the Req (per storageType)
|
|
|
|
|
|
## and cast back via `.cstring` on unpack — safe because `decodedIdent`
|
|
|
|
|
|
## outlives the cstring use within the generated proc body.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Produces one of:
|
|
|
|
|
|
## let <field>: cstring = (<decoded>.<field>).cstring # for cstring
|
|
|
|
|
|
## let <field> = <decoded>.<field> # for everything else
|
|
|
|
|
|
##
|
|
|
|
|
|
## Built with the runtime AST API rather than `quote do:` so the proc is
|
|
|
|
|
|
## callable from both macro context and ordinary code (e.g. unit tests).
|
|
|
|
|
|
let storedAsString = userType.kind == nnkIdent and $userType == "cstring"
|
|
|
|
|
|
if not storedAsString:
|
|
|
|
|
|
return newLetStmt(fieldIdent, newDotExpr(decodedIdent, fieldIdent))
|
|
|
|
|
|
|
|
|
|
|
|
let fieldAccess = newDotExpr(decodedIdent, fieldIdent)
|
|
|
|
|
|
let castExpr = newDotExpr(fieldAccess, ident("cstring"))
|
|
|
|
|
|
return
|
|
|
|
|
|
nnkLetSection.newTree(nnkIdentDefs.newTree(fieldIdent, ident("cstring"), castExpr))
|
|
|
|
|
|
|
|
|
|
|
|
proc cExportedParams(ctxType: NimNode): seq[NimNode] =
|
|
|
|
|
|
## Standard parameter list for the C-exported wrapper of a .ffi. proc:
|
|
|
|
|
|
## (returns cint; ctx, callback, userData, reqCbor, reqCborLen)
|
|
|
|
|
|
## Shared by the async and sync paths so both wrappers carry the same ABI.
|
|
|
|
|
|
var params: seq[NimNode] = @[]
|
|
|
|
|
|
params.add(ident("cint"))
|
|
|
|
|
|
params.add(newIdentDefs(ident("ctx"), ctxType))
|
|
|
|
|
|
params.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
|
|
|
|
|
params.add(newIdentDefs(ident("userData"), ident("pointer")))
|
|
|
|
|
|
params.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte"))))
|
|
|
|
|
|
params.add(newIdentDefs(ident("reqCborLen"), ident("csize_t")))
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
proc buildReqTypeFromFields(
|
|
|
|
|
|
reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode]
|
|
|
|
|
|
): NimNode =
|
|
|
|
|
|
## Builds the per-proc Req `nnkTypeSection` (exported) from explicit
|
|
|
|
|
|
## parallel lists of parameter names and types. The result is the AST for
|
|
|
|
|
|
## a `type Foo* = object` declaration that the codegen later emits.
|
|
|
|
|
|
##
|
|
|
|
|
|
## `cstring` parameter types are rewritten to `string` (via storageType)
|
|
|
|
|
|
## so the request can ride a plain CBOR text string on the wire. Empty
|
|
|
|
|
|
## parameter lists get a single `_placeholder: uint8` field so the object
|
|
|
|
|
|
## type is well-formed (Nim won't accept an empty `object` body here).
|
|
|
|
|
|
##
|
|
|
|
|
|
## Examples (in pseudo-Nim, showing the AST this proc produces):
|
|
|
|
|
|
##
|
|
|
|
|
|
## buildReqTypeFromFields(
|
|
|
|
|
|
## reqTypeName = ident("EchoReq"),
|
|
|
|
|
|
## paramNames = @["message", "delayMs"],
|
|
|
|
|
|
## paramTypes = @[ident("cstring"), ident("int")])
|
|
|
|
|
|
## # → type EchoReq* = object
|
|
|
|
|
|
## # message: string # cstring rewritten to string
|
|
|
|
|
|
## # delayMs: int
|
|
|
|
|
|
##
|
|
|
|
|
|
## buildReqTypeFromFields(
|
|
|
|
|
|
## reqTypeName = ident("VersionReq"),
|
|
|
|
|
|
## paramNames = @[],
|
|
|
|
|
|
## paramTypes = @[])
|
|
|
|
|
|
## # → type VersionReq* = object
|
|
|
|
|
|
## # _placeholder: uint8 # placeholder for the empty-params case
|
|
|
|
|
|
##
|
|
|
|
|
|
## If `reqTypeName` is already a postfix node (e.g. `EchoReq*`) it is used
|
|
|
|
|
|
## as-is; otherwise the `*` export marker is added.
|
|
|
|
|
|
var fields: seq[NimNode] = @[]
|
|
|
|
|
|
for i in 0 ..< paramNames.len:
|
|
|
|
|
|
let storedType = storageType(paramTypes[i])
|
|
|
|
|
|
fields.add newTree(nnkIdentDefs, ident(paramNames[i]), storedType, newEmptyNode())
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let recList =
|
|
|
|
|
|
if fields.len > 0:
|
|
|
|
|
|
newTree(nnkRecList, fields)
|
|
|
|
|
|
else:
|
|
|
|
|
|
newTree(
|
|
|
|
|
|
nnkRecList,
|
|
|
|
|
|
newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()),
|
|
|
|
|
|
)
|
2025-09-05 21:31:01 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
2025-09-05 21:31:01 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let typeName =
|
|
|
|
|
|
if reqTypeName.kind == nnkPostfix:
|
|
|
|
|
|
reqTypeName
|
|
|
|
|
|
else:
|
|
|
|
|
|
postfix(reqTypeName, "*")
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-09-05 21:31:01 +02:00
|
|
|
|
proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Builds the per-proc Req object type from a registerReqFFI lambda body.
|
|
|
|
|
|
## Field names match the lambda params; field types match the user-typed
|
|
|
|
|
|
## param types (with `cstring` rewritten to `string` for transport).
|
|
|
|
|
|
##
|
2025-09-05 21:31:01 +02:00
|
|
|
|
## Builds:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## type <reqTypeName>* = object
|
|
|
|
|
|
## <lambdaParam1Name>: <lambdaParam1Type>
|
|
|
|
|
|
## ...
|
|
|
|
|
|
##
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## e.g.:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## type EchoRequest* = object
|
|
|
|
|
|
## message: string
|
|
|
|
|
|
## delayMs: int
|
2025-09-05 21:31:01 +02:00
|
|
|
|
|
|
|
|
|
|
var procNode = body
|
|
|
|
|
|
if procNode.kind == nnkStmtList and procNode.len == 1:
|
|
|
|
|
|
procNode = procNode[0]
|
2025-12-10 17:43:26 +01:00
|
|
|
|
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
|
2025-09-05 21:31:01 +02:00
|
|
|
|
error "registerReqFFI expects a lambda proc, found: " & $procNode.kind
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let params = procNode[3]
|
|
|
|
|
|
var paramNames: seq[string] = @[]
|
|
|
|
|
|
var paramTypes: seq[NimNode] = @[]
|
|
|
|
|
|
for p in params[1 .. ^1]:
|
|
|
|
|
|
paramNames.add($p[0])
|
|
|
|
|
|
paramTypes.add(p[1])
|
2025-09-05 21:31:01 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let typeSection = buildReqTypeFromFields(reqTypeName, paramNames, paramTypes)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo typeSection.repr
|
|
|
|
|
|
return typeSection
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc buildFFINewReqProc(reqTypeName, body: NimNode): NimNode =
|
|
|
|
|
|
## Builds ffiNewReq: takes the user's typed params, packs them into a Req
|
|
|
|
|
|
## object, CBOR-encodes the Req into one byte buffer, and constructs the
|
|
|
|
|
|
## FFIThreadRequest that owns the buffer.
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
var formalParams = newSeq[NimNode]()
|
|
|
|
|
|
|
|
|
|
|
|
var procNode: NimNode
|
|
|
|
|
|
if body.kind == nnkStmtList and body.len == 1:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
procNode = body[0]
|
2025-08-16 02:14:29 +02:00
|
|
|
|
else:
|
|
|
|
|
|
procNode = body
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2025-12-10 17:43:26 +01:00
|
|
|
|
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
|
2025-09-05 21:31:01 +02:00
|
|
|
|
error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# T: typedesc[XxxReq]
|
|
|
|
|
|
let typedescParam =
|
|
|
|
|
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
2025-08-16 02:14:29 +02:00
|
|
|
|
formalParams.add(typedescParam)
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# User-typed lambda params (kept as-is so callers see their original signature)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
let procParams = procNode[3]
|
|
|
|
|
|
for p in procParams[1 .. ^1]:
|
|
|
|
|
|
formalParams.add(p)
|
|
|
|
|
|
|
|
|
|
|
|
let retType = newNimNode(nnkPtrTy)
|
|
|
|
|
|
retType.add(ident("FFIThreadRequest"))
|
|
|
|
|
|
|
|
|
|
|
|
formalParams = @[retType] & formalParams
|
|
|
|
|
|
|
|
|
|
|
|
let reqObjIdent = ident("reqObj")
|
|
|
|
|
|
var newBody = newStmtList()
|
|
|
|
|
|
newBody.add(
|
|
|
|
|
|
quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
var `reqObjIdent`: T
|
2025-08-16 02:14:29 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for p in procParams[1 .. ^1]:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let fieldName = ident($p[0])
|
|
|
|
|
|
let userType = p[1]
|
|
|
|
|
|
let storeAsString = userType.kind == nnkIdent and $userType == "cstring"
|
|
|
|
|
|
if storeAsString:
|
2025-08-16 02:14:29 +02:00
|
|
|
|
newBody.add(
|
|
|
|
|
|
quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
`reqObjIdent`.`fieldName` = $`fieldName`
|
2025-08-16 02:14:29 +02:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
newBody.add(
|
|
|
|
|
|
quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
`reqObjIdent`.`fieldName` = `fieldName`
|
2025-08-16 02:14:29 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
newBody.add(
|
|
|
|
|
|
quote do:
|
|
|
|
|
|
let typeStr = $T
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# Encode directly into shared memory and hand ownership to the request,
|
|
|
|
|
|
# avoiding the seq[byte] → allocShared+copyMem second copy.
|
|
|
|
|
|
let (sharedData, sharedLen) = cborEncodeShared(`reqObjIdent`)
|
|
|
|
|
|
return FFIThreadRequest.initFromOwnedShared(
|
|
|
|
|
|
callback, userData, typeStr.cstring, sharedData, sharedLen
|
|
|
|
|
|
)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
)
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let newReqProc = newProc(
|
2025-08-16 02:14:29 +02:00
|
|
|
|
name = postfix(ident("ffiNewReq"), "*"),
|
|
|
|
|
|
params = formalParams,
|
|
|
|
|
|
body = newBody,
|
|
|
|
|
|
pragmas = newEmptyNode(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo newReqProc.repr
|
|
|
|
|
|
return newReqProc
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Generates the FFI-thread-side processor for the Req type.
|
|
|
|
|
|
## Decodes the CBOR payload into a Req struct, unpacks each field into a
|
|
|
|
|
|
## local, then runs the user lambda body.
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
if reqHandler.kind != nnkExprColonExpr:
|
2025-08-09 22:56:44 +02:00
|
|
|
|
error(
|
2025-08-16 02:14:29 +02:00
|
|
|
|
"Second argument must be a typed parameter, e.g., waku: ptr Waku. Found: " &
|
|
|
|
|
|
$reqHandler.kind
|
2025-08-09 22:56:44 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
let rhs = reqHandler[1]
|
|
|
|
|
|
if rhs.kind != nnkPtrTy:
|
|
|
|
|
|
error("Second argument must be a pointer type, e.g., waku: ptr Waku")
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
var procNode = body
|
|
|
|
|
|
if procNode.kind == nnkStmtList and procNode.len == 1:
|
|
|
|
|
|
procNode = procNode[0]
|
2025-12-10 17:43:26 +01:00
|
|
|
|
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
|
2025-09-05 21:31:01 +02:00
|
|
|
|
error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
let typedescParam =
|
|
|
|
|
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2025-08-16 02:14:29 +02:00
|
|
|
|
let procParams = procNode[3]
|
2025-09-02 23:47:08 +02:00
|
|
|
|
var formalParams: seq[NimNode] = @[]
|
|
|
|
|
|
formalParams.add(procParams[0]) # return type
|
|
|
|
|
|
formalParams.add(typedescParam)
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("request"), ident("pointer")))
|
|
|
|
|
|
formalParams.add(newIdentDefs(reqHandler[0], rhs)) # e.g. waku: ptr Waku
|
2025-08-16 02:14:29 +02:00
|
|
|
|
|
|
|
|
|
|
let bodyNode =
|
|
|
|
|
|
if procNode.body.kind == nnkStmtList:
|
|
|
|
|
|
procNode.body
|
|
|
|
|
|
else:
|
|
|
|
|
|
newStmtList(procNode.body)
|
|
|
|
|
|
|
2025-09-02 23:47:08 +02:00
|
|
|
|
let newBody = newStmtList()
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let reqIdent = genSym(nskLet, "ffiReq")
|
|
|
|
|
|
let decodedIdent = genSym(nskLet, "decoded")
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
|
|
|
|
|
newBody.add quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let `reqIdent`: ptr FFIThreadRequest = cast[ptr FFIThreadRequest](request)
|
|
|
|
|
|
let `decodedIdent` = cborDecodePtr(
|
|
|
|
|
|
cast[ptr UncheckedArray[byte]](`reqIdent`[].data),
|
|
|
|
|
|
`reqIdent`[].dataLen,
|
|
|
|
|
|
`reqTypeName`,
|
|
|
|
|
|
).valueOr:
|
|
|
|
|
|
return err("CBOR decode failed for " & $T & ": " & $error)
|
|
|
|
|
|
|
|
|
|
|
|
# Unpack each field as a local typed as the user's original param type.
|
2025-09-02 23:47:08 +02:00
|
|
|
|
for p in procParams[1 ..^ 1]:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
newBody.add unpackReqField(p[0], p[1], decodedIdent)
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
|
|
|
|
|
newBody.add(bodyNode)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let processProc = newProc(
|
2025-08-16 02:14:29 +02:00
|
|
|
|
name = postfix(ident("processFFIRequest"), "*"),
|
|
|
|
|
|
params = formalParams,
|
2025-09-02 23:47:08 +02:00
|
|
|
|
body = newBody,
|
2025-08-16 02:14:29 +02:00
|
|
|
|
procType = nnkProcDef,
|
2025-09-02 23:47:08 +02:00
|
|
|
|
pragmas =
|
|
|
|
|
|
if procNode.len >= 5:
|
|
|
|
|
|
procNode[4]
|
|
|
|
|
|
else:
|
|
|
|
|
|
newEmptyNode(),
|
2025-08-16 02:14:29 +02:00
|
|
|
|
)
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo processProc.repr
|
|
|
|
|
|
return processProc
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-09-02 23:47:08 +02:00
|
|
|
|
proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Generates the dispatcher that the FFI thread calls: it invokes
|
|
|
|
|
|
## processFFIRequest (which returns the user's typed Result[T, string]) and
|
|
|
|
|
|
## encodes a successful T value with cborEncode into the seq[byte] payload.
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
|
|
|
|
|
let returnType = nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Future"),
|
2026-05-16 01:08:42 +02:00
|
|
|
|
nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Result"),
|
|
|
|
|
|
nnkBracketExpr.newTree(ident("seq"), ident("byte")),
|
|
|
|
|
|
ident("string"),
|
|
|
|
|
|
),
|
2025-09-02 23:47:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let rhsType =
|
|
|
|
|
|
if reqHandler.kind == nnkExprColonExpr:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
reqHandler[1]
|
2025-09-02 23:47:08 +02:00
|
|
|
|
else:
|
|
|
|
|
|
error "Second argument must be a typed parameter, e.g. waku: ptr Waku"
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let castedHandler = newTree(nnkCast, rhsType, ident("reqHandler"))
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
|
|
|
|
|
let callExpr = newCall(
|
|
|
|
|
|
newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let typedResIdent = genSym(nskLet, "typedRes")
|
|
|
|
|
|
|
2025-09-02 23:47:08 +02:00
|
|
|
|
var newBody = newStmtList()
|
2026-05-16 01:08:42 +02:00
|
|
|
|
newBody.add quote do:
|
|
|
|
|
|
let `typedResIdent` = await `callExpr`
|
|
|
|
|
|
if `typedResIdent`.isErr:
|
|
|
|
|
|
return err(`typedResIdent`.error)
|
|
|
|
|
|
when typeof(`typedResIdent`.value) is seq[byte]:
|
|
|
|
|
|
return ok(`typedResIdent`.value)
|
|
|
|
|
|
elif typeof(`typedResIdent`.value) is void:
|
|
|
|
|
|
return ok(newSeq[byte]())
|
|
|
|
|
|
else:
|
|
|
|
|
|
return ok(cborEncode(`typedResIdent`.value))
|
2025-09-02 23:47:08 +02:00
|
|
|
|
|
|
|
|
|
|
let asyncProc = newProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
name = newEmptyNode(),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
params = @[
|
|
|
|
|
|
returnType,
|
|
|
|
|
|
newIdentDefs(ident("request"), ident("pointer")),
|
|
|
|
|
|
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
|
|
|
|
|
],
|
2025-09-02 23:47:08 +02:00
|
|
|
|
body = newBody,
|
|
|
|
|
|
pragmas = nnkPragma.newTree(ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let key = newLit($reqTypeName)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let regAssign =
|
2025-09-02 23:47:08 +02:00
|
|
|
|
newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo regAssign.repr
|
|
|
|
|
|
return regAssign
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-09-05 21:31:01 +02:00
|
|
|
|
macro registerReqFFI*(reqTypeName, reqHandler, body: untyped): untyped =
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## Registers a request that will be handled by the FFI/working thread.
|
2025-09-05 21:31:01 +02:00
|
|
|
|
## The request should be sent from the ffi consumer thread.
|
2026-05-16 01:08:42 +02:00
|
|
|
|
##
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## The lambda passed to this macro must:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## - Only have no-GC'ed types as parameters (cstring is allowed; it gets
|
|
|
|
|
|
## transported as `string` in the per-proc Req struct).
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## - Return Future[Result[string, string]] and be annotated with {.async.}
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## The returned values are sent back to the ffi consumer thread.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example:
|
|
|
|
|
|
## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
|
|
|
|
|
|
## proc(
|
|
|
|
|
|
## config: NodeConfig, appCallbacks: AppCallbacks
|
|
|
|
|
|
## ): Future[Result[string, string]] {.async.} =
|
|
|
|
|
|
## ctx.myLib[] = (await createWaku(config, appCallbacks)).valueOr:
|
|
|
|
|
|
## return err($error)
|
|
|
|
|
|
## return ok("")
|
|
|
|
|
|
##
|
|
|
|
|
|
## The created FFI request is then dispatched from the ffi consumer thread
|
|
|
|
|
|
## (generally the main thread) following something like:
|
|
|
|
|
|
##
|
|
|
|
|
|
## ffi.sendRequestToFFIThread(
|
|
|
|
|
|
## ctx, CreateNodeRequest.ffiNewReq(callback, userData, config, appCallbacks)
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## ).isOkOr:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## ...
|
2025-09-05 21:31:01 +02:00
|
|
|
|
|
|
|
|
|
|
# Extract lambda params to generate fields
|
|
|
|
|
|
let typeDef = buildRequestType(reqTypeName, body)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let ffiNewReqProc = buildFFINewReqProc(reqTypeName, body)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
let processProc = buildProcessFFIRequestProc(reqTypeName, reqHandler, body)
|
2025-09-02 23:47:08 +02:00
|
|
|
|
let addNewReqToReg = addNewRequestToRegistry(reqTypeName, reqHandler)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let stmts = newStmtList(typeDef, ffiNewReqProc, processProc, addNewReqToReg)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo stmts.repr
|
|
|
|
|
|
return stmts
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
macro processReq*(
|
|
|
|
|
|
reqType, ctx, callback, userData: untyped, args: varargs[untyped]
|
|
|
|
|
|
): untyped =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Expands T.processReq(ctx, callback, userData, a, b, ...) into a
|
|
|
|
|
|
## sendRequestToFFIThread call that wraps the args in a freshly-built
|
|
|
|
|
|
## FFIThreadRequest, with inline error reporting via `callback`.
|
|
|
|
|
|
##
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## e.g.:
|
|
|
|
|
|
## waku_dial_peerReq.processReq(ctx, callback, userData, peerMultiAddr, protocol, timeoutMs)
|
|
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
var callArgs = @[reqType, callback, userData]
|
2025-09-05 21:31:01 +02:00
|
|
|
|
for a in args:
|
|
|
|
|
|
callArgs.add a
|
2025-08-09 22:56:44 +02:00
|
|
|
|
|
2025-09-05 21:31:01 +02:00
|
|
|
|
let newReqCall = newCall(ident("ffiNewReq"), callArgs)
|
|
|
|
|
|
|
|
|
|
|
|
let sendCall = newCall(
|
2025-12-09 18:51:50 +01:00
|
|
|
|
newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), ctx, newReqCall
|
2025-09-05 21:31:01 +02:00
|
|
|
|
)
|
2025-08-16 02:14:29 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let blockExpr = quote:
|
2025-09-05 21:31:01 +02:00
|
|
|
|
block:
|
|
|
|
|
|
let res = `sendCall`
|
2025-12-11 17:11:59 +01:00
|
|
|
|
if res.isErr():
|
2025-09-05 21:31:01 +02:00
|
|
|
|
let msg = "error in sendRequestToFFIThread: " & res.error
|
2025-12-09 18:51:50 +01:00
|
|
|
|
`callback`(RET_ERR, unsafeAddr msg[0], cast[csize_t](msg.len), `userData`)
|
2025-09-05 21:31:01 +02:00
|
|
|
|
return RET_ERR
|
|
|
|
|
|
return RET_OK
|
2025-12-09 18:51:50 +01:00
|
|
|
|
|
2025-12-13 23:53:59 +01:00
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo blockExpr.repr
|
|
|
|
|
|
return blockExpr
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
macro ffiRaw*(prc: untyped): untyped =
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## Defines an FFI-exported proc that registers a request handler to be executed
|
|
|
|
|
|
## asynchronously in the FFI thread.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## This is the "raw" / legacy form of the macro where the developer writes
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## the ctx, callback, and userData parameters explicitly. Additional parameters
|
|
|
|
|
|
## travel as one CBOR blob.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## {.ffiRaw.} implicitly implies a Future[Result[string, string]] {.async.}
|
|
|
|
|
|
## return type.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## When using {.ffiRaw.}, the first three parameters must be:
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## - ctx: ptr FFIContext[T] <-- T is the type that handles the FFI requests
|
|
|
|
|
|
## - callback: FFICallBack
|
|
|
|
|
|
## - userData: pointer
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## 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.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
2025-12-13 23:53:59 +01:00
|
|
|
|
## e.g.:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## proc waku_version(
|
|
|
|
|
|
## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
|
|
|
|
|
|
## ) {.ffiRaw.} =
|
|
|
|
|
|
## return ok(WakuNodeVersionString)
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
let procName = prc[0]
|
|
|
|
|
|
let formalParams = prc[3]
|
|
|
|
|
|
let bodyNode = prc[^1]
|
|
|
|
|
|
|
|
|
|
|
|
if formalParams.len < 2:
|
2026-05-11 23:28:17 +02:00
|
|
|
|
error("`.ffiRaw.` procs require at least 1 parameter")
|
2025-12-09 18:51:50 +01:00
|
|
|
|
|
|
|
|
|
|
let firstParam = formalParams[1]
|
|
|
|
|
|
let paramIdent = firstParam[0]
|
|
|
|
|
|
let paramType = firstParam[1]
|
|
|
|
|
|
|
2026-05-13 00:02:23 +02:00
|
|
|
|
let libTypeName = paramType[0][1]
|
|
|
|
|
|
let poolIdent = ident($libTypeName & "FFIPool")
|
|
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
let reqName = ident($procName & "Req")
|
|
|
|
|
|
let returnType = ident("cint")
|
|
|
|
|
|
|
|
|
|
|
|
var newParams = newSeq[NimNode]()
|
|
|
|
|
|
newParams.add(returnType)
|
|
|
|
|
|
for i in 1 ..< formalParams.len:
|
|
|
|
|
|
newParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1]))
|
|
|
|
|
|
|
2025-12-10 17:43:26 +01:00
|
|
|
|
let futReturnType = quote:
|
|
|
|
|
|
Future[Result[string, string]]
|
|
|
|
|
|
|
|
|
|
|
|
var userParams = newSeq[NimNode]()
|
|
|
|
|
|
userParams.add(futReturnType)
|
|
|
|
|
|
if formalParams.len > 3:
|
|
|
|
|
|
for i in 4 ..< formalParams.len:
|
|
|
|
|
|
userParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1]))
|
|
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
var argsList = newSeq[NimNode]()
|
|
|
|
|
|
for i in 1 ..< formalParams.len:
|
|
|
|
|
|
argsList.add(formalParams[i][0])
|
|
|
|
|
|
|
|
|
|
|
|
let dotExpr = newTree(nnkDotExpr, reqName, ident"processReq")
|
|
|
|
|
|
|
|
|
|
|
|
let callNode = newTree(nnkCall, dotExpr)
|
|
|
|
|
|
for arg in argsList:
|
|
|
|
|
|
callNode.add(arg)
|
|
|
|
|
|
|
|
|
|
|
|
let ffiBody = newStmtList(
|
|
|
|
|
|
quote do:
|
|
|
|
|
|
initializeLibrary()
|
2026-05-13 00:02:23 +02:00
|
|
|
|
if not `poolIdent`.isValidCtx(cast[pointer](ctx)):
|
2026-05-11 19:21:40 -03:00
|
|
|
|
return RET_ERR
|
|
|
|
|
|
ctx[].userData = userData
|
2025-12-09 18:51:50 +01:00
|
|
|
|
if isNil(callback):
|
|
|
|
|
|
return RET_MISSING_CALLBACK
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add(callNode)
|
|
|
|
|
|
|
|
|
|
|
|
let ffiProc = newProc(
|
|
|
|
|
|
name = procName,
|
|
|
|
|
|
params = newParams,
|
|
|
|
|
|
body = ffiBody,
|
2025-12-11 17:11:59 +01:00
|
|
|
|
pragmas = newTree(nnkPragma, ident "dynlib", ident "exportc", ident "cdecl"),
|
2025-12-09 18:51:50 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-10 17:43:26 +01:00
|
|
|
|
var anonymousProcNode = newProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
name = newEmptyNode(),
|
2025-12-10 17:43:26 +01:00
|
|
|
|
params = userParams,
|
|
|
|
|
|
body = newStmtList(bodyNode),
|
|
|
|
|
|
pragmas = newTree(nnkPragma, ident"async"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-09 18:51:50 +01:00
|
|
|
|
let registerReq = quote:
|
|
|
|
|
|
registerReqFFI(`reqName`, `paramIdent`: `paramType`):
|
2025-12-10 17:43:26 +01:00
|
|
|
|
`anonymousProcNode`
|
2025-12-09 18:51:50 +01:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let stmts = newStmtList(registerReq, ffiProc)
|
2025-12-13 23:53:59 +01:00
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo stmts.repr
|
|
|
|
|
|
return stmts
|
|
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
macro ffi*(prc: untyped): untyped =
|
|
|
|
|
|
## Simplified FFI macro — applies to procs or types.
|
|
|
|
|
|
##
|
|
|
|
|
|
## On a type: `type Foo {.ffi.} = object` registers Foo for binding generation
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## and lets the generic cborEncode/cborDecode overloads handle serialization.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## On a proc: the annotated proc must have a first parameter of the library
|
|
|
|
|
|
## type, optionally additional Nim-typed parameters, and return
|
|
|
|
|
|
## Future[Result[RetType, string]]. It must NOT include ctx, callback, or
|
|
|
|
|
|
## 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.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## Example (type):
|
|
|
|
|
|
## type EchoRequest {.ffi.} = object
|
|
|
|
|
|
## message: string
|
|
|
|
|
|
## delayMs: int
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example (proc):
|
|
|
|
|
|
## proc mylib_send*(w: MyLib, cfg: SendConfig): Future[Result[string, string]] {.ffi.} =
|
|
|
|
|
|
## return ok("done")
|
|
|
|
|
|
|
|
|
|
|
|
if prc.kind == nnkTypeDef:
|
|
|
|
|
|
var cleanTypeDef = prc.copyNimTree()
|
|
|
|
|
|
if cleanTypeDef[0].kind == nnkPragmaExpr:
|
|
|
|
|
|
cleanTypeDef[0] = cleanTypeDef[0][0]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return registerFFITypeInfo(cleanTypeDef)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let procName = prc[0]
|
|
|
|
|
|
let formalParams = prc[3]
|
|
|
|
|
|
let bodyNode = prc[^1]
|
|
|
|
|
|
|
|
|
|
|
|
if formalParams.len < 2:
|
|
|
|
|
|
error("`.ffi.` procs require at least 1 parameter (the library type)")
|
|
|
|
|
|
|
|
|
|
|
|
let firstParam = formalParams[1]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let libParamName = firstParam[0]
|
|
|
|
|
|
let libTypeName = firstParam[1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let retTypeNode = formalParams[0]
|
|
|
|
|
|
if retTypeNode.kind == nnkEmpty:
|
|
|
|
|
|
error(
|
|
|
|
|
|
"`.ffi.` proc must have an explicit return type Future[Result[RetType, string]]"
|
|
|
|
|
|
)
|
|
|
|
|
|
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
|
|
|
|
|
error(
|
|
|
|
|
|
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
|
|
|
|
|
|
retTypeNode.repr
|
|
|
|
|
|
)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let resultInner = retTypeNode[1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
|
|
|
|
|
error(
|
|
|
|
|
|
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
|
|
|
|
|
|
retTypeNode.repr
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let resultRetType = resultInner[1]
|
|
|
|
|
|
rejectRawPtrType(resultRetType, "`.ffi.` proc " & $procName & " return type")
|
|
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
var extraParamNames: seq[string] = @[]
|
|
|
|
|
|
var extraParamTypes: seq[NimNode] = @[]
|
|
|
|
|
|
for i in 2 ..< formalParams.len:
|
|
|
|
|
|
let p = formalParams[i]
|
|
|
|
|
|
for j in 0 ..< p.len - 2:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
rejectRawPtrType(p[^2], "`.ffi.` proc " & $procName & " parameter " & $p[j])
|
2026-05-11 23:28:17 +02:00
|
|
|
|
extraParamNames.add($p[j])
|
|
|
|
|
|
extraParamTypes.add(p[^2])
|
|
|
|
|
|
|
|
|
|
|
|
let procNameStr = block:
|
|
|
|
|
|
let raw = $procName
|
|
|
|
|
|
if raw.endsWith("*"):
|
|
|
|
|
|
raw[0 ..^ 2]
|
|
|
|
|
|
else:
|
|
|
|
|
|
raw
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let cExportName = camelToSnakeCase(procNameStr)
|
|
|
|
|
|
let camelName = snakeToPascalCase(procNameStr)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let reqTypeName = ident(camelName & "Req")
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
var userProcName = procName
|
|
|
|
|
|
if procName.kind == nnkPostfix:
|
|
|
|
|
|
userProcName = procName[1]
|
|
|
|
|
|
## Both the user-facing Nim proc and the C-exported wrapper share the user's
|
|
|
|
|
|
## original name; their signatures differ so Nim resolves the call by
|
|
|
|
|
|
## overload. The C wrapper additionally carries `{.exportc.}` so the foreign
|
|
|
|
|
|
## ABI symbol is unchanged.
|
|
|
|
|
|
let cExportProcName = userProcName
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let ctxType =
|
|
|
|
|
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc buildAsyncHelperProc(): NimNode =
|
|
|
|
|
|
## proc <userProcName>*(lib: LibType, extras...): Future[Result[T, string]] {.async.} = <body>
|
2026-05-11 23:28:17 +02:00
|
|
|
|
var helperParams = newSeq[NimNode]()
|
|
|
|
|
|
helperParams.add(retTypeNode)
|
|
|
|
|
|
helperParams.add(newIdentDefs(libParamName, libTypeName))
|
|
|
|
|
|
for i in 0 ..< extraParamNames.len:
|
|
|
|
|
|
helperParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
|
2026-05-16 01:08:42 +02:00
|
|
|
|
newProc(
|
|
|
|
|
|
name = postfix(userProcName, "*"),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
params = helperParams,
|
|
|
|
|
|
body = newStmtList(bodyNode),
|
|
|
|
|
|
pragmas = newTree(nnkPragma, ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc asyncPath(): NimNode =
|
|
|
|
|
|
## Emits the C-exported wrapper and registers the request handler.
|
|
|
|
|
|
## All `.ffi.` procs dispatch through the FFI thread channel and reply
|
|
|
|
|
|
## through the callback when the future resolves — the previous "sync
|
|
|
|
|
|
## fast-path" that ran inline on the foreign caller thread was removed
|
|
|
|
|
|
## (PR #23 review, items 1–5) because it bypassed `foreignThreadGc`,
|
|
|
|
|
|
## `ctx.lock`, and chronos's single-thread invariant.
|
|
|
|
|
|
let helperProc = buildAsyncHelperProc()
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# registerReqFFI lambda: typed params, returns user's typed Result.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let ctxHandlerName = ident("ffiCtxHandler")
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let ptrFFICtx =
|
2026-05-11 23:28:17 +02:00
|
|
|
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
|
|
|
|
|
|
|
|
|
|
|
var lambdaParams = newSeq[NimNode]()
|
2026-05-16 01:08:42 +02:00
|
|
|
|
lambdaParams.add(retTypeNode) # Future[Result[RetType, string]]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
for i in 0 ..< extraParamNames.len:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
lambdaParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
|
|
|
|
|
|
let libValDeref = newTree(nnkDerefExpr, ctxMyLib)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let helperCall = newTree(nnkCall, userProcName, libValDeref)
|
|
|
|
|
|
for name in extraParamNames:
|
|
|
|
|
|
helperCall.add(ident(name))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let lambdaBody = newStmtList()
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let retValIdent = ident("retVal")
|
|
|
|
|
|
lambdaBody.add quote do:
|
|
|
|
|
|
let `retValIdent` = (await `helperCall`).valueOr:
|
|
|
|
|
|
return err($error)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return ok(`retValIdent`)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let lambdaNode = newProc(
|
|
|
|
|
|
name = newEmptyNode(),
|
|
|
|
|
|
params = lambdaParams,
|
|
|
|
|
|
body = lambdaBody,
|
|
|
|
|
|
pragmas = newTree(nnkPragma, ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let registerReq = quote:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
registerReqFFI(`reqTypeName`, `ctxHandlerName`: `ptrFFICtx`):
|
2026-05-11 23:28:17 +02:00
|
|
|
|
`lambdaNode`
|
|
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# C-exported wrapper: takes (ctx, callback, userData, reqCbor, reqCborLen)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
# -------------------------------------------------------------------------
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let exportedParams = cExportedParams(ctxType)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let ffiBody = newStmtList()
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
if callback.isNil:
|
|
|
|
|
|
return RET_MISSING_CALLBACK
|
|
|
|
|
|
|
2026-05-13 00:02:23 +02:00
|
|
|
|
let asyncPoolIdent = ident($libTypeName & "FFIPool")
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiBody.add quote do:
|
2026-05-13 00:02:23 +02:00
|
|
|
|
if not `asyncPoolIdent`.isValidCtx(cast[pointer](ctx)):
|
|
|
|
|
|
let errStr = "ctx is not a valid FFI context"
|
2026-05-11 23:28:17 +02:00
|
|
|
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
|
|
|
|
|
return RET_ERR
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# Build the FFIThreadRequest payload directly from the incoming bytes.
|
|
|
|
|
|
let reqPtrIdent = genSym(nskLet, "reqPtr")
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
let typeStr = $`reqTypeName`
|
|
|
|
|
|
let `reqPtrIdent` = FFIThreadRequest.initFromPtr(
|
|
|
|
|
|
callback, userData, typeStr.cstring, reqCbor, int(reqCborLen)
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let sendResIdent = genSym(nskLet, "sendRes")
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
let `sendResIdent` =
|
|
|
|
|
|
try:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
ffi_context.sendRequestToFFIThread(ctx, `reqPtrIdent`)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
|
|
|
|
|
if `sendResIdent`.isErr():
|
|
|
|
|
|
let errStr = "error in sendRequestToFFIThread: " & `sendResIdent`.error
|
|
|
|
|
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
|
|
|
|
|
return RET_ERR
|
|
|
|
|
|
return RET_OK
|
|
|
|
|
|
|
|
|
|
|
|
let ffiProc = newProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
name = postfix(cExportProcName, "*"),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
params = exportedParams,
|
|
|
|
|
|
body = ffiBody,
|
|
|
|
|
|
pragmas = newTree(
|
|
|
|
|
|
nnkPragma,
|
|
|
|
|
|
ident("dynlib"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)),
|
|
|
|
|
|
ident("cdecl"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
block:
|
|
|
|
|
|
var ffiExtraParams: seq[FFIParamMeta] = @[]
|
|
|
|
|
|
for i in 0 ..< extraParamNames.len:
|
|
|
|
|
|
let ptype = extraParamTypes[i]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let isPointer = isPtr(ptype)
|
|
|
|
|
|
let tn =
|
|
|
|
|
|
if isPointer:
|
|
|
|
|
|
nimTypeNameRepr(ptype[0])
|
|
|
|
|
|
else:
|
|
|
|
|
|
nimTypeNameRepr(ptype)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiExtraParams.add(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPointer)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let retTypeInner = resultInner[1]
|
|
|
|
|
|
let retIsPtr = isPtr(retTypeInner)
|
|
|
|
|
|
let retTn =
|
|
|
|
|
|
if retIsPtr:
|
|
|
|
|
|
nimTypeNameRepr(retTypeInner[0])
|
|
|
|
|
|
else:
|
|
|
|
|
|
nimTypeNameRepr(retTypeInner)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiProcRegistry.add(
|
|
|
|
|
|
FFIProcMeta(
|
|
|
|
|
|
procName: cExportName,
|
|
|
|
|
|
libName: currentLibName,
|
2026-05-16 01:08:42 +02:00
|
|
|
|
kind: FFIKind.FFI,
|
2026-05-11 23:28:17 +02:00
|
|
|
|
libTypeName: $libTypeName,
|
|
|
|
|
|
extraParams: ffiExtraParams,
|
|
|
|
|
|
returnTypeName: retTn,
|
|
|
|
|
|
returnIsPtr: retIsPtr,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return newStmtList(helperProc, registerReq, ffiProc)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let stmts = asyncPath()
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo stmts.repr
|
|
|
|
|
|
return stmts
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc buildCtorRequestType(
|
|
|
|
|
|
reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode]
|
|
|
|
|
|
): NimNode =
|
|
|
|
|
|
## Builds the ctor's Req object using the user's actual Nim types.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
var fields: seq[NimNode] = @[]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
for i in 0 ..< paramNames.len:
|
|
|
|
|
|
let fieldName = ident(paramNames[i])
|
|
|
|
|
|
let storedType = storageType(paramTypes[i])
|
|
|
|
|
|
fields.add newTree(nnkIdentDefs, fieldName, storedType, newEmptyNode())
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let recList =
|
|
|
|
|
|
if fields.len > 0:
|
|
|
|
|
|
newTree(nnkRecList, fields)
|
|
|
|
|
|
else:
|
|
|
|
|
|
newTree(
|
|
|
|
|
|
nnkRecList,
|
2026-05-16 01:08:42 +02:00
|
|
|
|
newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
|
|
|
|
|
let typeName = postfix(reqTypeName, "*")
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let typeSection =
|
2026-05-11 23:28:17 +02:00
|
|
|
|
newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo typeSection.repr
|
|
|
|
|
|
return typeSection
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
proc buildCtorFFINewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimNode =
|
|
|
|
|
|
## Wraps a CBOR byte buffer into an FFIThreadRequest for the ctor request type.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
var formalParams = newSeq[NimNode]()
|
|
|
|
|
|
|
|
|
|
|
|
let typedescParam =
|
|
|
|
|
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
|
|
|
|
|
formalParams.add(typedescParam)
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
2026-05-16 01:08:42 +02:00
|
|
|
|
formalParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte"))))
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t")))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let retType = newTree(nnkPtrTy, ident("FFIThreadRequest"))
|
|
|
|
|
|
formalParams = @[retType] & formalParams
|
|
|
|
|
|
|
|
|
|
|
|
var newBody = newStmtList()
|
|
|
|
|
|
newBody.add quote do:
|
|
|
|
|
|
let typeStr = $T
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return FFIThreadRequest.initFromPtr(
|
|
|
|
|
|
callback, userData, typeStr.cstring, reqCbor, int(reqCborLen)
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let newReqProc = newProc(
|
2026-05-11 23:28:17 +02:00
|
|
|
|
name = postfix(ident("ffiNewReq"), "*"),
|
|
|
|
|
|
params = formalParams,
|
|
|
|
|
|
body = newBody,
|
|
|
|
|
|
pragmas = newEmptyNode(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo newReqProc.repr
|
|
|
|
|
|
return newReqProc
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
proc buildCtorBodyProc(
|
|
|
|
|
|
helperName: NimNode,
|
|
|
|
|
|
paramNames: seq[string],
|
|
|
|
|
|
paramTypes: seq[NimNode],
|
|
|
|
|
|
libTypeName: NimNode,
|
|
|
|
|
|
userBody: NimNode,
|
|
|
|
|
|
): NimNode =
|
|
|
|
|
|
let innerRetType = nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Future"),
|
|
|
|
|
|
nnkBracketExpr.newTree(ident("Result"), libTypeName, ident("string")),
|
|
|
|
|
|
)
|
|
|
|
|
|
var innerParams = newSeq[NimNode]()
|
|
|
|
|
|
innerParams.add(innerRetType)
|
|
|
|
|
|
for i in 0 ..< paramNames.len:
|
|
|
|
|
|
innerParams.add(newIdentDefs(ident(paramNames[i]), paramTypes[i]))
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let bodyProc = newProc(
|
2026-05-11 23:28:17 +02:00
|
|
|
|
name = postfix(helperName, "*"),
|
|
|
|
|
|
params = innerParams,
|
|
|
|
|
|
body = newStmtList(userBody),
|
|
|
|
|
|
pragmas = newTree(nnkPragma, ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo bodyProc.repr
|
|
|
|
|
|
return bodyProc
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
proc buildCtorProcessFFIRequestProc(
|
|
|
|
|
|
reqTypeName: NimNode,
|
|
|
|
|
|
helperName: NimNode,
|
|
|
|
|
|
paramNames: seq[string],
|
|
|
|
|
|
paramTypes: seq[NimNode],
|
|
|
|
|
|
libTypeName: NimNode,
|
|
|
|
|
|
): NimNode =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Decodes the CBOR payload, unpacks fields, runs the user body, and stores
|
|
|
|
|
|
## the resulting library value in ctx.myLib.
|
|
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let returnType = nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Future"),
|
|
|
|
|
|
nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let ctxType =
|
|
|
|
|
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
|
|
|
|
|
|
|
|
|
|
|
let typedescParam =
|
|
|
|
|
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
|
|
|
|
|
|
|
|
|
|
|
var formalParams: seq[NimNode] = @[]
|
|
|
|
|
|
formalParams.add(returnType)
|
|
|
|
|
|
formalParams.add(typedescParam)
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("request"), ident("pointer")))
|
|
|
|
|
|
formalParams.add(newIdentDefs(ident("ctx"), ctxType))
|
|
|
|
|
|
|
|
|
|
|
|
let newBody = newStmtList()
|
|
|
|
|
|
let reqIdent = ident("req")
|
|
|
|
|
|
let ctxIdent = ident("ctx")
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let decodedIdent = ident("decoded")
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
newBody.add quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let `reqIdent` = cast[ptr FFIThreadRequest](request)
|
|
|
|
|
|
let `decodedIdent` = cborDecodePtr(
|
|
|
|
|
|
cast[ptr UncheckedArray[byte]](`reqIdent`[].data),
|
|
|
|
|
|
`reqIdent`[].dataLen,
|
|
|
|
|
|
`reqTypeName`,
|
|
|
|
|
|
).valueOr:
|
|
|
|
|
|
return err("CBOR decode failed for " & $T & ": " & $error)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
for i in 0 ..< paramNames.len:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
newBody.add unpackReqField(ident(paramNames[i]), paramTypes[i], decodedIdent)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let helperCallNode = newTree(nnkCall, helperName)
|
|
|
|
|
|
for name in paramNames:
|
|
|
|
|
|
helperCallNode.add(ident(name))
|
|
|
|
|
|
|
|
|
|
|
|
let libValIdent = ident("libVal")
|
|
|
|
|
|
newBody.add quote do:
|
|
|
|
|
|
let `libValIdent` = (await `helperCallNode`).valueOr:
|
|
|
|
|
|
return err($error)
|
|
|
|
|
|
|
|
|
|
|
|
let myLibIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("myLib"))
|
|
|
|
|
|
newBody.add quote do:
|
|
|
|
|
|
`myLibIdent` = createShared(`libTypeName`)
|
|
|
|
|
|
`myLibIdent`[] = `libValIdent`
|
|
|
|
|
|
|
|
|
|
|
|
newBody.add quote do:
|
|
|
|
|
|
return ok($cast[uint](`ctxIdent`))
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let processProc = newProc(
|
2026-05-11 23:28:17 +02:00
|
|
|
|
name = postfix(ident("processFFIRequest"), "*"),
|
|
|
|
|
|
params = formalParams,
|
|
|
|
|
|
body = newBody,
|
|
|
|
|
|
procType = nnkProcDef,
|
|
|
|
|
|
pragmas = newTree(nnkPragma, ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo processProc.repr
|
|
|
|
|
|
return processProc
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Wraps the ctor processFFIRequest result in a seq[byte] dispatcher.
|
|
|
|
|
|
## The ctor uniquely returns the ctx address as a decimal string; we wrap
|
|
|
|
|
|
## it as raw UTF-8 bytes so the foreign side can read it back uniformly.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let ctxType =
|
|
|
|
|
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
|
|
|
|
|
|
|
|
|
|
|
let returnType = nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Future"),
|
2026-05-16 01:08:42 +02:00
|
|
|
|
nnkBracketExpr.newTree(
|
|
|
|
|
|
ident("Result"),
|
|
|
|
|
|
nnkBracketExpr.newTree(ident("seq"), ident("byte")),
|
|
|
|
|
|
ident("string"),
|
|
|
|
|
|
),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let callExpr = newCall(
|
|
|
|
|
|
newDotExpr(reqTypeName, ident("processFFIRequest")),
|
|
|
|
|
|
ident("request"),
|
|
|
|
|
|
newTree(nnkCast, ctxType, ident("reqHandler")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let resIdent = genSym(nskLet, "ctorRes")
|
2026-05-11 23:28:17 +02:00
|
|
|
|
var newBody = newStmtList()
|
|
|
|
|
|
newBody.add quote do:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let `resIdent` = await `callExpr`
|
|
|
|
|
|
if `resIdent`.isErr:
|
|
|
|
|
|
return err(`resIdent`.error)
|
|
|
|
|
|
# The ctor returns the ctx address as a decimal string; encode it as CBOR text
|
|
|
|
|
|
# for uniform decoding on the foreign side.
|
|
|
|
|
|
return ok(cborEncode(`resIdent`.value))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let asyncProc = newProc(
|
|
|
|
|
|
name = newEmptyNode(),
|
|
|
|
|
|
params = @[
|
|
|
|
|
|
returnType,
|
|
|
|
|
|
newIdentDefs(ident("request"), ident("pointer")),
|
|
|
|
|
|
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
|
|
|
|
|
],
|
|
|
|
|
|
body = newBody,
|
|
|
|
|
|
pragmas = nnkPragma.newTree(ident("async")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let key = newLit($reqTypeName)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let regAssign =
|
2026-05-11 23:28:17 +02:00
|
|
|
|
newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo regAssign.repr
|
|
|
|
|
|
return regAssign
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
macro ffiCtor*(prc: untyped): untyped =
|
|
|
|
|
|
## Defines a C-exported constructor that creates an FFIContext and populates
|
|
|
|
|
|
## ctx.myLib asynchronously in the FFI thread.
|
|
|
|
|
|
##
|
|
|
|
|
|
## The annotated proc must:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## - Have Nim-typed parameters (carried over the wire as a single CBOR blob)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
## - Return Future[Result[LibType, string]]
|
|
|
|
|
|
## - NOT include ctx, callback, or userData in its signature
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example:
|
|
|
|
|
|
## proc mylib_create*(config: SimpleConfig): Future[Result[SimpleLib, string]] {.ffiCtor.} =
|
|
|
|
|
|
## return ok(SimpleLib(value: config.initialValue))
|
|
|
|
|
|
##
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## The generated C-exported proc has the signature:
|
|
|
|
|
|
## proc mylib_create(reqCbor: ptr byte, reqCborLen: csize_t,
|
|
|
|
|
|
## callback: FFICallBack, userData: pointer): pointer
|
|
|
|
|
|
## {.exportc, cdecl, raises: [].}
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Returns the context pointer synchronously, NULL on failure. The callback
|
|
|
|
|
|
## also fires when async initialization completes, passing the ctx address as
|
|
|
|
|
|
## a decimal string on success. The caller should hold the returned pointer
|
|
|
|
|
|
## and pass it to subsequent .ffi. calls.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let procName = prc[0]
|
|
|
|
|
|
let formalParams = prc[3]
|
|
|
|
|
|
let bodyNode = prc[^1]
|
|
|
|
|
|
|
|
|
|
|
|
let retTypeNode = formalParams[0]
|
|
|
|
|
|
if retTypeNode.kind == nnkEmpty:
|
|
|
|
|
|
error(
|
|
|
|
|
|
"ffiCtor: proc must have an explicit return type Future[Result[LibType, string]]"
|
|
|
|
|
|
)
|
|
|
|
|
|
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
|
|
|
|
|
error(
|
|
|
|
|
|
"ffiCtor: return type must be Future[Result[LibType, string]], got: " &
|
|
|
|
|
|
retTypeNode.repr
|
|
|
|
|
|
)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let resultInner = retTypeNode[1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
|
|
|
|
|
error(
|
|
|
|
|
|
"ffiCtor: return type must be Future[Result[LibType, string]], got: " &
|
|
|
|
|
|
retTypeNode.repr
|
|
|
|
|
|
)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let libTypeName = resultInner[1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
var paramNames: seq[string] = @[]
|
|
|
|
|
|
var paramTypes: seq[NimNode] = @[]
|
|
|
|
|
|
for i in 1 ..< formalParams.len:
|
|
|
|
|
|
let p = formalParams[i]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
for j in 0 ..< p.len - 2:
|
|
|
|
|
|
rejectRawPtrType(p[^2], "`.ffiCtor.` proc " & $procName & " parameter " & $p[j])
|
2026-05-11 23:28:17 +02:00
|
|
|
|
paramNames.add($p[j])
|
|
|
|
|
|
paramTypes.add(p[^2])
|
|
|
|
|
|
|
|
|
|
|
|
let procNameStr = $procName
|
|
|
|
|
|
let cleanName =
|
|
|
|
|
|
if procNameStr.endsWith("*"):
|
|
|
|
|
|
procNameStr[0 ..^ 2]
|
|
|
|
|
|
else:
|
|
|
|
|
|
procNameStr
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let cExportName = camelToSnakeCase(cleanName)
|
|
|
|
|
|
let reqTypeNameStr = snakeToPascalCase(cleanName) & "CtorReq"
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let reqTypeName = ident(reqTypeNameStr)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let typeDef = buildCtorRequestType(reqTypeName, paramNames, paramTypes)
|
|
|
|
|
|
let ffiNewReqProc = buildCtorFFINewReqProc(reqTypeName, paramNames)
|
|
|
|
|
|
# The user-facing Nim proc keeps the user's original name with their declared
|
|
|
|
|
|
# signature; the C-exported wrapper moves to `<userProcName>ExportC` and
|
|
|
|
|
|
# binds the snake_case C symbol via `{.exportc.}`.
|
|
|
|
|
|
var userProcName = procName
|
|
|
|
|
|
if procName.kind == nnkPostfix:
|
|
|
|
|
|
userProcName = procName[1]
|
|
|
|
|
|
# Both the Nim-facing async ctor and the C-exported wrapper share the user's
|
|
|
|
|
|
# name as overloads; the C wrapper's `{.exportc.}` keeps the ABI symbol.
|
|
|
|
|
|
let cExportProcName = userProcName
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let helperProc =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
buildCtorBodyProc(userProcName, paramNames, paramTypes, libTypeName, bodyNode)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let processProc = buildCtorProcessFFIRequestProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
reqTypeName, userProcName, paramNames, paramTypes, libTypeName
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
|
|
|
|
|
let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# C-exported proc: (reqCbor, reqCborLen, callback, userData) -> pointer
|
2026-05-11 23:28:17 +02:00
|
|
|
|
var exportedParams = newSeq[NimNode]()
|
2026-05-16 01:08:42 +02:00
|
|
|
|
exportedParams.add(ident("pointer"))
|
|
|
|
|
|
exportedParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte"))))
|
|
|
|
|
|
exportedParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t")))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
|
|
|
|
|
exportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
|
|
|
|
|
|
|
|
|
|
|
let ffiBody = newStmtList()
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
when declared(initializeLibrary):
|
|
|
|
|
|
initializeLibrary()
|
|
|
|
|
|
|
|
|
|
|
|
let ctxSym = genSym(nskLet, "ctx")
|
2026-05-13 00:02:23 +02:00
|
|
|
|
let poolIdent = ident($libTypeName & "FFIPool")
|
|
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiBody.add quote do:
|
2026-05-13 00:02:23 +02:00
|
|
|
|
let `ctxSym` = `poolIdent`.createFFIContext().valueOr:
|
2026-05-11 23:28:17 +02:00
|
|
|
|
if not callback.isNil:
|
|
|
|
|
|
let errStr = "ffiCtor: failed to create FFIContext: " & $error
|
|
|
|
|
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
# Early validation: decode the CBOR payload to verify it parses cleanly.
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
block:
|
|
|
|
|
|
let validateRes = cborDecodePtr(
|
|
|
|
|
|
cast[ptr UncheckedArray[byte]](reqCbor), int(reqCborLen), `reqTypeName`
|
|
|
|
|
|
)
|
|
|
|
|
|
if validateRes.isErr():
|
|
|
|
|
|
if not callback.isNil:
|
|
|
|
|
|
let errStr = "ffiCtor: failed to decode request: " & $validateRes.error
|
|
|
|
|
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
|
|
let newReqCall = newCall(
|
|
|
|
|
|
ident("ffiNewReq"),
|
|
|
|
|
|
reqTypeName,
|
|
|
|
|
|
ident("callback"),
|
|
|
|
|
|
ident("userData"),
|
|
|
|
|
|
ident("reqCbor"),
|
|
|
|
|
|
ident("reqCborLen"),
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let sendCall =
|
|
|
|
|
|
newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall)
|
|
|
|
|
|
|
|
|
|
|
|
let sendResIdent = genSym(nskLet, "sendRes")
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
let `sendResIdent` =
|
|
|
|
|
|
try:
|
|
|
|
|
|
`sendCall`
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
|
|
|
|
|
if `sendResIdent`.isErr():
|
|
|
|
|
|
if not callback.isNil:
|
|
|
|
|
|
let errStr = "ffiCtor: failed to send request: " & $`sendResIdent`.error
|
|
|
|
|
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
return cast[pointer](`ctxSym`)
|
|
|
|
|
|
|
|
|
|
|
|
let ffiProc = newProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
name = postfix(cExportProcName, "*"),
|
2026-05-11 23:28:17 +02:00
|
|
|
|
params = exportedParams,
|
|
|
|
|
|
body = ffiBody,
|
|
|
|
|
|
pragmas = newTree(
|
|
|
|
|
|
nnkPragma,
|
|
|
|
|
|
ident("dynlib"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)),
|
|
|
|
|
|
ident("cdecl"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
block:
|
|
|
|
|
|
var ctorExtraParams: seq[FFIParamMeta] = @[]
|
|
|
|
|
|
for i in 0 ..< paramNames.len:
|
|
|
|
|
|
let ptype = paramTypes[i]
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let isPointer = isPtr(ptype)
|
|
|
|
|
|
let tn =
|
|
|
|
|
|
if isPointer:
|
|
|
|
|
|
nimTypeNameRepr(ptype[0])
|
|
|
|
|
|
else:
|
|
|
|
|
|
nimTypeNameRepr(ptype)
|
|
|
|
|
|
ctorExtraParams.add(
|
|
|
|
|
|
FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPointer)
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiProcRegistry.add(
|
|
|
|
|
|
FFIProcMeta(
|
|
|
|
|
|
procName: cExportName,
|
|
|
|
|
|
libName: currentLibName,
|
2026-05-16 01:08:42 +02:00
|
|
|
|
kind: FFIKind.CTOR,
|
2026-05-11 23:28:17 +02:00
|
|
|
|
libTypeName: $libTypeName,
|
|
|
|
|
|
extraParams: ctorExtraParams,
|
|
|
|
|
|
returnTypeName: $libTypeName,
|
|
|
|
|
|
returnIsPtr: false,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let poolDecl = quote:
|
2026-05-13 00:02:23 +02:00
|
|
|
|
when not declared(`poolIdent`):
|
|
|
|
|
|
var `poolIdent`: FFIContextPool[`libTypeName`]
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let stmts = newStmtList(
|
|
|
|
|
|
typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo stmts.repr
|
|
|
|
|
|
return stmts
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
macro ffiDtor*(prc: untyped): untyped =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Defines a C-exported destructor that tears down the FFIContext after the
|
|
|
|
|
|
## body runs.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## The annotated proc must have exactly one parameter of the library type.
|
|
|
|
|
|
## The body contains any library-level cleanup to run before context teardown.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example:
|
|
|
|
|
|
## proc waku_destroy*(w: Waku) {.ffiDtor.} =
|
|
|
|
|
|
## w.cleanup()
|
|
|
|
|
|
##
|
|
|
|
|
|
## The generated C-exported proc has the signature:
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## int waku_destroy(void* ctx)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## It extracts the library value from ctx, runs the body, then calls
|
|
|
|
|
|
## destroyFFIContext to tear down the FFI thread and free the context.
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## Returns RET_OK on success, RET_ERR on failure (null/invalid ctx, or
|
|
|
|
|
|
## destroyFFIContext failure).
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let procName = prc[0]
|
|
|
|
|
|
let formalParams = prc[3]
|
|
|
|
|
|
let bodyNode = prc[^1]
|
|
|
|
|
|
|
|
|
|
|
|
if formalParams.len < 2:
|
|
|
|
|
|
error("ffiDtor: proc must have exactly one parameter (w: LibType)")
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let libParamName = formalParams[1][0]
|
|
|
|
|
|
let libTypeName = formalParams[1][1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let procNameStr = block:
|
|
|
|
|
|
let raw = $procName
|
2026-05-16 01:08:42 +02:00
|
|
|
|
if raw.endsWith("*"):
|
|
|
|
|
|
raw[0 ..^ 2]
|
|
|
|
|
|
else:
|
|
|
|
|
|
raw
|
|
|
|
|
|
let cExportName = camelToSnakeCase(procNameStr)
|
|
|
|
|
|
# The dtor only needs a C-exported wrapper; rename to a synthetic Nim ident
|
|
|
|
|
|
# so it doesn't shadow the user's chosen name (consistent with .ffi. / .ffiCtor.).
|
|
|
|
|
|
# The dtor only generates a C-exported wrapper; it uses the user's name
|
|
|
|
|
|
# directly (no overload needed — there's no Nim-facing helper here).
|
|
|
|
|
|
var cExportProcName = procName
|
|
|
|
|
|
if procName.kind == nnkPostfix:
|
|
|
|
|
|
cExportProcName = procName[1]
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
let destroyResIdent = genSym(nskLet, "destroyRes")
|
|
|
|
|
|
|
|
|
|
|
|
let ffiBody = newStmtList()
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
when declared(initializeLibrary):
|
|
|
|
|
|
initializeLibrary()
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
if ctx.isNil or cast[ptr FFIContext[`libTypeName`]](ctx)[].myLib.isNil:
|
|
|
|
|
|
return RET_ERR
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
let `libParamName` = cast[ptr FFIContext[`libTypeName`]](ctx)[].myLib[]
|
|
|
|
|
|
|
|
|
|
|
|
let isNoop =
|
2026-05-16 01:08:42 +02:00
|
|
|
|
bodyNode.kind == nnkEmpty or (
|
|
|
|
|
|
bodyNode.kind == nnkStmtList and bodyNode.len == 1 and
|
|
|
|
|
|
bodyNode[0].kind == nnkDiscardStmt
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
if not isNoop:
|
|
|
|
|
|
ffiBody.add(bodyNode)
|
|
|
|
|
|
|
2026-05-13 00:02:23 +02:00
|
|
|
|
let poolIdent = ident($libTypeName & "FFIPool")
|
2026-05-11 23:28:17 +02:00
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
let `destroyResIdent` =
|
2026-05-13 00:02:23 +02:00
|
|
|
|
`poolIdent`.destroyFFIContext(cast[ptr FFIContext[`libTypeName`]](ctx))
|
2026-05-11 23:28:17 +02:00
|
|
|
|
if `destroyResIdent`.isErr():
|
|
|
|
|
|
return RET_ERR
|
|
|
|
|
|
|
|
|
|
|
|
ffiBody.add quote do:
|
|
|
|
|
|
return RET_OK
|
|
|
|
|
|
|
|
|
|
|
|
let ffiProc = newProc(
|
2026-05-16 01:08:42 +02:00
|
|
|
|
name = postfix(cExportProcName, "*"),
|
|
|
|
|
|
params = @[ident("cint"), newIdentDefs(ident("ctx"), ident("pointer"))],
|
2026-05-11 23:28:17 +02:00
|
|
|
|
body = ffiBody,
|
|
|
|
|
|
pragmas = newTree(
|
|
|
|
|
|
nnkPragma,
|
|
|
|
|
|
ident("dynlib"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)),
|
|
|
|
|
|
ident("cdecl"),
|
|
|
|
|
|
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ffiProcRegistry.add(
|
|
|
|
|
|
FFIProcMeta(
|
|
|
|
|
|
procName: cExportName,
|
|
|
|
|
|
libName: currentLibName,
|
2026-05-16 01:08:42 +02:00
|
|
|
|
kind: FFIKind.DTOR,
|
2026-05-11 23:28:17 +02:00
|
|
|
|
libTypeName: $libTypeName,
|
|
|
|
|
|
extraParams: @[],
|
|
|
|
|
|
returnTypeName: "",
|
|
|
|
|
|
returnIsPtr: false,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let poolDecl = quote:
|
2026-05-13 00:02:23 +02:00
|
|
|
|
when not declared(`poolIdent`):
|
|
|
|
|
|
var `poolIdent`: FFIContextPool[`libTypeName`]
|
|
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let stmts = newStmtList(poolDecl, ffiProc)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
2026-05-16 01:08:42 +02:00
|
|
|
|
echo stmts.repr
|
|
|
|
|
|
return stmts
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-25 15:51:56 +02:00
|
|
|
|
macro ffiEvent*(wireName: static[string], prc: 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.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example:
|
|
|
|
|
|
## type PeerInfo {.ffi.} = object
|
|
|
|
|
|
## id: string
|
|
|
|
|
|
## address: string
|
|
|
|
|
|
##
|
|
|
|
|
|
## proc onPeerConnected*(peer: PeerInfo) {.ffiEvent: "on_peer_connected".}
|
|
|
|
|
|
##
|
|
|
|
|
|
## # ... then from inside any {.ffi.} handler:
|
|
|
|
|
|
## onPeerConnected(PeerInfo(id: "p-1", address: "127.0.0.1"))
|
|
|
|
|
|
##
|
|
|
|
|
|
## Restriction (first pass): exactly one parameter. Multi-param events
|
|
|
|
|
|
## need a synthesised envelope struct; planned for a follow-up.
|
|
|
|
|
|
|
|
|
|
|
|
if prc.kind notin {nnkProcDef, nnkFuncDef}:
|
|
|
|
|
|
error("ffiEvent must be applied to a proc declaration")
|
|
|
|
|
|
|
|
|
|
|
|
let procName = prc[0]
|
|
|
|
|
|
let formalParams = prc[3]
|
|
|
|
|
|
|
|
|
|
|
|
if formalParams.len != 2:
|
|
|
|
|
|
error(
|
|
|
|
|
|
"ffiEvent (first pass) supports exactly one parameter; got " &
|
|
|
|
|
|
$(formalParams.len - 1)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let paramDef = formalParams[1]
|
|
|
|
|
|
let payloadParamName = paramDef[0]
|
|
|
|
|
|
let payloadTypeNode = paramDef[1]
|
|
|
|
|
|
|
|
|
|
|
|
let payloadTypeNameStr =
|
|
|
|
|
|
case payloadTypeNode.kind
|
2026-06-08 08:21:17 -03:00
|
|
|
|
of nnkIdent:
|
|
|
|
|
|
$payloadTypeNode
|
|
|
|
|
|
else:
|
|
|
|
|
|
payloadTypeNode.repr
|
2026-05-25 15:51:56 +02:00
|
|
|
|
|
|
|
|
|
|
var userProcName = procName
|
|
|
|
|
|
if procName.kind == nnkPostfix:
|
|
|
|
|
|
userProcName = procName[1]
|
|
|
|
|
|
|
|
|
|
|
|
# The generated body: dispatchFFIEventCbor("wire_name", payload).
|
|
|
|
|
|
let wireNameLit = newStrLitNode(wireName)
|
2026-06-08 08:21:17 -03:00
|
|
|
|
let dispatchBody =
|
|
|
|
|
|
newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, payloadParamName))
|
2026-05-25 15:51:56 +02:00
|
|
|
|
|
|
|
|
|
|
var newParams = newSeq[NimNode]()
|
|
|
|
|
|
newParams.add(formalParams[0]) # return type (typically empty/void)
|
|
|
|
|
|
newParams.add(paramDef)
|
|
|
|
|
|
|
|
|
|
|
|
let pragmas =
|
|
|
|
|
|
if prc.len >= 5 and prc[4].kind != nnkEmpty:
|
|
|
|
|
|
prc[4]
|
|
|
|
|
|
else:
|
|
|
|
|
|
newEmptyNode()
|
|
|
|
|
|
|
|
|
|
|
|
let generated = newProc(
|
|
|
|
|
|
name = procName,
|
|
|
|
|
|
params = newParams,
|
|
|
|
|
|
body = dispatchBody,
|
|
|
|
|
|
procType = prc.kind,
|
|
|
|
|
|
pragmas = pragmas,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ffiEventRegistry.add(
|
|
|
|
|
|
FFIEventMeta(
|
|
|
|
|
|
wireName: wireName,
|
|
|
|
|
|
nimProcName: $userProcName,
|
|
|
|
|
|
libName: currentLibName,
|
|
|
|
|
|
payloadTypeName: payloadTypeNameStr,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
when defined(ffiDumpMacros):
|
|
|
|
|
|
echo generated.repr
|
|
|
|
|
|
return generated
|
|
|
|
|
|
|
2026-05-11 23:28:17 +02:00
|
|
|
|
macro genBindings*(
|
2026-05-18 20:00:57 +02:00
|
|
|
|
outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiSrcPath
|
2026-05-11 23:28:17 +02:00
|
|
|
|
): untyped =
|
|
|
|
|
|
## Emits C++ or Rust binding files from the compile-time FFI registries.
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## The foreign-side wrapper encodes one CBOR buffer per request.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## PLACEMENT REQUIREMENT: genBindings() must be called AFTER every {.ffi.},
|
|
|
|
|
|
## {.ffiCtor.} and {.ffiDtor.} annotation in the compilation unit. Each
|
|
|
|
|
|
## pragma populates ffiProcRegistry / ffiTypeRegistry as the compiler
|
|
|
|
|
|
## expands the AST; calling genBindings() earlier produces incomplete
|
|
|
|
|
|
## bindings.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
##
|
|
|
|
|
|
## In a single-file library, place it at the bottom of the file.
|
|
|
|
|
|
## In a multi-file library, import all sub-modules first and call
|
|
|
|
|
|
## genBindings() once at the bottom of the top-level compilation-root file.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Supported languages (-d:targetLang): "rust" (default), "cpp".
|
|
|
|
|
|
## Output path and nim source path default to -d:ffiOutputDir and
|
2026-05-18 20:00:57 +02:00
|
|
|
|
## -d:ffiSrcPath, or can be passed as explicit arguments.
|
2026-05-11 23:28:17 +02:00
|
|
|
|
## This macro is a no-op unless -d:ffiGenBindings is set.
|
|
|
|
|
|
##
|
|
|
|
|
|
## Example (all via compile flags):
|
|
|
|
|
|
## genBindings()
|
|
|
|
|
|
## # nim c -d:ffiGenBindings -d:targetLang=rust \
|
2026-05-16 01:08:42 +02:00
|
|
|
|
## # -d:ffiOutputDir=examples/timer/rust_bindings \
|
2026-05-18 20:00:57 +02:00
|
|
|
|
## # -d:ffiSrcPath=../timer.nim mylib.nim
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
|
|
|
|
|
when defined(ffiGenBindings):
|
|
|
|
|
|
if outputDir.len == 0:
|
|
|
|
|
|
error(
|
|
|
|
|
|
"genBindings: output directory is empty." &
|
2026-05-16 01:08:42 +02:00
|
|
|
|
" Pass it as an argument or set -d:ffiOutputDir=path/to/output"
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
2026-05-16 01:08:42 +02:00
|
|
|
|
let lang = string_helpers.toLower(targetLang)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
let libName = deriveLibName(ffiProcRegistry)
|
|
|
|
|
|
case lang
|
|
|
|
|
|
of "rust":
|
|
|
|
|
|
generateRustCrate(
|
2026-05-25 15:51:56 +02:00
|
|
|
|
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
|
|
|
|
|
|
ffiEventRegistry,
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
|
|
|
|
|
of "cpp", "c++":
|
|
|
|
|
|
generateCppBindings(
|
2026-05-25 15:51:56 +02:00
|
|
|
|
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
|
|
|
|
|
|
ffiEventRegistry,
|
2026-05-11 23:28:17 +02:00
|
|
|
|
)
|
2026-05-18 20:00:57 +02:00
|
|
|
|
of "cddl":
|
|
|
|
|
|
generateCddlBindings(
|
|
|
|
|
|
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
else:
|
2026-05-18 20:00:57 +02:00
|
|
|
|
error(
|
|
|
|
|
|
"genBindings: unknown targetLang '" & lang & "'. Use 'rust', 'cpp', or 'cddl'."
|
|
|
|
|
|
)
|
2026-05-11 23:28:17 +02:00
|
|
|
|
|
2026-05-16 01:08:42 +02:00
|
|
|
|
return newEmptyNode()
|