feat: ffiHandle

(cherry picked from commit 498416b5ea06e2bb130d6feedd53a28aa96a476a)
This commit is contained in:
Gabriel Cruz 2026-06-12 12:04:20 -03:00
parent 16dc1b3573
commit 2947813df4
No known key found for this signature in database
GPG Key ID: 2E467754A6BA9BA5
11 changed files with 385 additions and 35 deletions

View File

@ -3,12 +3,12 @@ import chronos, chronicles
import
ffi/internal/[ffi_library, ffi_macro],
ffi/[
alloc, ffi_types, ffi_events, ffi_context, ffi_context_pool, ffi_thread_request,
cbor_serial,
alloc, ffi_types, ffi_events, ffi_handles, ffi_context, ffi_context_pool,
ffi_thread_request, cbor_serial,
]
export atomics, tables
export chronos, chronicles
export
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_events, ffi_context,
ffi_context_pool, ffi_thread_request, cbor_serial
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_events, ffi_handles,
ffi_context, ffi_context_pool, ffi_thread_request, cbor_serial

View File

@ -92,7 +92,7 @@ proc emitObjectFields(t: FFITypeMeta): string =
proc emitReqFields(p: FFIProcMeta): string =
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
for ep in p.extraParams:
fields.add((name: ep.name, typeName: ep.typeName, isPtr: ep.isPtr))
fields.add((name: ep.name, typeName: ep.typeName, isPtr: ep.ridesAsPtr()))
emitMap(fields)
proc responseRule(p: FFIProcMeta): string =
@ -106,7 +106,7 @@ proc responseRule(p: FFIProcMeta): string =
# The dtor has no meaningful payload — handleRes sends a CBOR null sentinel.
"nil"
of FFIKind.FFI:
if p.returnIsPtr:
if p.returnRidesAsPtr():
"uint"
else:
nimTypeToCddl(p.returnTypeName)

View File

@ -299,7 +299,7 @@ proc generateCppHeader*(
lines.add("struct $1 {" % [reqName])
for ep in p.extraParams:
let cppType =
if ep.isPtr:
if ep.ridesAsPtr():
CppPtrType
else:
nimTypeToCpp(ep.typeName)
@ -308,7 +308,7 @@ proc generateCppHeader*(
var fields: seq[(string, string)] = @[]
for ep in p.extraParams:
let cppType =
if ep.isPtr:
if ep.ridesAsPtr():
CppPtrType
else:
nimTypeToCpp(ep.typeName)
@ -388,7 +388,7 @@ proc generateCppHeader*(
var epNames: seq[string] = @[]
for ep in ctor.extraParams:
let cppType =
if ep.isPtr:
if ep.ridesAsPtr():
CppPtrType
else:
nimTypeToCpp(ep.typeName)
@ -492,7 +492,7 @@ proc generateCppHeader*(
for m in methods:
let methodName = stripLibPrefixCpp(m.procName, libName)
let retCppType =
if m.returnIsPtr:
if m.returnRidesAsPtr():
CppPtrType
else:
nimTypeToCpp(m.returnTypeName)
@ -502,7 +502,7 @@ proc generateCppHeader*(
var methParamNames: seq[string] = @[]
for ep in m.extraParams:
let cppType =
if ep.isPtr:
if ep.ridesAsPtr():
CppPtrType
else:
nimTypeToCpp(ep.typeName)

View File

@ -6,6 +6,7 @@ type
name*: string # Nim param name, e.g. "req"
typeName*: string # Nim type name, e.g. "EchoRequest"
isPtr*: bool # true if the type is `ptr T`
isHandle*: bool # true if the type is an {.ffiHandle.} type (wire form uint64)
FFIKind* {.pure.} = enum
FFI
@ -20,6 +21,7 @@ type
extraParams*: seq[FFIParamMeta] # all params except the lib param
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
returnIsPtr*: bool # true if return type is ptr T
returnIsHandle*: bool # true if return type is an {.ffiHandle.} type
FFIFieldMeta* = object
name*: string # e.g. "delayMs"
@ -46,6 +48,24 @@ var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
var ffiEventRegistry* {.compileTime.}: seq[FFIEventMeta]
var currentLibName* {.compileTime.}: string
# Lib type name (set by declareLibrary) so handle-receiver procs resolve the pool.
var currentLibType* {.compileTime.}: string
# Names of types marked `{.ffiHandle.}` (wire form uint64).
var ffiHandleTypeNames* {.compileTime.}: seq[string]
proc isFFIHandleTypeName*(name: string): bool {.compileTime.} =
name in ffiHandleTypeNames
proc ridesAsPtr*(ep: FFIParamMeta): bool =
## True if the param crosses the wire as an opaque uint64 — a raw `ptr` or an
## `{.ffiHandle.}` id. Both share the codegen pointer type.
ep.isPtr or ep.isHandle
proc returnRidesAsPtr*(p: FFIProcMeta): bool =
## True if the return crosses the wire as an opaque uint64 (raw `ptr` or handle).
p.returnIsPtr or p.returnIsHandle
# Target language for binding generation; override with -d:targetLang=cpp
const targetLang* {.strdefine.} = "rust"

View File

@ -256,7 +256,7 @@ proc generateTypesRs*(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): string
for ep in p.extraParams:
let snake = camelToSnakeCase(ep.name)
let rustType =
if ep.isPtr:
if ep.ridesAsPtr():
RustPtrType
else:
nimTypeToRust(ep.typeName)
@ -550,7 +550,7 @@ proc generateApiRs*(
for ep in ctor.extraParams:
let snake = camelToSnakeCase(ep.name)
let rustType =
if ep.isPtr:
if ep.ridesAsPtr():
RustPtrType
else:
nimTypeToRust(ep.typeName)
@ -711,7 +711,7 @@ proc generateApiRs*(
for ep in m.extraParams:
let snake = camelToSnakeCase(ep.name)
let rustType =
if ep.isPtr:
if ep.ridesAsPtr():
RustPtrType
else:
nimTypeToRust(ep.typeName)
@ -729,7 +729,8 @@ proc generateApiRs*(
else:
reqName & " {}"
let retTypeForApi = if m.returnIsPtr: RustPtrType else: retRustType
let retTypeForApi =
if m.returnRidesAsPtr(): RustPtrType else: retRustType
# -- blocking method --
lines.add(

View File

@ -8,9 +8,15 @@
import std/[atomics, locks, options, tables]
import chronicles, chronos, chronos/threadsync, taskpools/channels_spsc_single, results
import ./ffi_types, ./ffi_events, ./ffi_thread_request, ./logging, ./cbor_serial
import
./ffi_types,
./ffi_events,
./ffi_handles,
./ffi_thread_request,
./logging,
./cbor_serial
export ffi_events
export ffi_events, ffi_handles
type FFIContext*[T] = object
myLib*: ptr T # main library object (Waku, LibP2P, SDS, …)
@ -27,6 +33,7 @@ type FFIContext*[T] = object
eventThreadExitSignal: ThreadSignalPtr # mirrors threadExitSignal for the event thread
userData*: pointer
eventRegistry*: FFIEventRegistry
handles*: FFIHandleRegistry # live {.ffiHandle.} objects, keyed by uint64 id
eventQueue*: EventQueue
ffiHeartbeat*: Atomic[int64]
# advanced each FFI-thread loop; event thread reads for liveness
@ -57,6 +64,7 @@ proc deinitContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] =
## fields are nil'd after close so re-init on the same slot is safe.
ctx.lock.deinitLock()
deinitEventRegistry(ctx[].eventRegistry)
deinitHandleRegistry(ctx[].handles)
deinitEventQueue(ctx[].eventQueue)
when defined(gcRefc):
# ThreadSignalPtr.close() under refc traps in safeUnregisterAndCloseFd
@ -95,6 +103,7 @@ proc initContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] =
ctx.eventThreadExitSignal = nil
ctx.lock.initLock()
initEventRegistry(ctx[].eventRegistry)
initHandleRegistry(ctx[].handles)
initEventQueue(ctx[].eventQueue)
ctx.ffiHeartbeat.store(0)
ctx.eventQueueStuck.store(false)

55
ffi/ffi_handles.nim Normal file
View File

@ -0,0 +1,55 @@
## Per-context registry of live `{.ffiHandle.}` objects. The object stays here,
## in `FFIContext.handles`; only its `uint64` id crosses the boundary. Ids are
## monotonic and never recycled (0 = null), so a stale/forged id misses cleanly.
## FFI-thread-only access, so no locking.
import std/tables
type
FFIHandleRoot* = ref object of RootObj
## Base every `{.ffiHandle.}` type inherits from, so handle refs are storable
## under one static type.
FFIHandleEntry = object
obj: FFIHandleRoot
typeName: string
FFIHandleRegistry* = object
nextId*: uint64
byHandle*: Table[uint64, FFIHandleEntry]
proc initHandleRegistry*(reg: var FFIHandleRegistry) =
reg.nextId = 0'u64
reg.byHandle = initTable[uint64, FFIHandleEntry]()
proc deinitHandleRegistry*(reg: var FFIHandleRegistry) =
reg.byHandle = default(Table[uint64, FFIHandleEntry])
reg.nextId = 0'u64
proc register*(
reg: var FFIHandleRegistry, obj: FFIHandleRoot, typeName: string
): uint64 =
## Stores `obj`, returns its fresh handle id (>0).
reg.nextId.inc()
reg.byHandle[reg.nextId] = FFIHandleEntry(obj: obj, typeName: typeName)
reg.nextId
proc lookup*(
reg: var FFIHandleRegistry, handle: uint64, typeName: string
): FFIHandleRoot =
## Live ref for `handle`, or nil if absent or registered under another type.
let entry = reg.byHandle.getOrDefault(handle)
if entry.obj.isNil() or entry.typeName != typeName:
return nil
entry.obj
proc release*(reg: var FFIHandleRegistry, handle: uint64): bool {.discardable.} =
## Drops the entry; true iff it existed.
if not reg.byHandle.hasKey(handle):
return false
reg.byHandle.del(handle)
true
proc releaseAll*(reg: var FFIHandleRegistry) =
## Drops every entry. Must run on the FFI thread that allocated the refs.
reg.byHandle.clear()

View File

@ -101,6 +101,8 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
defer:
onFFIThread = false
# Free handle refs on the FFI thread that allocated them (refc heap is thread-local).
ctx[].handles.releaseAll()
# Unblocks destroyFFIContext's bounded wait so cleanup can proceed.
let fireRes = ctx.threadExitSignal.fireSync()
if fireRes.isErr():

View File

@ -121,11 +121,19 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
## `libType` is the Nim type of the main library object, used to type
## the `ctx: ptr FFIContext[libType]` parameter. See
## `examples/timer/timer.nim` for a working call site.
currentLibType = $libType # so handle-receiver `.ffi.` procs can resolve the pool
var stmts = newStmtList()
# Emit the base bootstrap (pragmas, linker flags, NimMain, initializeLibrary)
stmts.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName)))
# The pool the generated wrappers validate against; ffiCtor/ffiDtor guard alike.
let poolIdent = ident($libType & "FFIPool")
stmts.add quote do:
when not declared(`poolIdent`):
var `poolIdent`*: FFIContextPool[`libType`]
let ctxType = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libType))
let cdeclExportPragma = newTree(
nnkPragma,

View File

@ -82,13 +82,18 @@ proc nimTypeNameRepr(typ: NimNode): string =
else:
typ.repr
proc isHandleType(typ: NimNode): bool =
## True iff `typ` is an `{.ffiHandle.}` type — its wire form is `uint64`.
typ.kind == nnkIdent and isFFIHandleTypeName($typ)
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.
## In-Req-struct storage type. `cstring` rides as `string`; an {.ffiHandle.}
## type rides as its `uint64` id; everything else as-is.
if typ.kind == nnkIdent and $typ == "cstring":
return ident("string")
return typ
if isHandleType(typ):
return ident("uint64")
typ
proc unpackReqField*(fieldIdent, userType, decodedIdent: NimNode): NimNode =
## Emits AST for unpacking one field from a CBOR-decoded Req struct into a
@ -113,6 +118,19 @@ proc unpackReqField*(fieldIdent, userType, decodedIdent: NimNode): NimNode =
return
nnkLetSection.newTree(nnkIdentDefs.newTree(fieldIdent, ident("cstring"), castExpr))
proc unpackHandleField*(
fieldIdent, userType, ctxIdent, decodedIdent: NimNode
): NimNode =
## Reconstitutes a handle param from its wire `uint64` via the ctx registry,
## returning `RET_ERR` (Result.err) on a stale/forged/wrong-type id.
let errMsg = "invalid or stale ffiHandle for parameter '" & $fieldIdent & "'"
quote:
let `fieldIdent` = block:
let ffiH = `ctxIdent`[].handles.lookup(`decodedIdent`.`fieldIdent`, $`userType`)
if ffiH.isNil():
return err(`errMsg`)
cast[`userType`](ffiH)
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)
@ -239,9 +257,12 @@ proc buildFFINewReqProc(reqTypeName, body: NimNode): NimNode =
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
# User-typed lambda params (kept as-is so callers see their original signature)
# Handle params travel as their uint64 id; others keep the user's type.
let procParams = procNode[3]
for p in procParams[1 .. ^1]:
if isHandleType(p[1]):
formalParams.add(newIdentDefs(p[0], ident("uint64")))
continue
formalParams.add(p)
let retType = newNimNode(nnkPtrTy)
@ -345,6 +366,9 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode
# Unpack each field as a local typed as the user's original param type.
for p in procParams[1 ..^ 1]:
if isHandleType(p[1]):
newBody.add unpackHandleField(p[0], p[1], reqHandler[0], decodedIdent)
continue
newBody.add unpackReqField(p[0], p[1], decodedIdent)
newBody.add(bodyNode)
@ -385,16 +409,19 @@ proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode =
else:
error "Second argument must be a typed parameter, e.g. waku: ptr Waku"
let castedHandler = newTree(nnkCast, rhsType, ident("reqHandler"))
let handlerCtxIdent = genSym(nskLet, "handlerCtx")
let callExpr = newCall(
newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler
newDotExpr(reqTypeName, ident("processFFIRequest")),
ident("request"),
handlerCtxIdent,
)
let typedResIdent = genSym(nskLet, "typedRes")
var newBody = newStmtList()
newBody.add quote do:
let `handlerCtxIdent` = cast[`rhsType`](reqHandler)
let `typedResIdent` = await `callExpr`
if `typedResIdent`.isErr:
return err(`typedResIdent`.error)
@ -402,6 +429,14 @@ proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode =
return ok(`typedResIdent`.value)
elif typeof(`typedResIdent`.value) is void:
return ok(newSeq[byte]())
elif typeof(`typedResIdent`.value) is FFIHandleRoot:
return ok(
cborEncode(
`handlerCtxIdent`[].handles.register(
`typedResIdent`.value, $typeof(`typedResIdent`.value)
)
)
)
else:
return ok(cborEncode(`typedResIdent`.value))
@ -596,6 +631,40 @@ macro ffiRaw*(prc: untyped): untyped =
echo stmts.repr
return stmts
macro ffiHandle*(prc: untyped): untyped =
## Marks a `ref object` as an opaque FFI handle. Its wire form is a `uint64`
## id; the live object stays in the per-ctx handle registry and never crosses.
##
## type Kernel {.ffiHandle.} = ref object
## ...
if prc.kind != nnkTypeDef:
error("`.ffiHandle.` must be applied to a type definition")
var clean = prc.copyNimTree()
if clean[0].kind == nnkPragmaExpr:
clean[0] = clean[0][0]
let typeName =
if clean[0].kind == nnkPostfix:
clean[0][1]
else:
clean[0]
let refTy = clean[2]
if refTy.kind != nnkRefTy or refTy[0].kind != nnkObjectTy:
error("`.ffiHandle.` type " & $typeName & " must be a `ref object`")
let objTy = refTy[0]
if objTy[1].kind != nnkEmpty:
error("`.ffiHandle.` type " & $typeName & " must not already inherit a base")
# Inherit the registry's storable base so handle refs share one static type.
objTy[1] = nnkOfInherit.newTree(ident("FFIHandleRoot"))
ffiHandleTypeNames.add($typeName)
when defined(ffiDumpMacros):
echo clean.repr
return clean
macro ffi*(prc: untyped): untyped =
## Simplified FFI macro — applies to procs or types.
##
@ -631,8 +700,20 @@ macro ffi*(prc: untyped): untyped =
error("`.ffi.` procs require at least 1 parameter (the library type)")
let firstParam = formalParams[1]
let libParamName = firstParam[0]
let libTypeName = firstParam[1]
let recvName = firstParam[0]
let recvType = firstParam[1]
let firstIsHandle = isHandleType(recvType)
if firstIsHandle and currentLibType.len == 0:
error(
"`.ffi.` proc " & $procName & " has an {.ffiHandle.} receiver but no " &
"library is declared; call declareLibrary(name, LibType) first"
)
# A handle receiver carries no library type, so fall back to the declared one.
let libTypeName =
if firstIsHandle:
ident(currentLibType)
else:
recvType
let retTypeNode = formalParams[0]
if retTypeNode.kind == nnkEmpty:
@ -654,9 +735,11 @@ macro ffi*(prc: untyped): untyped =
let resultRetType = resultInner[1]
rejectRawPtrType(resultRetType, "`.ffi.` proc " & $procName & " return type")
# A handle receiver rides the wire; a value-type lib receiver binds to ctx.myLib.
var extraParamNames: seq[string] = @[]
var extraParamTypes: seq[NimNode] = @[]
for i in 2 ..< formalParams.len:
let wireStart = if firstIsHandle: 1 else: 2
for i in wireStart ..< formalParams.len:
let p = formalParams[i]
for j in 0 ..< p.len - 2:
rejectRawPtrType(p[^2], "`.ffi.` proc " & $procName & " parameter " & $p[j])
@ -687,12 +770,14 @@ macro ffi*(prc: untyped): untyped =
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
proc buildAsyncHelperProc(): NimNode =
## proc <userProcName>*(lib: LibType, extras...): Future[Result[T, string]] {.async.} = <body>
## Reproduces the user's exact signature so it stays callable from Nim.
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]))
helperParams.add(newIdentDefs(recvName, recvType))
for i in 2 ..< formalParams.len:
let p = formalParams[i]
for j in 0 ..< p.len - 2:
helperParams.add(newIdentDefs(p[j], p[^2]))
newProc(
name = postfix(userProcName, "*"),
params = helperParams,
@ -719,9 +804,10 @@ macro ffi*(prc: untyped): untyped =
for i in 0 ..< extraParamNames.len:
lambdaParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
let libValDeref = newTree(nnkDerefExpr, ctxMyLib)
let helperCall = newTree(nnkCall, userProcName, libValDeref)
let helperCall = newTree(nnkCall, userProcName)
if not firstIsHandle:
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
helperCall.add(newTree(nnkDerefExpr, ctxMyLib))
for name in extraParamNames:
helperCall.add(ident(name))
@ -800,16 +886,20 @@ macro ffi*(prc: untyped): untyped =
for i in 0 ..< extraParamNames.len:
let ptype = extraParamTypes[i]
let isPointer = isPtr(ptype)
let handle = isHandleType(ptype)
let tn =
if isPointer:
nimTypeNameRepr(ptype[0])
else:
nimTypeNameRepr(ptype)
ffiExtraParams.add(
FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPointer)
FFIParamMeta(
name: extraParamNames[i], typeName: tn, isPtr: isPointer, isHandle: handle
)
)
let retTypeInner = resultInner[1]
let retIsPtr = isPtr(retTypeInner)
let retIsHandle = isHandleType(retTypeInner)
let retTn =
if retIsPtr:
nimTypeNameRepr(retTypeInner[0])
@ -824,6 +914,7 @@ macro ffi*(prc: untyped): untyped =
extraParams: ffiExtraParams,
returnTypeName: retTn,
returnIsPtr: retIsPtr,
returnIsHandle: retIsHandle,
)
)

View File

@ -0,0 +1,164 @@
## {.ffiHandle.} round-trip through the full C-shape dispatch path: a handle
## returned by one `.ffi.` proc crosses as a uint64 and is reconstituted as the
## live object by another; stale/forged/null ids miss cleanly with RET_ERR.
import std/[locks, strutils]
import unittest2
import results
import ffi
type HandleLib = object
base: int
# Stub the dylib NimMain importc that declareLibrary emits (this links as a plain exe).
{.emit: "void libhandletestNimMain(void) {}".}
declareLibrary("handletest", HandleLib)
type Session {.ffiHandle.} = ref object
token: string
hits: int
type OpenReq {.ffi.} = object
name: string
proc handletest_open*(
lib: HandleLib, req: OpenReq
): Future[Result[Session, string]] {.ffi.} =
return ok(Session(token: req.name & ":" & $lib.base, hits: 0))
proc handletest_token*(
lib: HandleLib, s: Session
): Future[Result[string, string]] {.ffi.} =
s.hits.inc()
return ok(s.token & "#" & $s.hits)
# Handle as the receiver (first param) — lib type comes from declareLibrary.
proc handletest_session_bump*(s: Session): Future[Result[int, string]] {.ffi.} =
s.hits.inc()
return ok(s.hits)
type CallbackData = object
lock: Lock
cond: Cond
called: bool
retCode: cint
msg: array[1024, byte]
msgLen: int
proc initCallbackData(d: var CallbackData) =
d.lock.initLock()
d.cond.initCond()
proc deinitCallbackData(d: var CallbackData) =
d.cond.deinitCond()
d.lock.deinitLock()
proc testCallback(
retCode: cint, msg: ptr cchar, len: csize_t, userData: pointer
) {.cdecl, gcsafe, raises: [].} =
let d = cast[ptr CallbackData](userData)
acquire(d[].lock)
d[].retCode = retCode
let n = min(int(len), d[].msg.len)
if n > 0 and not msg.isNil:
copyMem(addr d[].msg[0], msg, n)
d[].msgLen = n
d[].called = true
signal(d[].cond)
release(d[].lock)
proc waitCallback(d: var CallbackData) =
acquire(d.lock)
while not d.called:
wait(d.cond, d.lock)
release(d.lock)
proc payload(d: var CallbackData): seq[byte] =
var b = newSeq[byte](d.msgLen)
if d.msgLen > 0:
copyMem(addr b[0], addr d.msg[0], d.msgLen)
b
proc text(d: var CallbackData): string =
var s = newString(d.msgLen)
if d.msgLen > 0:
copyMem(addr s[0], addr d.msg[0], d.msgLen)
s
proc encodedPtr(b: var seq[byte]): ptr byte =
if b.len == 0:
nil
else:
cast[ptr byte](addr b[0])
template runCall(d, ctx, reqBytes, exportProc) =
## Fires `exportProc` with `reqBytes` and blocks until the callback lands in `d`.
initCallbackData(d)
var rb = reqBytes
check exportProc(ctx, testCallback, addr d, encodedPtr(rb), rb.len.csize_t) == RET_OK
waitCallback(d)
suite "{.ffiHandle.} round-trip":
setup:
let ctx {.inject.} = HandleLibFFIPool.createFFIContext().get()
teardown:
discard HandleLibFFIPool.destroyFFIContext(ctx)
test "handle returned as uint64, reconstituted on the next call":
var od: CallbackData
runCall(
od,
ctx,
cborEncode(HandletestOpenReq(req: OpenReq(name: "alpha"))),
handletest_open,
)
defer:
deinitCallbackData(od)
check od.retCode == RET_OK
let handle = cborDecode(payload(od), uint64).value
check handle == 1'u64 # ids start at 1
var td: CallbackData
runCall(td, ctx, cborEncode(HandletestTokenReq(s: handle)), handletest_token)
defer:
deinitCallbackData(td)
check td.retCode == RET_OK
check cborDecode(payload(td), string).value == "alpha:0#1"
test "handle as receiver (first param)":
var od: CallbackData
runCall(
od,
ctx,
cborEncode(HandletestOpenReq(req: OpenReq(name: "beta"))),
handletest_open,
)
defer:
deinitCallbackData(od)
let handle = cborDecode(payload(od), uint64).value
var bd: CallbackData
runCall(
bd, ctx, cborEncode(HandletestSessionBumpReq(s: handle)), handletest_session_bump
)
defer:
deinitCallbackData(bd)
check bd.retCode == RET_OK
check cborDecode(payload(bd), int).value == 1
test "forged handle misses cleanly with RET_ERR":
var td: CallbackData
runCall(td, ctx, cborEncode(HandletestTokenReq(s: 9999'u64)), handletest_token)
defer:
deinitCallbackData(td)
check td.retCode == RET_ERR
check "ffiHandle" in text(td)
test "null handle (0) misses with RET_ERR":
var td: CallbackData
runCall(td, ctx, cborEncode(HandletestTokenReq(s: 0'u64)), handletest_token)
defer:
deinitCallbackData(td)
check td.retCode == RET_ERR