22{.pragma : callback, cdecl , raises : [], gcsafe .}
33{.passc : " -fPIC" .}
44
5- import std/ [options, atomics, os, net, locks, json, tables, sets ]
5+ import std/ [atomics, locks, json, tables]
66import chronicles, chronos, chronos/ threadsync, taskpools/ channels_spsc_single, results
77import ./ ffi_types, ./ ffi_thread_request, ./ internal/ ffi_macro, ./ logging
88
@@ -41,30 +41,6 @@ var ffiCurrentCallbackState* {.threadvar.}: ptr FFICallbackState
4141
4242const git_version* {.strdefine .} = " n/a"
4343
44- var contextRegistry = initHashSet [pointer ]()
45- var contextRegistryLock: Lock
46- contextRegistryLock.initLock ()
47-
48- proc registerCtx (ctx: pointer ) =
49- {.cast (gcsafe).}:
50- contextRegistryLock.acquire ()
51- defer : contextRegistryLock.release ()
52- contextRegistry.incl (ctx)
53-
54- proc unregisterCtx (ctx: pointer ) =
55- {.cast (gcsafe).}:
56- contextRegistryLock.acquire ()
57- defer : contextRegistryLock.release ()
58- contextRegistry.excl (ctx)
59-
60- proc isValidCtx * (ctx: pointer ): bool =
61- # # Returns true only if ctx was created by createFFIContext and not yet destroyed.
62- # # Rejects nil, offset-invalid, and dangling pointers at the API boundary.
63- {.cast (gcsafe).}:
64- contextRegistryLock.acquire ()
65- defer : contextRegistryLock.release ()
66- return contextRegistry.contains (ctx)
67-
6844template callEventCallback * (ctx: ptr FFIContext , eventName: string , body: untyped ) =
6945 if isNil (ctx[].callbackState.callback):
7046 chronicles.error eventName & " - eventCallback is nil"
@@ -74,14 +50,20 @@ template callEventCallback*(ctx: ptr FFIContext, eventName: string, body: untype
7450 try :
7551 let event = body
7652 cast [FFICallBack ](ctx[].callbackState.callback)(
77- RET_OK , unsafeAddr event [0 ], cast [csize_t ](len (event)), ctx[].callbackState.userData
53+ RET_OK ,
54+ unsafeAddr event[0 ],
55+ cast [csize_t ](len (event)),
56+ ctx[].callbackState.userData,
7857 )
7958 except Exception , CatchableError :
8059 let msg =
8160 " Exception " & eventName & " when calling 'eventCallBack': " &
8261 getCurrentExceptionMsg ()
8362 cast [FFICallBack ](ctx[].callbackState.callback)(
84- RET_ERR , unsafeAddr msg [0 ], cast [csize_t ](len (msg)), ctx[].callbackState.userData
63+ RET_ERR ,
64+ unsafeAddr msg[0 ],
65+ cast [csize_t ](len (msg)),
66+ ctx[].callbackState.userData,
8567 )
8668
8769template dispatchFfiEvent * (eventName: string , body: untyped ) =
@@ -99,18 +81,14 @@ template dispatchFfiEvent*(eventName: string, body: untyped) =
9981 RET_OK , unsafeAddr event [0 ], cast [csize_t ](len (event)), ffiState[].userData
10082 )
10183 except Exception , CatchableError :
102- let msg =
103- " Exception dispatching " & eventName & " : " & getCurrentExceptionMsg ()
84+ let msg = " Exception dispatching " & eventName & " : " & getCurrentExceptionMsg ()
10485 cast [FFICallBack ](ffiState[].callback)(
10586 RET_ERR , unsafeAddr msg [0 ], cast [csize_t ](len (msg)), ffiState[].userData
10687 )
10788
10889proc sendRequestToFFIThread * (
10990 ctx: ptr FFIContext , ffiRequest: ptr FFIThreadRequest , timeout = InfiniteDuration
11091): Result [void , string ] =
111- if not isValidCtx (cast [pointer ](ctx)):
112- deleteRequest (ffiRequest)
113- return err (" ctx is not a valid FFI context" )
11492 ctx.lock.acquire ()
11593 # This lock is only necessary while we use a SP Channel and while the signalling
11694 # between threads assumes that there aren't concurrent requests.
@@ -229,15 +207,17 @@ proc processRequest[T](
229207 try :
230208 await retFut
231209 except AsyncError as exc:
232- Result [string , string ].err (" Async error in processRequest for " & reqId & " : " & exc.msg)
210+ Result [string , string ].err (
211+ " Async error in processRequest for " & reqId & " : " & exc.msg
212+ )
233213
234214 # # handleRes may raise (OOM, GC setup) even though it is rare. Catching here
235215 # # keeps the async proc raises:[] compatible. The defer inside handleRes
236216 # # guarantees request is freed before the exception propagates.
237217 try :
238218 handleRes (res, request)
239219 except Exception as exc:
240- error " Unexpected exception in handleRes" , exc = exc.msg
220+ error " Unexpected exception in handleRes" , error = exc.msg
241221
242222proc ffiThreadBody [T](ctx: ptr FFIContext [T]) {.thread .} =
243223 # # FFI thread body that attends library user API requests
@@ -250,8 +230,7 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
250230 # wait can unblock and proceed with cleanup.
251231 let fireRes = ctx.threadExitSignal.fireSync ()
252232 if fireRes.isErr ():
253- error " failed to fire threadExitSignal on FFI thread exit" ,
254- err = fireRes.error
233+ error " failed to fire threadExitSignal on FFI thread exit" , err = fireRes.error
255234
256235 let ffiRun = proc (ctx: ptr FFIContext [T]) {.async .} =
257236 var ffiReqHandler: T
@@ -281,6 +260,7 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
281260 waitFor ffiRun (ctx)
282261
283262proc cleanUpResources [T](ctx: ptr FFIContext [T]): Result [void , string ] =
263+ # # Full cleanup for heap-allocated contexts: closes all resources and frees memory.
284264 defer :
285265 freeShared (ctx)
286266 ctx.lock.deinitLock ()
@@ -316,18 +296,18 @@ proc cleanUpResources[T](ctx: ptr FFIContext[T]): Result[void, string] =
316296 ? ctx.threadExitSignal.close ()
317297 return ok ()
318298
319- proc createFFIContext * [T](): Result [ ptr FFIContext [T], string ] =
320- # # This proc is called from the main thread and it creates
321- # # the FFI working thread.
322- var ctx = createShared ( FFIContext [T], 1 )
299+ proc initContextResources * [T](ctx: ptr FFIContext [T]): Result [ void , string ] =
300+ # # Initialises all resources inside an already-allocated FFIContext slot.
301+ # # On failure every partially-initialised resource is closed; the caller
302+ # # is responsible for releasing the slot (freeShared or pool.releaseSlot).
323303 ctx.lock.initLock ()
324304
325305 var success = false
326306 defer :
327307 if not success:
328308 ctx.cleanUpResources ().isOkOr:
329309 error " failed to clean up resources after createFFIContext failure" ,
330- err = error
310+ error = error
331311
332312 ctx.reqSignal = ThreadSignalPtr .new ().valueOr:
333313 return err (" couldn't create reqSignal ThreadSignalPtr: " & $ error)
@@ -358,61 +338,58 @@ proc createFFIContext*[T](): Result[ptr FFIContext[T], string] =
358338 ctx.running.store (false )
359339 let fireRes = ctx.reqSignal.fireSync ()
360340 if fireRes.isErr ():
361- error " failed to signal ffiThread during watchdog cleanup" ,
362- err = fireRes.error
341+ error " failed to signal ffiThread during watchdog cleanup" , error = fireRes.error
363342 joinThread (ctx.ffiThread)
364343 return err (" failed to create the watchdog thread: " & getCurrentExceptionMsg ())
365344
366- registerCtx (cast [pointer ](ctx))
367345 success = true
368- return ok (ctx)
369-
370- proc destroyFFIContext * [T](ctx: ptr FFIContext [T]): Result [void , string ] =
371- # # If the FFI thread's event loop is blocked by a synchronous handler
372- # # (e.g. blocking I/O), it cannot process reqSignal in time to exit.
373- # # In that case we leak ctx and the thread rather than hanging forever:
374- # # the thread will eventually exit on its own, but cleanup is skipped
375- # # because the thread may still be touching ctx fields.
376- const ThreadExitTimeout = 1500 .milliseconds
377- unregisterCtx (cast [pointer ](ctx))
346+ return ok ()
378347
348+ proc signalStop * [T](ctx: ptr FFIContext [T]): Result [void , string ] =
379349 ctx.running.store (false )
380-
381- let signaledOnTime = ctx.reqSignal.fireSync ().valueOr:
350+ let reqSignaled = ctx.reqSignal.fireSync ().valueOr:
382351 ctx.onNotResponding ()
383- return err (" error in destroyFFIContext : " & $ error)
384- if not signaledOnTime :
352+ return err (" error signaling reqSignal in signalStop : " & $ error)
353+ if not reqSignaled :
385354 ctx.onNotResponding ()
386- return err (" failed to signal reqSignal on time in destroyFFIContext" )
355+ return err (" failed to signal reqSignal on time in signalStop" )
356+ let stopSignaled = ctx.stopSignal.fireSync ().valueOr:
357+ return err (" error signaling stopSignal in signalStop: " & $ error)
358+ if not stopSignaled:
359+ return err (" failed to signal stopSignal on time in signalStop" )
360+ return ok ()
387361
388- ctx.stopSignal.fireSync ().isOkOr:
389- error " failed to fire stopSignal in destroyFFIContext" , err = $ error
362+ # # If the FFI thread's event loop is blocked by a synchronous handler
363+ # # (e.g. blocking I/O), it cannot process reqSignal in time to exit.
364+ # # clearContext waits on threadExitSignal up to this bound; on timeout it
365+ # # returns err and skips joinThread/cleanup (leaking the thread + ctx slot)
366+ # # rather than hanging the caller forever.
367+ const ThreadExitTimeout * = 1500 .milliseconds
368+
369+ proc stopAndJoinThreads * [T](ctx: ptr FFIContext [T]): Result [void , string ] =
370+ # # Signals the FFI and watchdog threads to stop, waits up to ThreadExitTimeout
371+ # # for the FFI thread to exit, and joins both. On timeout returns err and
372+ # # skips joinThread (leaving the threads live) rather than hanging the caller.
373+ # # Resource cleanup (signal fds, lock) is the caller's responsibility.
374+ ctx.signalStop ().isOkOr:
375+ return err (" signalStop failed: " & $ error)
390376
391- # # Bounded wait for ffiThread to exit. waitSync blocks the calling thread
392- # # up to the timeout; ffiThread fires threadExitSignal in its defer block.
393377 let exitedOnTime = ctx.threadExitSignal.waitSync (ThreadExitTimeout ).valueOr:
394378 ctx.onNotResponding ()
395379 return err (" error waiting for FFI thread exit: " & $ error)
396380
397381 if not exitedOnTime:
398- # # Event loop is blocked by a synchronous handler. Leak the thread and
399- # # ctx to avoid hanging the caller forever.
400382 ctx.onNotResponding ()
401383 return err (" FFI thread did not exit in time; leaking ctx to avoid hang" )
402384
403385 joinThread (ctx.ffiThread)
404386 joinThread (ctx.watchdogThread)
387+ return ok ()
388+
389+ proc clearContext [T](ctx: ptr FFIContext [T]): Result [void , string ] =
390+ # # Stops the FFI context that was created via createFFIContext[T]() (heap).
391+ ctx.stopAndJoinThreads ().isOkOr:
392+ return err (" clearContext: " & $ error)
405393 ctx.cleanUpResources ().isOkOr:
406- error " failed to clean up resources in destroyFFIContext" , err = error
407394 return err (" cleanUpResources failed: " & $ error)
408-
409395 return ok ()
410-
411- template checkParams * (ctx: ptr FFIContext , callback: FFICallBack , userData: pointer ) =
412- if not isValidCtx (cast [pointer ](ctx)):
413- return RET_ERR
414-
415- ctx[].userData = userData
416-
417- if isNil (callback):
418- return RET_MISSING_CALLBACK
0 commit comments