allow simpler ffi usage

This commit is contained in:
Ivan FB 2026-05-06 11:56:10 +02:00
parent 233d6d726b
commit 863b15d2dc
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
4 changed files with 128 additions and 60 deletions

View File

@ -46,6 +46,26 @@ template callEventCallback*(ctx: ptr FFIContext, eventName: string, body: untype
RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData
)
template dispatchFfiEvent*(w: typed, eventName: string, body: untyped) =
## Fires an FFI event via the library object's event callback fields.
## Works with any type that has ffiEventCallback and ffiEventUserData fields.
if w.ffiEventCallback.isNil():
chronicles.error eventName & " - ffiEventCallback is nil"
return
foreignThreadGc:
try:
let event = body
cast[FFICallBack](w.ffiEventCallback)(
RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), w.ffiEventUserData
)
except Exception, CatchableError:
let msg =
"Exception " & eventName & " when calling 'ffiEventCallback': " &
getCurrentExceptionMsg()
cast[FFICallBack](w.ffiEventCallback)(
RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), w.ffiEventUserData
)
proc sendRequestToFFIThread*(
ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest, timeout = InfiniteDuration
): Result[void, string] =

View File

@ -1,7 +1,7 @@
import std/[macros, atomics], strformat, chronicles, chronos
import ../codegen/meta
macro declareLibrary*(libraryName: static[string]): untyped =
macro declareLibraryBase(libraryName: static[string]): untyped =
# Record the library name for binding generation
currentLibName = libraryName
@ -90,3 +90,56 @@ macro declareLibrary*(libraryName: static[string]): untyped =
res.add(initializeLibraryProc)
return res
macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
## Declares a library with the given name and automatically generates
## `{libraryName}_set_event_callback`, a C-exported function that lets callers
## register an event callback on both the FFIContext and the library object.
##
## `libType` is the Nim type of the main library object (e.g. `Waku`). It is used
## to type the `ctx: ptr FFIContext[libType]` parameter of the generated
## `{libraryName}_set_event_callback` proc, and to conditionally propagate the
## callback to `ctx.myLib[].ffiEventCallback` when that field exists on `libType`.
result = newStmtList()
# Emit the base bootstrap (pragmas, linker flags, NimMain, initializeLibrary)
result.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName)))
let funcName = libraryName & "_set_event_callback"
let funcIdent = ident(funcName)
let errorMsg = "error: invalid context in " & funcName
let ctxType = nnkPtrTy.newTree(
nnkBracketExpr.newTree(ident("FFIContext"), libType)
)
let procBody = quote do:
if isNil(ctx):
echo `errorMsg`
return
ctx[].eventCallback = cast[pointer](callback)
ctx[].eventUserData = userData
when compiles(ctx.myLib[].ffiEventCallback):
if not isNil(ctx.myLib) and not isNil(ctx.myLib[]):
ctx.myLib[].ffiEventCallback = cast[pointer](callback)
ctx.myLib[].ffiEventUserData = userData
let procNode = newProc(
name = funcIdent,
params = @[
newEmptyNode(),
newIdentDefs(ident("ctx"), ctxType),
newIdentDefs(ident("callback"), ident("FFICallBack")),
newIdentDefs(ident("userData"), ident("pointer")),
],
body = procBody,
pragmas = newTree(
nnkPragma,
ident("dynlib"),
ident("exportc"),
ident("cdecl"),
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
),
)
result.add(procNode)

View File

@ -44,6 +44,14 @@ proc registerFfiTypeInfo(typeDef: NimNode): NimNode {.compileTime.} =
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
result = typeDef
proc cParamName(paramName: string, paramType: NimNode): string =
## C export parameter name. string params are passed as-is from C and need
## no Json suffix; other types carry Json to signal they require JSON encoding.
if paramType.kind == nnkIdent and $paramType == "string":
paramName
else:
paramName & "Json"
proc capitalizeFirstLetter(s: string): string =
## Returns `s` with the first character uppercased.
if s.len == 0:
@ -725,17 +733,19 @@ macro ffi*(prc: untyped): untyped =
var lambdaParams = newSeq[NimNode]()
lambdaParams.add(futStrStr)
for name in extraParamNames:
lambdaParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
for i in 0 ..< extraParamNames.len:
lambdaParams.add(
newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring"))
)
let lambdaBody = newStmtList()
for i in 0 ..< extraParamNames.len:
let jsonIdent = ident(extraParamNames[i] & "Json")
let cIdent = ident(cParamName(extraParamNames[i], extraParamTypes[i]))
let paramIdent = ident(extraParamNames[i])
let ptype = extraParamTypes[i]
lambdaBody.add quote do:
let `paramIdent` = ffiDeserialize(`jsonIdent`, `ptype`).valueOr:
let `paramIdent` = ffiDeserialize(`cIdent`, `ptype`).valueOr:
return err($error)
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
@ -770,8 +780,10 @@ macro ffi*(prc: untyped): untyped =
exportedParams.add(newIdentDefs(ident("ctx"), ctxType))
exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
exportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
for name in extraParamNames:
exportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
for i in 0 ..< extraParamNames.len:
exportedParams.add(
newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring"))
)
let ffiBody = newStmtList()
@ -789,8 +801,8 @@ macro ffi*(prc: untyped): untyped =
newReqCall.add(reqTypeName)
newReqCall.add(ident("callback"))
newReqCall.add(ident("userData"))
for name in extraParamNames:
newReqCall.add(ident(name & "Json"))
for i in 0 ..< extraParamNames.len:
newReqCall.add(ident(cParamName(extraParamNames[i], extraParamTypes[i])))
let sendCall = newCall(
newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")),
@ -894,8 +906,10 @@ macro ffi*(prc: untyped): untyped =
syncExportedParams.add(newIdentDefs(ident("ctx"), ctxType))
syncExportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
syncExportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
for name in extraParamNames:
syncExportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
for i in 0 ..< extraParamNames.len:
syncExportedParams.add(
newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring"))
)
let syncFfiBody = newStmtList()
@ -911,11 +925,11 @@ macro ffi*(prc: untyped): untyped =
# Inline deserialization of each extra param
for i in 0 ..< extraParamNames.len:
let jsonIdent = ident(extraParamNames[i] & "Json")
let cIdent = ident(cParamName(extraParamNames[i], extraParamTypes[i]))
let paramIdent = ident(extraParamNames[i])
let ptype = extraParamTypes[i]
syncFfiBody.add quote do:
let `paramIdent` = ffiDeserialize(`jsonIdent`, `ptype`).valueOr:
let `paramIdent` = ffiDeserialize(`cIdent`, `ptype`).valueOr:
let errStr = "deserialization failed: " & $error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
@ -1266,10 +1280,12 @@ macro ffiCtor*(prc: untyped): untyped =
##
## The generated C-exported proc will have the signature:
## proc mylib_create(configJson: cstring, callback: FFICallBack,
## userData: pointer): cint {.exportc, cdecl, raises: [].}
## userData: pointer): pointer {.exportc, cdecl, raises: [].}
##
## On success the callback receives the ctx address as a decimal string.
## The caller should hold this pointer and pass it to subsequent .ffi. calls.
## 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.
let procName = prc[0]
let formalParams = prc[3]
@ -1333,9 +1349,9 @@ macro ffiCtor*(prc: untyped): untyped =
let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName)
# Build the C-exported proc params:
# (<paramName>Json: cstring, ..., callback: FFICallBack, userData: pointer): cint
# (<paramName>Json: cstring, ..., callback: FFICallBack, userData: pointer): pointer
var exportedParams = newSeq[NimNode]()
exportedParams.add(ident("cint")) # return type
exportedParams.add(ident("pointer")) # return type: ctx pointer or nil on failure
for name in paramNames:
exportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
@ -1349,10 +1365,16 @@ macro ffiCtor*(prc: untyped): untyped =
when declared(initializeLibrary):
initializeLibrary()
# if callback.isNil: return RET_MISSING_CALLBACK
# Use a gensym'd ctx identifier so both the let binding and usage match
let ctxSym = genSym(nskLet, "ctx")
# Create the FFIContext synchronously; return nil on failure
ffiBody.add quote do:
if callback.isNil:
return RET_MISSING_CALLBACK
let `ctxSym` = createFFIContext[`libTypeName`]().valueOr:
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
# Deserialize each param for early validation
for i in 0 ..< paramNames.len:
@ -1362,9 +1384,10 @@ macro ffiCtor*(prc: untyped): untyped =
block:
let validateRes = ffiDeserialize(`jsonIdent`, `ptype`)
if validateRes.isErr():
let errStr = "ffiCtor: failed to deserialize param: " & $validateRes.error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
if not callback.isNil:
let errStr = "ffiCtor: failed to deserialize param: " & $validateRes.error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return nil
# Build the ffiNewReq call with all cstring params
var newReqArgs: seq[NimNode] = @[reqTypeName, ident("callback"), ident("userData")]
@ -1372,15 +1395,6 @@ macro ffiCtor*(prc: untyped): untyped =
newReqArgs.add(ident(name & "Json"))
let newReqCall = newCall(ident("ffiNewReq"), newReqArgs)
# Use a gensym'd ctx identifier so both the let binding and usage match
let ctxSym = genSym(nskLet, "ctx")
ffiBody.add quote do:
let `ctxSym` = createFFIContext[`libTypeName`]().valueOr:
let errStr = "ffiCtor: failed to create FFIContext: " & $error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
# sendRequestToFFIThread using the gensym'd ctx
let sendCall =
newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall)
@ -1393,12 +1407,13 @@ macro ffiCtor*(prc: untyped): untyped =
except Exception as exc:
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
if `sendResIdent`.isErr():
let errStr = "ffiCtor: failed to send request: " & $`sendResIdent`.error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
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 RET_OK
return cast[pointer](`ctxSym`)
# Strip the * from proc name for the C exported version
let exportedProcName =

View File

@ -2,25 +2,11 @@ import std/[json, macros, options]
import results
import ./codegen/meta
## RawString passes the C string through as-is, with no JSON encoding/decoding.
## Use this when the C caller provides a value that should not be treated as a
## JSON-encoded string (e.g. a raw config JSON blob, a multiaddress, an ENR).
type RawString* = distinct string
proc ffiSerialize*(x: RawString): string =
string(x)
proc ffiDeserialize*(s: cstring, _: typedesc[RawString]): Result[RawString, string] =
ok(RawString($s))
proc ffiSerialize*(x: string): string =
$(%*x)
x
proc ffiSerialize*(x: cstring): string =
if x.isNil:
"null"
else:
ffiSerialize($x)
if x.isNil: "" else: $x
proc ffiSerialize*(x: int): string =
$x
@ -38,13 +24,7 @@ proc ffiSerialize*(x: pointer): string =
$cast[uint](x)
proc ffiDeserialize*(s: cstring, _: typedesc[string]): Result[string, string] =
try:
let node = parseJson($s)
if node.kind != JString:
return err("expected JSON string")
ok(node.getStr())
except Exception as e:
err(e.msg)
ok($s)
proc ffiDeserialize*(s: cstring, _: typedesc[int]): Result[int, string] =
try: