mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-21 00:40:16 +00:00
destroyFFIContext stopped/joined the worker threads and marked the slot for rebuild, but no longer deinited the context — so the six ThreadSignalPtr fds were orphaned every full teardown (the exact leak this path exists to prevent), and the still-initialised Lock + event registry/queue locks were left live. createFFIContext's rebuild path (initialized == false) reruns initContextResources, which calls initLock / initEventRegistry / initEventQueue and installs fresh signals over the stale handles — re-initialising a live lock is UB. Restore the deinitContextResources() call (as the pre-recycle code did) before marking the slot uninitialised so the rebuild starts from clean state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.3 KiB
Nim
77 lines
3.3 KiB
Nim
import std/atomics
|
|
import results
|
|
import ./ffi_context, ./ffi_types
|
|
|
|
const MaxFFIContexts* = 32
|
|
## Maximum number of concurrently live FFI contexts when using FFIContextPool.
|
|
## Each slot's threads, signals and dispatcher kqueue fds are created once and
|
|
## reused, so this also caps the process's steady-state fd usage.
|
|
|
|
type FFIContextPool*[T] = object
|
|
contexts: array[MaxFFIContexts, FFIContext[T]]
|
|
initialized: array[MaxFFIContexts, Atomic[bool]]
|
|
## Whether the slot's worker has been built. Once true it stays true for the
|
|
## process lifetime — destroy recycles the context rather than tearing it down.
|
|
|
|
proc createFFIContext*[T](
|
|
pool: var FFIContextPool[T]
|
|
): Result[ptr FFIContext[T], string] =
|
|
## Acquires a context from the fixed pool. The worker (threads + signals +
|
|
## dispatcher kqueues) is built once on first use and REUSED on every later
|
|
## acquisition — chronos never frees a dispatcher's kqueue fd, so a
|
|
## thread-per-context model would leak fds unboundedly.
|
|
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 destroy drained and parked it; worker still alive.
|
|
ctx.markAsActive()
|
|
return ok(ctx)
|
|
initContextResources(ctx).isOkOr:
|
|
ctx.release()
|
|
return err("createFFIContext: initContextResources failed: " & $error)
|
|
pool.initialized[i].store(true)
|
|
return ok(ctx)
|
|
return err("FFI context pool exhausted (max " & $MaxFFIContexts & " contexts)")
|
|
|
|
proc releaseFFIContext*[T](
|
|
ctx: ptr FFIContext[T], callback: FFICallBack, userData: pointer
|
|
): Result[void, string] =
|
|
## Recycle/park the context for reuse without stopping its worker threads.
|
|
## `callback` fires once the FFI thread has drained and parked it.
|
|
ctx.requestRecycle(callback, userData)
|
|
|
|
proc destroyFFIContext*[T](
|
|
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
|
|
): Result[void, string] =
|
|
## Full teardown: stops/joins the worker threads and returns the context to the
|
|
## pool, marking it uninitialised so a later createFFIContext rebuilds it. Only
|
|
## for process/pool shutdown — normal destruction uses releaseFFIContext.
|
|
ctx.stopAndJoinThreads().isOkOr:
|
|
return err("destroyFFIContext(pool): " & $error)
|
|
# Close the ThreadSignalPtr fds and deinit the lock + event registry/queue
|
|
# BEFORE the slot is marked for rebuild. createFFIContext's rebuild path reruns
|
|
# initContextResources (initLock / initEventRegistry / initEventQueue + fresh
|
|
# signals); skipping deinit here would re-init still-live locks (UB) and orphan
|
|
# the old fds — the very leak this teardown exists to prevent.
|
|
let deinitRes = ctx.deinitContextResources()
|
|
for i in 0 ..< MaxFFIContexts:
|
|
if pool.contexts[i].addr == ctx:
|
|
pool.initialized[i].store(false)
|
|
break
|
|
ctx.release()
|
|
deinitRes.isOkOr:
|
|
return err("destroyFFIContext(pool): " & $error)
|
|
return ok()
|
|
|
|
proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =
|
|
## True only if ctx points to one of the pool's contexts that is currently
|
|
## claimed (in use). Rejects nil / dangling / parked contexts at the API boundary.
|
|
if ctx.isNil():
|
|
return false
|
|
for i in 0 ..< MaxFFIContexts:
|
|
if cast[pointer](pool.contexts[i].addr) == ctx:
|
|
return cast[ptr FFIContext[T]](ctx).isInUse()
|
|
return false
|