mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-24 18:30:08 +00:00
fix(codegen): emit 3-arg async destroy ABI in C++/Rust bindings
The recycle/async-destroy work changed the Nim `ffiDtor` export from `int destroy(ctx)` to `int destroy(ctx, callback, userData)`, but the C++ and Rust generators still emitted the 1-arg signature. Foreign callers therefore passed only `ctx`; inside Nim, `callback`/`userData` held uninitialised register garbage. `requestRecycle` stored the garbage callback and the recycle handler later invoked it — a jump through a wild pointer that segfaulted in every C++ E2E / ASan / TSan job (the crash surfaced at teardown, after each test's assertions had already passed). Generate the 3-arg ABI and have the destructor/Drop block on the recycle callback via the existing sync-call helper, so the pool slot is fully drained and parked before the handle goes away — otherwise rapid create/destroy churn (StressShortLivedPerThreadContext, ThreadedHammer) could outrun the recycle and exhaust the pool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fe6749d3af
commit
6bc626946e
@ -406,7 +406,7 @@ typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_dat
|
||||
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int echo_destroy(void* ctx);
|
||||
int echo_destroy(void* ctx, FFICallback callback, void* user_data);
|
||||
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
|
||||
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
} // extern "C"
|
||||
@ -516,7 +516,14 @@ public:
|
||||
// context.
|
||||
~EchoCtx() {
|
||||
if (ptr_) {
|
||||
echo_destroy(ptr_);
|
||||
// `echo_destroy` is non-blocking at the C ABI: it parks the
|
||||
// context for reuse and reports the outcome via the callback. Block
|
||||
// here until that callback fires so the pool slot is fully drained
|
||||
// and parked before this object goes away — otherwise a rapid
|
||||
// create/destroy churn could outrun the recycle and exhaust the pool.
|
||||
(void)ffi_call_([this](FFICallback cb, void* ud) {
|
||||
return echo_destroy(ptr_, cb, ud);
|
||||
}, timeout_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -706,7 +706,7 @@ int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_
|
||||
int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int my_timer_destroy(void* ctx);
|
||||
int my_timer_destroy(void* ctx, FFICallback callback, void* user_data);
|
||||
uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
|
||||
int my_timer_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
} // extern "C"
|
||||
@ -816,7 +816,14 @@ public:
|
||||
// context.
|
||||
~MyTimerCtx() {
|
||||
if (ptr_) {
|
||||
my_timer_destroy(ptr_);
|
||||
// `my_timer_destroy` is non-blocking at the C ABI: it parks the
|
||||
// context for reuse and reports the outcome via the callback. Block
|
||||
// here until that callback fires so the pool slot is fully drained
|
||||
// and parked before this object goes away — otherwise a rapid
|
||||
// create/destroy churn could outrun the recycle and exhaust the pool.
|
||||
(void)ffi_call_([this](FFICallback cb, void* ud) {
|
||||
return my_timer_destroy(ptr_, cb, ud);
|
||||
}, timeout_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +141,9 @@ unsafe impl Sync for MyTimerCtx {}
|
||||
impl Drop for MyTimerCtx {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
unsafe { ffi::my_timer_destroy(self.ptr); }
|
||||
let _ = ffi_call_sync(self.timeout, |cb, ud| unsafe {
|
||||
ffi::my_timer_destroy(self.ptr, cb, ud)
|
||||
});
|
||||
self.ptr = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ extern "C" {
|
||||
pub fn my_timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
|
||||
pub fn my_timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
|
||||
pub fn my_timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
|
||||
pub fn my_timer_destroy(ctx: *mut c_void) -> c_int;
|
||||
pub fn my_timer_destroy(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void) -> c_int;
|
||||
pub fn my_timer_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;
|
||||
pub fn my_timer_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;
|
||||
}
|
||||
|
||||
@ -339,7 +339,9 @@ proc generateCppHeader*(
|
||||
[p.procName]
|
||||
)
|
||||
of FFIKind.DTOR:
|
||||
lines.add("int $1(void* ctx);" % [p.procName])
|
||||
lines.add(
|
||||
"int $1(void* ctx, FFICallback callback, void* user_data);" % [p.procName]
|
||||
)
|
||||
# `declareLibrary` always exports the listener-registration ABI. Declare
|
||||
# it here so the typed event-handler wiring below can call into it.
|
||||
lines.add(
|
||||
@ -570,8 +572,8 @@ proc generateCppHeader*(
|
||||
lines.add(" std::chrono::milliseconds timeout_;")
|
||||
if events.len > 0:
|
||||
# One owning entry per live listener, keyed by id. Destroyed after
|
||||
# the destructor body runs `<lib>_destroy(ptr_)`, by which point the
|
||||
# FFI side has joined its threads so no callback is mid-flight.
|
||||
# the destructor blocks on `<lib>_destroy`'s recycle callback, by which
|
||||
# point the FFI side has drained/parked the slot so no callback is mid-flight.
|
||||
lines.add(
|
||||
" std::unordered_map<std::uint64_t, std::unique_ptr<ListenerBase>> listeners_;"
|
||||
)
|
||||
|
||||
@ -207,6 +207,8 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
|
||||
lines.add(" pub fn $1($2) -> *mut c_void;" % [p.procName, params.join(", ")])
|
||||
of FFIKind.DTOR:
|
||||
params.add("ctx: *mut c_void")
|
||||
params.add("callback: FFICallback")
|
||||
params.add("user_data: *mut c_void")
|
||||
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")])
|
||||
|
||||
# Listener-registration ABI — emitted on the Nim side by `declareLibrary`,
|
||||
@ -531,7 +533,14 @@ proc generateApiRs*(
|
||||
lines.add("impl Drop for $1 {" % [ctxTypeName])
|
||||
lines.add(" fn drop(&mut self) {")
|
||||
lines.add(" if !self.ptr.is_null() {")
|
||||
lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName])
|
||||
# `<lib>_destroy` is non-blocking at the C ABI: it parks the context for
|
||||
# reuse and reports the outcome via the callback. Block until that callback
|
||||
# fires so the pool slot is fully drained and parked before this handle goes
|
||||
# away — otherwise rapid create/destroy churn could outrun the recycle and
|
||||
# exhaust the pool. The recycle outcome is best-effort on drop, so discard it.
|
||||
lines.add(" let _ = ffi_call_sync(self.timeout, |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1(self.ptr, cb, ud)" % [dtorProcName])
|
||||
lines.add(" });")
|
||||
lines.add(" self.ptr = std::ptr::null_mut();")
|
||||
lines.add(" }")
|
||||
# `listeners` is dropped automatically after this body returns. By
|
||||
|
||||
@ -9,7 +9,14 @@
|
||||
// context.
|
||||
~{{CTX}}() {
|
||||
if (ptr_) {
|
||||
{{LIB}}_destroy(ptr_);
|
||||
// `{{LIB}}_destroy` is non-blocking at the C ABI: it parks the
|
||||
// context for reuse and reports the outcome via the callback. Block
|
||||
// here until that callback fires so the pool slot is fully drained
|
||||
// and parked before this object goes away — otherwise a rapid
|
||||
// create/destroy churn could outrun the recycle and exhaust the pool.
|
||||
(void)ffi_call_([this](FFICallback cb, void* ud) {
|
||||
return {{LIB}}_destroy(ptr_, cb, ud);
|
||||
}, timeout_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user