Skip to content

Commit 6bc6269

Browse files
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>
1 parent fe6749d commit 6bc6269

7 files changed

Lines changed: 45 additions & 11 deletions

File tree

examples/echo/cpp_bindings/echo.hpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_dat
406406
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
407407
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
408408
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
409-
int echo_destroy(void* ctx);
409+
int echo_destroy(void* ctx, FFICallback callback, void* user_data);
410410
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
411411
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
412412
} // extern "C"
@@ -516,7 +516,14 @@ class EchoCtx {
516516
// context.
517517
~EchoCtx() {
518518
if (ptr_) {
519-
echo_destroy(ptr_);
519+
// `echo_destroy` is non-blocking at the C ABI: it parks the
520+
// context for reuse and reports the outcome via the callback. Block
521+
// here until that callback fires so the pool slot is fully drained
522+
// and parked before this object goes away — otherwise a rapid
523+
// create/destroy churn could outrun the recycle and exhaust the pool.
524+
(void)ffi_call_([this](FFICallback cb, void* ud) {
525+
return echo_destroy(ptr_, cb, ud);
526+
}, timeout_);
520527
ptr_ = nullptr;
521528
}
522529
}

examples/timer/cpp_bindings/my_timer.hpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_
706706
int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
707707
int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
708708
int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
709-
int my_timer_destroy(void* ctx);
709+
int my_timer_destroy(void* ctx, FFICallback callback, void* user_data);
710710
uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
711711
int my_timer_remove_event_listener(void* ctx, uint64_t listener_id);
712712
} // extern "C"
@@ -816,7 +816,14 @@ class MyTimerCtx {
816816
// context.
817817
~MyTimerCtx() {
818818
if (ptr_) {
819-
my_timer_destroy(ptr_);
819+
// `my_timer_destroy` is non-blocking at the C ABI: it parks the
820+
// context for reuse and reports the outcome via the callback. Block
821+
// here until that callback fires so the pool slot is fully drained
822+
// and parked before this object goes away — otherwise a rapid
823+
// create/destroy churn could outrun the recycle and exhaust the pool.
824+
(void)ffi_call_([this](FFICallback cb, void* ud) {
825+
return my_timer_destroy(ptr_, cb, ud);
826+
}, timeout_);
820827
ptr_ = nullptr;
821828
}
822829
}

examples/timer/rust_bindings/src/api.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ unsafe impl Sync for MyTimerCtx {}
141141
impl Drop for MyTimerCtx {
142142
fn drop(&mut self) {
143143
if !self.ptr.is_null() {
144-
unsafe { ffi::my_timer_destroy(self.ptr); }
144+
let _ = ffi_call_sync(self.timeout, |cb, ud| unsafe {
145+
ffi::my_timer_destroy(self.ptr, cb, ud)
146+
});
145147
self.ptr = std::ptr::null_mut();
146148
}
147149
}

examples/timer/rust_bindings/src/ffi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ extern "C" {
1414
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;
1515
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;
1616
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;
17-
pub fn my_timer_destroy(ctx: *mut c_void) -> c_int;
17+
pub fn my_timer_destroy(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void) -> c_int;
1818
pub fn my_timer_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;
1919
pub fn my_timer_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;
2020
}

ffi/codegen/cpp.nim

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ proc generateCppHeader*(
339339
[p.procName]
340340
)
341341
of FFIKind.DTOR:
342-
lines.add("int $1(void* ctx);" % [p.procName])
342+
lines.add(
343+
"int $1(void* ctx, FFICallback callback, void* user_data);" % [p.procName]
344+
)
343345
# `declareLibrary` always exports the listener-registration ABI. Declare
344346
# it here so the typed event-handler wiring below can call into it.
345347
lines.add(
@@ -570,8 +572,8 @@ proc generateCppHeader*(
570572
lines.add(" std::chrono::milliseconds timeout_;")
571573
if events.len > 0:
572574
# One owning entry per live listener, keyed by id. Destroyed after
573-
# the destructor body runs `<lib>_destroy(ptr_)`, by which point the
574-
# FFI side has joined its threads so no callback is mid-flight.
575+
# the destructor blocks on `<lib>_destroy`'s recycle callback, by which
576+
# point the FFI side has drained/parked the slot so no callback is mid-flight.
575577
lines.add(
576578
" std::unordered_map<std::uint64_t, std::unique_ptr<ListenerBase>> listeners_;"
577579
)

ffi/codegen/rust.nim

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
207207
lines.add(" pub fn $1($2) -> *mut c_void;" % [p.procName, params.join(", ")])
208208
of FFIKind.DTOR:
209209
params.add("ctx: *mut c_void")
210+
params.add("callback: FFICallback")
211+
params.add("user_data: *mut c_void")
210212
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")])
211213

212214
# Listener-registration ABI — emitted on the Nim side by `declareLibrary`,
@@ -531,7 +533,14 @@ proc generateApiRs*(
531533
lines.add("impl Drop for $1 {" % [ctxTypeName])
532534
lines.add(" fn drop(&mut self) {")
533535
lines.add(" if !self.ptr.is_null() {")
534-
lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName])
536+
# `<lib>_destroy` is non-blocking at the C ABI: it parks the context for
537+
# reuse and reports the outcome via the callback. Block until that callback
538+
# fires so the pool slot is fully drained and parked before this handle goes
539+
# away — otherwise rapid create/destroy churn could outrun the recycle and
540+
# exhaust the pool. The recycle outcome is best-effort on drop, so discard it.
541+
lines.add(" let _ = ffi_call_sync(self.timeout, |cb, ud| unsafe {")
542+
lines.add(" ffi::$1(self.ptr, cb, ud)" % [dtorProcName])
543+
lines.add(" });")
535544
lines.add(" self.ptr = std::ptr::null_mut();")
536545
lines.add(" }")
537546
# `listeners` is dropped automatically after this body returns. By

ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
// context.
1010
~{{CTX}}() {
1111
if (ptr_) {
12-
{{LIB}}_destroy(ptr_);
12+
// `{{LIB}}_destroy` is non-blocking at the C ABI: it parks the
13+
// context for reuse and reports the outcome via the callback. Block
14+
// here until that callback fires so the pool slot is fully drained
15+
// and parked before this object goes away — otherwise a rapid
16+
// create/destroy churn could outrun the recycle and exhaust the pool.
17+
(void)ffi_call_([this](FFICallback cb, void* ud) {
18+
return {{LIB}}_destroy(ptr_, cb, ud);
19+
}, timeout_);
1320
ptr_ = nullptr;
1421
}
1522
}

0 commit comments

Comments
 (0)