2026-05-31 18:51:06 +02:00
## Native (zero-serialization) Rust binding generator.
##
## Emits a `<lib>_native` crate that wraps the *native* C ABI (the `<name>`
## entry points + flat C-POD structs) — no CBOR. Each `{.ffi.}` type is a
## `#[repr(C)]` mirror (`ffi`) plus an idiomatic Rust struct with `to_c`/`from_c`
## (`types`), and `<Lib>Node` marshals typed args in / reads typed struct
## returns out (`api`). Counterpart of the CBOR generator in `rust_cbor.nim`, and
## the Rust analogue of `cpp_native.nim`.
##
## Commit 1 covers scalar / string / bool / float / nested-struct fields and the
## procs that use only those (the timer's create / version / echo). Sequences,
## optionals and native events are layered on next.
import std / [ os , strutils ]
import . / meta , . / string_helpers
proc isSeqT ( t : string ) : bool =
t . strip ( ) . startsWith ( " seq[ " ) and t . strip ( ) . endsWith ( " ] " )
proc isOptT ( t : string ) : bool =
let s = t . strip ( )
( s . startsWith ( " Option[ " ) or s . startsWith ( " Maybe[ " ) ) and s . endsWith ( " ] " )
proc isStringT ( t : string ) : bool =
t . strip ( ) in [ " string " , " cstring " ]
proc isStructT ( t : string , types : seq [ FFITypeMeta ] ) : bool =
for ty in types :
if ty . name = = t . strip ( ) :
return true
false
2026-05-31 18:59:57 +02:00
proc seqElem ( t : string ) : string =
t . strip ( ) [ " seq[ " . len .. ^ 2 ] . strip ( )
proc optElem ( t : string ) : string =
let s = t . strip ( )
let p = if s . startsWith ( " Maybe[ " ) : " Maybe[ " . len else : " Option[ " . len
s [ p .. ^ 2 ] . strip ( )
2026-05-31 18:51:06 +02:00
proc rustScalar ( t : string ) : string =
case t . strip ( )
of " int " , " int64 " , " clong " : " i64 "
of " int32 " , " cint " : " i32 "
of " int16 " : " i16 "
of " int8 " : " i8 "
of " uint " , " uint64 " , " csize_t " : " u64 "
of " uint32 " , " cuint " : " u32 "
of " uint16 " : " u16 "
of " uint8 " , " byte " : " u8 "
of " bool " : " bool "
of " float " , " float32 " : " f32 "
of " float64 " : " f64 "
else : capitalizeFirstLetter ( t . strip ( ) ) # nested struct -> its idiomatic name
proc rustIdiomatic ( t : string ) : string =
let s = t . strip ( )
2026-05-31 18:59:57 +02:00
if isSeqT ( s ) : " Vec< " & rustIdiomatic ( seqElem ( s ) ) & " > "
elif isOptT ( s ) : " Option< " & rustIdiomatic ( optElem ( s ) ) & " > "
elif isStringT ( s ) : " String "
2026-05-31 18:51:06 +02:00
else : rustScalar ( s )
2026-05-31 18:59:57 +02:00
proc rustCElem ( t : string , types : seq [ FFITypeMeta ] , qualify = false ) : string =
## repr(C) type for one seq element / option payload (matches emitCStructs).
## `qualify` prefixes struct names with `ffi::` (needed outside the ffi module).
2026-05-31 18:51:06 +02:00
let s = t . strip ( )
if isStringT ( s ) : " *const c_char "
elif s = = " bool " : " c_int "
2026-05-31 18:59:57 +02:00
elif isStructT ( s , types ) : ( if qualify : " ffi:: " else : " " ) & capitalizeFirstLetter ( s )
2026-05-31 18:51:06 +02:00
else : rustScalar ( s )
proc procSimple ( p : FFIProcMeta , types : seq [ FFITypeMeta ] ) : bool =
2026-05-31 18:59:57 +02:00
## All scalar / string / struct params are supported (structs may carry
## seq/Option fields). Bare seq/Option top-level params and raw pointers are
## not (the native ABI doesn't expose them either).
2026-05-31 18:51:06 +02:00
for ep in p . extraParams :
let t = ep . typeName . strip ( )
if ep . isPtr or isSeqT ( t ) or isOptT ( t ) :
return false
true
# ── ffi.rs ──────────────────────────────────────────────────────────────────
proc emitFfiRs (
2026-05-31 19:05:38 +02:00
procs : seq [ FFIProcMeta ] , types : seq [ FFITypeMeta ] , libName : string ,
events : seq [ FFIEventMeta ] = @ [ ]
2026-05-31 18:51:06 +02:00
) : string =
var L : seq [ string ] = @ [ ]
L . add ( " // Generated by nim-ffi native Rust codegen. Do not edit by hand. " )
L . add ( " #![allow(non_snake_case, dead_code)] " )
L . add ( " use std::os::raw::{c_char, c_int, c_void}; " )
L . add ( " " )
L . add ( " pub type FFICallback = " )
L . add ( " unsafe extern \" C \" fn(ret: c_int, msg: *const c_char, len: usize, user_data: *mut c_void); " )
L . add ( " " )
2026-05-31 18:59:57 +02:00
# repr(C) POD mirrors — field layout mirrors codegen/c.emitCStructs.
2026-05-31 18:51:06 +02:00
for t in types :
L . add ( " #[repr(C)] " )
L . add ( " #[derive(Clone, Copy)] " )
L . add ( " pub struct " & capitalizeFirstLetter ( t . name ) & " { " )
for f in t . fields :
2026-05-31 18:59:57 +02:00
let snake = camelToSnakeCase ( f . name )
let ft = f . typeName . strip ( )
if isSeqT ( ft ) :
L . add ( " pub " & snake & " : *const " & rustCElem ( seqElem ( ft ) , types ) & " , " )
L . add ( " pub " & snake & " _len: usize, " )
elif isOptT ( ft ) :
L . add ( " pub " & snake & " _present: c_int, " )
L . add ( " pub " & snake & " : " & rustCElem ( optElem ( ft ) , types ) & " , " )
else :
L . add ( " pub " & snake & " : " & rustCElem ( ft , types ) & " , " )
2026-05-31 18:51:06 +02:00
L . add ( " } " )
L . add ( " " )
L . add ( " #[link(name = \" " & libName & " \" )] " )
L . add ( " extern \" C \" { " )
for p in procs :
case p . kind
of FFIKind . CTOR :
var ps : seq [ string ] = @ [ ]
for ep in p . extraParams :
2026-05-31 18:59:57 +02:00
ps . add ( camelToSnakeCase ( ep . name ) & " : " & rustCElem ( ep . typeName , types ) )
2026-05-31 18:51:06 +02:00
ps . add ( " callback: FFICallback " )
ps . add ( " user_data: *mut c_void " )
L . add ( " pub fn " & p . procName & " ( " & ps . join ( " , " ) & " ) -> *mut c_void; " )
of FFIKind . FFI :
if not procSimple ( p , types ) :
continue
var ps = @ [ " ctx: *mut c_void " , " callback: FFICallback " , " user_data: *mut c_void " ]
for ep in p . extraParams :
2026-05-31 18:59:57 +02:00
ps . add ( camelToSnakeCase ( ep . name ) & " : " & rustCElem ( ep . typeName , types ) )
2026-05-31 18:51:06 +02:00
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; " )
2026-05-31 19:05:38 +02:00
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; " )
2026-05-31 18:51:06 +02:00
L . add ( " } " )
return L . join ( " \n " )
# ── types.rs (idiomatic structs + to_c / from_c) ─────────────────────────────
proc emitTypesRs ( types : seq [ FFITypeMeta ] ) : string =
var L : seq [ string ] = @ [ ]
L . add ( " // Generated by nim-ffi native Rust codegen. Do not edit by hand. " )
2026-05-31 18:59:57 +02:00
L . add ( " #![allow(dead_code)] " )
2026-05-31 18:51:06 +02:00
L . add ( " use std::ffi::{CStr, CString}; " )
L . add ( " use std::os::raw::c_char; " )
L . add ( " use super::ffi; " )
L . add ( " " )
L . add ( " fn cstr(p: *const c_char) -> String { " )
L . add ( " if p.is_null() { String::new() } " )
L . add ( " else { unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned() } " )
L . add ( " } " )
L . add ( " " )
for t in types :
let rn = capitalizeFirstLetter ( t . name )
2026-05-31 18:59:57 +02:00
let hn = rn & " C "
# idiomatic struct
2026-05-31 18:51:06 +02:00
L . add ( " #[derive(Clone, Debug, Default)] " )
L . add ( " pub struct " & rn & " { " )
for f in t . fields :
L . add ( " pub " & camelToSnakeCase ( f . name ) & " : " & rustIdiomatic ( f . typeName ) & " , " )
L . add ( " } " )
L . add ( " " )
2026-05-31 18:59:57 +02:00
# holder: owns the C struct + all heap backing (CStrings, arrays, nested
# holders). Returned by value; the backing lives on the heap, so the C
# struct's raw pointers stay valid across the move and for the call.
L . add ( " pub struct " & hn & " { " )
L . add ( " pub c: ffi:: " & rn & " , " )
L . add ( " _s: Vec<CString>, " )
for f in t . fields :
let s = camelToSnakeCase ( f . name )
let ft = f . typeName . strip ( )
if isSeqT ( ft ) :
L . add ( " _a_ " & s & " : Vec< " & rustCElem ( seqElem ( ft ) , types , qualify = true ) & " >, " )
if isStructT ( seqElem ( ft ) , types ) :
L . add ( " _h_ " & s & " : Vec< " & capitalizeFirstLetter ( seqElem ( ft ) ) & " C>, " )
elif isOptT ( ft ) and isStructT ( optElem ( ft ) , types ) :
L . add ( " _h_ " & s & " : Vec< " & capitalizeFirstLetter ( optElem ( ft ) ) & " C>, " )
elif isStructT ( ft , types ) :
L . add ( " _h_ " & s & " : Vec< " & capitalizeFirstLetter ( ft ) & " C>, " )
L . add ( " } " )
L . add ( " " )
2026-05-31 18:51:06 +02:00
L . add ( " impl " & rn & " { " )
2026-05-31 18:59:57 +02:00
L . add ( " pub fn to_c(&self) -> " & hn & " { " )
L . add ( " #[allow(unused_mut)] " )
L . add ( " let mut h = " & hn & " { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), " )
2026-05-31 18:51:06 +02:00
for f in t . fields :
2026-05-31 18:59:57 +02:00
let s = camelToSnakeCase ( f . name )
2026-05-31 18:51:06 +02:00
let ft = f . typeName . strip ( )
2026-05-31 18:59:57 +02:00
if isSeqT ( ft ) :
L . add ( " _a_ " & s & " : Vec::new(), " )
if isStructT ( seqElem ( ft ) , types ) :
L . add ( " _h_ " & s & " : Vec::new(), " )
elif ( isOptT ( ft ) and isStructT ( optElem ( ft ) , types ) ) or isStructT ( ft , types ) :
L . add ( " _h_ " & s & " : Vec::new(), " )
L . add ( " }; " )
2026-05-31 18:51:06 +02:00
for f in t . fields :
2026-05-31 18:59:57 +02:00
let s = camelToSnakeCase ( f . name )
2026-05-31 18:51:06 +02:00
let ft = f . typeName . strip ( )
2026-05-31 18:59:57 +02:00
if isSeqT ( ft ) :
let e = seqElem ( ft )
L . add ( " for x in &self. " & s & " { " )
if isStringT ( e ) :
L . add ( " let cs = CString::new(x.as_str()).unwrap_or_default(); " )
L . add ( " h._a_ " & s & " .push(cs.as_ptr()); " )
L . add ( " h._s.push(cs); " )
elif isStructT ( e , types ) :
L . add ( " let eh = x.to_c(); " )
L . add ( " h._a_ " & s & " .push(eh.c); " )
L . add ( " h._h_ " & s & " .push(eh); " )
elif e = = " bool " :
L . add ( " h._a_ " & s & " .push(if *x { 1 } else { 0 }); " )
else :
L . add ( " h._a_ " & s & " .push(*x); " )
L . add ( " } " )
L . add ( " h.c. " & s & " = if h._a_ " & s & " .is_empty() { std::ptr::null() } else { h._a_ " & s & " .as_ptr() }; " )
L . add ( " h.c. " & s & " _len = h._a_ " & s & " .len(); " )
elif isOptT ( ft ) :
let e = optElem ( ft )
if isStringT ( e ) :
L . add ( " if let Some(x) = &self. " & s & " { " )
L . add ( " let cs = CString::new(x.as_str()).unwrap_or_default(); " )
L . add ( " h.c. " & s & " = cs.as_ptr(); h.c. " & s & " _present = 1; h._s.push(cs); " )
L . add ( " } " )
elif isStructT ( e , types ) :
L . add ( " if let Some(x) = &self. " & s & " { " )
L . add ( " let eh = x.to_c(); h.c. " & s & " = eh.c; h.c. " & s & " _present = 1; h._h_ " & s & " .push(eh); " )
L . add ( " } " )
elif e = = " bool " :
L . add ( " if let Some(v) = self. " & s & " { h.c. " & s & " _present = 1; h.c. " & s & " = if v { 1 } else { 0 }; } " )
else :
L . add ( " if let Some(v) = self. " & s & " { h.c. " & s & " _present = 1; h.c. " & s & " = v; } " )
elif isStringT ( ft ) :
L . add ( " { let cs = CString::new(self. " & s & " .as_str()).unwrap_or_default(); h.c. " & s & " = cs.as_ptr(); h._s.push(cs); } " )
2026-05-31 18:51:06 +02:00
elif ft = = " bool " :
2026-05-31 18:59:57 +02:00
L . add ( " h.c. " & s & " = if self. " & s & " { 1 } else { 0 }; " )
2026-05-31 18:51:06 +02:00
elif isStructT ( ft , types ) :
2026-05-31 18:59:57 +02:00
L . add ( " h._h_ " & s & " .push(self. " & s & " .to_c()); h.c. " & s & " = h._h_ " & s & " [0].c; " )
2026-05-31 18:51:06 +02:00
else :
2026-05-31 18:59:57 +02:00
L . add ( " h.c. " & s & " = self. " & s & " ; " )
L . add ( " h " )
2026-05-31 18:51:06 +02:00
L . add ( " } " )
# from_c
L . add ( " pub fn from_c(c: &ffi:: " & rn & " ) -> Self { " )
L . add ( " " & rn & " { " )
for f in t . fields :
2026-05-31 18:59:57 +02:00
let s = camelToSnakeCase ( f . name )
2026-05-31 18:51:06 +02:00
let ft = f . typeName . strip ( )
2026-05-31 18:59:57 +02:00
if isSeqT ( ft ) :
let e = seqElem ( ft )
var conv = " |&v| v "
if isStringT ( e ) : conv = " |&p| cstr(p) "
elif isStructT ( e , types ) : conv = " |e| " & capitalizeFirstLetter ( e ) & " ::from_c(e) "
elif e = = " bool " : conv = " |&v| v != 0 "
L . add ( " " & s & " : if c. " & s & " .is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c. " & s & " , c. " & s & " _len) }.iter().map( " & conv & " ).collect() }, " )
elif isOptT ( ft ) :
let e = optElem ( ft )
var val = " c. " & s
if isStringT ( e ) : val = " cstr(c. " & s & " ) "
elif isStructT ( e , types ) : val = capitalizeFirstLetter ( e ) & " ::from_c(&c. " & s & " ) "
elif e = = " bool " : val = " c. " & s & " != 0 "
L . add ( " " & s & " : if c. " & s & " _present != 0 { Some( " & val & " ) } else { None }, " )
elif isStringT ( ft ) :
L . add ( " " & s & " : cstr(c. " & s & " ), " )
2026-05-31 18:51:06 +02:00
elif ft = = " bool " :
2026-05-31 18:59:57 +02:00
L . add ( " " & s & " : c. " & s & " != 0, " )
2026-05-31 18:51:06 +02:00
elif isStructT ( ft , types ) :
2026-05-31 18:59:57 +02:00
L . add ( " " & s & " : " & capitalizeFirstLetter ( ft ) & " ::from_c(&c. " & s & " ), " )
2026-05-31 18:51:06 +02:00
else :
2026-05-31 18:59:57 +02:00
L . add ( " " & s & " : c. " & s & " , " )
2026-05-31 18:51:06 +02:00
L . add ( " } " )
L . add ( " } " )
L . add ( " } " )
L . add ( " " )
return L . join ( " \n " )
# ── api.rs (Node + blocking calls) ───────────────────────────────────────────
proc rustMethod ( procName , libName : string ) : string =
let prefix = libName & " _ "
let bare =
if procName . startsWith ( prefix ) : procName [ prefix . len .. ^ 1 ] else : procName
camelToSnakeCase ( bare )
proc emitApiRs (
2026-05-31 19:05:38 +02:00
procs : seq [ FFIProcMeta ] , types : seq [ FFITypeMeta ] , libName : string ,
events : seq [ FFIEventMeta ] = @ [ ]
2026-05-31 18:51:06 +02:00
) : string =
let nodeT = snakeToPascalCase ( libName ) & " Node " # idiomatic Rust: MyTimerNode
2026-05-31 19:05:38 +02:00
let hasEvents = events . len > 0
2026-05-31 18:51:06 +02:00
var L : seq [ string ] = @ [ ]
L . add ( " // Generated by nim-ffi native Rust codegen. Do not edit by hand. " )
L . add ( " #![allow(dead_code)] " )
L . add ( " use std::os::raw::{c_char, c_int, c_void}; " )
L . add ( " use std::sync::mpsc::{sync_channel, SyncSender}; " )
L . add ( " use super::ffi; " )
L . add ( " use super::types::*; " )
L . add ( " " )
L . add ( " const RET_OK: c_int = 0; " )
L . add ( " " )
L . add ( " unsafe fn err_text(msg: *const c_char, len: usize) -> String { " )
L . add ( " if msg.is_null() || len == 0 { return String::from( \" error \" ); } " )
L . add ( " String::from_utf8_lossy(std::slice::from_raw_parts(msg as *const u8, len)).into_owned() " )
L . add ( " } " )
L . add ( " " )
L . add ( " unsafe extern \" C \" fn ack_cb(ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { " )
L . add ( " let tx = Box::from_raw(ud as *mut SyncSender<Result<(), String>>); " )
L . add ( " let _ = tx.send(if ret == RET_OK { Ok(()) } else { Err(err_text(msg, len)) }); " )
L . add ( " } " )
L . add ( " unsafe extern \" C \" fn str_cb(ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { " )
L . add ( " let tx = Box::from_raw(ud as *mut SyncSender<Result<String, String>>); " )
L . add ( " let r = if ret == RET_OK { " )
L . add ( " let b = if msg.is_null() || len == 0 { Vec::new() } else { std::slice::from_raw_parts(msg as *const u8, len).to_vec() }; " )
L . add ( " Ok(String::from_utf8_lossy(&b).into_owned()) " )
L . add ( " } else { Err(err_text(msg, len)) }; " )
L . add ( " let _ = tx.send(r); " )
L . add ( " } " )
# Per-struct-return callbacks.
for p in procs :
if p . kind ! = FFIKind . FFI or not procSimple ( p , types ) : continue
if not isStructT ( p . returnTypeName , types ) : continue
let rt = capitalizeFirstLetter ( p . returnTypeName )
L . add ( " unsafe extern \" C \" fn cb_ " & p . procName & " (ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { " )
L . add ( " let tx = Box::from_raw(ud as *mut SyncSender<Result< " & rt & " , String>>); " )
L . add ( " let r = if ret == RET_OK && !msg.is_null() { " )
L . add ( " Ok( " & rt & " ::from_c(&*(msg as *const ffi:: " & rt & " ))) " )
L . add ( " } else { Err(err_text(msg, len)) }; " )
L . add ( " let _ = tx.send(r); " )
L . add ( " } " )
2026-05-31 19:05:38 +02:00
# 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 } " )
2026-05-31 18:51:06 +02:00
L . add ( " " )
2026-05-31 19:05:38 +02:00
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 } " )
2026-05-31 18:51:06 +02:00
L . add ( " unsafe impl Send for " & nodeT & " {} " )
L . add ( " unsafe impl Sync for " & nodeT & " {} " )
L . add ( " " )
L . add ( " impl " & nodeT & " { " )
# ctor
for p in procs :
if p . kind ! = FFIKind . CTOR : continue
var params : seq [ string ] = @ [ ]
for ep in p . extraParams :
params . add ( camelToSnakeCase ( ep . name ) & " : " & capitalizeFirstLetter ( ep . typeName ) )
L . add ( " pub fn new( " & params . join ( " , " ) & " ) -> Result<Self, String> { " )
var args : seq [ string ] = @ [ ]
for ep in p . extraParams :
2026-05-31 18:59:57 +02:00
# The holder must outlive the call (it owns the CStrings/arrays the C
# struct points into); the native ctor deep-copies before returning.
L . add ( " let c_ " & camelToSnakeCase ( ep . name ) & " = " & camelToSnakeCase ( ep . name ) & " .to_c(); " )
args . add ( " c_ " & camelToSnakeCase ( ep . name ) & " .c " )
2026-05-31 18:51:06 +02:00
L . add ( " let (tx, rx) = sync_channel::<Result<(), String>>(1); " )
L . add ( " let raw = Box::into_raw(Box::new(tx)) as *mut c_void; " )
let cargs = ( args & @ [ " ack_cb " , " raw " ] ) . join ( " , " )
L . add ( " let ctx = unsafe { ffi:: " & p . procName & " ( " & cargs & " ) }; " )
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 \" )); } " )
2026-05-31 19:05:38 +02:00
if hasEvents :
L . add ( " Ok( " & nodeT & " { ctx, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) }) " )
else :
L . add ( " Ok( " & nodeT & " { ctx }) " )
2026-05-31 18:51:06 +02:00
L . add ( " } " )
L . add ( " " )
# methods
for p in procs :
if p . kind ! = FFIKind . FFI : continue
if not procSimple ( p , types ) :
L . add ( " // SKIPPED " & p . procName & " : seq/Option params not yet supported by native Rust codegen " )
continue
let structRet = isStructT ( p . returnTypeName , types )
let retT = if structRet : capitalizeFirstLetter ( p . returnTypeName ) else : " String "
let cbName = if structRet : " cb_ " & p . procName else : " str_cb "
var params : seq [ string ] = @ [ ]
var conv : seq [ string ] = @ [ ]
var callArgs : seq [ string ] = @ [ " self.ctx " , cbName , " raw " ]
for ep in p . extraParams :
params . add ( camelToSnakeCase ( ep . name ) & " : " & capitalizeFirstLetter ( ep . typeName ) )
2026-05-31 18:59:57 +02:00
conv . add ( " let c_ " & camelToSnakeCase ( ep . name ) & " = " & camelToSnakeCase ( ep . name ) & " .to_c(); " )
callArgs . add ( " c_ " & camelToSnakeCase ( ep . name ) & " .c " )
2026-05-31 18:51:06 +02:00
L . add ( " pub fn " & rustMethod ( p . procName , libName ) & " (&self " & ( if params . len > 0 : " , " & params . join ( " , " ) else : " " ) & " ) -> Result< " & retT & " , String> { " )
2026-05-31 18:59:57 +02:00
for c in conv : L . add ( c )
2026-05-31 18:51:06 +02:00
L . add ( " let (tx, rx) = sync_channel::<Result< " & retT & " , String>>(1); " )
L . add ( " let raw = Box::into_raw(Box::new(tx)) as *mut c_void; " )
L . add ( " let rc = unsafe { ffi:: " & p . procName & " ( " & callArgs . join ( " , " ) & " ) }; " )
L . add ( " if rc != RET_OK { " )
L . add ( " drop(unsafe { Box::from_raw(raw as *mut SyncSender<Result< " & retT & " , String>>) }); " )
L . add ( " return Err(String::from( \" " & p . procName & " dispatch failed \" )); " )
L . add ( " } " )
L . add ( " rx.recv().map_err(|_| String::from( \" callback channel closed \" ))? " )
L . add ( " } " )
L . add ( " " )
2026-05-31 19:05:38 +02:00
# 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 ( " " )
2026-05-31 18:51:06 +02:00
# dtor
for p in procs :
if p . kind = = FFIKind . DTOR :
L . add ( " } " )
L . add ( " " )
L . add ( " impl Drop for " & nodeT & " { " )
L . add ( " fn drop(&mut self) { unsafe { ffi:: " & p . procName & " (self.ctx); } } " )
L . add ( " } " )
return L . join ( " \n " )
proc generateRustNativeCrate * (
procs : seq [ FFIProcMeta ] ,
types : seq [ FFITypeMeta ] ,
libName : string ,
outputDir : string ,
nimSrcRelPath : string ,
events : seq [ FFIEventMeta ] = @ [ ] ,
) =
createDir ( outputDir / " src " )
writeFile ( outputDir / " Cargo.toml " ,
" [package] \n name = \" " & libName & " _native \" \n version = \" 0.1.0 \" \n edition = \" 2021 \" \n " )
writeFile ( outputDir / " src " / " lib.rs " ,
" mod ffi; \n mod types; \n mod api; \n pub use types::*; \n pub use api::*; \n " )
2026-05-31 19:05:38 +02:00
writeFile ( outputDir / " src " / " ffi.rs " , emitFfiRs ( procs , types , libName , events ) )
2026-05-31 18:51:06 +02:00
writeFile ( outputDir / " src " / " types.rs " , emitTypesRs ( types ) )
2026-05-31 19:05:38 +02:00
writeFile ( outputDir / " src " / " api.rs " , emitApiRs ( procs , types , libName , events ) )