mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-03 06:39:30 +00:00
Two foreign-host concurrency fixes for refc, both needed so a Go host can hammer the FFI under load without corrupting Nim's per-thread GC. 1. Compile-time request ids. ffiNewReq / the method + ctor wrappers built the request id with `$T` at runtime, allocating a Nim GC string on the foreign caller's (often transient) thread. Emit a `cstring` literal of the type name instead — no allocation on the caller thread. 2. Recycle pooled contexts instead of destroy/recreate. Restores the release/v0.1 model that v0.2 dropped: a pool slot's worker + event threads and signal fds are built once and reused. The ffiDtor now requests a synchronous recycle (drain in-flight handlers, free the lib, clear listeners, release the slot) on the FFI thread, keeping the threads alive; createFFIContext reuses an initialised slot. Without this every create/destroy churned ~6 signal fds, so fd numbers climbed past FD_SETSIZE (1024) and ThreadSignalPtr.waitSync's select() failed with EINVAL under create/destroy load. Adds CtxLifecycle (Active/RecyclePending/Recycling), ctx-level inUse/tryClaim/release/markAsActive, requestRecycle (waits on a new recycleDoneSignal), freeLib (refc GC_unref / orc =destroy of ctor-owned libs), recycleContext, FFIEventRegistry.clearListeners, and roots the ctor-stored ref lib under refc (GC_ref, balanced in freeLib). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.7 KiB
Nim
70 lines
2.7 KiB
Nim
import std/atomics
|
|
import results
|
|
import ./ffi_context
|
|
|
|
const MaxFFIContexts* = 32
|
|
# Only affects upfront pool memory; fds/threads consumed per acquired slot.
|
|
|
|
type FFIContextPool*[T] = object
|
|
## Fixed pool. 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]]
|
|
|
|
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.release()
|
|
return err("createFFIContext: initContextResources failed: " & $error)
|
|
pool.initialized[i].store(true)
|
|
return ok(ctx)
|
|
err("FFI context pool exhausted (max " & $MaxFFIContexts & " contexts)")
|
|
|
|
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).
|
|
ctx.requestRecycle()
|
|
|
|
proc destroyFFIContext*[T](
|
|
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
|
|
): Result[void, string] =
|
|
## Full teardown: stops/joins the worker + event threads and frees resources,
|
|
## marking the slot uninitialised so a later createFFIContext rebuilds it.
|
|
## Used for process-level shutdown; normal cleanup uses recycleFFIContext.
|
|
ctx.stopAndJoinThreads().isOkOr:
|
|
return err("destroyFFIContext(pool): " & $error)
|
|
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)
|
|
ok()
|
|
|
|
proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =
|
|
## Rejects nil / offset-invalid / 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 cast[ptr FFIContext[T]](ctx).isInUse()
|
|
false
|