nim-ffi/ffi/ffi_context_pool.nim
2026-07-23 15:34:11 -03:00

91 lines
3.3 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
StaticCtxReady
FFIContextPool*[T] = object
## Fixed pool. Bounds ThreadSignalPtr fds at MaxFFIContexts * 2.
slots: array[MaxFFIContexts, FFIContext[T]]
inUse: array[MaxFFIContexts, Atomic[bool]]
staticCtx: Atomic[pointer]
staticState: Atomic[StaticCtxState]
proc acquireSlot[T](pool: var FFIContextPool[T]): Result[ptr FFIContext[T], string] =
for i in 0 ..< MaxFFIContexts:
var expected = false
if pool.inUse[i].compareExchange(expected, true):
return ok(pool.slots[i].addr)
err("FFI context pool exhausted (max " & $MaxFFIContexts & " contexts)")
proc releaseSlot[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]) =
for i in 0 ..< MaxFFIContexts:
if pool.slots[i].addr == ctx:
pool.inUse[i].store(false)
return
proc createFFIContext*[T](
pool: var FFIContextPool[T]
): Result[ptr FFIContext[T], string] =
let ctx = pool.acquireSlot().valueOr:
return err("createFFIContext: acquireSlot failed: " & $error)
initContextResources(ctx).isOkOr:
pool.releaseSlot(ctx)
return err("createFFIContext: initContextResources failed: " & $error)
ok(ctx)
proc destroyFFIContext*[T](
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
): Result[void, string] =
## On thread-exit timeout the slot is leaked; closing live-thread resources is unsafe.
ctx.stopAndJoinThreads().isOkOr:
return err("destroyFFIContext(pool): " & $error)
# Required: next acquisition would otherwise re-init a live lock (UB).
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: a static proc has no ctx of its own, but
## its handler still needs an FFI thread. Created on first use and never
## destroyed, so it holds a slot for good and `pool` must outlive its threads —
## only ever call this on the global `declareLibrary` emits. `myLib` stays the
## zero value; a static handler must not touch it. A failed create resets to
## `StaticCtxNone` so the spinning losers retry instead of hanging.
while true:
case pool.staticState.load()
of StaticCtxReady:
return ok(cast[ptr FFIContext[T]](pool.staticCtx.load()))
of StaticCtxCreating:
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 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.slots[i].addr) == ctx:
return pool.inUse[i].load()
false