mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-05-11 12:49:28 +00:00
allow simpler ffi usage
This commit is contained in:
parent
233d6d726b
commit
863b15d2dc
@ -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] =
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user