mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
feat(codegen): native typed events for the Rust generator
Adds native (zero-CBOR) event support to the Rust generator, mirroring the cpp_native / Go event path: a per-event `add_<wire>_listener` registrar takes a closure, boxes it, and registers it through the bare `<lib>_add_event_listener` (native) entry point. The extern "C" trampoline reads the payload as the raw C-POD struct and hands the consumer a borrowed idiomatic value via from_c — no serialization on the hot path. The node owns the boxed closures in a Mutex<HashMap<id, Box<dyn Any>>> keyed by listener id so they outlive the call, and `remove_event_listener` drops them and calls the bare remove entry point. Event externs are only emitted when the library declares events, so event-free crates stay minimal. Verified end-to-end: the demo registers a listener, echo fires on_echo_fired inline, the typed EchoEvent reaches the closure, and removal returns true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
99fdcfdff7
commit
88c7c8b8b7
@ -27,6 +27,11 @@ complex, schedule all generate and round-trip typed values. `to_c` returns a
|
||||
holder that owns the `CString`s and C-array backing (heap, so the C struct's raw
|
||||
pointers stay valid across the move and for the call).
|
||||
|
||||
Still to come: native typed events and the native-bare / `_cbor` reconciliation.
|
||||
Native typed events are supported too: `add_<event>_listener` takes a closure
|
||||
receiving the payload as a borrowed idiomatic struct (the trampoline reads the
|
||||
raw C-POD directly — no CBOR), the handle goes through `remove_event_listener`,
|
||||
and the node owns the boxed closures for their lifetime.
|
||||
|
||||
Still to come: the native-bare / `_cbor` filename reconciliation.
|
||||
Linking is left to the consumer (`-L <dir> -l my_timer` + rpath, as in
|
||||
`examples/demo.rs`); a build.rs that compiles the dylib can be added later.
|
||||
|
||||
@ -2,8 +2,16 @@ use my_timer_native::*;
|
||||
fn main() {
|
||||
let node = MyTimerNode::new(TimerConfig { name: "rust-native-gen".into() }).unwrap();
|
||||
println!("version: {}", node.version().unwrap());
|
||||
|
||||
// Native typed event: echo fires on_echo_fired inline with a raw C-POD
|
||||
// EchoEvent payload, delivered straight to the closure (no CBOR decode).
|
||||
let h = node.add_on_echo_fired_listener(|e: &EchoEvent| {
|
||||
println!("event on_echo_fired: message={:?} echo_count={}", e.message, e.echo_count);
|
||||
});
|
||||
|
||||
let r = node.echo(EchoRequest { message: "hello from generated Rust".into(), delay_ms: 5 }).unwrap();
|
||||
println!("echo: echoed={} timer_name={}", r.echoed, r.timer_name);
|
||||
println!("removed listener: {}", node.remove_event_listener(h));
|
||||
|
||||
let c = node.complex(ComplexRequest {
|
||||
messages: vec![EchoRequest { message: "one".into(), delay_ms: 0 },
|
||||
|
||||
@ -46,7 +46,20 @@ unsafe extern "C" fn cb_my_timer_schedule(ret: c_int, msg: *const c_char, len: u
|
||||
let _ = tx.send(r);
|
||||
}
|
||||
|
||||
pub struct MyTimerNode { ctx: *mut c_void }
|
||||
struct OnEchoFiredHandler { f: Box<dyn Fn(&EchoEvent) + Send + Sync> }
|
||||
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 != RET_OK || msg.is_null() { return; }
|
||||
let h = &*(ud as *const OnEchoFiredHandler);
|
||||
(h.f)(&EchoEvent::from_c(&*(msg as *const ffi::EchoEvent)));
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ListenerHandle { pub id: u64 }
|
||||
|
||||
pub struct MyTimerNode {
|
||||
ctx: *mut c_void,
|
||||
listeners: std::sync::Mutex<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,
|
||||
}
|
||||
unsafe impl Send for MyTimerNode {}
|
||||
unsafe impl Sync for MyTimerNode {}
|
||||
|
||||
@ -59,7 +72,7 @@ impl MyTimerNode {
|
||||
let res = rx.recv().map_err(|_| String::from("callback channel closed"))?;
|
||||
res?;
|
||||
if ctx.is_null() { return Err(String::from("my_timer_create returned null")); }
|
||||
Ok(MyTimerNode { ctx })
|
||||
Ok(MyTimerNode { ctx, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })
|
||||
}
|
||||
|
||||
pub fn echo(&self, req: EchoRequest) -> Result<EchoResponse, String> {
|
||||
@ -111,6 +124,22 @@ impl MyTimerNode {
|
||||
rx.recv().map_err(|_| String::from("callback channel closed"))?
|
||||
}
|
||||
|
||||
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;
|
||||
let id = unsafe { ffi::my_timer_add_event_listener(self.ctx, b"on_echo_fired\0".as_ptr() as *const c_char, on_echo_fired_trampoline, raw) };
|
||||
if id != 0 { self.listeners.lock().unwrap().insert(id, owned as Box<dyn std::any::Any + Send>); }
|
||||
ListenerHandle { id }
|
||||
}
|
||||
|
||||
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.ctx, handle.id) };
|
||||
self.listeners.lock().unwrap().remove(&handle.id);
|
||||
rc == RET_OK
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Drop for MyTimerNode {
|
||||
|
||||
@ -97,4 +97,6 @@ extern "C" {
|
||||
pub fn my_timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req: ComplexRequest) -> c_int;
|
||||
pub fn my_timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> c_int;
|
||||
pub fn my_timer_destroy(ctx: *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;
|
||||
}
|
||||
@ -81,7 +81,8 @@ proc procSimple(p: FFIProcMeta, types: seq[FFITypeMeta]): bool =
|
||||
|
||||
# ── ffi.rs ──────────────────────────────────────────────────────────────────
|
||||
proc emitFfiRs(
|
||||
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string
|
||||
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string,
|
||||
events: seq[FFIEventMeta] = @[]
|
||||
): string =
|
||||
var L: seq[string] = @[]
|
||||
L.add("// Generated by nim-ffi native Rust codegen. Do not edit by hand.")
|
||||
@ -129,6 +130,11 @@ proc emitFfiRs(
|
||||
L.add(" pub fn " & p.procName & "(" & ps.join(", ") & ") -> c_int;")
|
||||
of FFIKind.DTOR:
|
||||
L.add(" pub fn " & p.procName & "(ctx: *mut c_void) -> c_int;")
|
||||
if events.len > 0:
|
||||
# Native event registration: the bare (non-_cbor) listener delivers the
|
||||
# payload as the raw C-POD struct, so the trampoline reads it directly.
|
||||
L.add(" pub fn " & libName & "_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;")
|
||||
L.add(" pub fn " & libName & "_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;")
|
||||
L.add("}")
|
||||
return L.join("\n")
|
||||
|
||||
@ -277,9 +283,11 @@ proc rustMethod(procName, libName: string): string =
|
||||
camelToSnakeCase(bare)
|
||||
|
||||
proc emitApiRs(
|
||||
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string
|
||||
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string,
|
||||
events: seq[FFIEventMeta] = @[]
|
||||
): string =
|
||||
let nodeT = snakeToPascalCase(libName) & "Node" # idiomatic Rust: MyTimerNode
|
||||
let hasEvents = events.len > 0
|
||||
var L: seq[string] = @[]
|
||||
L.add("// Generated by nim-ffi native Rust codegen. Do not edit by hand.")
|
||||
L.add("#![allow(dead_code)]")
|
||||
@ -319,8 +327,29 @@ proc emitApiRs(
|
||||
L.add(" } else { Err(err_text(msg, len)) };")
|
||||
L.add(" let _ = tx.send(r);")
|
||||
L.add("}")
|
||||
# Per-event handler boxes + native trampolines (read the raw C-POD payload).
|
||||
for e in events:
|
||||
let pt = capitalizeFirstLetter(e.payloadTypeName)
|
||||
let hs = snakeToPascalCase(e.wireName) & "Handler"
|
||||
L.add("")
|
||||
L.add("struct " & hs & " { f: Box<dyn Fn(&" & pt & ") + Send + Sync> }")
|
||||
L.add("unsafe extern \"C\" fn " & e.wireName & "_trampoline(ret: c_int, msg: *const c_char, _len: usize, ud: *mut c_void) {")
|
||||
L.add(" if ud.is_null() || ret != RET_OK || msg.is_null() { return; }")
|
||||
L.add(" let h = &*(ud as *const " & hs & ");")
|
||||
L.add(" (h.f)(&" & pt & "::from_c(&*(msg as *const ffi::" & pt & ")));")
|
||||
L.add("}")
|
||||
if hasEvents:
|
||||
L.add("")
|
||||
L.add("#[derive(Clone, Copy)]")
|
||||
L.add("pub struct ListenerHandle { pub id: u64 }")
|
||||
L.add("")
|
||||
L.add("pub struct " & nodeT & " { ctx: *mut c_void }")
|
||||
if hasEvents:
|
||||
L.add("pub struct " & nodeT & " {")
|
||||
L.add(" ctx: *mut c_void,")
|
||||
L.add(" listeners: std::sync::Mutex<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,")
|
||||
L.add("}")
|
||||
else:
|
||||
L.add("pub struct " & nodeT & " { ctx: *mut c_void }")
|
||||
L.add("unsafe impl Send for " & nodeT & " {}")
|
||||
L.add("unsafe impl Sync for " & nodeT & " {}")
|
||||
L.add("")
|
||||
@ -346,7 +375,10 @@ proc emitApiRs(
|
||||
L.add(" let res = rx.recv().map_err(|_| String::from(\"callback channel closed\"))?;")
|
||||
L.add(" res?;")
|
||||
L.add(" if ctx.is_null() { return Err(String::from(\"" & p.procName & " returned null\")); }")
|
||||
L.add(" Ok(" & nodeT & " { ctx })")
|
||||
if hasEvents:
|
||||
L.add(" Ok(" & nodeT & " { ctx, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })")
|
||||
else:
|
||||
L.add(" Ok(" & nodeT & " { ctx })")
|
||||
L.add(" }")
|
||||
L.add("")
|
||||
|
||||
@ -379,6 +411,28 @@ proc emitApiRs(
|
||||
L.add(" }")
|
||||
L.add("")
|
||||
|
||||
# event listeners: one typed registrar per event + a remove by handle.
|
||||
for e in events:
|
||||
let pt = capitalizeFirstLetter(e.payloadTypeName)
|
||||
let hs = snakeToPascalCase(e.wireName) & "Handler"
|
||||
L.add(" pub fn add_" & e.wireName & "_listener<F>(&self, handler: F) -> ListenerHandle")
|
||||
L.add(" where F: Fn(&" & pt & ") + Send + Sync + 'static {")
|
||||
L.add(" let owned: Box<" & hs & "> = Box::new(" & hs & " { f: Box::new(handler) });")
|
||||
L.add(" let raw = &*owned as *const " & hs & " as *mut c_void;")
|
||||
L.add(" let id = unsafe { ffi::" & libName & "_add_event_listener(self.ctx, b\"" & e.wireName & "\\0\".as_ptr() as *const c_char, " & e.wireName & "_trampoline, raw) };")
|
||||
L.add(" if id != 0 { self.listeners.lock().unwrap().insert(id, owned as Box<dyn std::any::Any + Send>); }")
|
||||
L.add(" ListenerHandle { id }")
|
||||
L.add(" }")
|
||||
L.add("")
|
||||
if hasEvents:
|
||||
L.add(" pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {")
|
||||
L.add(" if handle.id == 0 { return false; }")
|
||||
L.add(" let rc = unsafe { ffi::" & libName & "_remove_event_listener(self.ctx, handle.id) };")
|
||||
L.add(" self.listeners.lock().unwrap().remove(&handle.id);")
|
||||
L.add(" rc == RET_OK")
|
||||
L.add(" }")
|
||||
L.add("")
|
||||
|
||||
# dtor
|
||||
for p in procs:
|
||||
if p.kind == FFIKind.DTOR:
|
||||
@ -402,6 +456,6 @@ proc generateRustNativeCrate*(
|
||||
"[package]\nname = \"" & libName & "_native\"\nversion = \"0.1.0\"\nedition = \"2021\"\n")
|
||||
writeFile(outputDir / "src" / "lib.rs",
|
||||
"mod ffi;\nmod types;\nmod api;\npub use types::*;\npub use api::*;\n")
|
||||
writeFile(outputDir / "src" / "ffi.rs", emitFfiRs(procs, types, libName))
|
||||
writeFile(outputDir / "src" / "ffi.rs", emitFfiRs(procs, types, libName, events))
|
||||
writeFile(outputDir / "src" / "types.rs", emitTypesRs(types))
|
||||
writeFile(outputDir / "src" / "api.rs", emitApiRs(procs, types, libName))
|
||||
writeFile(outputDir / "src" / "api.rs", emitApiRs(procs, types, libName, events))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user