mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 14:33:13 +00:00
chore: tidying up
This commit is contained in:
parent
41053a50a4
commit
4d0e566cdf
14
CHANGELOG.md
14
CHANGELOG.md
@ -11,7 +11,19 @@ All notable changes to this project are documented in this file.
|
||||
longer need a hand-written payload type. A single parameter still rides the
|
||||
wire directly (a scalar, or an existing `{.ffi.}` object). The foreign
|
||||
bindings gain the envelope as a first-class struct plus a typed handler.
|
||||
- `{.ffiExport.}`, for simple synchronous C exports, from the 0.2 line.
|
||||
- `{.ffiExport.}`, for simple synchronous C exports, from the 0.2 line. Each
|
||||
wrapper carries `raises: []` and catches every exception of the body, because
|
||||
an exception must not cross the C ABI. A return type with no C ABI mapping is
|
||||
a compile error. `int` maps to `long long`, so a 64-bit value keeps its full
|
||||
width. A `string` return rides in a buffer that belongs to the calling thread
|
||||
and stays valid until that thread calls another string export.
|
||||
- Pooled FFI contexts are recycled instead of destroyed. Each slot builds its
|
||||
worker thread, its event thread and its signal fds once, then reuses them, so
|
||||
repeated create/destroy no longer churns fds past `FD_SETSIZE`. The `ffiDtor`
|
||||
asks the FFI thread to drain the in-flight handlers, free the library and
|
||||
return the slot, while the threads stay alive. A recycle also fails every
|
||||
request still queued for that slot, because such a request carries the
|
||||
`userData` of a host that is gone.
|
||||
- `{.ffi.}` now picks the path from the shape of the signature. One pragma
|
||||
covers the context method, the static call, the synchronous export, the
|
||||
destructor and the event. `ffi/internal/ffi_route.nim` holds the rules:
|
||||
|
||||
@ -37,8 +37,8 @@ type FFIContext*[T] = object
|
||||
# the FFI thread so the slot returns to the pool without recreating threads.
|
||||
lifecycle*: Atomic[CtxLifecycle]
|
||||
recycleDoneSignal: ThreadSignalPtr
|
||||
# fired by the recycle handler once the lib is freed and the slot released;
|
||||
# the synchronous recycleFFIContext caller waits on it.
|
||||
# fired by the recycle handler once the lib is freed, just before it releases
|
||||
# the slot; the synchronous recycleFFIContext caller waits on it.
|
||||
libReady*: Atomic[bool]
|
||||
# False until a {.ffiCtor.} stores the library. Before that, `myLib` points
|
||||
# at the default fallback of the FFI thread. For a `ref` type that fallback
|
||||
@ -67,10 +67,16 @@ var onFFIThread* {.threadvar.}: bool
|
||||
|
||||
const git_version* {.strdefine.} = "n/a"
|
||||
|
||||
const RecycleTimeoutMs* {.intdefine: "ffiRecycleTimeoutMs".} = 1500
|
||||
## Bounds one drain round of the recycle handler. The handler runs at most two
|
||||
## rounds: it waits for the in-flight handlers, then cancels them and waits
|
||||
## again. Override with `-d:ffiRecycleTimeoutMs=<ms>`.
|
||||
const RecycleTimeout* = RecycleTimeoutMs.milliseconds
|
||||
|
||||
const
|
||||
RecycleWaitTimeout* = 5.seconds
|
||||
## Caller-side bound for synchronous recycle; the FFI-thread drain itself is
|
||||
## bounded by RecycleTimeout, so this only guards against a wedged worker.
|
||||
RecycleWaitTimeout* = 2 * RecycleTimeout + 2.seconds
|
||||
## Caller-side bound for synchronous recycle. It covers both drain rounds
|
||||
## plus slack, so it only fires when the worker itself is wedged.
|
||||
EventThreadTickInterval* = 1.seconds
|
||||
FFIHeartbeatStartDelay* = 10.seconds
|
||||
FFIHeartbeatStaleThreshold* = 1.seconds
|
||||
@ -209,7 +215,7 @@ proc tryClaim*[T](ctx: ptr FFIContext[T]): bool =
|
||||
var expected = false
|
||||
ctx.inUse.compareExchange(expected, true)
|
||||
|
||||
proc release*[T](ctx: ptr FFIContext[T]) =
|
||||
proc releaseClaim*[T](ctx: ptr FFIContext[T]) =
|
||||
ctx.inUse.store(false)
|
||||
|
||||
proc isInUse*[T](ctx: ptr FFIContext[T]): bool =
|
||||
@ -227,6 +233,10 @@ proc requestRecycle*[T](ctx: ptr FFIContext[T]): Result[void, string] =
|
||||
if not ctx.lifecycle.compareExchange(expected, CtxLifecycle.RecyclePending):
|
||||
return err("requestRecycle: context is not Active (already recycling)")
|
||||
|
||||
# A recycle that timed out can fire late. The CAS makes this the only recycle
|
||||
# in flight, so drop that stale fire before the wait below can answer to it.
|
||||
discard ctx.recycleDoneSignal.waitSync(ZeroDuration)
|
||||
|
||||
let fired = ctx.reqSignal.fireSync().valueOr:
|
||||
return err("requestRecycle: failed to signal the FFI thread: " & $error)
|
||||
if not fired:
|
||||
|
||||
@ -29,7 +29,7 @@ proc releaseSlot[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]) =
|
||||
if pool.contexts[i].addr == ctx:
|
||||
pool.initialized[i].store(false)
|
||||
break
|
||||
ctx.release()
|
||||
ctx.releaseClaim()
|
||||
|
||||
proc createFFIContext*[T](
|
||||
pool: var FFIContextPool[T]
|
||||
@ -45,7 +45,7 @@ proc createFFIContext*[T](
|
||||
ctx.markAsActive()
|
||||
return ok(ctx)
|
||||
initContextResources(ctx).isOkOr:
|
||||
ctx.release()
|
||||
ctx.releaseClaim()
|
||||
return err("createFFIContext: initContextResources failed: " & $error)
|
||||
pool.initialized[i].store(true)
|
||||
return ok(ctx)
|
||||
@ -138,5 +138,5 @@ proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =
|
||||
return false
|
||||
for i in 0 ..< MaxFFIContexts:
|
||||
if cast[pointer](pool.contexts[i].addr) == ctx:
|
||||
return cast[ptr FFIContext[T]](ctx).isInUse()
|
||||
return pool.contexts[i].addr.isInUse()
|
||||
false
|
||||
|
||||
@ -86,10 +86,6 @@ proc processRequest[T](
|
||||
except Exception as e:
|
||||
error "Unexpected exception in handleRes", error = e.msg
|
||||
|
||||
const RecycleTimeout = 1500.milliseconds
|
||||
## Bounds how long the recycle handler waits for in-flight handlers before it
|
||||
## cancels them, so a wedged handler cannot block reuse forever.
|
||||
|
||||
proc freeLib[T](ctx: ptr FFIContext[T]) {.gcsafe.} =
|
||||
## Releases the library object the ctor stored in ctx.myLib. Only owned libs
|
||||
## (createShared'd by a ctor) are freed; the worker's stack fallback is not.
|
||||
@ -102,8 +98,8 @@ proc freeLib[T](ctx: ptr FFIContext[T]) {.gcsafe.} =
|
||||
try:
|
||||
{.cast(gcsafe).}:
|
||||
`=destroy`(ctx.myLib[])
|
||||
except Exception:
|
||||
discard
|
||||
except Exception as e:
|
||||
error "destroying the library on recycle raised; freeing it anyway", error = e.msg
|
||||
else:
|
||||
when T is ref:
|
||||
if ctx.myLibRefd:
|
||||
@ -113,6 +109,23 @@ proc freeLib[T](ctx: ptr FFIContext[T]) {.gcsafe.} =
|
||||
ctx.myLib = nil
|
||||
ctx.myLibOwned = false
|
||||
|
||||
const RecycledReason =
|
||||
"FFI context was recycled before this request ran; the caller is gone"
|
||||
|
||||
proc rejectQueuedRequests[T](ctx: ptr FFIContext[T]) =
|
||||
## Fails every queued request instead of dispatching it. A request that a
|
||||
## destroyed context left behind still carries that host's `userData`, which
|
||||
## the host has freed; running it would answer a dead callback, and running it
|
||||
## after the slot is reused would run it against the library of the next owner.
|
||||
var request = ctx.reqQueueBank.mergeQueues()
|
||||
while not request.isNil():
|
||||
let nextRequest = request[].next # read before handleRes frees it
|
||||
try:
|
||||
handleRes(Result[seq[byte], string].err(RecycledReason), request)
|
||||
except Exception as e:
|
||||
error "rejecting a queued request raised", error = e.msg
|
||||
request = nextRequest
|
||||
|
||||
proc recycleContext[T](
|
||||
ctx: ptr FFIContext[T], ongoing: ptr seq[Future[void]]
|
||||
) {.async.} =
|
||||
@ -125,18 +138,20 @@ proc recycleContext[T](
|
||||
drained = await allFutures(ongoing[]).withTimeout(RecycleTimeout)
|
||||
if not drained:
|
||||
for fut in ongoing[]:
|
||||
if not fut.finished():
|
||||
fut.cancelSoon()
|
||||
fut.cancelSoon()
|
||||
drained = await allFutures(ongoing[]).withTimeout(RecycleTimeout)
|
||||
|
||||
freeLib(ctx)
|
||||
clearListeners(ctx[].eventRegistry)
|
||||
rejectQueuedRequests(ctx)
|
||||
ongoing[].setLen(0)
|
||||
ctx.release()
|
||||
|
||||
# Fire before the release: a thread that claims the freed slot first would
|
||||
# otherwise take this fire as the answer to its own recycle.
|
||||
let fireRes = ctx.recycleDoneSignal.fireSync()
|
||||
if fireRes.isErr():
|
||||
error "failed to fire recycleDoneSignal", err = fireRes.error
|
||||
ctx.releaseClaim()
|
||||
|
||||
var ffiEventQueueSignalPtr {.threadvar.}: ThreadSignalPtr
|
||||
# Stashed so the hook has no closure env.
|
||||
@ -215,6 +230,13 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
|
||||
await recycleContext(ctx, addr pending)
|
||||
continue
|
||||
|
||||
# A submit that read `Active` just before the recycle can still land here.
|
||||
# Fail it rather than run it against the library of the next owner.
|
||||
if ctx.lifecycle.load() != CtxLifecycle.Active:
|
||||
rejectQueuedRequests(ctx)
|
||||
discard await ctx.reqSignal.wait().withTimeout(chronos.milliseconds(100))
|
||||
continue
|
||||
|
||||
cleanFinishedRequests()
|
||||
|
||||
# Block until a submit signals us, or at most 100ms.
|
||||
|
||||
29
ffi/internal/ffi_codegen_common.nim
Normal file
29
ffi/internal/ffi_codegen_common.nim
Normal file
@ -0,0 +1,29 @@
|
||||
## Compile-time pieces that more than one `{.ffi.}` codegen path shares.
|
||||
|
||||
import std/macros
|
||||
|
||||
func unwrapPostfix*(n: NimNode): NimNode =
|
||||
## Strips the `*` of an exported name, so a name node reads the same whether
|
||||
## the writer exported it or not.
|
||||
return
|
||||
if n.kind == nnkPostfix:
|
||||
n[1]
|
||||
else:
|
||||
n
|
||||
|
||||
func procIdent*(prc: NimNode): NimNode =
|
||||
return unwrapPostfix(prc[0])
|
||||
|
||||
proc buildLibReadyGuard*(
|
||||
ctxHandlerName, libTypeName: NimNode
|
||||
): NimNode {.compileTime.} =
|
||||
## Rejects a request that reaches the FFI thread before the ctor stores a
|
||||
## library. The guard applies only to a `ref` type. For an `object` type the
|
||||
## fallback is a usable zero value, but for a `ref` type it is nil. The guard
|
||||
## runs in the handler, behind the ctor in the queue. Thus a host can send a
|
||||
## call before it waits for the create callback.
|
||||
quote:
|
||||
when `libTypeName` is ref:
|
||||
if not `ctxHandlerName`[].libReady.load():
|
||||
return
|
||||
err("library is not initialized: the constructor failed or has not run yet")
|
||||
@ -8,15 +8,22 @@
|
||||
## return value of the function crosses the ABI directly.
|
||||
##
|
||||
## Write native Nim types. `ffiExport` maps them to the C ABI:
|
||||
## int / bool -> C int
|
||||
## uint64 -> C unsigned long long
|
||||
## string -> C const char* (stays alive in shared memory until the next call)
|
||||
## (no return) -> C void
|
||||
## int / int64 -> C long long
|
||||
## int32 / bool -> C int
|
||||
## uint / uint64 -> C unsigned long long
|
||||
## float -> C double
|
||||
## string -> C const char* (valid until the same thread calls again)
|
||||
## (no return) -> C void
|
||||
## A return type with no mapping is a compile error.
|
||||
## The `const char*` buffer belongs to the calling thread. Copy the bytes before
|
||||
## that thread calls another `{.ffiExport.}` proc that returns a string.
|
||||
## `ffiExport` also injects the `initializeLibrary()` call of the library. The Nim
|
||||
## runtime therefore starts on the first call, and the host never calls NimMain.
|
||||
## The wrapper catches every exception of the body, prints it and returns the
|
||||
## zero value, because an exception must not cross the C ABI.
|
||||
##
|
||||
## declareLibraryBase("myLib") # emits initializeLibrary()
|
||||
## proc my_start(): int {.ffiExport.} = 0 # -> int my_start(void)
|
||||
## proc my_start(): int {.ffiExport.} = 0 # -> long long my_start(void)
|
||||
## proc my_alive(): uint64 {.ffiExport.} = beats # -> unsigned long long my_alive(void)
|
||||
## proc my_error(): string {.ffiExport.} = lastErr # -> const char* my_error(void)
|
||||
##
|
||||
@ -25,22 +32,65 @@
|
||||
|
||||
import std/macros
|
||||
import ./ffi_route
|
||||
import ./ffi_codegen_common
|
||||
|
||||
proc cReturnType(t: NimNode): NimNode =
|
||||
const passthroughCTypes = [
|
||||
"cint", "cuint", "clong", "culong", "clonglong", "culonglong", "cfloat", "cdouble",
|
||||
"cstring", "pointer",
|
||||
]
|
||||
|
||||
proc cReturnType(t: NimNode, exportName: string): NimNode =
|
||||
## Maps the native Nim return type to the C ABI type that crosses the boundary.
|
||||
## A type with no mapping is an error: emitting the Nim type as-is would export
|
||||
## a symbol whose ABI no host can call.
|
||||
if t.kind == nnkEmpty:
|
||||
return t # void
|
||||
return t
|
||||
if t.kind == nnkIdent:
|
||||
case $t
|
||||
of "int", "int32", "bool":
|
||||
of "int", "int64":
|
||||
# `int` is pointer-wide, so `cint` would truncate it on a 64-bit host.
|
||||
return ident("clonglong")
|
||||
of "int8", "int16", "int32", "bool":
|
||||
return ident("cint")
|
||||
of "uint", "uint64":
|
||||
return ident("culonglong")
|
||||
of "uint8", "uint16", "uint32":
|
||||
return ident("cuint")
|
||||
of "float", "float64":
|
||||
return ident("cdouble")
|
||||
of "float32":
|
||||
return ident("cfloat")
|
||||
of "string":
|
||||
return ident("cstring")
|
||||
else:
|
||||
discard
|
||||
return t # already a C-compatible type
|
||||
if $t in passthroughCTypes:
|
||||
return t
|
||||
error(
|
||||
"`.ffiExport.` proc " & exportName & " returns " & t.repr &
|
||||
", which has no C ABI mapping. Return a scalar, a bool, a string, a C type, " &
|
||||
"or nothing. For a richer return type, use `{.ffi.}`."
|
||||
)
|
||||
|
||||
proc withoutFFIPragmas(pragmas: NimNode): NimNode =
|
||||
## Drops only the pragma that routed the proc here, so a `raises` or `gcsafe`
|
||||
## the writer asked for still applies to the body.
|
||||
if pragmas.kind != nnkPragma:
|
||||
return newEmptyNode()
|
||||
var kept = nnkPragma.newTree()
|
||||
for p in pragmas:
|
||||
let name =
|
||||
if p.kind in {nnkExprColonExpr, nnkCall}:
|
||||
p[0]
|
||||
else:
|
||||
p
|
||||
if name.kind == nnkIdent and $name in ["ffi", "ffiExport"]:
|
||||
continue
|
||||
kept.add(p)
|
||||
return
|
||||
if kept.len == 0:
|
||||
newEmptyNode()
|
||||
else:
|
||||
kept
|
||||
|
||||
proc buildFFIExportProc*(prc: NimNode): NimNode {.compileTime.} =
|
||||
## Emits the synchronous C export. `{.ffi.}` and `{.ffiExport.}` share it.
|
||||
@ -48,50 +98,64 @@ proc buildFFIExportProc*(prc: NimNode): NimNode {.compileTime.} =
|
||||
let exportName = $procIdent(prc)
|
||||
let params = prc.params
|
||||
let nativeRet = params[0]
|
||||
let cRet = cReturnType(nativeRet)
|
||||
let cRet = cReturnType(nativeRet, exportName)
|
||||
|
||||
# The user body becomes a private impl proc. The exported wrapper converts the
|
||||
# result.
|
||||
# The user body becomes a private impl proc that the exported wrapper calls.
|
||||
let implName = genSym(nskProc, exportName & "Impl")
|
||||
var impl = copyNimTree(prc)
|
||||
impl[0] = implName # rename
|
||||
impl[4] = newEmptyNode() # remove the pragmas: this proc stays internal
|
||||
impl[0] = implName
|
||||
impl[4] = withoutFFIPragmas(prc[4])
|
||||
|
||||
let wrapName = ident(exportName)
|
||||
let boot = quote:
|
||||
when declared(initializeLibrary):
|
||||
initializeLibrary()
|
||||
|
||||
# A Nim exception that unwinds through a cdecl frame into the host is
|
||||
# undefined behaviour, so every wrapper catches and reports instead.
|
||||
let raiseNote = newLit("error: " & exportName & " raised: ")
|
||||
|
||||
var res = newStmtList(impl)
|
||||
|
||||
if nativeRet.kind == nnkIdent and $nativeRet == "string":
|
||||
# string -> const char*: keep the bytes alive in shared memory across the call.
|
||||
# One buffer per calling thread: a process-wide buffer would let one thread
|
||||
# free the bytes another thread is still reading.
|
||||
let buf = genSym(nskVar, exportName & "Buf")
|
||||
res.add quote do:
|
||||
var `buf` {.global.}: pointer = nil
|
||||
proc `wrapName`(): cstring {.exportc: `exportName`, cdecl, dynlib.} =
|
||||
var `buf` {.threadvar.}: pointer
|
||||
proc `wrapName`(): cstring {.exportc: `exportName`, cdecl, dynlib, raises: [].} =
|
||||
`boot`
|
||||
let s = `implName`()
|
||||
if `buf` != nil:
|
||||
var s = ""
|
||||
try:
|
||||
s = `implName`()
|
||||
except CatchableError as e:
|
||||
echo `raiseNote`, e.msg
|
||||
if not `buf`.isNil():
|
||||
deallocShared(`buf`)
|
||||
`buf` = allocShared(s.len + 1)
|
||||
if s.len > 0:
|
||||
copyMem(`buf`, unsafeAddr s[0], s.len)
|
||||
cast[ptr char](cast[uint](`buf`) + uint(s.len))[] = '\0'
|
||||
cast[ptr UncheckedArray[char]](`buf`)[s.len] = '\0'
|
||||
return cast[cstring](`buf`)
|
||||
|
||||
elif nativeRet.kind == nnkEmpty:
|
||||
res.add quote do:
|
||||
proc `wrapName`() {.exportc: `exportName`, cdecl, dynlib.} =
|
||||
proc `wrapName`() {.exportc: `exportName`, cdecl, dynlib, raises: [].} =
|
||||
`boot`
|
||||
`implName`()
|
||||
try:
|
||||
`implName`()
|
||||
except CatchableError as e:
|
||||
echo `raiseNote`, e.msg
|
||||
|
||||
else:
|
||||
# scalar: convert the native result to the C return type (cint / culonglong / …).
|
||||
res.add quote do:
|
||||
proc `wrapName`(): `cRet` {.exportc: `exportName`, cdecl, dynlib.} =
|
||||
proc `wrapName`(): `cRet` {.exportc: `exportName`, cdecl, dynlib, raises: [].} =
|
||||
`boot`
|
||||
return `cRet`(`implName`())
|
||||
try:
|
||||
return `cRet`(`implName`())
|
||||
except CatchableError as e:
|
||||
echo `raiseNote`, e.msg
|
||||
return `cRet`(0)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import ./c_macro_helpers
|
||||
import ./ffi_scalar
|
||||
import ./ffi_route
|
||||
import ./ffi_export
|
||||
import ./ffi_codegen_common
|
||||
when defined(ffiGenBindings):
|
||||
import ../codegen/rust
|
||||
import ../codegen/cpp
|
||||
@ -394,12 +395,7 @@ proc buildFFINewReqProc(reqTypeName, body: NimNode): NimNode =
|
||||
`reqObjIdent`.`fieldName` = `fieldName`
|
||||
)
|
||||
|
||||
let reqNameLit = newLit(
|
||||
if reqTypeName.kind == nnkPostfix:
|
||||
$reqTypeName[1]
|
||||
else:
|
||||
$reqTypeName
|
||||
)
|
||||
let reqNameLit = newLit($unwrapPostfix(reqTypeName))
|
||||
newBody.add(
|
||||
quote do:
|
||||
# Encode into shared memory, avoiding a second seq[byte] copy.
|
||||
@ -1087,12 +1083,7 @@ proc buildFFIProc(
|
||||
ffiBody.add(stmt)
|
||||
|
||||
let reqPtrIdent = genSym(nskLet, "reqPtr")
|
||||
let reqNameLit = newLit(
|
||||
if reqTypeName.kind == nnkPostfix:
|
||||
$reqTypeName[1]
|
||||
else:
|
||||
$reqTypeName
|
||||
)
|
||||
let reqNameLit = newLit($unwrapPostfix(reqTypeName))
|
||||
ffiBody.add quote do:
|
||||
let `reqPtrIdent` = FFIThreadRequest.initFromPtr(
|
||||
callback, userData, cstring(`reqNameLit`), reqCbor, int(reqCborLen)
|
||||
@ -1173,15 +1164,16 @@ macro ffi*(args: varargs[untyped]): untyped =
|
||||
error("`.ffi.` must be applied to a type or a proc definition")
|
||||
requireLibraryDeclared("`.ffi.`")
|
||||
|
||||
let path = routeFFIProc(prc)
|
||||
proc gatedABIFormat(what: string): ABIFormat =
|
||||
let abiFormat = resolveFFISpecs(leading)
|
||||
gateABIFormat(abiFormat, what)
|
||||
return abiFormat
|
||||
|
||||
# An event may lead with a wire-name literal, which the ABI parser rejects, so
|
||||
# it resolves its own specs.
|
||||
if path == fpEvent:
|
||||
case routeFFIProc(prc)
|
||||
of fpEvent:
|
||||
# An event may lead with a wire-name literal, which the ABI parser rejects,
|
||||
# so it resolves its own specs.
|
||||
return buildFFIEventProc(prc, leading)
|
||||
|
||||
let abiFormat = resolveFFISpecs(leading)
|
||||
case path
|
||||
of fpExport:
|
||||
# The export crosses the ABI with its own return value, so no ABI applies.
|
||||
if leading.len > 0:
|
||||
@ -1191,15 +1183,11 @@ macro ffi*(args: varargs[untyped]): untyped =
|
||||
)
|
||||
return buildFFIExportProc(prc)
|
||||
of fpDtor:
|
||||
gateABIFormat(abiFormat, "`.ffi.` destructor")
|
||||
return buildFFIDtorProc(prc, abiFormat)
|
||||
return buildFFIDtorProc(prc, gatedABIFormat("`.ffi.` destructor"))
|
||||
of fpStatic:
|
||||
gateABIFormat(abiFormat, "`.ffi.` static proc")
|
||||
return buildFFIProc(prc, abiFormat, isStatic = true)
|
||||
return buildFFIProc(prc, gatedABIFormat("`.ffi.` static proc"), isStatic = true)
|
||||
of fpMethod:
|
||||
return buildFFIProc(prc, abiFormat, isStatic = false)
|
||||
of fpEvent:
|
||||
error("unreachable: the event path returns above")
|
||||
return buildFFIProc(prc, resolveFFISpecs(leading), isStatic = false)
|
||||
|
||||
macro ffiStatic*(args: varargs[untyped]): untyped =
|
||||
## Context-independent `{.ffi.}`: no library receiver, and no `ctx` in the C
|
||||
@ -1259,12 +1247,7 @@ proc buildCtorFFINewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN
|
||||
let retType = newTree(nnkPtrTy, ident("FFIThreadRequest"))
|
||||
formalParams = @[retType] & formalParams
|
||||
|
||||
let reqNameLit = newLit(
|
||||
if reqTypeName.kind == nnkPostfix:
|
||||
$reqTypeName[1]
|
||||
else:
|
||||
$reqTypeName
|
||||
)
|
||||
let reqNameLit = newLit($unwrapPostfix(reqTypeName))
|
||||
var newBody = newStmtList()
|
||||
newBody.add quote do:
|
||||
return FFIThreadRequest.initFromPtr(
|
||||
@ -1846,7 +1829,7 @@ proc buildFFIEventProc(prc: NimNode, leading: seq[NimNode]): NimNode {.compileTi
|
||||
newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, dispatchPayload))
|
||||
|
||||
var newParams = newSeq[NimNode]()
|
||||
newParams.add(formalParams[0]) # return type (typically empty/void)
|
||||
newParams.add(formalParams[0])
|
||||
for i in 1 ..< formalParams.len:
|
||||
newParams.add(formalParams[i])
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
import std/macros
|
||||
import ../codegen/meta
|
||||
import ./ffi_codegen_common
|
||||
|
||||
type FFIPath* = enum
|
||||
fpMethod ## A library or handle receiver, and an async result.
|
||||
@ -56,16 +57,9 @@ proc isLibReceiver(t: NimNode): bool {.compileTime.} =
|
||||
return false
|
||||
return ($t == currentLibType and currentLibType.len > 0) or isFFIHandleTypeName($t)
|
||||
|
||||
func procIdent*(prc: NimNode): NimNode =
|
||||
return
|
||||
if prc[0].kind == nnkPostfix:
|
||||
prc[0][1]
|
||||
else:
|
||||
prc[0]
|
||||
|
||||
proc routeFFIProc*(prc: NimNode): FFIPath {.compileTime.} =
|
||||
## Reads the receiver and the return type, then names the path.
|
||||
let params = prc[3]
|
||||
let params = prc.params
|
||||
let ret = params[0]
|
||||
let hasReceiver = params.len > 1 and isLibReceiver(params[1][1])
|
||||
|
||||
@ -88,7 +82,7 @@ proc assertFFIPath*(prc: NimNode, want: FFIPath) {.compileTime.} =
|
||||
# A receiver is the one mismatch a caller can read straight off the signature.
|
||||
if want == fpStatic and got == fpMethod:
|
||||
error(
|
||||
"`.ffiStatic.` proc " & name & " takes " & prc[3][1][1].repr &
|
||||
"`.ffiStatic.` proc " & name & " takes " & prc.params[1][1].repr &
|
||||
" as its first parameter, which is the library type or an {.ffiHandle.} type. " &
|
||||
"A receiver belongs to a context. Make it an `{.ffi.}` method instead."
|
||||
)
|
||||
|
||||
@ -2,20 +2,7 @@
|
||||
|
||||
import std/macros
|
||||
import ../codegen/meta
|
||||
|
||||
proc buildLibReadyGuard*(
|
||||
ctxHandlerName, libTypeName: NimNode
|
||||
): NimNode {.compileTime.} =
|
||||
## Rejects a request that reaches the FFI thread before the ctor stores a
|
||||
## library. The guard applies only to a `ref` type. For an `object` type the
|
||||
## fallback is a usable zero value, but for a `ref` type it is nil. The guard
|
||||
## runs in the handler, behind the ctor in the queue. Thus a host can send a
|
||||
## call before it waits for the create callback.
|
||||
quote:
|
||||
when `libTypeName` is ref:
|
||||
if not `ctxHandlerName`[].libReady.load():
|
||||
return
|
||||
err("library is not initialized: the constructor failed or has not run yet")
|
||||
import ./ffi_codegen_common
|
||||
|
||||
const scalarPodTypeNames = [
|
||||
"int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32",
|
||||
|
||||
@ -34,6 +34,21 @@ proc router_version*(): Future[Result[string, string]] {.ffi.} =
|
||||
proc router_alive*(): int {.ffi.} =
|
||||
7
|
||||
|
||||
# `int` is pointer-wide, so the export must not narrow it to a C `int`.
|
||||
proc router_big*(): int {.ffi.} =
|
||||
int(high(int32)) + 1
|
||||
|
||||
proc router_banner*(): string {.ffi.} =
|
||||
"router banner"
|
||||
|
||||
var routerBeats = 0
|
||||
|
||||
proc router_beat*() {.ffi.} =
|
||||
inc routerBeats
|
||||
|
||||
proc router_raises*(): int {.ffi.} =
|
||||
raise newException(ValueError, "boom")
|
||||
|
||||
# A library receiver and no result, so the router picks the destructor.
|
||||
proc router_destroy*(lib: RouterLib) {.ffi.} =
|
||||
discard
|
||||
@ -121,7 +136,21 @@ suite "{.ffi.} routes on the shape of the signature":
|
||||
|
||||
test "no arguments and a plain return type route to the synchronous export":
|
||||
# The export returns its value directly, with no context and no callback.
|
||||
check router_alive() == cint(7)
|
||||
check router_alive() == clonglong(7)
|
||||
|
||||
test "an int export keeps its full width across the C ABI":
|
||||
check router_big() == clonglong(int32.high) + 1
|
||||
|
||||
test "a string export returns bytes the caller can read":
|
||||
check $router_banner() == "router banner"
|
||||
|
||||
test "a no-return export routes to a void C symbol":
|
||||
let before = routerBeats
|
||||
router_beat()
|
||||
check routerBeats == before + 1
|
||||
|
||||
test "an exception in the body never crosses the C ABI":
|
||||
check router_raises() == clonglong(0)
|
||||
|
||||
test "a payload parameter and no result route to the event":
|
||||
var s: CallbackState
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user