From 23152d4fe72d3f42f061230dfbfd9ab8048283c1 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 18:51:06 +0200 Subject: [PATCH] feat(codegen): native (non-CBOR) Rust generator + rust.nim -> rust_cbor.nim split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the Rust codegen the way C++ is split: rename `rust.nim` -> `rust_cbor.nim` (CBOR) and add `rust_native.nim` (native). Dispatch on `targetLang=rust` now honours `-d:ffiMode` (native/cbor); the crates share file names so each mode writes its own dir (rust_bindings vs rust_native_bindings). `rust_native.nim` emits a `_native` crate — the Rust analogue of `cpp_native`: `#[repr(C)]` POD mirrors + `extern "C"` native entry points (ffi.rs); idiomatic structs with `to_c`/`from_c`, a holder owning the CStrings for the call (types.rs); and a `Node` whose methods marshal typed args in / read typed struct returns out, blocking via std mpsc (api.rs). First cut: scalar/string/bool/float/nested-struct fields (create/version/echo); seq/Option params are SKIPPED, native events next. Verified end-to-end — the generated crate builds and the demo round-trips a typed EchoResponse. Tasks: genbindings_rust (CBOR), genbindings_rust_native. Co-Authored-By: Claude Opus 4.8 --- .../timer/rust_native_bindings/.gitignore | 4 + .../timer/rust_native_bindings/Cargo.toml | 4 + examples/timer/rust_native_bindings/README.md | 28 ++ .../rust_native_bindings/examples/demo.rs | 7 + .../timer/rust_native_bindings/src/api.rs | 84 +++++ .../timer/rust_native_bindings/src/ffi.rs | 58 +++ .../timer/rust_native_bindings/src/lib.rs | 5 + .../timer/rust_native_bindings/src/types.rs | 162 +++++++++ ffi.nimble | 21 +- ffi/codegen/{rust.nim => rust_cbor.nim} | 0 ffi/codegen/rust_native.nim | 335 ++++++++++++++++++ ffi/internal/ffi_macro.nim | 17 +- 12 files changed, 711 insertions(+), 14 deletions(-) create mode 100644 examples/timer/rust_native_bindings/.gitignore create mode 100644 examples/timer/rust_native_bindings/Cargo.toml create mode 100644 examples/timer/rust_native_bindings/README.md create mode 100644 examples/timer/rust_native_bindings/examples/demo.rs create mode 100644 examples/timer/rust_native_bindings/src/api.rs create mode 100644 examples/timer/rust_native_bindings/src/ffi.rs create mode 100644 examples/timer/rust_native_bindings/src/lib.rs create mode 100644 examples/timer/rust_native_bindings/src/types.rs rename ffi/codegen/{rust.nim => rust_cbor.nim} (100%) create mode 100644 ffi/codegen/rust_native.nim diff --git a/examples/timer/rust_native_bindings/.gitignore b/examples/timer/rust_native_bindings/.gitignore new file mode 100644 index 0000000..26d7da7 --- /dev/null +++ b/examples/timer/rust_native_bindings/.gitignore @@ -0,0 +1,4 @@ +/target +/libmy_timer.dylib +/libmy_timer.so +Cargo.lock diff --git a/examples/timer/rust_native_bindings/Cargo.toml b/examples/timer/rust_native_bindings/Cargo.toml new file mode 100644 index 0000000..1ee2b70 --- /dev/null +++ b/examples/timer/rust_native_bindings/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "my_timer_native" +version = "0.1.0" +edition = "2021" diff --git a/examples/timer/rust_native_bindings/README.md b/examples/timer/rust_native_bindings/README.md new file mode 100644 index 0000000..55ffcac --- /dev/null +++ b/examples/timer/rust_native_bindings/README.md @@ -0,0 +1,28 @@ +# Rust bindings — native (generated) + +**Generated** native (zero-serialization) Rust crate for the timer library — the +Rust counterpart of `c_bindings` / `go_bindings` / `cpp_native_bindings`, and the +native sibling of the CBOR crate in [`../rust_bindings`](../rust_bindings). + +| File | Description | +|------|-------------| +| `src/ffi.rs` | `#[repr(C)]` POD mirrors + `extern "C"` native entry points. | +| `src/types.rs` | Idiomatic structs + `to_c`/`from_c` (a holder owns the `CString`s for the call). | +| `src/api.rs` | `Node` — methods marshal typed args in / read typed struct returns out; blocking via `std::sync::mpsc`. No CBOR. | +| `examples/demo.rs` | A small consumer. | + +```rust +let node = MyTimerNode::new(TimerConfig { name: "my-app".into() })?; +println!("{}", node.version()?); +let r = node.echo(EchoRequest { message: "hello".into(), delay_ms: 5 })?; // -> EchoResponse +``` + +Regenerate with `nimble genbindings_rust_native`. + +## Status + +First cut — scalar / string / bool / float / nested-struct fields (create, +version, echo). Methods taking sequences or optionals (complex, schedule) are +`// SKIPPED`; those plus native typed events are the next increments. Linking is +left to the consumer (`-L -l my_timer` + an rpath, as in `examples/demo.rs`); +a build.rs that compiles the dylib (like the CBOR crate) can be added later. diff --git a/examples/timer/rust_native_bindings/examples/demo.rs b/examples/timer/rust_native_bindings/examples/demo.rs new file mode 100644 index 0000000..691852c --- /dev/null +++ b/examples/timer/rust_native_bindings/examples/demo.rs @@ -0,0 +1,7 @@ +use my_timer_native::*; +fn main() { + let node = MyTimerNode::new(TimerConfig { name: "rust-native-gen".into() }).unwrap(); + println!("version: {}", node.version().unwrap()); + 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); +} diff --git a/examples/timer/rust_native_bindings/src/api.rs b/examples/timer/rust_native_bindings/src/api.rs new file mode 100644 index 0000000..9fee077 --- /dev/null +++ b/examples/timer/rust_native_bindings/src/api.rs @@ -0,0 +1,84 @@ +// Generated by nim-ffi native Rust codegen. Do not edit by hand. +#![allow(dead_code)] +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::mpsc::{sync_channel, SyncSender}; +use super::ffi; +use super::types::*; + +const RET_OK: c_int = 0; + +unsafe fn err_text(msg: *const c_char, len: usize) -> String { + if msg.is_null() || len == 0 { return String::from("error"); } + String::from_utf8_lossy(std::slice::from_raw_parts(msg as *const u8, len)).into_owned() +} + +unsafe extern "C" fn ack_cb(ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { + let tx = Box::from_raw(ud as *mut SyncSender>); + let _ = tx.send(if ret == RET_OK { Ok(()) } else { Err(err_text(msg, len)) }); +} +unsafe extern "C" fn str_cb(ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { + let tx = Box::from_raw(ud as *mut SyncSender>); + let r = if ret == RET_OK { + let b = if msg.is_null() || len == 0 { Vec::new() } else { std::slice::from_raw_parts(msg as *const u8, len).to_vec() }; + Ok(String::from_utf8_lossy(&b).into_owned()) + } else { Err(err_text(msg, len)) }; + let _ = tx.send(r); +} +unsafe extern "C" fn cb_my_timer_echo(ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void) { + let tx = Box::from_raw(ud as *mut SyncSender>); + let r = if ret == RET_OK && !msg.is_null() { + Ok(EchoResponse::from_c(&*(msg as *const ffi::EchoResponse))) + } else { Err(err_text(msg, len)) }; + let _ = tx.send(r); +} + +pub struct MyTimerNode { ctx: *mut c_void } +unsafe impl Send for MyTimerNode {} +unsafe impl Sync for MyTimerNode {} + +impl MyTimerNode { + pub fn new(config: TimerConfig) -> Result { + let mut strings = Vec::new(); + let c_config = config.to_c_inner(&mut strings); + let (tx, rx) = sync_channel::>(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let ctx = unsafe { ffi::my_timer_create(c_config, ack_cb, raw) }; + 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")); } + let _ = strings; + Ok(MyTimerNode { ctx }) + } + + pub fn echo(&self, req: EchoRequest) -> Result { + let mut strings = Vec::new(); + let c_req = req.to_c_inner(&mut strings); + let (tx, rx) = sync_channel::>(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let rc = unsafe { ffi::my_timer_echo(self.ctx, cb_my_timer_echo, raw, c_req) }; + if rc != RET_OK { + drop(unsafe { Box::from_raw(raw as *mut SyncSender>) }); + return Err(String::from("my_timer_echo dispatch failed")); + } + let _ = strings; + rx.recv().map_err(|_| String::from("callback channel closed"))? + } + + pub fn version(&self) -> Result { + let (tx, rx) = sync_channel::>(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let rc = unsafe { ffi::my_timer_version(self.ctx, str_cb, raw) }; + if rc != RET_OK { + drop(unsafe { Box::from_raw(raw as *mut SyncSender>) }); + return Err(String::from("my_timer_version dispatch failed")); + } + rx.recv().map_err(|_| String::from("callback channel closed"))? + } + + // SKIPPED my_timer_complex: seq/Option params not yet supported by native Rust codegen + // SKIPPED my_timer_schedule: seq/Option params not yet supported by native Rust codegen +} + +impl Drop for MyTimerNode { + fn drop(&mut self) { unsafe { ffi::my_timer_destroy(self.ctx); } } +} \ No newline at end of file diff --git a/examples/timer/rust_native_bindings/src/ffi.rs b/examples/timer/rust_native_bindings/src/ffi.rs new file mode 100644 index 0000000..1850461 --- /dev/null +++ b/examples/timer/rust_native_bindings/src/ffi.rs @@ -0,0 +1,58 @@ +// Generated by nim-ffi native Rust codegen. Do not edit by hand. +#![allow(non_snake_case, dead_code)] +use std::os::raw::{c_char, c_int, c_void}; + +pub type FFICallback = + unsafe extern "C" fn(ret: c_int, msg: *const c_char, len: usize, user_data: *mut c_void); + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TimerConfig { + pub name: *const c_char, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EchoRequest { + pub message: *const c_char, + pub delay_ms: i64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EchoResponse { + pub echoed: *const c_char, + pub timer_name: *const c_char, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ComplexResponse { + pub summary: *const c_char, + pub item_count: i64, + pub has_note: c_int, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct EchoEvent { + pub message: *const c_char, + pub echo_count: i64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ScheduleResult { + pub job_id: *const c_char, + pub will_run_count: i64, + pub first_run_at_ms: i64, + pub effective_backoff_ms: i64, +} + +#[link(name = "my_timer")] +extern "C" { + pub fn my_timer_create(config: TimerConfig, callback: FFICallback, user_data: *mut c_void) -> *mut c_void; + pub fn my_timer_echo(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req: EchoRequest) -> c_int; + pub fn my_timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void) -> c_int; + pub fn my_timer_destroy(ctx: *mut c_void) -> c_int; +} \ No newline at end of file diff --git a/examples/timer/rust_native_bindings/src/lib.rs b/examples/timer/rust_native_bindings/src/lib.rs new file mode 100644 index 0000000..29c439a --- /dev/null +++ b/examples/timer/rust_native_bindings/src/lib.rs @@ -0,0 +1,5 @@ +mod ffi; +mod types; +mod api; +pub use types::*; +pub use api::*; diff --git a/examples/timer/rust_native_bindings/src/types.rs b/examples/timer/rust_native_bindings/src/types.rs new file mode 100644 index 0000000..150a381 --- /dev/null +++ b/examples/timer/rust_native_bindings/src/types.rs @@ -0,0 +1,162 @@ +// Generated by nim-ffi native Rust codegen. Do not edit by hand. +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use super::ffi; + +fn cstr(p: *const c_char) -> String { + if p.is_null() { String::new() } + else { unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned() } +} + +#[derive(Clone, Debug, Default)] +pub struct TimerConfig { + pub name: String, +} + +impl TimerConfig { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::TimerConfig { + let name_c = CString::new(self.name.as_str()).unwrap_or_default(); + let name_p = name_c.as_ptr(); + strings.push(name_c); + ffi::TimerConfig { + name: name_p, + } + } + pub fn from_c(c: &ffi::TimerConfig) -> Self { + TimerConfig { + name: cstr(c.name), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct EchoRequest { + pub message: String, + pub delay_ms: i64, +} + +impl EchoRequest { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::EchoRequest { + let message_c = CString::new(self.message.as_str()).unwrap_or_default(); + let message_p = message_c.as_ptr(); + strings.push(message_c); + ffi::EchoRequest { + message: message_p, + delay_ms: self.delay_ms, + } + } + pub fn from_c(c: &ffi::EchoRequest) -> Self { + EchoRequest { + message: cstr(c.message), + delay_ms: c.delay_ms, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct EchoResponse { + pub echoed: String, + pub timer_name: String, +} + +impl EchoResponse { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::EchoResponse { + let echoed_c = CString::new(self.echoed.as_str()).unwrap_or_default(); + let echoed_p = echoed_c.as_ptr(); + strings.push(echoed_c); + let timer_name_c = CString::new(self.timer_name.as_str()).unwrap_or_default(); + let timer_name_p = timer_name_c.as_ptr(); + strings.push(timer_name_c); + ffi::EchoResponse { + echoed: echoed_p, + timer_name: timer_name_p, + } + } + pub fn from_c(c: &ffi::EchoResponse) -> Self { + EchoResponse { + echoed: cstr(c.echoed), + timer_name: cstr(c.timer_name), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ComplexResponse { + pub summary: String, + pub item_count: i64, + pub has_note: bool, +} + +impl ComplexResponse { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::ComplexResponse { + let summary_c = CString::new(self.summary.as_str()).unwrap_or_default(); + let summary_p = summary_c.as_ptr(); + strings.push(summary_c); + ffi::ComplexResponse { + summary: summary_p, + item_count: self.item_count, + has_note: if self.has_note { 1 } else { 0 }, + } + } + pub fn from_c(c: &ffi::ComplexResponse) -> Self { + ComplexResponse { + summary: cstr(c.summary), + item_count: c.item_count, + has_note: c.has_note != 0, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct EchoEvent { + pub message: String, + pub echo_count: i64, +} + +impl EchoEvent { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::EchoEvent { + let message_c = CString::new(self.message.as_str()).unwrap_or_default(); + let message_p = message_c.as_ptr(); + strings.push(message_c); + ffi::EchoEvent { + message: message_p, + echo_count: self.echo_count, + } + } + pub fn from_c(c: &ffi::EchoEvent) -> Self { + EchoEvent { + message: cstr(c.message), + echo_count: c.echo_count, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ScheduleResult { + pub job_id: String, + pub will_run_count: i64, + pub first_run_at_ms: i64, + pub effective_backoff_ms: i64, +} + +impl ScheduleResult { + pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::ScheduleResult { + let job_id_c = CString::new(self.job_id.as_str()).unwrap_or_default(); + let job_id_p = job_id_c.as_ptr(); + strings.push(job_id_c); + ffi::ScheduleResult { + job_id: job_id_p, + will_run_count: self.will_run_count, + first_run_at_ms: self.first_run_at_ms, + effective_backoff_ms: self.effective_backoff_ms, + } + } + pub fn from_c(c: &ffi::ScheduleResult) -> Self { + ScheduleResult { + job_id: cstr(c.job_id), + will_run_count: c.will_run_count, + first_run_at_ms: c.first_run_at_ms, + effective_backoff_ms: c.effective_backoff_ms, + } + } +} diff --git a/ffi.nimble b/ffi.nimble index effec1d..e3920cb 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -132,17 +132,20 @@ task genbindings_example, "Generate Rust bindings for the timer example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim" exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim" -task genbindings_rust, "Generate Rust bindings for the timer example": +task genbindings_rust, "Generate CBOR Rust bindings for the timer example": + # The native and CBOR Rust crates share file names, so each mode writes to its + # own output dir (mirroring rust_bindings vs rust_native_bindings). + for flags in [nimFlagsOrc, nimFlagsRefc]: + exec "nim c " & flags & " --app:lib --noMain --nimMainPrefix:libmy_timer" & + " -d:ffiGenBindings -d:targetLang=rust -d:ffiMode=cbor" & + " -d:ffiOutputDir=examples/timer/rust_bindings -d:ffiSrcPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" + +task genbindings_rust_native, "Generate native (non-CBOR) Rust bindings for the timer example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" & - " -d:ffiGenBindings -d:targetLang=rust" & - " -d:ffiOutputDir=examples/timer/rust_bindings" & - " -d:ffiSrcPath=../timer.nim" & - " -o:/dev/null examples/timer/timer.nim" - exec "nim c " & nimFlagsRefc & - " --app:lib --noMain --nimMainPrefix:libmy_timer" & - " -d:ffiGenBindings -d:targetLang=rust" & - " -d:ffiOutputDir=examples/timer/rust_bindings" & + " -d:ffiGenBindings -d:targetLang=rust -d:ffiMode=native" & + " -d:ffiOutputDir=examples/timer/rust_native_bindings" & " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust_cbor.nim similarity index 100% rename from ffi/codegen/rust.nim rename to ffi/codegen/rust_cbor.nim diff --git a/ffi/codegen/rust_native.nim b/ffi/codegen/rust_native.nim new file mode 100644 index 0000000..e23a905 --- /dev/null +++ b/ffi/codegen/rust_native.nim @@ -0,0 +1,335 @@ +## Native (zero-serialization) Rust binding generator. +## +## Emits a `_native` crate that wraps the *native* C ABI (the `` +## 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 `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 + +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() + if isStringT(s): "String" + else: rustScalar(s) + +proc rustCField(t: string, types: seq[FFITypeMeta]): string = + ## repr(C) field type, matching codegen/c.emitCStructs. + let s = t.strip() + if isStringT(s): "*const c_char" + elif s == "bool": "c_int" + elif isStructT(s, types): capitalizeFirstLetter(s) # sibling repr(C) struct + else: rustScalar(s) + +proc typeSimple(t: FFITypeMeta): bool = + for f in t.fields: + if isSeqT(f.typeName) or isOptT(f.typeName): + return false + true + +proc procSimple(p: FFIProcMeta, types: seq[FFITypeMeta]): bool = + ## A proc is "simple" if no param is a raw pointer, a bare seq/Option, or a + ## struct that itself carries seq/Option fields (those structs aren't emitted + ## yet, so referencing them would not compile). + for ep in p.extraParams: + let t = ep.typeName.strip() + if ep.isPtr or isSeqT(t) or isOptT(t): + return false + if isStructT(t, types): + for ty in types: + if ty.name == t and not typeSimple(ty): + return false + true + +# ── ffi.rs ────────────────────────────────────────────────────────────────── +proc emitFfiRs( + procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string +): 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("") + # repr(C) POD mirrors. + for t in types: + if not typeSimple(t): + continue + L.add("#[repr(C)]") + L.add("#[derive(Clone, Copy)]") + L.add("pub struct " & capitalizeFirstLetter(t.name) & " {") + for f in t.fields: + L.add(" pub " & camelToSnakeCase(f.name) & ": " & rustCField(f.typeName, types) & ",") + 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: + ps.add(camelToSnakeCase(ep.name) & ": " & rustCField(ep.typeName, types)) + 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: + ps.add(camelToSnakeCase(ep.name) & ": " & rustCField(ep.typeName, types)) + 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;") + 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.") + 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: + if not typeSimple(t): + continue + let rn = capitalizeFirstLetter(t.name) + 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("") + L.add("impl " & rn & " {") + # to_c_inner: build the C struct, pushing owned CStrings into `strings`. + L.add(" pub fn to_c_inner(&self, strings: &mut Vec) -> ffi::" & rn & " {") + for f in t.fields: + let snake = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isStringT(ft): + L.add(" let " & snake & "_c = CString::new(self." & snake & + ".as_str()).unwrap_or_default();") + L.add(" let " & snake & "_p = " & snake & "_c.as_ptr();") + L.add(" strings.push(" & snake & "_c);") + L.add(" ffi::" & rn & " {") + for f in t.fields: + let snake = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isStringT(ft): + L.add(" " & snake & ": " & snake & "_p,") + elif ft == "bool": + L.add(" " & snake & ": if self." & snake & " { 1 } else { 0 },") + elif isStructT(ft, types): + L.add(" " & snake & ": self." & snake & ".to_c_inner(strings),") + else: + L.add(" " & snake & ": self." & snake & ",") + L.add(" }") + L.add(" }") + # from_c + L.add(" pub fn from_c(c: &ffi::" & rn & ") -> Self {") + L.add(" " & rn & " {") + for f in t.fields: + let snake = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isStringT(ft): + L.add(" " & snake & ": cstr(c." & snake & "),") + elif ft == "bool": + L.add(" " & snake & ": c." & snake & " != 0,") + elif isStructT(ft, types): + L.add(" " & snake & ": " & capitalizeFirstLetter(ft) & + "::from_c(&c." & snake & "),") + else: + L.add(" " & snake & ": c." & snake & ",") + 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( + procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string +): string = + let nodeT = snakeToPascalCase(libName) & "Node" # idiomatic Rust: MyTimerNode + 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>);") + 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>);") + 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>);") + 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("}") + L.add("") + 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("") + 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 {") + L.add(" let mut strings = Vec::new();") + var args: seq[string] = @[] + for ep in p.extraParams: + L.add(" let c_" & camelToSnakeCase(ep.name) & " = " & camelToSnakeCase(ep.name) & ".to_c_inner(&mut strings);") + args.add("c_" & camelToSnakeCase(ep.name)) + L.add(" let (tx, rx) = sync_channel::>(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\")); }") + L.add(" let _ = strings;") + L.add(" Ok(" & nodeT & " { ctx })") + 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)) + conv.add(" let c_" & camelToSnakeCase(ep.name) & " = " & camelToSnakeCase(ep.name) & ".to_c_inner(&mut strings);") + callArgs.add("c_" & camelToSnakeCase(ep.name)) + L.add(" pub fn " & rustMethod(p.procName, libName) & "(&self" & (if params.len > 0: ", " & params.join(", ") else: "") & ") -> Result<" & retT & ", String> {") + if conv.len > 0: + L.add(" let mut strings = Vec::new();") + for c in conv: L.add(c) + L.add(" let (tx, rx) = sync_channel::>(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>) });") + L.add(" return Err(String::from(\"" & p.procName & " dispatch failed\"));") + L.add(" }") + if conv.len > 0: + L.add(" let _ = strings;") + L.add(" rx.recv().map_err(|_| String::from(\"callback channel closed\"))?") + L.add(" }") + L.add("") + + # 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]\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" / "types.rs", emitTypesRs(types)) + writeFile(outputDir / "src" / "api.rs", emitApiRs(procs, types, libName)) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 576a35a..19b6e98 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -4,7 +4,8 @@ import ../ffi_types import ../codegen/[meta, string_helpers] import ./native_pod when defined(ffiGenBindings): - import ../codegen/rust + import ../codegen/rust_cbor + import ../codegen/rust_native import ../codegen/cpp import ../codegen/cddl import ../codegen/c @@ -1966,10 +1967,16 @@ macro genBindings*( let libName = deriveLibName(ffiProcRegistry) case lang of "rust": - generateRustCrate( - ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, - ffiEventRegistry, - ) + if ffiEmitCbor(): + generateRustCrate( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) + if ffiEmitNative(): + generateRustNativeCrate( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) of "cpp", "c++": generateCppBindings( ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,