From c43563f82f364e4249858c4397a8967fca2f5b7d Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 29 May 2026 20:40:28 +0200 Subject: [PATCH] rust codegen: per-event typed add_on__listener + wildcard add_event_listener (#52) --- examples/timer/rust_bindings/Cargo.lock | 20 +- examples/timer/rust_bindings/src/api.rs | 166 ++++++++----- examples/timer/rust_client/Cargo.lock | 22 +- ffi/codegen/rust.nim | 309 ++++++++++++++++-------- 4 files changed, 328 insertions(+), 189 deletions(-) diff --git a/examples/timer/rust_bindings/Cargo.lock b/examples/timer/rust_bindings/Cargo.lock index 9e6f26d..73e8c29 100644 --- a/examples/timer/rust_bindings/Cargo.lock +++ b/examples/timer/rust_bindings/Cargo.lock @@ -84,6 +84,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "my_timer" +version = "0.1.0" +dependencies = [ + "ciborium", + "flume", + "serde", + "tokio", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -164,16 +174,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "timer" -version = "0.1.0" -dependencies = [ - "ciborium", - "flume", - "serde", - "tokio", -] - [[package]] name = "tokio" version = "1.52.3" diff --git a/examples/timer/rust_bindings/src/api.rs b/examples/timer/rust_bindings/src/api.rs index 6794d37..d8d9721 100644 --- a/examples/timer/rust_bindings/src/api.rs +++ b/examples/timer/rust_bindings/src/api.rs @@ -98,63 +98,72 @@ where } } -/// Typed event handlers for `MyTimerCtx`. Each field is `None` by -/// default; set the ones you care about and pass to -/// `MyTimerCtx::set_event_handlers`. -#[allow(non_snake_case)] -pub struct Events { - pub on_error: Option>, - pub onEchoFired: Option>, +struct OnEchoFiredHandler { + f: Box, } -impl Default for Events { - fn default() -> Self { - Self { on_error: None, onEchoFired: None } +unsafe extern "C" fn on_echo_fired_trampoline( + ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void, +) { + if ud.is_null() || ret != 0 || msg.is_null() || len == 0 { + return; + } + let h = &*(ud as *const OnEchoFiredHandler); + let bytes = slice::from_raw_parts(msg as *const u8, len); + #[derive(serde::Deserialize)] + struct Envelope { payload: EchoEvent } + if let Ok(env) = ciborium::de::from_reader::(bytes) { + (h.f)(&env.payload); } } -unsafe extern "C" fn my_timer_event_trampoline( +struct WildcardHandler { + f: Box, +} + +unsafe extern "C" fn my_timer_wildcard_trampoline( ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void, ) { if ud.is_null() { return; } - let events = &*(ud as *const Events); - if ret != 0 { - if let Some(ref on_err) = events.on_error { - let bytes = if !msg.is_null() && len > 0 { - slice::from_raw_parts(msg as *const u8, len) - } else { &[] }; - let s = String::from_utf8_lossy(bytes); - on_err(&s); - } - return; - } - if msg.is_null() || len == 0 { return; } - let bytes = slice::from_raw_parts(msg as *const u8, len); - #[derive(serde::Deserialize)] - struct EnvelopeMeta { - #[serde(rename = "eventType")] - event_type: String, - } - let meta: EnvelopeMeta = match ciborium::de::from_reader(bytes) { - Ok(m) => m, - Err(_) => return, - }; - if meta.event_type == "on_echo_fired" { + let h = &*(ud as *const WildcardHandler); + let bytes = if !msg.is_null() && len > 0 { + slice::from_raw_parts(msg as *const u8, len) + } else { &[] }; + let event_id = if ret == 0 && !bytes.is_empty() { #[derive(serde::Deserialize)] - struct Envelope { payload: EchoEvent } - if let Ok(env) = ciborium::de::from_reader::(bytes) { - if let Some(ref h) = events.onEchoFired { h(&env.payload); } + struct EnvelopeMeta { + #[serde(rename = "eventType")] + event_type: String, } - return; - } + ciborium::de::from_reader::(bytes) + .map(|m| m.event_type).unwrap_or_default() + } else { + String::new() + }; + (h.f)(ret, event_id.as_str(), bytes); +} + +#[derive(Debug, Clone, Copy)] +pub struct ListenerHandle { pub id: u64 } + +/// Decode the `payload` field of a CBOR `EventEnvelope` as `T`. +/// Returns `Err` if the envelope is empty / malformed / the payload +/// cannot be deserialised as `T`. +pub fn decode_event_payload( + envelope: &[u8], +) -> Result { + #[derive(serde::Deserialize)] + struct Envelope { payload: T } + let env: Envelope = ciborium::de::from_reader(envelope) + .map_err(|e| format!("decode event payload: {e}"))?; + Ok(env.payload) } /// High-level context for `MyTimer`. pub struct MyTimerCtx { ptr: *mut c_void, timeout: Duration, - events: *mut Events, - event_listener_id: u64, + listeners: std::sync::Mutex>>, } // SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime. @@ -174,10 +183,6 @@ impl Drop for MyTimerCtx { unsafe { ffi::my_timer_destroy(self.ptr); } self.ptr = std::ptr::null_mut(); } - if !self.events.is_null() { - unsafe { drop(Box::from_raw(self.events)); } - self.events = std::ptr::null_mut(); - } } } @@ -191,7 +196,7 @@ impl MyTimerCtx { })?; let addr_str: String = decode_cbor(&raw_bytes)?; let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; - Ok(Self { ptr: addr as *mut c_void, timeout, events: std::ptr::null_mut(), event_listener_id: 0 }) + Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) }) } pub async fn new_async(config: TimerConfig, timeout: Duration) -> Result { @@ -203,30 +208,57 @@ impl MyTimerCtx { }).await?; let addr_str: String = decode_cbor(&raw_bytes)?; let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; - Ok(Self { ptr: addr as *mut c_void, timeout, events: std::ptr::null_mut(), event_listener_id: 0 }) + Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) }) } - /// Attach typed event handlers. Each call removes any previous - /// listener via `_remove_event_listener` before adding the new - /// one, so the registry never holds a pointer into a freed box. - pub fn set_event_handlers(&mut self, handlers: Events) { - if self.event_listener_id != 0 { - unsafe { - let _ = ffi::my_timer_remove_event_listener(self.ptr, self.event_listener_id); - } - self.event_listener_id = 0; - } - if !self.events.is_null() { - unsafe { drop(Box::from_raw(self.events)); } - self.events = std::ptr::null_mut(); - } - let raw = Box::into_raw(Box::new(handlers)); - self.events = raw; - unsafe { - self.event_listener_id = ffi::my_timer_add_event_listener( - self.ptr, b"\0".as_ptr() as *const c_char, - my_timer_event_trampoline, raw as *mut c_void); + fn add_listener_inner( + &self, + event_name: *const c_char, + callback: ffi::FFICallback, + raw: *mut c_void, + owned: Box, + ) -> ListenerHandle { + let id = unsafe { + ffi::my_timer_add_event_listener(self.ptr, event_name, callback, raw) + }; + if id != 0 { + self.listeners.lock().unwrap().insert(id, owned); } + ListenerHandle { id } + } + + /// Register a typed listener for `on_echo_fired`. The returned handle can be + /// passed to `remove_event_listener` to unregister. + pub fn add_on_echo_fired_listener(&self, handler: F) -> ListenerHandle + where F: Fn(&EchoEvent) + Send + Sync + 'static, + { + let owned: Box = Box::new(OnEchoFiredHandler { f: Box::new(handler) }); + let raw = &*owned as *const OnEchoFiredHandler as *mut c_void; + self.add_listener_inner(b"on_echo_fired\0".as_ptr() as *const c_char, on_echo_fired_trampoline, raw, owned) + } + + /// Register a catch-all listener that receives every event. + /// The handler arguments are (return_code, event_id, envelope_bytes): + /// `event_id` is the wire `eventType` string extracted from the + /// envelope (empty on error or malformed envelope); `envelope_bytes` + /// is the full CBOR envelope, suitable for `decode_event_payload::`. + pub fn add_event_listener(&self, handler: F) -> ListenerHandle + where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static, + { + let owned: Box = Box::new(WildcardHandler { f: Box::new(handler) }); + let raw = &*owned as *const WildcardHandler as *mut c_void; + self.add_listener_inner(b"\0".as_ptr() as *const c_char, my_timer_wildcard_trampoline, raw, owned) + } + + /// Remove a previously-registered listener by handle. Returns true + /// if the listener existed and was removed; false otherwise. + pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool { + if handle.id == 0 { return false; } + let rc = unsafe { + ffi::my_timer_remove_event_listener(self.ptr, handle.id) + }; + self.listeners.lock().unwrap().remove(&handle.id); + rc == 0 } pub fn echo(&self, req: EchoRequest) -> Result { diff --git a/examples/timer/rust_client/Cargo.lock b/examples/timer/rust_client/Cargo.lock index 9e0ca6f..6f1d949 100644 --- a/examples/timer/rust_client/Cargo.lock +++ b/examples/timer/rust_client/Cargo.lock @@ -96,6 +96,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "my_timer" +version = "0.1.0" +dependencies = [ + "ciborium", + "flume", + "serde", + "tokio", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -124,8 +134,8 @@ dependencies = [ name = "rust_client" version = "0.1.0" dependencies = [ + "my_timer", "serde_json", - "timer", "tokio", ] @@ -198,16 +208,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "timer" -version = "0.1.0" -dependencies = [ - "ciborium", - "flume", - "serde", - "tokio", -] - [[package]] name = "tokio" version = "1.52.1" diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 20c46c0..4afbefb 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -207,8 +207,8 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string = params.add("ctx: *mut c_void") lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")]) - # Listener-registration ABI — emitted by `declareLibrary`, always - # present in the dylib. + # Listener-registration ABI — emitted on the Nim side by `declareLibrary`, + # always present in the dylib. lines.add( " pub fn $1_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;" % [linkLibName] @@ -446,77 +446,105 @@ proc generateApiRs*( lines.add("}") lines.add("") - # ── Typed event handler struct + trampoline (only if events declared) ──── - # The Events struct holds optional boxed closures, one per registered - # `{.ffiEvent.}`. The struct lives on the heap (Box::into_raw); its raw - # pointer is handed to the dylib as `user_data` for the event callback. - # The trampoline parses the CBOR `EventEnvelope`, picks the matching - # field on Events, decodes the payload as the registered type, and - # invokes the closure. + # ── Per-listener handler boxes + extern "C" trampolines ───────────────── + # Each registered listener owns a `Box<…Handler>` that is kept alive in + # `$1::listeners` (keyed by listener id). The raw pointer to the inner + # handler is handed to the dylib as `user_data` for the per-event or + # wildcard trampoline below. if events.len > 0: - lines.add("/// Typed event handlers for `$1`. Each field is `None` by" % [ctxTypeName]) - lines.add("/// default; set the ones you care about and pass to") - lines.add("/// `$1::set_event_handlers`." % [ctxTypeName]) - lines.add("#[allow(non_snake_case)]") - lines.add("pub struct Events {") - lines.add(" pub on_error: Option>,") for ev in events: + let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler" + let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline" + lines.add("struct $1 {" % [handlerStruct]) lines.add( - " pub $1: Option>," % - [ev.nimProcName, ev.payloadTypeName] + " f: Box," % [ev.payloadTypeName] ) - lines.add("}") - lines.add("") - lines.add("impl Default for Events {") - lines.add(" fn default() -> Self {") - lines.add(" Self { on_error: None, " & - events.mapIt(it.nimProcName & ": None").join(", ") & " }") - lines.add(" }") - lines.add("}") - lines.add("") + lines.add("}") + lines.add("") + lines.add("unsafe extern \"C\" fn $1(" % [trampolineName]) + lines.add(" ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,") + lines.add(") {") + lines.add(" if ud.is_null() || ret != 0 || msg.is_null() || len == 0 {") + lines.add(" return;") + lines.add(" }") + lines.add(" let h = &*(ud as *const $1);" % [handlerStruct]) + lines.add(" let bytes = slice::from_raw_parts(msg as *const u8, len);") + lines.add(" #[derive(serde::Deserialize)]") + lines.add( + " struct Envelope { payload: $1 }" % [ev.payloadTypeName] + ) + lines.add( + " if let Ok(env) = ciborium::de::from_reader::(bytes) {" + ) + lines.add(" (h.f)(&env.payload);") + lines.add(" }") + lines.add("}") + lines.add("") - # Trampoline — `extern "C"` free function. Deserialises the envelope - # twice: once for `eventType`, then with the typed payload via serde. - lines.add("unsafe extern \"C\" fn $1_event_trampoline(" % [libName]) + # Wildcard handler — receives every event as raw envelope bytes, + # the FFI return code, and the `eventType` string pre-extracted + # from the CBOR envelope. `event_id` is empty when `ret != 0` or + # the envelope is malformed (the bytes are an error string, not a + # CBOR envelope, in that case). + lines.add("struct WildcardHandler {") + lines.add(" f: Box,") + lines.add("}") + lines.add("") + lines.add("unsafe extern \"C\" fn $1_wildcard_trampoline(" % [libName]) lines.add(" ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,") lines.add(") {") lines.add(" if ud.is_null() { return; }") - lines.add(" let events = &*(ud as *const Events);") - lines.add(" if ret != 0 {") - lines.add(" if let Some(ref on_err) = events.on_error {") - lines.add(" let bytes = if !msg.is_null() && len > 0 {") - lines.add(" slice::from_raw_parts(msg as *const u8, len)") - lines.add(" } else { &[] };") - lines.add(" let s = String::from_utf8_lossy(bytes);") - lines.add(" on_err(&s);") + lines.add(" let h = &*(ud as *const WildcardHandler);") + lines.add(" let bytes = if !msg.is_null() && len > 0 {") + lines.add(" slice::from_raw_parts(msg as *const u8, len)") + lines.add(" } else { &[] };") + lines.add(" let event_id = if ret == 0 && !bytes.is_empty() {") + lines.add(" #[derive(serde::Deserialize)]") + lines.add(" struct EnvelopeMeta {") + lines.add(" #[serde(rename = \"eventType\")]") + lines.add(" event_type: String,") lines.add(" }") - lines.add(" return;") - lines.add(" }") - lines.add(" if msg.is_null() || len == 0 { return; }") - lines.add(" let bytes = slice::from_raw_parts(msg as *const u8, len);") - lines.add(" #[derive(serde::Deserialize)]") - lines.add(" struct EnvelopeMeta {") - lines.add(" #[serde(rename = \"eventType\")]") - lines.add(" event_type: String,") - lines.add(" }") - lines.add(" let meta: EnvelopeMeta = match ciborium::de::from_reader(bytes) {") - lines.add(" Ok(m) => m,") - lines.add(" Err(_) => return,") + lines.add( + " ciborium::de::from_reader::(bytes)" + ) + lines.add(" .map(|m| m.event_type).unwrap_or_default()") + lines.add(" } else {") + lines.add(" String::new()") lines.add(" };") - for ev in events: - lines.add(" if meta.event_type == \"$1\" {" % [ev.wireName]) - lines.add(" #[derive(serde::Deserialize)]") - lines.add(" struct Envelope { payload: $1 }" % [ev.payloadTypeName]) - lines.add( - " if let Ok(env) = ciborium::de::from_reader::(bytes) {" - ) - lines.add( - " if let Some(ref h) = events.$1 { h(&env.payload); }" % - [ev.nimProcName] - ) - lines.add(" }") - lines.add(" return;") - lines.add(" }") + lines.add(" (h.f)(ret, event_id.as_str(), bytes);") + lines.add("}") + lines.add("") + + # Public handle returned by every add_…_listener call. + lines.add("#[derive(Debug, Clone, Copy)]") + lines.add("pub struct ListenerHandle { pub id: u64 }") + lines.add("") + + # Helper: decode an event envelope's `payload` field into any typed + # `T` that the generated `types.rs` already derives `Deserialize` on. + # Pair with `add_event_listener` to lift raw envelope bytes into a + # typed payload without hand-rolling ciborium calls in each branch. + lines.add( + "/// Decode the `payload` field of a CBOR `EventEnvelope` as `T`." + ) + lines.add( + "/// Returns `Err` if the envelope is empty / malformed / the payload" + ) + lines.add("/// cannot be deserialised as `T`.") + lines.add( + "pub fn decode_event_payload(" + ) + lines.add(" envelope: &[u8],") + lines.add(") -> Result {") + lines.add(" #[derive(serde::Deserialize)]") + lines.add(" struct Envelope { payload: T }") + lines.add( + " let env: Envelope = ciborium::de::from_reader(envelope)" + ) + lines.add( + " .map_err(|e| format!(\"decode event payload: {e}\"))?;" + ) + lines.add(" Ok(env.payload)") lines.add("}") lines.add("") @@ -526,8 +554,14 @@ proc generateApiRs*( lines.add(" ptr: *mut c_void,") lines.add(" timeout: Duration,") if events.len > 0: - lines.add(" events: *mut Events,") - lines.add(" event_listener_id: u64,") + # Keeps each registered handler box alive while its listener id is + # live on the Nim side. Removing an entry from the map drops the + # Box and frees the user's closure; the Nim-side registry has + # already guaranteed no callback for that id is in flight by the + # time `_remove_event_listener` returns. + lines.add( + " listeners: std::sync::Mutex>>," + ) lines.add("}") lines.add("") # SAFETY block applies to both impls below (PR #23 Rust review, item 7). @@ -564,13 +598,9 @@ proc generateApiRs*( lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName]) lines.add(" self.ptr = std::ptr::null_mut();") lines.add(" }") - # Reclaim the Events box after the dylib's destroy has torn down the - # FFI thread (no more events will fire by this point). - if events.len > 0: - lines.add(" if !self.events.is_null() {") - lines.add(" unsafe { drop(Box::from_raw(self.events)); }") - lines.add(" self.events = std::ptr::null_mut();") - lines.add(" }") + # `listeners` is dropped automatically after this body returns. By + # that point the dylib has joined its threads, so no callback is mid- + # flight against any of the raw pointers we handed it. lines.add(" }") lines.add("}") lines.add("") @@ -627,7 +657,7 @@ proc generateApiRs*( " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" ) if events.len > 0: - lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, events: std::ptr::null_mut(), event_listener_id: 0 })") + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })") else: lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") lines.add(" }") @@ -653,49 +683,126 @@ proc generateApiRs*( " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" ) if events.len > 0: - lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, events: std::ptr::null_mut(), event_listener_id: 0 })") + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })") else: lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") lines.add(" }") lines.add("") - # ── Typed event registration ─────────────────────────────────────────── + # ── Listener-registration API ───────────────────────────────────────── if events.len > 0: + # Private helper shared by every public `add_*_listener`: the + # FFI call + map insertion is identical across the typed and + # wildcard variants, so it lives in one place. The caller owns + # the box (typed as the concrete handler struct so the raw + # pointer matches the trampoline's expected type) and only + # erases it to `dyn Any + Send` when handing ownership over. + lines.add(" fn add_listener_inner(") + lines.add(" &self,") + lines.add(" event_name: *const c_char,") + lines.add(" callback: ffi::FFICallback,") + lines.add(" raw: *mut c_void,") + lines.add(" owned: Box,") + lines.add(" ) -> ListenerHandle {") + lines.add(" let id = unsafe {") lines.add( - " /// Attach typed event handlers. Each call removes any previous" - ) - lines.add( - " /// listener via `_remove_event_listener` before adding the new" - ) - lines.add( - " /// one, so the registry never holds a pointer into a freed box." - ) - lines.add(" pub fn set_event_handlers(&mut self, handlers: Events) {") - lines.add(" if self.event_listener_id != 0 {") - lines.add(" unsafe {") - lines.add( - " let _ = ffi::$1_remove_event_listener(self.ptr, self.event_listener_id);" % + " ffi::$1_add_event_listener(self.ptr, event_name, callback, raw)" % [libName] ) - lines.add(" }") - lines.add(" self.event_listener_id = 0;") + lines.add(" };") + lines.add(" if id != 0 {") + lines.add(" self.listeners.lock().unwrap().insert(id, owned);") lines.add(" }") - lines.add(" if !self.events.is_null() {") - lines.add(" unsafe { drop(Box::from_raw(self.events)); }") - lines.add(" self.events = std::ptr::null_mut();") - lines.add(" }") - lines.add(" let raw = Box::into_raw(Box::new(handlers));") - lines.add(" self.events = raw;") - lines.add(" unsafe {") + lines.add(" ListenerHandle { id }") + lines.add(" }") + lines.add("") + + for ev in events: + let methodName = "add_" & camelToSnakeCase(ev.nimProcName) & "_listener" + let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler" + let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline" + lines.add( + " /// Register a typed listener for `$1`. The returned handle can be" % + [ev.wireName] + ) + lines.add(" /// passed to `remove_event_listener` to unregister.") + lines.add( + " pub fn $1(&self, handler: F) -> ListenerHandle" % [methodName] + ) + lines.add( + " where F: Fn(&$1) + Send + Sync + 'static," % [ev.payloadTypeName] + ) + lines.add(" {") + lines.add( + " let owned: Box<$1> = Box::new($1 { f: Box::new(handler) });" % + [handlerStruct] + ) + lines.add(" let raw = &*owned as *const $1 as *mut c_void;" % + [handlerStruct]) + lines.add( + " self.add_listener_inner(b\"$1\\0\".as_ptr() as *const c_char, $2, raw, owned)" % + [ev.wireName, trampolineName] + ) + lines.add(" }") + lines.add("") + + # Generic wildcard listener — receives every event with the wire + # `eventType` string pre-extracted plus the raw envelope bytes. Pair + # with `decode_event_payload::` to lift the payload into a typed + # value. lines.add( - " self.event_listener_id = ffi::$1_add_event_listener(" % + " /// Register a catch-all listener that receives every event." + ) + lines.add( + " /// The handler arguments are (return_code, event_id, envelope_bytes):" + ) + lines.add( + " /// `event_id` is the wire `eventType` string extracted from the" + ) + lines.add( + " /// envelope (empty on error or malformed envelope); `envelope_bytes`" + ) + lines.add( + " /// is the full CBOR envelope, suitable for `decode_event_payload::`." + ) + lines.add( + " pub fn add_event_listener(&self, handler: F) -> ListenerHandle" + ) + lines.add(" where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static,") + lines.add(" {") + lines.add( + " let owned: Box = Box::new(WildcardHandler { f: Box::new(handler) });" + ) + lines.add( + " let raw = &*owned as *const WildcardHandler as *mut c_void;" + ) + lines.add( + " self.add_listener_inner(b\"\\0\".as_ptr() as *const c_char, $1_wildcard_trampoline, raw, owned)" % [libName] ) - lines.add(" self.ptr, b\"\\0\".as_ptr() as *const c_char,") + lines.add(" }") + lines.add("") + + # Remove by handle. Drops the Box (and the user's closure) after the + # C ABI confirms the listener has been unregistered. lines.add( - " $1_event_trampoline, raw as *mut c_void);" % [libName] + " /// Remove a previously-registered listener by handle. Returns true" ) - lines.add(" }") + lines.add( + " /// if the listener existed and was removed; false otherwise." + ) + lines.add( + " pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {" + ) + lines.add(" if handle.id == 0 { return false; }") + lines.add(" let rc = unsafe {") + lines.add( + " ffi::$1_remove_event_listener(self.ptr, handle.id)" % + [libName] + ) + lines.add(" };") + lines.add(" self.listeners.lock().unwrap().remove(&handle.id);") + lines.add(" rc == 0") lines.add(" }") lines.add("")