mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-22 01:10:03 +00:00
feat: ffiHandle
(cherry picked from commit 498416b5ea06e2bb130d6feedd53a28aa96a476a)
This commit is contained in:
parent
16dc1b3573
commit
2947813df4
8
ffi.nim
8
ffi.nim
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
55
ffi/ffi_handles.nim
Normal 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()
|
||||
@ -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():
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
164
tests/unit/test_ffi_handle.nim
Normal file
164
tests/unit/test_ffi_handle.nim
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user