diff --git a/ffi/ffi_context.nim b/ffi/ffi_context.nim index 67a68e5..dd210a4 100644 --- a/ffi/ffi_context.nim +++ b/ffi/ffi_context.nim @@ -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] = diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index 1dbfd0f..527d6d9 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -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) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 7bc7c09..1d2754e 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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: - # (Json: cstring, ..., callback: FFICallBack, userData: pointer): cint + # (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 = diff --git a/ffi/serial.nim b/ffi/serial.nim index 28ff240..01d33af 100644 --- a/ffi/serial.nim +++ b/ffi/serial.nim @@ -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: