@@ -18,13 +18,27 @@ type FFIContext*[T] = object
1818 reqSignal: ThreadSignalPtr # to notify the FFI Thread that a new request is sent
1919 reqReceivedSignal: ThreadSignalPtr
2020 # to signal main thread, interfacing with the FFI thread, that FFI thread received the request
21+ watchdogStopSignal: ThreadSignalPtr
22+ # fired by destroyFFIContext so the watchdog exits immediately instead of waiting out its sleep
2123 userData* : pointer
2224 eventCallback* : pointer
2325 eventUserdata* : pointer
2426 running: Atomic [bool ] # To control when the threads are running
2527 registeredRequests: ptr Table [cstring , FFIRequestProc ]
2628 # Pointer to with the registered requests at compile time
2729
30+ const MaxFFIContexts * = 32
31+ # # Maximum number of concurrently live FFI contexts when using FFIContextPool.
32+ # # Fds and threads are only consumed for slots that are actually acquired,
33+ # # so this value only affects the upfront memory of the pool array.
34+
35+ type FFIContextPool * [T] = object
36+ # # Fixed-size pool of FFI contexts. Avoids dynamic heap allocation per context
37+ # # and bounds the total number of file descriptors consumed by ThreadSignalPtrs
38+ # # to at most MaxFFIContexts * 2.
39+ slots: array [MaxFFIContexts , FFIContext [T]]
40+ inUse: array [MaxFFIContexts , Atomic [bool ]]
41+
2842const git_version* {.strdefine .} = " n/a"
2943
3044template callEventCallback * (ctx: ptr FFIContext , eventName: string , body: untyped ) =
@@ -110,13 +124,19 @@ proc watchdogThreadBody(ctx: ptr FFIContext) {.thread.} =
110124 const WatchdogTimeinterval = 1 .seconds
111125 const WatchdogTimeout = 20 .seconds
112126
113- # Give time for the node to be created and up before sending watchdog requests
114- await sleepAsync (WatchdogStartDelay )
115- while true :
116- await sleepAsync (WatchdogTimeinterval )
117-
118- if ctx.running.load == false :
119- debug " Watchdog thread exiting because FFIContext is not running"
127+ # Give time for the node to be created and up before sending watchdog requests.
128+ # waitSync returns early if watchdogStopSignal fires (i.e. on destroy).
129+ let startWait = ctx.watchdogStopSignal.waitSync (WatchdogStartDelay )
130+ if startWait.isErr ():
131+ error " watchdog: start-delay waitSync failed" , err = startWait.error
132+ elif startWait.get ():
133+ return # stop signal fired during start delay
134+
135+ while ctx.running.load:
136+ let intervalWait = ctx.watchdogStopSignal.waitSync (WatchdogTimeinterval )
137+ if intervalWait.isErr ():
138+ error " watchdog: interval waitSync failed" , err = intervalWait.error
139+ elif intervalWait.get () or not ctx.running.load:
120140 break
121141
122142 let callback = proc (
@@ -164,7 +184,9 @@ proc processRequest[T](
164184 try :
165185 await retFut
166186 except CatchableError as exc:
167- Result [string , string ].err (" Exception in processRequest for " & reqId & " : " & exc.msg)
187+ Result [string , string ].err (
188+ " Exception in processRequest for " & reqId & " : " & exc.msg
189+ )
168190
169191 # # handleRes may raise (OOM, GC setup) even though it is rare. Catching here
170192 # # keeps the async proc raises:[] compatible. The defer inside handleRes
@@ -208,42 +230,56 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
208230
209231 waitFor ffiRun (ctx)
210232
211- proc cleanUpResources [T](ctx: ptr FFIContext [T]): Result [void , string ] =
212- defer :
213- freeShared (ctx)
233+ proc closeResources [T](ctx: ptr FFIContext [T]): Result [void , string ] =
234+ # # Closes file descriptors and deinits the lock. Does NOT free ctx memory.
235+ # # Used by initContextResources error paths and pool destroy, where ctx is
236+ # # not heap-allocated (pool slots live in a fixed array, not on the heap).
214237 ctx.lock.deinitLock ()
215238 if not ctx.reqSignal.isNil ():
216239 ? ctx.reqSignal.close ()
217240 if not ctx.reqReceivedSignal.isNil ():
218241 ? ctx.reqReceivedSignal.close ()
242+ if not ctx.watchdogStopSignal.isNil ():
243+ ? ctx.watchdogStopSignal.close ()
219244 return ok ()
220245
221- proc createFFIContext * [T](): Result [ptr FFIContext [T], string ] =
222- # # This proc is called from the main thread and it creates
223- # # the FFI working thread.
224- var ctx = createShared (FFIContext [T], 1 )
246+ proc cleanUpResources [T](ctx: ptr FFIContext [T]): Result [void , string ] =
247+ # # Full cleanup for heap-allocated contexts: closes all resources and frees memory.
248+ defer :
249+ freeShared (ctx)
250+ return ctx.closeResources ()
251+
252+ proc initContextResources [T](ctx: ptr FFIContext [T]): Result [void , string ] =
253+ # # Initialises all resources inside an already-allocated FFIContext slot.
254+ # # On failure every partially-initialised resource is closed; the caller
255+ # # is responsible for releasing the slot (freeShared or pool.releaseSlot).
225256 ctx.lock.initLock ()
226257
227258 ctx.reqSignal = ThreadSignalPtr .new ().valueOr:
228- ctx.cleanUpResources ().isOkOr:
229- return err (" could not clean resources in a failure new reqSignal : " & $ error)
259+ ctx.closeResources ().isOkOr:
260+ return err (" could not close resources after reqSignal failure: " & $ error)
230261 return err (" couldn't create reqSignal ThreadSignalPtr: " & $ error)
231262
232263 ctx.reqReceivedSignal = ThreadSignalPtr .new ().valueOr:
233- ctx.cleanUpResources ().isOkOr:
234- return
235- err (" could not clean resources in a failure new reqReceivedSignal: " & $ error)
264+ ctx.closeResources ().isOkOr:
265+ return err (" could not close resources after reqReceivedSignal failure: " & $ error)
236266 return err (" couldn't create reqReceivedSignal ThreadSignalPtr" )
237267
268+ ctx.watchdogStopSignal = ThreadSignalPtr .new ().valueOr:
269+ ctx.closeResources ().isOkOr:
270+ return
271+ err (" could not close resources after watchdogStopSignal failure: " & $ error)
272+ return err (" couldn't create watchdogStopSignal ThreadSignalPtr" )
273+
238274 ctx.registeredRequests = addr ffi_types.registeredRequests
239275
240276 ctx.running.store (true )
241277
242278 try :
243279 createThread (ctx.ffiThread, ffiThreadBody[T], ctx)
244280 except ValueError , ResourceExhaustedError :
245- ctx.cleanUpResources ().isOkOr:
246- error " failed to clean up resources after ffiThread creation failure" , err = error
281+ ctx.closeResources ().isOkOr:
282+ error " failed to close resources after ffiThread creation failure" , err = error
247283 return err (" failed to create the FFI thread: " & getCurrentExceptionMsg ())
248284
249285 try :
@@ -254,28 +290,84 @@ proc createFFIContext*[T](): Result[ptr FFIContext[T], string] =
254290 if fireRes.isErr ():
255291 error " failed to signal ffiThread during watchdog cleanup" , err = fireRes.error
256292 joinThread (ctx.ffiThread)
257- ctx.cleanUpResources ().isOkOr:
258- error " failed to clean up resources after watchdogThread creation failure" , err = error
293+ ctx.closeResources ().isOkOr:
294+ error " failed to close resources after watchdogThread creation failure" ,
295+ err = error
259296 return err (" failed to create the watchdog thread: " & getCurrentExceptionMsg ())
260297
298+ return ok ()
299+
300+ # ── Pool helpers ─────────────────────────────────────────────────────────────
301+
302+ proc acquireSlot [T](pool: var FFIContextPool [T]): Result [ptr FFIContext [T], string ] =
303+ for i in 0 ..< MaxFFIContexts :
304+ var expected = false
305+ if pool.inUse[i].compareExchange (expected, true ):
306+ return ok (pool.slots[i].addr )
307+ return err (" FFI context pool exhausted (max " & $ MaxFFIContexts & " contexts)" )
308+
309+ proc releaseSlot [T](pool: var FFIContextPool [T], ctx: ptr FFIContext [T]) =
310+ for i in 0 ..< MaxFFIContexts :
311+ if pool.slots[i].addr == ctx:
312+ pool.inUse[i].store (false )
313+ return
314+
315+ # ── Public API ────────────────────────────────────────────────────────────────
316+
317+ proc createFFIContext * [T](): Result [ptr FFIContext [T], string ] =
318+ # # Creates a heap-allocated FFI context. The caller must call destroyFFIContext(ctx)
319+ # # to release it. Prefer the pool overload when the maximum context count is known.
320+ var ctx = createShared (FFIContext [T], 1 )
321+ initContextResources (ctx).isOkOr:
322+ freeShared (ctx)
323+ return err (error)
261324 return ok (ctx)
262325
263- proc destroyFFIContext * [T](ctx: ptr FFIContext [T]): Result [void , string ] =
326+ proc createFFIContext * [T](
327+ pool: var FFIContextPool [T]
328+ ): Result [ptr FFIContext [T], string ] =
329+ # # Acquires a slot from the fixed pool and initialises it as an FFI context.
330+ # # Bounded fd usage: at most MaxFFIContexts * 2 ThreadSignalPtr fds are ever open.
331+ let ctx = pool.acquireSlot ().valueOr:
332+ return err (error)
333+ initContextResources (ctx).isOkOr:
334+ pool.releaseSlot (ctx)
335+ return err (error)
336+ return ok (ctx)
337+
338+ proc signalStop [T](ctx: ptr FFIContext [T]): Result [void , string ] =
264339 ctx.running.store (false )
340+ let ffiSignaled = ctx.reqSignal.fireSync ().valueOr:
341+ ctx.onNotResponding ()
342+ return err (" error signaling reqSignal in destroyFFIContext: " & $ error)
343+ if not ffiSignaled:
344+ ctx.onNotResponding ()
345+ return err (" failed to signal reqSignal on time in destroyFFIContext" )
346+ let wdSignaled = ctx.watchdogStopSignal.fireSync ().valueOr:
347+ return err (" error signaling watchdogStopSignal in destroyFFIContext: " & $ error)
348+ if not wdSignaled:
349+ return err (" failed to signal watchdogStopSignal on time in destroyFFIContext" )
350+ return ok ()
351+
352+ proc destroyFFIContext * [T](ctx: ptr FFIContext [T]): Result [void , string ] =
265353 defer :
266354 joinThread (ctx.ffiThread)
267355 joinThread (ctx.watchdogThread)
268356 ctx.cleanUpResources ().isOkOr:
269357 error " failed to clean up resources in destroyFFIContext" , err = error
358+ return ctx.signalStop ()
270359
271- let signaledOnTime = ctx.reqSignal.fireSync ().valueOr:
272- ctx.onNotResponding ()
273- return err (" error in destroyFFIContext: " & $ error)
274- if not signaledOnTime:
275- ctx.onNotResponding ()
276- return err (" failed to signal reqSignal on time in destroyFFIContext" )
277-
278- return ok ()
360+ proc destroyFFIContext * [T](
361+ pool: var FFIContextPool [T], ctx: ptr FFIContext [T]
362+ ): Result [void , string ] =
363+ # # Stops the FFI context and returns its slot to the pool.
364+ defer :
365+ joinThread (ctx.ffiThread)
366+ joinThread (ctx.watchdogThread)
367+ ctx.closeResources ().isOkOr:
368+ error " failed to close resources in destroyFFIContext" , err = error
369+ pool.releaseSlot (ctx)
370+ return ctx.signalStop ()
279371
280372template checkParams * (ctx: ptr FFIContext , callback: FFICallBack , userData: pointer ) =
281373 if not isNil (ctx):
0 commit comments