2026-07-15 11:46:05 -03:00
## Rust binding generator: emits a complete Rust crate using CBOR (ciborium).
2026-05-11 23:28:17 +02:00
2026-07-06 11:43:34 -03:00
import std / [ os , strutils ]
import . / meta , . / string_helpers , . / types_ir
2026-05-11 23:28:17 +02:00
2026-07-15 11:46:05 -03:00
## Wire-format Rust type for any Nim `ptr T`/`pointer`; fixed 64-bit for a
## host-independent CBOR payload size (mirrors CppPtrType).
2026-05-16 01:08:42 +02:00
const RustPtrType * = " u64 "
2026-05-11 23:28:17 +02:00
2026-07-06 11:43:34 -03:00
func rustScalar ( s : ScalarKind ) : string =
case s
of skBool : " bool "
of skI8 : " i8 "
of skI16 : " i16 "
of skI32 : " i32 "
of skI64 : " i64 "
of skU8 : " u8 "
of skU16 : " u16 "
of skU32 : " u32 "
of skU64 : " u64 "
of skF32 : " f32 "
of skF64 : " f64 "
func rustSeq ( elem : string ) : string =
" Vec< " & elem & " > "
func rustOpt ( elem : string ) : string =
" Option< " & elem & " > "
const rustMap = NativeTypeMap (
scalar : rustScalar ,
str : " String " ,
bytes : " Vec<u8> " ,
ptrType : RustPtrType ,
seqOf : rustSeq ,
optOf : rustOpt ,
structName : capitalizeFirstLetter ,
)
2026-05-11 23:28:17 +02:00
proc nimTypeToRust * ( typeName : string ) : string =
## Maps Nim type names to Rust type names, including generics.
2026-07-06 11:43:34 -03:00
renderNative ( rustMap , parseFFIType ( typeName ) )
2026-05-11 23:28:17 +02:00
proc deriveLibName * ( procs : seq [ FFIProcMeta ] ) : string =
2026-07-15 11:46:05 -03:00
## Common prefix before the first `_` in proc names, e.g. "timer_create" → "timer".
2026-05-11 23:28:17 +02:00
if currentLibName . len > 0 :
return currentLibName
if procs . len = = 0 :
return " unknown "
let first = procs [ 0 ] . procName
let parts = first . split ( ' _ ' )
if parts . len > 0 :
return parts [ 0 ]
return " unknown "
proc stripLibPrefix * ( procName : string , libName : string ) : string =
2026-07-15 11:46:05 -03:00
## Strips the library prefix, e.g. ("timer_echo", "timer") → "echo".
2026-05-11 23:28:17 +02:00
let prefix = libName & " _ "
if procName . startsWith ( prefix ) :
return procName [ prefix . len .. ^ 1 ]
return procName
2026-05-16 01:08:42 +02:00
proc reqStructName ( p : FFIProcMeta ) : string =
## Mirrors the Nim macro: <CamelCase(procName)>Req or CtorReq for ctors.
let camel = snakeToPascalCase ( p . procName )
if p . kind = = FFIKind . CTOR :
camel & " CtorReq "
else :
camel & " Req "
2026-05-11 23:28:17 +02:00
proc generateCargoToml * ( libName : string ) : string =
2026-07-15 11:46:05 -03:00
# flume: callback channel (recv_timeout + recv_async), default-features off. tokio: only the async timeout.
2026-05-16 01:08:42 +02:00
return
2026-05-11 23:28:17 +02:00
""" [package]
name = " $1 "
version = " 0.1.0 "
edition = " 2021 "
[ dependencies ]
serde = { version = " 1 " , features = [ " derive " ] }
2026-05-16 01:08:42 +02:00
ciborium = " 0.2 "
flume = { version = " 0.11 " , default - features = false , features = [ " async " ] }
tokio = { version = " 1 " , features = [ " sync " , " time " ] }
2026-06-01 18:34:56 +02:00
[ dev - dependencies ]
tokio = { version = " 1 " , features = [ " rt-multi-thread " , " macros " , " sync " , " time " ] }
2026-05-11 23:28:17 +02:00
""" %
[ libName ]
proc generateBuildRs * ( libName : string , nimSrcRelPath : string ) : string =
2026-07-15 11:46:05 -03:00
## Generates build.rs that compiles the Nim library; nimSrcRelPath is relative
## to the crate directory.
2026-05-11 23:28:17 +02:00
let escapedSrc = nimSrcRelPath . replace ( " \\ " , " \\ \\ " )
2026-05-16 01:08:42 +02:00
return
2026-05-11 23:28:17 +02:00
""" use std::path::PathBuf;
use std : : process : : Command ;
fn main ( ) {
let manifest = PathBuf : : from ( std : : env : : var ( " CARGO_MANIFEST_DIR " ) . unwrap ( ) ) ;
let nim_src = manifest . join ( " $1 " ) ;
let nim_src = nim_src . canonicalize ( ) . unwrap_or ( manifest . join ( " $1 " ) ) ;
/ / Walk up to find the nim - ffi repo root ( directory containing nim_src ' s l i b r a r y )
/ / The repo root is where nim c should be run from ( contains config . nims ) .
/ / We assume nim_src lives somewhere under repo_root .
/ / Derive repo_root as the ancestor that contains the . nimble file or config . nims .
let mut repo_root = nim_src . clone ( ) ;
loop {
repo_root = match repo_root . parent ( ) {
Some ( p ) = > p . to_path_buf ( ) ,
None = > break ,
} ;
if repo_root . join ( " config.nims " ) . exists ( ) | | repo_root . join ( " ffi.nimble " ) . exists ( ) {
break ;
}
}
#[cfg(target_os = "macos")]
let lib_ext = " dylib " ;
#[cfg(target_os = "linux")]
let lib_ext = " so " ;
let out_lib = repo_root . join ( format ! ( " lib $2 .{lib_ext} " ) ) ;
let mut cmd = Command : : new ( " nim " ) ;
cmd . arg ( " c " )
. arg ( " --mm:orc " )
. arg ( " -d:chronicles_log_level=WARN " )
. arg ( " --app:lib " )
. arg ( " --noMain " )
. arg ( format ! ( " --nimMainPrefix:lib $2 " ) )
. arg ( format ! ( " -o:{} " , out_lib . display ( ) ) ) ;
cmd . arg ( & nim_src ) . current_dir ( & repo_root ) ;
let status = cmd . status ( ) . expect ( " failed to run nim compiler " ) ;
assert ! ( status . success ( ) , " Nim compilation failed " ) ;
println ! ( " cargo:rustc-link-search={} " , repo_root . display ( ) ) ;
println ! ( " cargo:rustc-link-lib= $2 " ) ;
println ! ( " cargo:rerun-if-changed={} " , nim_src . display ( ) ) ;
}
""" %
[ escapedSrc , libName ]
proc generateLibRs * ( ) : string =
2026-05-16 01:08:42 +02:00
return """ mod ffi;
2026-05-11 23:28:17 +02:00
mod types ;
mod api ;
pub use types : : * ;
pub use api : : * ;
"""
2026-05-16 01:08:42 +02:00
proc generateFFIRs * ( procs : seq [ FFIProcMeta ] ) : string =
2026-07-15 11:46:05 -03:00
## Generates ffi.rs with extern "C" declarations; each proc takes one CBOR
## buffer (ptr+len) as its request payload.
2026-05-11 23:28:17 +02:00
var lines : seq [ string ] = @ [ ]
lines . add ( " use std::os::raw::{c_char, c_int, c_void}; " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add ( " pub type FFICallback = unsafe extern \" C \" fn( " )
2026-05-11 23:28:17 +02:00
lines . add ( " ret: c_int, " )
lines . add ( " msg: *const c_char, " )
lines . add ( " len: usize, " )
lines . add ( " user_data: *mut c_void, " )
lines . add ( " ); " )
lines . add ( " " )
var libNames : seq [ string ] = @ [ ]
for p in procs :
if p . libName notin libNames :
libNames . add ( p . libName )
var linkLibName = " "
if libNames . len > 0 and libNames [ 0 ] . len > 0 :
linkLibName = libNames [ 0 ]
else :
if procs . len > 0 :
let parts = procs [ 0 ] . procName . split ( ' _ ' )
if parts . len > 0 :
linkLibName = parts [ 0 ]
lines . add ( " #[link(name = \" $1 \" )] " % [ linkLibName ] )
lines . add ( " extern \" C \" { " )
for p in procs :
var params : seq [ string ] = @ [ ]
2026-05-16 01:08:42 +02:00
case p . kind
of FFIKind . FFI :
2026-07-15 11:46:05 -03:00
# Method-style: ctx first.
2026-05-11 23:28:17 +02:00
params . add ( " ctx: *mut c_void " )
2026-05-16 01:08:42 +02:00
params . add ( " callback: FFICallback " )
2026-05-11 23:28:17 +02:00
params . add ( " user_data: *mut c_void " )
2026-05-16 01:08:42 +02:00
params . add ( " req_cbor: *const u8 " )
params . add ( " req_cbor_len: usize " )
lines . add ( " pub fn $1 ( $2 ) -> c_int; " % [ p . procName , params . join ( " , " ) ] )
of FFIKind . CTOR :
2026-07-15 11:46:05 -03:00
# Ctor: no ctx; returns the freshly-allocated handle.
2026-05-16 01:08:42 +02:00
params . add ( " req_cbor: *const u8 " )
params . add ( " req_cbor_len: usize " )
params . add ( " callback: FFICallback " )
2026-05-11 23:28:17 +02:00
params . add ( " user_data: *mut c_void " )
2026-05-16 01:08:42 +02:00
lines . add ( " pub fn $1 ( $2 ) -> *mut c_void; " % [ p . procName , params . join ( " , " ) ] )
of FFIKind . DTOR :
params . add ( " ctx: *mut c_void " )
lines . add ( " pub fn $1 ( $2 ) -> c_int; " % [ p . procName , params . join ( " , " ) ] )
2026-05-11 23:28:17 +02:00
2026-07-15 11:46:05 -03:00
# Listener-registration ABI, always present in the dylib.
2026-05-25 15:51:56 +02:00
lines . add (
2026-05-28 16:00:28 +02:00
" pub fn $1_add_event_listener (ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64; " %
[ linkLibName ]
)
lines . add (
" pub fn $1_remove_event_listener (ctx: *mut c_void, listener_id: u64) -> c_int; " %
2026-05-25 15:51:56 +02:00
[ linkLibName ]
)
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
2026-05-16 01:08:42 +02:00
return lines . join ( " \n " ) & " \n "
2026-05-11 23:28:17 +02:00
2026-05-16 01:08:42 +02:00
proc generateTypesRs * ( types : seq [ FFITypeMeta ] , procs : seq [ FFIProcMeta ] ) : string =
2026-07-15 11:46:05 -03:00
## Generates types.rs: Rust structs for user FFI types and each per-proc Req.
2026-05-11 23:28:17 +02:00
var lines : seq [ string ] = @ [ ]
lines . add ( " use serde::{Deserialize, Serialize}; " )
lines . add ( " " )
for t in types :
lines . add ( " #[derive(Debug, Clone, Serialize, Deserialize)] " )
lines . add ( " pub struct $1 { " % [ t . name ] )
for f in t . fields :
2026-05-16 01:08:42 +02:00
let snakeName = camelToSnakeCase ( f . name )
2026-05-11 23:28:17 +02:00
let rustType = nimTypeToRust ( f . typeName )
2026-07-15 11:46:05 -03:00
# serde rename when camelCase differs from snake_case.
2026-05-11 23:28:17 +02:00
if snakeName ! = f . name :
lines . add ( " #[serde(rename = \" $1 \" )] " % [ f . name ] )
lines . add ( " pub $1 : $2 , " % [ snakeName , rustType ] )
lines . add ( " } " )
lines . add ( " " )
2026-07-15 11:46:05 -03:00
# Per-proc Req structs: the unit of CBOR encoding sent across the boundary.
2026-05-16 01:08:42 +02:00
for p in procs :
if p . kind = = FFIKind . DTOR :
continue
let reqName = reqStructName ( p )
lines . add ( " #[derive(Debug, Clone, Serialize, Deserialize)] " )
if p . extraParams . len = = 0 :
lines . add ( " pub struct $1 {} " % [ reqName ] )
else :
lines . add ( " pub struct $1 { " % [ reqName ] )
for ep in p . extraParams :
let snake = camelToSnakeCase ( ep . name )
let rustType =
2026-06-16 14:11:31 -03:00
if ep . ridesAsPtr ( ) :
2026-05-16 01:08:42 +02:00
RustPtrType
else :
nimTypeToRust ( ep . typeName )
if snake ! = ep . name :
lines . add ( " #[serde(rename = \" $1 \" )] " % [ ep . name ] )
lines . add ( " pub $1 : $2 , " % [ snake , rustType ] )
lines . add ( " } " )
lines . add ( " " )
return lines . join ( " \n " )
2026-05-11 23:28:17 +02:00
2026-05-25 15:51:56 +02:00
proc generateApiRs * (
procs : seq [ FFIProcMeta ] , libName : string , events : seq [ FFIEventMeta ] = @ [ ]
) : string =
2026-07-15 11:46:05 -03:00
## Generates api.rs with a blocking and a tokio-async high-level API.
2026-05-16 01:08:42 +02:00
## Requests/responses are CBOR (ciborium); errors are raw UTF-8 strings.
2026-05-11 23:28:17 +02:00
var lines : seq [ string ] = @ [ ]
var ctors : seq [ FFIProcMeta ] = @ [ ]
var methods : seq [ FFIProcMeta ] = @ [ ]
2026-05-16 01:08:42 +02:00
var dtorProcName = " "
2026-05-11 23:28:17 +02:00
for p in procs :
2026-05-16 01:08:42 +02:00
case p . kind
of FFIKind . CTOR :
ctors . add ( p )
of FFIKind . FFI :
methods . add ( p )
of FFIKind . DTOR :
if dtorProcName . len = = 0 :
dtorProcName = p . procName
2026-05-11 23:28:17 +02:00
var libTypeName = " "
2026-05-16 01:08:42 +02:00
if ctors . len > 0 :
libTypeName = ctors [ 0 ] . libTypeName
else :
libTypeName = capitalizeFirstLetter ( libName )
2026-05-11 23:28:17 +02:00
let ctxTypeName = libTypeName & " Ctx "
lines . add ( " use std::os::raw::{c_char, c_int, c_void}; " )
2026-05-16 01:08:42 +02:00
lines . add ( " use std::slice; " )
2026-05-11 23:28:17 +02:00
lines . add ( " use std::time::Duration; " )
2026-05-16 01:08:42 +02:00
lines . add ( " use serde::de::DeserializeOwned; " )
lines . add ( " use serde::Serialize; " )
2026-05-11 23:28:17 +02:00
lines . add ( " use super::ffi; " )
lines . add ( " use super::types::*; " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add ( " fn encode_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, String> { " )
lines . add ( " let mut buf = Vec::new(); " )
lines . add (
" ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?; "
)
lines . add ( " Ok(buf) " )
lines . add ( " } " )
lines . add ( " " )
lines . add ( " fn decode_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> { " )
lines . add ( " ciborium::de::from_reader(bytes).map_err(|e| e.to_string()) " )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
2026-07-15 11:46:05 -03:00
# FFI trampoline: user_data owns a Box<flume::Sender>; a late callback sends into a closed receiver, which is harmless.
2026-05-16 01:08:42 +02:00
lines . add ( " type FFIResult = Result<Vec<u8>, String>; " )
lines . add ( " type FFISender = flume::Sender<FFIResult>; " )
lines . add ( " " )
lines . add ( " // Reconstruct the (ret, msg, len) tuple delivered by the C callback " )
lines . add (
" // into a Result<Vec<u8>, String>: payload on success, UTF-8 message on error. "
)
lines . add (
" // `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the "
)
lines . add (
" // alternative would be to dispatch a separate Err for invalid UTF-8, but the "
)
lines . add ( " // codegen contract is that Nim handlers emit `string` error payloads, so " )
lines . add ( " // invalid UTF-8 here would be a Nim-side bug. " )
lines . add (
" unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult { "
)
lines . add ( " let bytes = if msg.is_null() || len == 0 { " )
lines . add ( " Vec::new() " )
lines . add ( " } else { " )
lines . add ( " slice::from_raw_parts(msg as *const u8, len).to_vec() " )
lines . add ( " }; " )
2026-07-14 17:07:33 +02:00
lines . add ( " if ret == NIMFFI_RET_OK { Ok(bytes) } " )
2026-05-16 01:08:42 +02:00
lines . add ( " else { Err(String::from_utf8_lossy(&bytes).into_owned()) } " )
lines . add ( " } " )
2026-05-11 23:28:17 +02:00
lines . add ( " " )
2026-07-14 17:07:33 +02:00
lines . add ( " // nim-ffi result-callback status codes (mirror ffi/ffi_types.nim). " )
lines . add ( " const NIMFFI_RET_OK: c_int = 0; " )
lines . add ( " const NIMFFI_RET_MISSING_CALLBACK: c_int = 2; " )
lines . add ( " const NIMFFI_RET_STALE_WARN: c_int = 3; " )
lines . add ( " " )
2026-05-11 23:28:17 +02:00
lines . add ( " unsafe extern \" C \" fn on_result( " )
lines . add ( " ret: c_int, " )
lines . add ( " msg: *const c_char, " )
2026-05-16 01:08:42 +02:00
lines . add ( " len: usize, " )
2026-05-11 23:28:17 +02:00
lines . add ( " user_data: *mut c_void, " )
lines . add ( " ) { " )
2026-07-14 17:07:33 +02:00
lines . add (
" // NIMFFI_RET_STALE_WARN (3) is a non-terminal progress ping: the request "
)
lines . add (
" // is still running. This wrapper only delivers the final result, so ignore "
)
lines . add (
" // it WITHOUT reclaiming the box — a terminal callback still owns the Sender. "
)
lines . add ( " if ret == NIMFFI_RET_STALE_WARN { return; } " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add ( " // Take ownership of the boxed Sender — dropping it at end of scope " )
lines . add ( " // releases the only outstanding handle. " )
lines . add ( " let tx = Box::from_raw(user_data as *mut FFISender); " )
lines . add ( " " )
lines . add (
" // `tx.send` returns Err only if the awaiting future was dropped (and with it "
)
lines . add (
" // the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch "
)
lines . add (
" // lost the race, or the future was dropped before being awaited. This cannot "
)
lines . add (
" // happen with the current rust_client demo but may occur in arbitrary "
)
lines . add ( " // downstream consumers, so we discard the Err safely. " )
lines . add (
" // Given that this is invoked from a Nim thread, we can ' t propagate the error by panicking or "
)
lines . add (
" // returning a Result. Furthermore, an API dev may intentionally set a timeout in the await, "
)
lines . add (
" // in which case is also fine to discard the send error in this case because the API user will "
)
lines . add ( " // handle the timeout expiry in their own code. " )
lines . add (
" // The important part is to ensure that the callback doesn ' t panic or block indefinitely if the "
)
lines . add ( " // receiver is gone. " )
lines . add ( " let _ = tx.send(ffi_payload(ret, msg, len)); " )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add ( " fn ffi_call_sync<F>(timeout: Duration, f: F) -> FFIResult " )
2026-05-11 23:28:17 +02:00
lines . add ( " where " )
2026-05-16 01:08:42 +02:00
lines . add ( " F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int, " )
2026-05-11 23:28:17 +02:00
lines . add ( " { " )
2026-05-16 01:08:42 +02:00
lines . add ( " let (tx, rx) = flume::bounded::<FFIResult>(1); " )
lines . add ( " let raw = Box::into_raw(Box::new(tx)) as *mut c_void; " )
2026-05-11 23:28:17 +02:00
lines . add ( " let ret = f(on_result, raw); " )
2026-07-14 17:07:33 +02:00
lines . add ( " if ret == NIMFFI_RET_MISSING_CALLBACK { " )
2026-05-16 01:08:42 +02:00
lines . add ( " // Callback will never fire; reclaim the box to avoid a leak. " )
lines . add ( " drop(unsafe { Box::from_raw(raw as *mut FFISender) }); " )
2026-05-11 23:28:17 +02:00
lines . add ( " return Err( \" RET_MISSING_CALLBACK (internal error) \" .into()); " )
lines . add ( " } " )
2026-05-16 01:08:42 +02:00
lines . add ( " match rx.recv_timeout(timeout) { " )
lines . add ( " Ok(payload) => payload, " )
lines . add ( " Err(flume::RecvTimeoutError::Timeout) => " )
lines . add ( " Err(format!( \" timed out after {:?} \" , timeout)), " )
lines . add ( " Err(flume::RecvTimeoutError::Disconnected) => " )
lines . add (
" Err( \" callback channel disconnected before delivery \" .into()), "
)
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " } " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add ( " async fn ffi_call_async<F>(timeout: Duration, f: F) -> FFIResult " )
2026-05-11 23:28:17 +02:00
lines . add ( " where " )
2026-05-16 01:08:42 +02:00
lines . add ( " F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int, " )
2026-05-11 23:28:17 +02:00
lines . add ( " { " )
2026-05-16 01:08:42 +02:00
lines . add ( " let (tx, rx) = flume::bounded::<FFIResult>(1); " )
lines . add ( " let raw = Box::into_raw(Box::new(tx)) as *mut c_void; " )
lines . add ( " let ret = f(on_result, raw); " )
2026-07-14 17:07:33 +02:00
lines . add ( " if ret == NIMFFI_RET_MISSING_CALLBACK { " )
2026-05-16 01:08:42 +02:00
lines . add ( " drop(unsafe { Box::from_raw(raw as *mut FFISender) }); " )
lines . add ( " return Err( \" RET_MISSING_CALLBACK (internal error) \" .into()); " )
lines . add ( " } " )
lines . add ( " match tokio::time::timeout(timeout, rx.recv_async()).await { " )
lines . add ( " Ok(Ok(payload)) => payload, " )
lines . add (
" Ok(Err(_)) => Err( \" callback channel disconnected before delivery \" .into()), "
)
lines . add ( " Err(_) => Err(format!( \" timed out after {:?} \" , timeout)), " )
lines . add ( " } " )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-07-15 11:46:05 -03:00
# Per-listener handler boxes + extern "C" trampolines: the Box is kept alive in `listeners`, its raw pointer is the per-event `user_data`.
2026-05-25 15:51:56 +02:00
if events . len > 0 :
for ev in events :
2026-05-29 20:40:28 +02:00
let handlerStruct = capitalizeFirstLetter ( ev . nimProcName ) & " Handler "
let trampolineName = camelToSnakeCase ( ev . nimProcName ) & " _trampoline "
lines . add ( " struct $1 { " % [ handlerStruct ] )
2026-06-10 16:30:30 -03:00
lines . add ( " f: Box<dyn Fn(& $1 ) + Send + Sync>, " % [ ev . payloadTypeName ] )
2026-05-29 20:40:28 +02:00
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)] " )
2026-06-10 16:30:30 -03:00
lines . add ( " struct Envelope { payload: $1 } " % [ ev . payloadTypeName ] )
2026-05-29 20:40:28 +02:00
lines . add (
" if let Ok(env) = ciborium::de::from_reader::<Envelope, _>(bytes) { "
)
lines . add ( " (h.f)(&env.payload); " )
lines . add ( " } " )
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 ( " " )
2026-05-11 23:28:17 +02:00
lines . add ( " /// High-level context for ` $1 `. " % [ libTypeName ] )
lines . add ( " pub struct $1 { " % [ ctxTypeName ] )
lines . add ( " ptr: *mut c_void, " )
lines . add ( " timeout: Duration, " )
2026-05-25 15:51:56 +02:00
if events . len > 0 :
2026-07-15 11:46:05 -03:00
# Keeps each handler box alive while its listener id is live on the Nim side.
2026-05-29 20:40:28 +02:00
lines . add (
" listeners: std::sync::Mutex<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>, "
)
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-07-15 11:46:05 -03:00
# SAFETY block applies to both impls below.
2026-05-16 01:08:42 +02:00
lines . add (
" // SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime. "
)
lines . add ( " // Every call through the generated FFI proc goes through " )
lines . add (
2026-06-27 19:08:10 +02:00
" // `sendRequestToFFIThread` on the Nim side, which only enqueues the request "
2026-05-16 01:08:42 +02:00
)
2026-06-27 19:08:10 +02:00
lines . add ( " // onto a mutex-guarded MPSC queue (sound from any number of threads) and " )
2026-05-16 01:08:42 +02:00
lines . add (
2026-06-27 19:08:10 +02:00
" // wakes the single FFI thread that dispatches every handler. The context is "
2026-05-16 01:08:42 +02:00
)
lines . add (
2026-06-27 19:08:10 +02:00
" // thus never mutated non-atomically from the caller ' s thread. The Nim-side "
2026-05-16 01:08:42 +02:00
)
2026-06-27 19:08:10 +02:00
lines . add ( " // reentrancy guard (`onFFIThread` threadvar) prevents handlers from " )
lines . add ( " // re-entering the dispatcher. These invariants make it sound to mark the " )
lines . add ( " // wrapper as Send + Sync. " )
2026-05-11 23:28:17 +02:00
lines . add ( " unsafe impl Send for $1 {} " % [ ctxTypeName ] )
lines . add ( " unsafe impl Sync for $1 {} " % [ ctxTypeName ] )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
2026-07-15 11:46:05 -03:00
# Drop tears down the Nim runtime when the ctx goes out of scope; without it, forgetting the ctx leaks the entire runtime (FFI thread, watchdog, chronos).
2026-05-16 01:08:42 +02:00
if dtorProcName . len > 0 :
lines . add ( " impl Drop for $1 { " % [ ctxTypeName ] )
lines . add ( " fn drop(&mut self) { " )
lines . add ( " if !self.ptr.is_null() { " )
lines . add ( " unsafe { ffi:: $1 (self.ptr); } " % [ dtorProcName ] )
lines . add ( " self.ptr = std::ptr::null_mut(); " )
lines . add ( " } " )
2026-07-15 11:46:05 -03:00
# `listeners` drops after this body; the dylib has joined its threads by then, so no callback is mid-flight against the raw pointers we handed it.
2026-05-16 01:08:42 +02:00
lines . add ( " } " )
lines . add ( " } " )
lines . add ( " " )
2026-05-11 23:28:17 +02:00
lines . add ( " impl $1 { " % [ ctxTypeName ] )
for ctor in ctors :
2026-05-16 01:08:42 +02:00
let reqName = reqStructName ( ctor )
var paramsList : seq [ string ] = @ [ ]
var fieldInits : seq [ string ] = @ [ ]
2026-05-11 23:28:17 +02:00
for ep in ctor . extraParams :
2026-05-16 01:08:42 +02:00
let snake = camelToSnakeCase ( ep . name )
let rustType =
2026-06-16 14:11:31 -03:00
if ep . ridesAsPtr ( ) :
2026-05-16 01:08:42 +02:00
RustPtrType
else :
nimTypeToRust ( ep . typeName )
paramsList . add ( " $1 : $2 " % [ snake , rustType ] )
fieldInits . add ( snake )
2026-07-15 11:46:05 -03:00
# `create` and `new_async` take an explicit `timeout: Duration` that flows into `self.timeout` so subsequent method calls inherit it.
2026-05-16 01:08:42 +02:00
let ctorParamsStr =
if paramsList . len > 0 :
paramsList . join ( " , " ) & " , timeout: Duration "
2026-05-11 23:28:17 +02:00
else :
2026-05-16 01:08:42 +02:00
" timeout: Duration "
let reqLit =
if fieldInits . len > 0 :
reqName & " { " & fieldInits . join ( " , " ) & " } "
else :
reqName & " {} "
2026-05-11 23:28:17 +02:00
2026-05-16 01:08:42 +02:00
lines . add ( " pub fn create( $1 ) -> Result<Self, String> { " % [ ctorParamsStr ] )
lines . add ( " let req = $1 ; " % [ reqLit ] )
lines . add ( " let req_bytes = encode_cbor(&req)?; " )
2026-07-15 11:46:05 -03:00
# Ctor also fires the callback carrying the payload, so discard the synchronous *mut c_void and yield RET_OK to wait on the callback.
2026-05-16 01:08:42 +02:00
lines . add ( " let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe { " )
lines . add (
" let _ = ffi:: $1 (req_bytes.as_ptr(), req_bytes.len(), cb, ud); " %
[ ctor . procName ]
)
lines . add ( " 0 " )
2026-05-11 23:28:17 +02:00
lines . add ( " })?; " )
2026-07-15 11:46:05 -03:00
# Ctor success payload is a CBOR text string holding the ctx address.
2026-05-16 01:08:42 +02:00
lines . add ( " let addr_str: String = decode_cbor(&raw_bytes)?; " )
lines . add (
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; "
)
2026-05-25 15:51:56 +02:00
if events . len > 0 :
2026-06-10 16:30:30 -03:00
lines . add (
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) }) "
)
2026-05-25 15:51:56 +02:00
else :
lines . add ( " Ok(Self { ptr: addr as *mut c_void, timeout }) " )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-05-16 01:08:42 +02:00
lines . add (
" pub async fn new_async( $1 ) -> Result<Self, String> { " % [ ctorParamsStr ]
)
lines . add ( " let req = $1 ; " % [ reqLit ] )
lines . add ( " let req_bytes = encode_cbor(&req)?; " )
2026-07-15 11:46:05 -03:00
# See `create`: discard the ctor's synchronous return; the callback delivers the ctx address.
2026-05-16 01:08:42 +02:00
lines . add ( " let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe { " )
lines . add (
" let _ = ffi:: $1 (req_bytes.as_ptr(), req_bytes.len(), cb, ud); " %
[ ctor . procName ]
)
lines . add ( " 0 " )
2026-05-11 23:28:17 +02:00
lines . add ( " }).await?; " )
2026-05-16 01:08:42 +02:00
lines . add ( " let addr_str: String = decode_cbor(&raw_bytes)?; " )
lines . add (
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; "
)
2026-05-25 15:51:56 +02:00
if events . len > 0 :
2026-06-10 16:30:30 -03:00
lines . add (
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) }) "
)
2026-05-25 15:51:56 +02:00
else :
lines . add ( " Ok(Self { ptr: addr as *mut c_void, timeout }) " )
lines . add ( " } " )
lines . add ( " " )
if events . len > 0 :
2026-07-15 11:46:05 -03:00
# Shared by every public `add_*_listener`: caller owns the concrete-typed box, erased to `dyn Any + Send` only on hand-off.
2026-05-29 20:40:28 +02:00
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 { " )
2026-05-28 16:00:28 +02:00
lines . add (
2026-05-29 20:40:28 +02:00
" ffi:: $1_add_event_listener (self.ptr, event_name, callback, raw) " %
[ libName ]
2026-05-28 16:00:28 +02:00
)
2026-05-29 20:40:28 +02:00
lines . add ( " }; " )
lines . add ( " if id != 0 { " )
lines . add ( " self.listeners.lock().unwrap().insert(id, owned); " )
lines . add ( " } " )
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. " )
2026-06-10 16:30:30 -03:00
lines . add ( " pub fn $1 <F>(&self, handler: F) -> ListenerHandle " % [ methodName ] )
lines . add ( " where F: Fn(& $1 ) + Send + Sync + ' static, " % [ ev . payloadTypeName ] )
2026-05-29 20:40:28 +02:00
lines . add ( " { " )
lines . add (
" let owned: Box< $1 > = Box::new( $1 { f: Box::new(handler) }); " %
[ handlerStruct ]
)
2026-06-10 16:30:30 -03:00
lines . add (
" let raw = &*owned as *const $1 as *mut c_void; " % [ handlerStruct ]
)
2026-05-29 20:40:28 +02:00
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 ( " " )
2026-07-15 11:46:05 -03:00
# Remove by handle; drops the Box after the C ABI confirms unregistration.
2026-06-10 16:30:30 -03:00
lines . add ( " /// Remove a previously-registered listener by handle. Returns true " )
lines . add ( " /// if the listener existed and was removed; false otherwise. " )
2026-05-29 20:40:28 +02:00
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 (
2026-06-10 16:30:30 -03:00
" ffi:: $1_remove_event_listener (self.ptr, handle.id) " % [ libName ]
2026-05-29 20:40:28 +02:00
)
lines . add ( " }; " )
lines . add ( " self.listeners.lock().unwrap().remove(&handle.id); " )
lines . add ( " rc == 0 " )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
for m in methods :
let methodName = stripLibPrefix ( m . procName , libName )
let retRustType = nimTypeToRust ( m . returnTypeName )
2026-05-16 01:08:42 +02:00
let reqName = reqStructName ( m )
2026-05-11 23:28:17 +02:00
var paramsList : seq [ string ] = @ [ ]
2026-05-16 01:08:42 +02:00
var fieldInits : seq [ string ] = @ [ ]
2026-05-11 23:28:17 +02:00
for ep in m . extraParams :
2026-05-16 01:08:42 +02:00
let snake = camelToSnakeCase ( ep . name )
let rustType =
2026-06-16 14:11:31 -03:00
if ep . ridesAsPtr ( ) :
2026-05-16 01:08:42 +02:00
RustPtrType
else :
nimTypeToRust ( ep . typeName )
paramsList . add ( " $1 : $2 " % [ snake , rustType ] )
fieldInits . add ( snake )
let paramsStr =
if paramsList . len > 0 :
" , " & paramsList . join ( " , " )
2026-05-11 23:28:17 +02:00
else :
2026-05-16 01:08:42 +02:00
" "
let reqLit =
if fieldInits . len > 0 :
reqName & " { " & fieldInits . join ( " , " ) & " } "
2026-05-11 23:28:17 +02:00
else :
2026-05-16 01:08:42 +02:00
reqName & " {} "
2026-06-16 14:11:31 -03:00
let retTypeForApi = if m . returnRidesAsPtr ( ) : RustPtrType else : retRustType
2026-05-11 23:28:17 +02:00
2026-05-16 01:08:42 +02:00
lines . add (
" pub fn $1 (&self $2 ) -> Result< $3 , String> { " %
[ methodName , paramsStr , retTypeForApi ]
)
lines . add ( " let req = $1 ; " % [ reqLit ] )
lines . add ( " let req_bytes = encode_cbor(&req)?; " )
lines . add ( " let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { " )
lines . add (
" ffi:: $1 (self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) " %
[ m . procName ]
)
2026-05-11 23:28:17 +02:00
lines . add ( " })?; " )
2026-05-16 01:08:42 +02:00
lines . add ( " decode_cbor::< $1 >(&raw_bytes) " % [ retTypeForApi ] )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
2026-07-15 11:46:05 -03:00
# async method: ptr cast to usize (Copy + Send) keeps the move closure and returned future Send for multi-threaded tokio runtimes.
2026-05-16 01:08:42 +02:00
lines . add (
" pub async fn $1_async (&self $2 ) -> Result< $3 , String> { " %
[ methodName , paramsStr , retTypeForApi ]
)
lines . add ( " let req = $1 ; " % [ reqLit ] )
lines . add ( " let req_bytes = encode_cbor(&req)?; " )
2026-05-11 23:28:17 +02:00
lines . add ( " let ptr = self.ptr as usize; " )
2026-05-16 01:08:42 +02:00
lines . add (
" let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { "
)
lines . add (
" ffi:: $1 (ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) " %
[ m . procName ]
)
2026-05-11 23:28:17 +02:00
lines . add ( " }).await?; " )
2026-05-16 01:08:42 +02:00
lines . add ( " decode_cbor::< $1 >(&raw_bytes) " % [ retTypeForApi ] )
2026-05-11 23:28:17 +02:00
lines . add ( " } " )
lines . add ( " " )
lines . add ( " } " )
2026-05-16 01:08:42 +02:00
return lines . join ( " \n " ) & " \n "
2026-05-11 23:28:17 +02:00
proc generateRustCrate * (
procs : seq [ FFIProcMeta ] ,
types : seq [ FFITypeMeta ] ,
libName : string ,
outputDir : string ,
nimSrcRelPath : string ,
2026-05-25 15:51:56 +02:00
events : seq [ FFIEventMeta ] = @ [ ] ,
2026-05-11 23:28:17 +02:00
) =
## Generates a complete Rust crate in outputDir.
createDir ( outputDir )
createDir ( outputDir / " src " )
writeFile ( outputDir / " Cargo.toml " , generateCargoToml ( libName ) )
writeFile ( outputDir / " build.rs " , generateBuildRs ( libName , nimSrcRelPath ) )
writeFile ( outputDir / " src " / " lib.rs " , generateLibRs ( ) )
2026-05-16 01:08:42 +02:00
writeFile ( outputDir / " src " / " ffi.rs " , generateFFIRs ( procs ) )
writeFile ( outputDir / " src " / " types.rs " , generateTypesRs ( types , procs ) )
2026-05-25 15:51:56 +02:00
writeFile ( outputDir / " src " / " api.rs " , generateApiRs ( procs , libName , events ) )