From 5305c20c22b04375b35fbe3510adb5fe4bb9f040 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 3 May 2026 23:45:49 +0200 Subject: [PATCH] simplify auto-generate cpp and rust --- examples/nim_timer/nim_timer.nimble | 16 +- examples/nim_timer/rust_bindings/Cargo.toml | 6 +- examples/nim_timer/rust_bindings/src/api.rs | 101 +++++++--- examples/nim_timer/rust_client/Cargo.toml | 2 +- ffi/codegen/cpp.nim | 2 +- ffi/codegen/rust.nim | 212 ++++++++++++-------- 6 files changed, 212 insertions(+), 127 deletions(-) diff --git a/examples/nim_timer/nim_timer.nimble b/examples/nim_timer/nim_timer.nimble index 103e922..a21b837 100644 --- a/examples/nim_timer/nim_timer.nimble +++ b/examples/nim_timer/nim_timer.nimble @@ -8,14 +8,20 @@ requires "nim >= 2.2.4" requires "chronos" requires "chronicles" requires "taskpools" -requires "ffi >= 0.1.3" +requires "https://github.com/logos-messaging/nim-ffi >= 0.1.3" + +const nimFlags = "--mm:orc -d:chronicles_log_level=WARN" -# Build the example library and optionally generate bindings. task build, "Compile the nimtimer library": - exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim" + exec "nim c " & nimFlags & + " --app:lib --noMain --nimMainPrefix:libnimtimer nim_timer.nim" task genbindings_rust, "Generate Rust bindings for the nimtimer example": - exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim" + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" & + " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" task genbindings_cpp, "Generate C++ bindings for the nimtimer example": - exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=cpp nim_timer.nim" + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" & + " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" diff --git a/examples/nim_timer/rust_bindings/Cargo.toml b/examples/nim_timer/rust_bindings/Cargo.toml index 40596c0..838af9a 100644 --- a/examples/nim_timer/rust_bindings/Cargo.toml +++ b/examples/nim_timer/rust_bindings/Cargo.toml @@ -3,11 +3,7 @@ name = "nimtimer" version = "0.1.0" edition = "2021" -[features] -default = [] -tokio-runtime = ["tokio"] - [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["sync"] } diff --git a/examples/nim_timer/rust_bindings/src/api.rs b/examples/nim_timer/rust_bindings/src/api.rs index aaf440c..a9e88e8 100644 --- a/examples/nim_timer/rust_bindings/src/api.rs +++ b/examples/nim_timer/rust_bindings/src/api.rs @@ -5,8 +5,6 @@ use std::time::Duration; use super::ffi; use super::types::*; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); - #[derive(Default)] struct FfiCallbackResult { payload: Option>, @@ -55,6 +53,44 @@ where guard.payload.clone().unwrap() } +unsafe extern "C" fn on_result_async( + ret: c_int, + msg: *const c_char, + _len: usize, + user_data: *mut c_void, +) { + let tx = Box::from_raw( + user_data as *mut tokio::sync::oneshot::Sender>, + ); + let value = if ret == 0 { + Ok(CStr::from_ptr(msg).to_string_lossy().into_owned()) + } else { + Err(CStr::from_ptr(msg).to_string_lossy().into_owned()) + }; + let _ = tx.send(value); +} + +async fn ffi_call_async(f: F) -> Result +where + F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int, +{ + let rx = { + let (tx, rx) = tokio::sync::oneshot::channel::>(); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let ret = f(on_result_async, raw); + if ret == 2 { + drop(unsafe { + Box::from_raw( + raw as *mut tokio::sync::oneshot::Sender>, + ) + }); + return Err("RET_MISSING_CALLBACK (internal error)".into()); + } + rx + }; + rx.await.map_err(|_| "channel closed before callback fired".to_string())? +} + /// High-level context for `NimTimer`. pub struct NimTimerCtx { ptr: *mut c_void, @@ -71,18 +107,18 @@ impl NimTimerCtx { let raw = ffi_call(timeout, |cb, ud| unsafe { ffi::nimtimer_create(config_c.as_ptr(), cb, ud) })?; - // ctor returns the context address as a plain decimal string let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; Ok(Self { ptr: addr as *mut c_void, timeout }) } - pub fn new(config: TimerConfig) -> Result { - Self::create(config, DEFAULT_TIMEOUT) - } - - #[cfg(feature = "tokio")] pub async fn new_async(config: TimerConfig) -> Result { - tokio::task::block_in_place(move || Self::new(config)) + let config_json = serde_json::to_string(&config).map_err(|e| e.to_string())?; + let config_c = CString::new(config_json).unwrap(); + let raw = ffi_call_async(move |cb, ud| unsafe { + ffi::nimtimer_create(config_c.as_ptr(), cb, ud) + }).await?; + let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; + Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) }) } pub fn echo(&self, req: EchoRequest) -> Result { @@ -94,6 +130,16 @@ impl NimTimerCtx { serde_json::from_str::(&raw).map_err(|e| e.to_string()) } + pub async fn echo_async(&self, req: EchoRequest) -> Result { + let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?; + let req_c = CString::new(req_json).unwrap(); + let ptr = self.ptr as usize; + let raw = ffi_call_async(move |cb, ud| unsafe { + ffi::nimtimer_echo(ptr as *mut c_void, cb, ud, req_c.as_ptr()) + }).await?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) + } + pub fn version(&self) -> Result { let raw = ffi_call(self.timeout, |cb, ud| unsafe { ffi::nimtimer_version(self.ptr, cb, ud) @@ -101,14 +147,12 @@ impl NimTimerCtx { serde_json::from_str::(&raw).map_err(|e| e.to_string()) } - #[cfg(feature = "tokio")] pub async fn version_async(&self) -> Result { - let ptr = self.ptr; - let timeout = self.timeout; - tokio::task::block_in_place(move || { - let ctx = Self { ptr, timeout }; - ctx.version() - }) + let ptr = self.ptr as usize; + let raw = ffi_call_async(move |cb, ud| unsafe { + ffi::nimtimer_version(ptr as *mut c_void, cb, ud) + }).await?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) } pub fn complex(&self, req: ComplexRequest) -> Result { @@ -120,23 +164,14 @@ impl NimTimerCtx { serde_json::from_str::(&raw).map_err(|e| e.to_string()) } - #[cfg(feature = "tokio")] - pub async fn echo_async(&self, req: EchoRequest) -> Result { - let ptr = self.ptr; - let timeout = self.timeout; - tokio::task::block_in_place(move || { - let ctx = Self { ptr, timeout }; - ctx.echo(req) - }) + pub async fn complex_async(&self, req: ComplexRequest) -> Result { + let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?; + let req_c = CString::new(req_json).unwrap(); + let ptr = self.ptr as usize; + let raw = ffi_call_async(move |cb, ud| unsafe { + ffi::nimtimer_complex(ptr as *mut c_void, cb, ud, req_c.as_ptr()) + }).await?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) } - #[cfg(feature = "tokio")] - pub async fn complex_async(&self, req: ComplexRequest) -> Result { - let ptr = self.ptr; - let timeout = self.timeout; - tokio::task::block_in_place(move || { - let ctx = Self { ptr, timeout }; - ctx.complex(req) - }) - } } diff --git a/examples/nim_timer/rust_client/Cargo.toml b/examples/nim_timer/rust_client/Cargo.toml index d28fdeb..cd21745 100644 --- a/examples/nim_timer/rust_client/Cargo.toml +++ b/examples/nim_timer/rust_client/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nimtimer = { path = "../rust_bindings", features = ["tokio-runtime"] } +nimtimer = { path = "../rust_bindings" } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index cc23652..fea7dc0 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -294,7 +294,7 @@ proc generateCppHeader*( else: lines.add(" std::future<$1> $2Async() const {" % [retCppType, methodName]) lines.add( - " return std::async(std::launch::async, [this]() { return $2(); });" % + " return std::async(std::launch::async, [this]() { return $1(); });" % [methodName] ) lines.add(" }") diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index da000e3..6291a5e 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -77,6 +77,7 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +tokio = { version = "1", features = ["sync"] } """ % [libName] @@ -220,29 +221,27 @@ proc generateTypesRs*(types: seq[FFITypeMeta]): string = result = lines.join("\n") proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = - ## Generates api.rs with the high-level Rust API. + ## Generates api.rs with both a blocking and a tokio-async high-level API. + ## + ## Blocking: ctx.echo(req) — thread-blocks via Condvar + ## Async: ctx.echo_async(req).await — non-blocking via oneshot channel; + ## the FFI callback fires from the Nim/chronos thread and wakes + ## the awaiting task without ever blocking a thread. var lines: seq[string] = @[] - # Find ctor and method procs var ctors: seq[FFIProcMeta] = @[] var methods: seq[FFIProcMeta] = @[] for p in procs: - if p.kind == ffiCtorKind: - ctors.add(p) - else: - methods.add(p) + if p.kind == ffiCtorKind: ctors.add(p) + else: methods.add(p) - # Derive the lib type name from ctor or from libName var libTypeName = "" - if ctors.len > 0: - libTypeName = ctors[0].libTypeName - else: - # Fallback: PascalCase of libName - libTypeName = toPascalCase(libName) + if ctors.len > 0: libTypeName = ctors[0].libTypeName + else: libTypeName = toPascalCase(libName) let ctxTypeName = libTypeName & "Ctx" - # Imports + # ── Imports ──────────────────────────────────────────────────────────────── lines.add("use std::ffi::{CStr, CString};") lines.add("use std::os::raw::{c_char, c_int, c_void};") lines.add("use std::sync::{Arc, Condvar, Mutex};") @@ -251,7 +250,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("use super::types::*;") lines.add("") - # FfiCallbackResult + Pair + # ── Blocking trampoline ──────────────────────────────────────────────────── lines.add("#[derive(Default)]") lines.add("struct FfiCallbackResult {") lines.add(" payload: Option>,") @@ -259,8 +258,6 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("") lines.add("type Pair = Arc<(Mutex, Condvar)>;") lines.add("") - - # on_result callback (Arc-based, blocking) lines.add("unsafe extern \"C\" fn on_result(") lines.add(" ret: c_int,") lines.add(" msg: *const c_char,") @@ -281,8 +278,6 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add(" std::mem::forget(pair);") lines.add("}") lines.add("") - - # Blocking ffi_call helper using Condvar::wait_timeout_while lines.add("fn ffi_call(timeout: Duration, f: F) -> Result") lines.add("where") lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,") @@ -305,7 +300,52 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("}") lines.add("") - # Ctx struct + # ── Async (tokio oneshot) trampoline ─────────────────────────────────────── + # The callback is invoked from the Nim/chronos thread and sends the result + # through the oneshot channel, waking the awaiting tokio task without + # blocking any thread. + lines.add("unsafe extern \"C\" fn on_result_async(") + 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(" let tx = Box::from_raw(") + lines.add(" user_data as *mut tokio::sync::oneshot::Sender>,") + lines.add(" );") + lines.add(" let value = if ret == 0 {") + lines.add(" Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())") + lines.add(" } else {") + lines.add(" Err(CStr::from_ptr(msg).to_string_lossy().into_owned())") + lines.add(" };") + lines.add(" let _ = tx.send(value);") + lines.add("}") + lines.add("") + # Scoped block keeps raw/tx/F dead at the single await point so the + # returned future is Send regardless of whether F itself is Send. + lines.add("async fn ffi_call_async(f: F) -> Result") + lines.add("where") + lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,") + lines.add("{") + lines.add(" let rx = {") + lines.add(" let (tx, rx) = tokio::sync::oneshot::channel::>();") + lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") + lines.add(" let ret = f(on_result_async, raw);") + lines.add(" if ret == 2 {") + lines.add(" drop(unsafe {") + lines.add(" Box::from_raw(") + lines.add(" raw as *mut tokio::sync::oneshot::Sender>,") + lines.add(" )") + lines.add(" });") + lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") + lines.add(" }") + lines.add(" rx") + lines.add(" };") + lines.add(" rx.await.map_err(|_| \"channel closed before callback fired\".to_string())?") + lines.add("}") + lines.add("") + + # ── Context struct ───────────────────────────────────────────────────────── lines.add("/// High-level context for `$1`." % [libTypeName]) lines.add("pub struct $1 {" % [ctxTypeName]) lines.add(" ptr: *mut c_void,") @@ -315,35 +355,28 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("unsafe impl Send for $1 {}" % [ctxTypeName]) lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName]) lines.add("") - - # impl block lines.add("impl $1 {" % [ctxTypeName]) - # Constructor method(s) + # ── Constructors ─────────────────────────────────────────────────────────── for ctor in ctors: - var paramsList: seq[string] = @[] + var asyncParamsList: seq[string] = @[] for ep in ctor.extraParams: - let rustType = nimTypeToRust(ep.typeName) - let snakeName = toSnakeCase(ep.name) - paramsList.add("$1: $2" % [snakeName, rustType]) - paramsList.add("timeout: Duration") - let paramsStr = paramsList.join(", ") + asyncParamsList.add( + "$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)] + ) + let asyncParamsStr = asyncParamsList.join(", ") + let blockingParamsStr = + if asyncParamsList.len > 0: asyncParamsList.join(", ") & ", timeout: Duration" + else: "timeout: Duration" - lines.add(" pub fn create($1) -> Result {" % [paramsStr]) - - # Serialize extra params - for ep in ctor.extraParams: - let snakeName = toSnakeCase(ep.name) - let rustType = nimTypeToRust(ep.typeName) + # Helper: emit JSON serialization lines for extra params + template emitSerialize(snakeName, rustType: string) = if rustType == "String": - # Primitive string — wrap it in JSON lines.add( " let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName] ) - lines.add( - " let $1_c = CString::new($1_json_str).unwrap();" % [snakeName] - ) + lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName]) else: lines.add( " let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % @@ -351,59 +384,56 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = ) lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName]) - # Build the ffi_call closure + # Build the ordered arg list for the raw FFI call (ctor: params, cb, ud) var ffiCallArgs: seq[string] = @[] for ep in ctor.extraParams: - let snakeName = toSnakeCase(ep.name) - ffiCallArgs.add("$1_c.as_ptr()" % [snakeName]) + ffiCallArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) ffiCallArgs.add("cb") ffiCallArgs.add("ud") let ffiCallArgsStr = ffiCallArgs.join(", ") + # -- blocking create -- + lines.add(" pub fn create($1) -> Result {" % [blockingParamsStr]) + for ep in ctor.extraParams: + emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) lines.add(" let raw = ffi_call(timeout, |cb, ud| unsafe {") lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr]) lines.add(" })?;") - lines.add(" // ctor returns the context address as a plain decimal string") - lines.add( - " let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" - ) + lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;") lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") lines.add(" }") lines.add("") - # Method implementations + # -- async new_async -- + # move closure: each CString is moved in (Send), no raw ptr escapes the block + lines.add(" pub async fn new_async($1) -> Result {" % [asyncParamsStr]) + for ep in ctor.extraParams: + emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) + lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {") + lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr]) + lines.add(" }).await?;") + lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;") + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) })") + lines.add(" }") + lines.add("") + + # ── Methods ──────────────────────────────────────────────────────────────── for m in methods: let methodName = stripLibPrefix(m.procName, libName) let retRustType = nimTypeToRust(m.returnTypeName) var paramsList: seq[string] = @[] for ep in m.extraParams: - let rustType = nimTypeToRust(ep.typeName) - let snakeName = toSnakeCase(ep.name) - paramsList.add("$1: $2" % [snakeName, rustType]) - let paramsStr = - if paramsList.len > 0: - ", " & paramsList.join(", ") - else: - "" + paramsList.add("$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)]) + let paramsStr = if paramsList.len > 0: ", " & paramsList.join(", ") else: "" - lines.add( - " pub fn $1(&self$2) -> Result<$3, String> {" % - [methodName, paramsStr, retRustType] - ) - - # Serialize extra params - for ep in m.extraParams: - let snakeName = toSnakeCase(ep.name) - let rustType = nimTypeToRust(ep.typeName) + template emitSerialize(snakeName, rustType: string) = if rustType == "String": lines.add( " let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName] ) - lines.add( - " let $1_c = CString::new($1_json_str).unwrap();" % [snakeName] - ) + lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName]) else: lines.add( " let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % @@ -411,29 +441,47 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = ) lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName]) - # Build ffi call args: ctx first, then callback/ud, then json args + template emitDeserialize(retRustType: string) = + if retRustType == "String": + lines.add(" serde_json::from_str::(&raw).map_err(|e| e.to_string())") + elif retRustType == "usize": + lines.add(" raw.parse::().map_err(|e| e.to_string())") + else: + lines.add( + " serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType] + ) + + # -- blocking method -- + lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType]) + for ep in m.extraParams: + emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) var ffiArgs: seq[string] = @["self.ptr", "cb", "ud"] for ep in m.extraParams: - let snakeName = toSnakeCase(ep.name) - ffiArgs.add("$1_c.as_ptr()" % [snakeName]) + ffiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) let ffiArgsStr = ffiArgs.join(", ") - lines.add(" let raw = ffi_call(self.timeout, |cb, ud| unsafe {") lines.add(" ffi::$1($2)" % [m.procName, ffiArgsStr]) lines.add(" })?;") + emitDeserialize(retRustType) + lines.add(" }") + lines.add("") - # Deserialize return value - if retRustType == "String": - lines.add( - " serde_json::from_str::(&raw).map_err(|e| e.to_string())" - ) - elif retRustType == "usize": - lines.add(" raw.parse::().map_err(|e| e.to_string())") - else: - lines.add( - " serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % - [retRustType] - ) + # -- async method -- + # ptr is cast to usize (Copy + Send) so the move closure is Send, + # keeping the returned future Send for multi-threaded tokio runtimes. + lines.add(" pub async fn $1_async(&self$2) -> Result<$3, String> {" % + [methodName, paramsStr, retRustType]) + for ep in m.extraParams: + emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) + lines.add(" let ptr = self.ptr as usize;") + var asyncFfiArgs: seq[string] = @["ptr as *mut c_void", "cb", "ud"] + for ep in m.extraParams: + asyncFfiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) + let asyncFfiArgsStr = asyncFfiArgs.join(", ") + lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {") + lines.add(" ffi::$1($2)" % [m.procName, asyncFfiArgsStr]) + lines.add(" }).await?;") + emitDeserialize(retRustType) lines.add(" }") lines.add("")