`{.ffiEvent.}` previously accepted exactly one parameter, forcing every
multi-field event to declare a hand-written {.ffi.} payload type. The macro
now bundles two or more parameters into a synthesised, registered envelope
object named `<WireNamePascalCase>Payload`, whose fields are the parameters,
and dispatches an instance of it. A single parameter still rides the wire
directly (scalar or existing {.ffi.} object), so this is backwards
compatible. Because the envelope is registered like any {.ffi.} type, the
foreign bindings gain it as a first-class struct plus a typed handler.
The timer example gains an `on_job_scheduled(jobId, willRunCount)` event to
exercise the path; the C++ and Rust bindings are regenerated accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
sds_add_event_listener / sds_remove_event_listener run on the foreign
caller thread and allocate Nim memory ($eventName, the listener registry
Table+seq), but unlike the ctor/dtor they never called initializeLibrary.
A foreign host (Go) migrates goroutines across OS threads, so the thread
running add_event_listener may not be the one that ran a prior entry
point; without setupForeignThreadGc its per-thread allocator region is
uninitialised and the first Nim allocation faults in the allocator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* protect against mem leak in case of failures sending requests to ffi thread
* better cleanup if failures in createFFIContext
* avoid dangling cstring in handleRes under ARC/ORC
* better resource cleanup in destroyFFIContext
* invoke onNotResponding if failure in destroyFFIContext
* correct seq copy in alloc
* make sure the lock is init before cleanUpResources
* better possible exception handling in processReq
* guard allocSharedSeq if given seq is empty
* enhance error handling in ffi_context
* add new tests and some corrections