mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
feat(codegen): native (non-CBOR) Rust generator + rust.nim -> rust_cbor.nim split
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 `<lib>_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 `<Lib>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 <noreply@anthropic.com>
This commit is contained in:
parent
f08cb7971d
commit
23152d4fe7
4
examples/timer/rust_native_bindings/.gitignore
vendored
Normal file
4
examples/timer/rust_native_bindings/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
/libmy_timer.dylib
|
||||
/libmy_timer.so
|
||||
Cargo.lock
|
||||
4
examples/timer/rust_native_bindings/Cargo.toml
Normal file
4
examples/timer/rust_native_bindings/Cargo.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "my_timer_native"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
28
examples/timer/rust_native_bindings/README.md
Normal file
28
examples/timer/rust_native_bindings/README.md
Normal file
@ -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` | `<Lib>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 <dir> -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.
|
||||
7
examples/timer/rust_native_bindings/examples/demo.rs
Normal file
7
examples/timer/rust_native_bindings/examples/demo.rs
Normal file
@ -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);
|
||||
}
|
||||
84
examples/timer/rust_native_bindings/src/api.rs
Normal file
84
examples/timer/rust_native_bindings/src/api.rs
Normal file
@ -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<Result<(), String>>);
|
||||
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<Result<String, String>>);
|
||||
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<Result<EchoResponse, String>>);
|
||||
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<Self, String> {
|
||||
let mut strings = Vec::new();
|
||||
let c_config = config.to_c_inner(&mut strings);
|
||||
let (tx, rx) = sync_channel::<Result<(), String>>(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<EchoResponse, String> {
|
||||
let mut strings = Vec::new();
|
||||
let c_req = req.to_c_inner(&mut strings);
|
||||
let (tx, rx) = sync_channel::<Result<EchoResponse, String>>(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<Result<EchoResponse, String>>) });
|
||||
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<String, String> {
|
||||
let (tx, rx) = sync_channel::<Result<String, String>>(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<Result<String, String>>) });
|
||||
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); } }
|
||||
}
|
||||
58
examples/timer/rust_native_bindings/src/ffi.rs
Normal file
58
examples/timer/rust_native_bindings/src/ffi.rs
Normal file
@ -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;
|
||||
}
|
||||
5
examples/timer/rust_native_bindings/src/lib.rs
Normal file
5
examples/timer/rust_native_bindings/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod ffi;
|
||||
mod types;
|
||||
mod api;
|
||||
pub use types::*;
|
||||
pub use api::*;
|
||||
162
examples/timer/rust_native_bindings/src/types.rs
Normal file
162
examples/timer/rust_native_bindings/src/types.rs
Normal file
@ -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<CString>) -> 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<CString>) -> 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<CString>) -> 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<CString>) -> 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<CString>) -> 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<CString>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
21
ffi.nimble
21
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"
|
||||
|
||||
|
||||
335
ffi/codegen/rust_native.nim
Normal file
335
ffi/codegen/rust_native.nim
Normal file
@ -0,0 +1,335 @@
|
||||
## 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
|
||||
|
||||
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<CString>) -> 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<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("}")
|
||||
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<Self, String> {")
|
||||
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::<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\")); }")
|
||||
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::<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(" }")
|
||||
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))
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user