mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
rust codegen: per-event typed add_on_<x>_listener + wildcard add_event_listener (#52)
This commit is contained in:
parent
7ccf34591d
commit
c43563f82f
20
examples/timer/rust_bindings/Cargo.lock
generated
20
examples/timer/rust_bindings/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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<Box<dyn Fn(&str) + Send + Sync>>,
|
||||
pub onEchoFired: Option<Box<dyn Fn(&EchoEvent) + Send + Sync>>,
|
||||
struct OnEchoFiredHandler {
|
||||
f: Box<dyn Fn(&EchoEvent) + Send + Sync>,
|
||||
}
|
||||
|
||||
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::<Envelope, _>(bytes) {
|
||||
(h.f)(&env.payload);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn my_timer_event_trampoline(
|
||||
struct WildcardHandler {
|
||||
f: Box<dyn Fn(c_int, &str, &[u8]) + Send + Sync>,
|
||||
}
|
||||
|
||||
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::<Envelope, _>(bytes) {
|
||||
if let Some(ref h) = events.onEchoFired { h(&env.payload); }
|
||||
struct EnvelopeMeta {
|
||||
#[serde(rename = "eventType")]
|
||||
event_type: String,
|
||||
}
|
||||
return;
|
||||
}
|
||||
ciborium::de::from_reader::<EnvelopeMeta, _>(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<T: serde::de::DeserializeOwned>(
|
||||
envelope: &[u8],
|
||||
) -> Result<T, String> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Envelope<T> { payload: T }
|
||||
let env: Envelope<T> = 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<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,
|
||||
}
|
||||
|
||||
// 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<Self, String> {
|
||||
@ -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<dyn std::any::Any + Send>,
|
||||
) -> 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<F>(&self, handler: F) -> ListenerHandle
|
||||
where F: Fn(&EchoEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let owned: Box<OnEchoFiredHandler> = 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::<T>`.
|
||||
pub fn add_event_listener<F>(&self, handler: F) -> ListenerHandle
|
||||
where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static,
|
||||
{
|
||||
let owned: Box<WildcardHandler> = 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<EchoResponse, String> {
|
||||
|
||||
22
examples/timer/rust_client/Cargo.lock
generated
22
examples/timer/rust_client/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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<Box<dyn Fn(&str) + Send + Sync>>,")
|
||||
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<Box<dyn Fn(&$2) + Send + Sync>>," %
|
||||
[ev.nimProcName, ev.payloadTypeName]
|
||||
" f: Box<dyn Fn(&$1) + Send + Sync>," % [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::<Envelope, _>(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<dyn Fn(c_int, &str, &[u8]) + Send + Sync>,")
|
||||
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::<EnvelopeMeta, _>(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::<Envelope, _>(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<T: serde::de::DeserializeOwned>("
|
||||
)
|
||||
lines.add(" envelope: &[u8],")
|
||||
lines.add(") -> Result<T, String> {")
|
||||
lines.add(" #[derive(serde::Deserialize)]")
|
||||
lines.add(" struct Envelope<T> { payload: T }")
|
||||
lines.add(
|
||||
" let env: Envelope<T> = 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<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,"
|
||||
)
|
||||
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<dyn std::any::Any + Send>,")
|
||||
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<F>(&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::<T>` 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::<T>`."
|
||||
)
|
||||
lines.add(
|
||||
" pub fn add_event_listener<F>(&self, handler: F) -> ListenerHandle"
|
||||
)
|
||||
lines.add(" where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static,")
|
||||
lines.add(" {")
|
||||
lines.add(
|
||||
" let owned: Box<WildcardHandler> = 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("")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user