mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 06:23:19 +00:00
143 lines
5.9 KiB
Nim
143 lines
5.9 KiB
Nim
import std/[atomics, sysatomics]
|
|
import results
|
|
import ./ffi_context
|
|
|
|
const MaxFFIContexts* = 32
|
|
|
|
type
|
|
StaticCtxState = enum
|
|
## Lifecycle of the pool's `{.ffiStatic.}` context; see `staticFFIContext`.
|
|
StaticCtxNone
|
|
StaticCtxCreating
|
|
StaticCtxDestroying
|
|
StaticCtxReady
|
|
|
|
FFIContextPool*[T] = object
|
|
## Fixed pool of FFI contexts, plus the one `{.ffiStatic.}` context. Each
|
|
## slot's worker + event threads and signal fds are built once (on first
|
|
## use) and reused across create/recycle cycles — recycle keeps them alive,
|
|
## so repeated create/destroy does not churn fds. Bounds ThreadSignalPtr fds
|
|
## at MaxFFIContexts * (signals per ctx).
|
|
contexts: array[MaxFFIContexts, FFIContext[T]]
|
|
initialized: array[MaxFFIContexts, Atomic[bool]]
|
|
staticCtx: Atomic[pointer]
|
|
staticState: Atomic[StaticCtxState]
|
|
|
|
proc releaseSlot[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]) =
|
|
## Full-teardown release: the slot must be rebuilt before it serves again.
|
|
for i in 0 ..< MaxFFIContexts:
|
|
if pool.contexts[i].addr == ctx:
|
|
pool.initialized[i].store(false)
|
|
break
|
|
ctx.releaseClaim()
|
|
|
|
proc createFFIContext*[T](
|
|
pool: var FFIContextPool[T]
|
|
): Result[ptr FFIContext[T], string] =
|
|
## Acquires a context from the fixed pool. A slot's worker is built once on
|
|
## first use and reused (markAsActive) on every later acquisition.
|
|
for i in 0 ..< MaxFFIContexts:
|
|
let ctx = pool.contexts[i].addr
|
|
if not ctx.tryClaim():
|
|
continue
|
|
if pool.initialized[i].load():
|
|
# Reused slot: a prior recycle drained and released it; worker still alive.
|
|
ctx.markAsActive()
|
|
return ok(ctx)
|
|
initContextResources(ctx).isOkOr:
|
|
ctx.releaseClaim()
|
|
return err("createFFIContext: initContextResources failed: " & $error)
|
|
pool.initialized[i].store(true)
|
|
return ok(ctx)
|
|
err("FFI context pool exhausted (max " & $MaxFFIContexts & " contexts)")
|
|
|
|
proc isStaticCtx[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]): bool =
|
|
## True while `ctx` is the pool's static context, including mid-teardown.
|
|
# `staticCtx` is cleared only once the slot is released, so matching on the
|
|
# pointer covers `Destroying` too.
|
|
pool.staticCtx.load() == cast[pointer](ctx)
|
|
|
|
proc recycleFFIContext*[T](
|
|
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
|
|
): Result[void, string] =
|
|
## Normal teardown: drains in-flight handlers, frees the lib and returns the
|
|
## slot to the pool WITHOUT stopping its threads, so a later createFFIContext
|
|
## reuses them. Synchronous (waits for the FFI thread to finish draining).
|
|
# Recycling it would release the slot while `staticState` still points at it.
|
|
if pool.isStaticCtx(ctx):
|
|
return err("recycleFFIContext(pool): the {.ffiStatic.} context outlives every ctx")
|
|
ctx.requestRecycle()
|
|
|
|
proc destroyFFIContext*[T](
|
|
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
|
|
): Result[void, string] =
|
|
## Full teardown: stops/joins the threads and frees resources, marking the slot
|
|
## uninitialised so a later createFFIContext rebuilds it; normal cleanup uses
|
|
## recycleFFIContext. On thread-exit timeout the slot is leaked; closing
|
|
## live-thread resources is unsafe.
|
|
# Destroying it would release the slot while `staticState` still points at it.
|
|
if pool.isStaticCtx(ctx):
|
|
return err("destroyFFIContext(pool): the {.ffiStatic.} context outlives every ctx")
|
|
ctx.stopAndJoinThreads().isOkOr:
|
|
return err("destroyFFIContext(pool): " & $error)
|
|
let deinitRes = ctx.deinitContextResources()
|
|
pool.releaseSlot(ctx)
|
|
deinitRes.isOkOr:
|
|
return err("destroyFFIContext(pool): " & $error)
|
|
ok()
|
|
|
|
proc staticFFIContext*[T](
|
|
pool: var FFIContextPool[T]
|
|
): Result[ptr FFIContext[T], string] =
|
|
## The pool's `{.ffiStatic.}` context, created on first use: a static proc has
|
|
## no ctx of its own, but its handler still needs an FFI thread.
|
|
# Holds its slot until `destroyStaticFFIContext`, so `pool` must outlive its
|
|
# threads: only call this on the global `declareLibrary` emits. `myLib` stays
|
|
# the zero value. A failed create resets to `StaticCtxNone` so waiters retry.
|
|
while true:
|
|
case pool.staticState.load()
|
|
of StaticCtxReady:
|
|
return ok(cast[ptr FFIContext[T]](pool.staticCtx.load()))
|
|
of StaticCtxCreating, StaticCtxDestroying:
|
|
cpuRelax()
|
|
of StaticCtxNone:
|
|
var expected = StaticCtxNone
|
|
if not pool.staticState.compareExchange(expected, StaticCtxCreating):
|
|
continue
|
|
let ctx = pool.createFFIContext().valueOr:
|
|
pool.staticState.store(StaticCtxNone)
|
|
return err("staticFFIContext: " & error)
|
|
pool.staticCtx.store(cast[pointer](ctx))
|
|
pool.staticState.store(StaticCtxReady)
|
|
return ok(ctx)
|
|
|
|
proc destroyStaticFFIContext*[T](pool: var FFIContextPool[T]): Result[void, string] =
|
|
## Teardown counterpart to `staticFFIContext`: stops the static context's
|
|
## threads and frees its slot. A no-op when there is no static context.
|
|
# Claiming `Ready -> Destroying` serialises concurrent teardowns; it does not
|
|
# make teardown safe against a static call already in flight.
|
|
var expected = StaticCtxReady
|
|
if not pool.staticState.compareExchange(expected, StaticCtxDestroying):
|
|
return ok()
|
|
let ctx = cast[ptr FFIContext[T]](pool.staticCtx.load())
|
|
ctx.stopAndJoinThreads().isOkOr:
|
|
# Threads are still live: leak the slot rather than free resources under them.
|
|
pool.staticState.store(StaticCtxReady)
|
|
return err("destroyStaticFFIContext: " & $error)
|
|
let deinitRes = ctx.deinitContextResources()
|
|
pool.releaseSlot(ctx)
|
|
pool.staticCtx.store(nil)
|
|
pool.staticState.store(StaticCtxNone)
|
|
deinitRes.isOkOr:
|
|
return err("destroyStaticFFIContext: " & $error)
|
|
ok()
|
|
|
|
proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =
|
|
## Rejects nil / dangling pointers at the API boundary.
|
|
if ctx.isNil():
|
|
return false
|
|
for i in 0 ..< MaxFFIContexts:
|
|
if cast[pointer](pool.contexts[i].addr) == ctx:
|
|
return pool.contexts[i].addr.isInUse()
|
|
false
|