When no {.ffiCtor.} has stored a library, the FFI thread points `myLib` at a
default-valued fallback so handlers always have something to bind. For an
`object` library that is a usable zero value and callers legitimately depend on
it (tests/unit/test_ffi_handle drives a context that never runs a ctor), but for
a `ref` library the default is `nil`: the user body received a nil ref and
faulted on its first field access. A failing ctor is the common way to get
there, since the C entry point hands back a live context before the ctor body
has run on the FFI thread.
Track whether a ctor actually stored a library and, for `ref` library types
only, reject such requests with a clear error instead of dispatching them. The
check sits in the generated handler rather than the C entry point, so it runs
behind any queued constructor — a host that fires a call without awaiting the
create callback still succeeds, as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULz7Md52AF6PmqZeCmh8b7
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>