diff --git a/examples/timer/rust_native_bindings/README.md b/examples/timer/rust_native_bindings/README.md index 55ffcac..1b4f55d 100644 --- a/examples/timer/rust_native_bindings/README.md +++ b/examples/timer/rust_native_bindings/README.md @@ -21,8 +21,12 @@ 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. +Requests are fully supported: scalar / string / bool / float / nested struct +**and now sequences (`Vec`) and optionals (`Option`)** — create, version, echo, +complex, schedule all generate and round-trip typed values. `to_c` returns a +holder that owns the `CString`s and C-array backing (heap, so the C struct's raw +pointers stay valid across the move and for the call). + +Still to come: native typed events and the native-bare / `_cbor` reconciliation. +Linking is left to the consumer (`-L -l my_timer` + rpath, as in +`examples/demo.rs`); a build.rs that compiles the dylib can be added later. diff --git a/examples/timer/rust_native_bindings/examples/demo.rs b/examples/timer/rust_native_bindings/examples/demo.rs index 691852c..e4b3815 100644 --- a/examples/timer/rust_native_bindings/examples/demo.rs +++ b/examples/timer/rust_native_bindings/examples/demo.rs @@ -4,4 +4,20 @@ fn main() { 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); + + let c = node.complex(ComplexRequest { + messages: vec![EchoRequest { message: "one".into(), delay_ms: 0 }, + EchoRequest { message: "two".into(), delay_ms: 0 }], + tags: vec!["a".into(), "b".into()], + note: Some("a note".into()), + retries: Some(3), + }).unwrap(); + println!("complex: item_count={} has_note={} summary={:?}", c.item_count, c.has_note, c.summary); + + let s = node.schedule( + JobSpec { name: "nightly".into(), payload: vec!["x".into()], priority: 5 }, + RetryPolicy { max_attempts: 3, backoff_ms: 100, retry_on: vec!["timeout".into()] }, + ScheduleConfig { start_at_ms: 1000, interval_ms: 5000, jitter: Some(50) }, + ).unwrap(); + println!("schedule: job_id={:?} will_run_count={}", s.job_id, s.will_run_count); } diff --git a/examples/timer/rust_native_bindings/src/api.rs b/examples/timer/rust_native_bindings/src/api.rs index 9fee077..38b2b30 100644 --- a/examples/timer/rust_native_bindings/src/api.rs +++ b/examples/timer/rust_native_bindings/src/api.rs @@ -31,6 +31,20 @@ unsafe extern "C" fn cb_my_timer_echo(ret: c_int, msg: *const c_char, len: usize } else { Err(err_text(msg, len)) }; let _ = tx.send(r); } +unsafe extern "C" fn cb_my_timer_complex(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(ComplexResponse::from_c(&*(msg as *const ffi::ComplexResponse))) + } else { Err(err_text(msg, len)) }; + let _ = tx.send(r); +} +unsafe extern "C" fn cb_my_timer_schedule(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(ScheduleResult::from_c(&*(msg as *const ffi::ScheduleResult))) + } else { Err(err_text(msg, len)) }; + let _ = tx.send(r); +} pub struct MyTimerNode { ctx: *mut c_void } unsafe impl Send for MyTimerNode {} @@ -38,29 +52,25 @@ 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 c_config = config.to_c(); 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 ctx = unsafe { ffi::my_timer_create(c_config.c, 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 c_req = req.to_c(); 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) }; + let rc = unsafe { ffi::my_timer_echo(self.ctx, cb_my_timer_echo, raw, c_req.c) }; 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"))? } @@ -75,8 +85,32 @@ impl MyTimerNode { 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 + pub fn complex(&self, req: ComplexRequest) -> Result { + let c_req = req.to_c(); + let (tx, rx) = sync_channel::>(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let rc = unsafe { ffi::my_timer_complex(self.ctx, cb_my_timer_complex, raw, c_req.c) }; + if rc != RET_OK { + drop(unsafe { Box::from_raw(raw as *mut SyncSender>) }); + return Err(String::from("my_timer_complex dispatch failed")); + } + rx.recv().map_err(|_| String::from("callback channel closed"))? + } + + pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result { + let c_job = job.to_c(); + let c_retry = retry.to_c(); + let c_schedule = schedule.to_c(); + let (tx, rx) = sync_channel::>(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let rc = unsafe { ffi::my_timer_schedule(self.ctx, cb_my_timer_schedule, raw, c_job.c, c_retry.c, c_schedule.c) }; + if rc != RET_OK { + drop(unsafe { Box::from_raw(raw as *mut SyncSender>) }); + return Err(String::from("my_timer_schedule dispatch failed")); + } + rx.recv().map_err(|_| String::from("callback channel closed"))? + } + } impl Drop for MyTimerNode { diff --git a/examples/timer/rust_native_bindings/src/ffi.rs b/examples/timer/rust_native_bindings/src/ffi.rs index 1850461..7ada8fd 100644 --- a/examples/timer/rust_native_bindings/src/ffi.rs +++ b/examples/timer/rust_native_bindings/src/ffi.rs @@ -25,6 +25,19 @@ pub struct EchoResponse { pub timer_name: *const c_char, } +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ComplexRequest { + pub messages: *const EchoRequest, + pub messages_len: usize, + pub tags: *const *const c_char, + pub tags_len: usize, + pub note_present: c_int, + pub note: *const c_char, + pub retries_present: c_int, + pub retries: i64, +} + #[repr(C)] #[derive(Clone, Copy)] pub struct ComplexResponse { @@ -40,6 +53,33 @@ pub struct EchoEvent { pub echo_count: i64, } +#[repr(C)] +#[derive(Clone, Copy)] +pub struct JobSpec { + pub name: *const c_char, + pub payload: *const *const c_char, + pub payload_len: usize, + pub priority: i64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RetryPolicy { + pub max_attempts: i64, + pub backoff_ms: i64, + pub retry_on: *const *const c_char, + pub retry_on_len: usize, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ScheduleConfig { + pub start_at_ms: i64, + pub interval_ms: i64, + pub jitter_present: c_int, + pub jitter: i64, +} + #[repr(C)] #[derive(Clone, Copy)] pub struct ScheduleResult { @@ -54,5 +94,7 @@ 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_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req: ComplexRequest) -> c_int; + pub fn my_timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> 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/types.rs b/examples/timer/rust_native_bindings/src/types.rs index 150a381..f33c54b 100644 --- a/examples/timer/rust_native_bindings/src/types.rs +++ b/examples/timer/rust_native_bindings/src/types.rs @@ -1,4 +1,5 @@ // Generated by nim-ffi native Rust codegen. Do not edit by hand. +#![allow(dead_code)] use std::ffi::{CStr, CString}; use std::os::raw::c_char; use super::ffi; @@ -13,14 +14,18 @@ pub struct TimerConfig { pub name: String, } +pub struct TimerConfigC { + pub c: ffi::TimerConfig, + _s: Vec, +} + 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 to_c(&self) -> TimerConfigC { + #[allow(unused_mut)] + let mut h = TimerConfigC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.name.as_str()).unwrap_or_default(); h.c.name = cs.as_ptr(); h._s.push(cs); } + h } pub fn from_c(c: &ffi::TimerConfig) -> Self { TimerConfig { @@ -35,15 +40,19 @@ pub struct EchoRequest { pub delay_ms: i64, } +pub struct EchoRequestC { + pub c: ffi::EchoRequest, + _s: Vec, +} + 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 to_c(&self) -> EchoRequestC { + #[allow(unused_mut)] + let mut h = EchoRequestC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.message.as_str()).unwrap_or_default(); h.c.message = cs.as_ptr(); h._s.push(cs); } + h.c.delay_ms = self.delay_ms; + h } pub fn from_c(c: &ffi::EchoRequest) -> Self { EchoRequest { @@ -59,18 +68,19 @@ pub struct EchoResponse { pub timer_name: String, } +pub struct EchoResponseC { + pub c: ffi::EchoResponse, + _s: Vec, +} + 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 to_c(&self) -> EchoResponseC { + #[allow(unused_mut)] + let mut h = EchoResponseC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.echoed.as_str()).unwrap_or_default(); h.c.echoed = cs.as_ptr(); h._s.push(cs); } + { let cs = CString::new(self.timer_name.as_str()).unwrap_or_default(); h.c.timer_name = cs.as_ptr(); h._s.push(cs); } + h } pub fn from_c(c: &ffi::EchoResponse) -> Self { EchoResponse { @@ -80,6 +90,61 @@ impl EchoResponse { } } +#[derive(Clone, Debug, Default)] +pub struct ComplexRequest { + pub messages: Vec, + pub tags: Vec, + pub note: Option, + pub retries: Option, +} + +pub struct ComplexRequestC { + pub c: ffi::ComplexRequest, + _s: Vec, + _a_messages: Vec, + _h_messages: Vec, + _a_tags: Vec<*const c_char>, +} + +impl ComplexRequest { + pub fn to_c(&self) -> ComplexRequestC { + #[allow(unused_mut)] + let mut h = ComplexRequestC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + _a_messages: Vec::new(), + _h_messages: Vec::new(), + _a_tags: Vec::new(), + }; + for x in &self.messages { + let eh = x.to_c(); + h._a_messages.push(eh.c); + h._h_messages.push(eh); + } + h.c.messages = if h._a_messages.is_empty() { std::ptr::null() } else { h._a_messages.as_ptr() }; + h.c.messages_len = h._a_messages.len(); + for x in &self.tags { + let cs = CString::new(x.as_str()).unwrap_or_default(); + h._a_tags.push(cs.as_ptr()); + h._s.push(cs); + } + h.c.tags = if h._a_tags.is_empty() { std::ptr::null() } else { h._a_tags.as_ptr() }; + h.c.tags_len = h._a_tags.len(); + if let Some(x) = &self.note { + let cs = CString::new(x.as_str()).unwrap_or_default(); + h.c.note = cs.as_ptr(); h.c.note_present = 1; h._s.push(cs); + } + if let Some(v) = self.retries { h.c.retries_present = 1; h.c.retries = v; } + h + } + pub fn from_c(c: &ffi::ComplexRequest) -> Self { + ComplexRequest { + messages: if c.messages.is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c.messages, c.messages_len) }.iter().map(|e| EchoRequest::from_c(e)).collect() }, + tags: if c.tags.is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c.tags, c.tags_len) }.iter().map(|&p| cstr(p)).collect() }, + note: if c.note_present != 0 { Some(cstr(c.note)) } else { None }, + retries: if c.retries_present != 0 { Some(c.retries) } else { None }, + } + } +} + #[derive(Clone, Debug, Default)] pub struct ComplexResponse { pub summary: String, @@ -87,16 +152,20 @@ pub struct ComplexResponse { pub has_note: bool, } +pub struct ComplexResponseC { + pub c: ffi::ComplexResponse, + _s: Vec, +} + 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 to_c(&self) -> ComplexResponseC { + #[allow(unused_mut)] + let mut h = ComplexResponseC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.summary.as_str()).unwrap_or_default(); h.c.summary = cs.as_ptr(); h._s.push(cs); } + h.c.item_count = self.item_count; + h.c.has_note = if self.has_note { 1 } else { 0 }; + h } pub fn from_c(c: &ffi::ComplexResponse) -> Self { ComplexResponse { @@ -113,15 +182,19 @@ pub struct EchoEvent { pub echo_count: i64, } +pub struct EchoEventC { + pub c: ffi::EchoEvent, + _s: Vec, +} + 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 to_c(&self) -> EchoEventC { + #[allow(unused_mut)] + let mut h = EchoEventC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.message.as_str()).unwrap_or_default(); h.c.message = cs.as_ptr(); h._s.push(cs); } + h.c.echo_count = self.echo_count; + h } pub fn from_c(c: &ffi::EchoEvent) -> Self { EchoEvent { @@ -131,6 +204,115 @@ impl EchoEvent { } } +#[derive(Clone, Debug, Default)] +pub struct JobSpec { + pub name: String, + pub payload: Vec, + pub priority: i64, +} + +pub struct JobSpecC { + pub c: ffi::JobSpec, + _s: Vec, + _a_payload: Vec<*const c_char>, +} + +impl JobSpec { + pub fn to_c(&self) -> JobSpecC { + #[allow(unused_mut)] + let mut h = JobSpecC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + _a_payload: Vec::new(), + }; + { let cs = CString::new(self.name.as_str()).unwrap_or_default(); h.c.name = cs.as_ptr(); h._s.push(cs); } + for x in &self.payload { + let cs = CString::new(x.as_str()).unwrap_or_default(); + h._a_payload.push(cs.as_ptr()); + h._s.push(cs); + } + h.c.payload = if h._a_payload.is_empty() { std::ptr::null() } else { h._a_payload.as_ptr() }; + h.c.payload_len = h._a_payload.len(); + h.c.priority = self.priority; + h + } + pub fn from_c(c: &ffi::JobSpec) -> Self { + JobSpec { + name: cstr(c.name), + payload: if c.payload.is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c.payload, c.payload_len) }.iter().map(|&p| cstr(p)).collect() }, + priority: c.priority, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct RetryPolicy { + pub max_attempts: i64, + pub backoff_ms: i64, + pub retry_on: Vec, +} + +pub struct RetryPolicyC { + pub c: ffi::RetryPolicy, + _s: Vec, + _a_retry_on: Vec<*const c_char>, +} + +impl RetryPolicy { + pub fn to_c(&self) -> RetryPolicyC { + #[allow(unused_mut)] + let mut h = RetryPolicyC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + _a_retry_on: Vec::new(), + }; + h.c.max_attempts = self.max_attempts; + h.c.backoff_ms = self.backoff_ms; + for x in &self.retry_on { + let cs = CString::new(x.as_str()).unwrap_or_default(); + h._a_retry_on.push(cs.as_ptr()); + h._s.push(cs); + } + h.c.retry_on = if h._a_retry_on.is_empty() { std::ptr::null() } else { h._a_retry_on.as_ptr() }; + h.c.retry_on_len = h._a_retry_on.len(); + h + } + pub fn from_c(c: &ffi::RetryPolicy) -> Self { + RetryPolicy { + max_attempts: c.max_attempts, + backoff_ms: c.backoff_ms, + retry_on: if c.retry_on.is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c.retry_on, c.retry_on_len) }.iter().map(|&p| cstr(p)).collect() }, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ScheduleConfig { + pub start_at_ms: i64, + pub interval_ms: i64, + pub jitter: Option, +} + +pub struct ScheduleConfigC { + pub c: ffi::ScheduleConfig, + _s: Vec, +} + +impl ScheduleConfig { + pub fn to_c(&self) -> ScheduleConfigC { + #[allow(unused_mut)] + let mut h = ScheduleConfigC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + h.c.start_at_ms = self.start_at_ms; + h.c.interval_ms = self.interval_ms; + if let Some(v) = self.jitter { h.c.jitter_present = 1; h.c.jitter = v; } + h + } + pub fn from_c(c: &ffi::ScheduleConfig) -> Self { + ScheduleConfig { + start_at_ms: c.start_at_ms, + interval_ms: c.interval_ms, + jitter: if c.jitter_present != 0 { Some(c.jitter) } else { None }, + } + } +} + #[derive(Clone, Debug, Default)] pub struct ScheduleResult { pub job_id: String, @@ -139,17 +321,21 @@ pub struct ScheduleResult { pub effective_backoff_ms: i64, } +pub struct ScheduleResultC { + pub c: ffi::ScheduleResult, + _s: Vec, +} + 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 to_c(&self) -> ScheduleResultC { + #[allow(unused_mut)] + let mut h = ScheduleResultC { c: unsafe { std::mem::zeroed() }, _s: Vec::new(), + }; + { let cs = CString::new(self.job_id.as_str()).unwrap_or_default(); h.c.job_id = cs.as_ptr(); h._s.push(cs); } + h.c.will_run_count = self.will_run_count; + h.c.first_run_at_ms = self.first_run_at_ms; + h.c.effective_backoff_ms = self.effective_backoff_ms; + h } pub fn from_c(c: &ffi::ScheduleResult) -> Self { ScheduleResult { diff --git a/ffi/codegen/rust_native.nim b/ffi/codegen/rust_native.nim index e23a905..bc0a75f 100644 --- a/ffi/codegen/rust_native.nim +++ b/ffi/codegen/rust_native.nim @@ -30,6 +30,14 @@ proc isStructT(t: string, types: seq[FFITypeMeta]): bool = return true false +proc seqElem(t: string): string = + t.strip()["seq[".len .. ^2].strip() + +proc optElem(t: string): string = + let s = t.strip() + let p = if s.startsWith("Maybe["): "Maybe[".len else: "Option[".len + s[p .. ^2].strip() + proc rustScalar(t: string): string = case t.strip() of "int", "int64", "clong": "i64" @@ -47,35 +55,28 @@ proc rustScalar(t: string): string = proc rustIdiomatic(t: string): string = let s = t.strip() - if isStringT(s): "String" + if isSeqT(s): "Vec<" & rustIdiomatic(seqElem(s)) & ">" + elif isOptT(s): "Option<" & rustIdiomatic(optElem(s)) & ">" + elif isStringT(s): "String" else: rustScalar(s) -proc rustCField(t: string, types: seq[FFITypeMeta]): string = - ## repr(C) field type, matching codegen/c.emitCStructs. +proc rustCElem(t: string, types: seq[FFITypeMeta], qualify = false): string = + ## repr(C) type for one seq element / option payload (matches emitCStructs). + ## `qualify` prefixes struct names with `ffi::` (needed outside the ffi module). 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 + elif isStructT(s, types): (if qualify: "ffi::" else: "") & capitalizeFirstLetter(s) 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). + ## All scalar / string / struct params are supported (structs may carry + ## seq/Option fields). Bare seq/Option top-level params and raw pointers are + ## not (the native ABI doesn't expose them either). 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 ────────────────────────────────────────────────────────────────── @@ -90,15 +91,22 @@ proc emitFfiRs( 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. + # repr(C) POD mirrors — field layout mirrors codegen/c.emitCStructs. 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) & ",") + let snake = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isSeqT(ft): + L.add(" pub " & snake & ": *const " & rustCElem(seqElem(ft), types) & ",") + L.add(" pub " & snake & "_len: usize,") + elif isOptT(ft): + L.add(" pub " & snake & "_present: c_int,") + L.add(" pub " & snake & ": " & rustCElem(optElem(ft), types) & ",") + else: + L.add(" pub " & snake & ": " & rustCElem(ft, types) & ",") L.add("}") L.add("") L.add("#[link(name = \"" & libName & "\")]") @@ -108,7 +116,7 @@ proc emitFfiRs( of FFIKind.CTOR: var ps: seq[string] = @[] for ep in p.extraParams: - ps.add(camelToSnakeCase(ep.name) & ": " & rustCField(ep.typeName, types)) + ps.add(camelToSnakeCase(ep.name) & ": " & rustCElem(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;") @@ -117,7 +125,7 @@ proc emitFfiRs( 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)) + ps.add(camelToSnakeCase(ep.name) & ": " & rustCElem(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;") @@ -128,6 +136,7 @@ proc emitFfiRs( 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("#![allow(dead_code)]") L.add("use std::ffi::{CStr, CString};") L.add("use std::os::raw::c_char;") L.add("use super::ffi;") @@ -138,55 +147,122 @@ proc emitTypesRs(types: seq[FFITypeMeta]): string = L.add("}") L.add("") for t in types: - if not typeSimple(t): - continue let rn = capitalizeFirstLetter(t.name) + let hn = rn & "C" + # idiomatic struct 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 & " {") + # holder: owns the C struct + all heap backing (CStrings, arrays, nested + # holders). Returned by value; the backing lives on the heap, so the C + # struct's raw pointers stay valid across the move and for the call. + L.add("pub struct " & hn & " {") + L.add(" pub c: ffi::" & rn & ",") + L.add(" _s: Vec,") for f in t.fields: - let snake = camelToSnakeCase(f.name) + let s = 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 },") + if isSeqT(ft): + L.add(" _a_" & s & ": Vec<" & rustCElem(seqElem(ft), types, qualify = true) & ">,") + if isStructT(seqElem(ft), types): + L.add(" _h_" & s & ": Vec<" & capitalizeFirstLetter(seqElem(ft)) & "C>,") + elif isOptT(ft) and isStructT(optElem(ft), types): + L.add(" _h_" & s & ": Vec<" & capitalizeFirstLetter(optElem(ft)) & "C>,") elif isStructT(ft, types): - L.add(" " & snake & ": self." & snake & ".to_c_inner(strings),") + L.add(" _h_" & s & ": Vec<" & capitalizeFirstLetter(ft) & "C>,") + L.add("}") + L.add("") + L.add("impl " & rn & " {") + L.add(" pub fn to_c(&self) -> " & hn & " {") + L.add(" #[allow(unused_mut)]") + L.add(" let mut h = " & hn & " { c: unsafe { std::mem::zeroed() }, _s: Vec::new(),") + for f in t.fields: + let s = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isSeqT(ft): + L.add(" _a_" & s & ": Vec::new(),") + if isStructT(seqElem(ft), types): + L.add(" _h_" & s & ": Vec::new(),") + elif (isOptT(ft) and isStructT(optElem(ft), types)) or isStructT(ft, types): + L.add(" _h_" & s & ": Vec::new(),") + L.add(" };") + for f in t.fields: + let s = camelToSnakeCase(f.name) + let ft = f.typeName.strip() + if isSeqT(ft): + let e = seqElem(ft) + L.add(" for x in &self." & s & " {") + if isStringT(e): + L.add(" let cs = CString::new(x.as_str()).unwrap_or_default();") + L.add(" h._a_" & s & ".push(cs.as_ptr());") + L.add(" h._s.push(cs);") + elif isStructT(e, types): + L.add(" let eh = x.to_c();") + L.add(" h._a_" & s & ".push(eh.c);") + L.add(" h._h_" & s & ".push(eh);") + elif e == "bool": + L.add(" h._a_" & s & ".push(if *x { 1 } else { 0 });") + else: + L.add(" h._a_" & s & ".push(*x);") + L.add(" }") + L.add(" h.c." & s & " = if h._a_" & s & ".is_empty() { std::ptr::null() } else { h._a_" & s & ".as_ptr() };") + L.add(" h.c." & s & "_len = h._a_" & s & ".len();") + elif isOptT(ft): + let e = optElem(ft) + if isStringT(e): + L.add(" if let Some(x) = &self." & s & " {") + L.add(" let cs = CString::new(x.as_str()).unwrap_or_default();") + L.add(" h.c." & s & " = cs.as_ptr(); h.c." & s & "_present = 1; h._s.push(cs);") + L.add(" }") + elif isStructT(e, types): + L.add(" if let Some(x) = &self." & s & " {") + L.add(" let eh = x.to_c(); h.c." & s & " = eh.c; h.c." & s & "_present = 1; h._h_" & s & ".push(eh);") + L.add(" }") + elif e == "bool": + L.add(" if let Some(v) = self." & s & " { h.c." & s & "_present = 1; h.c." & s & " = if v { 1 } else { 0 }; }") + else: + L.add(" if let Some(v) = self." & s & " { h.c." & s & "_present = 1; h.c." & s & " = v; }") + elif isStringT(ft): + L.add(" { let cs = CString::new(self." & s & ".as_str()).unwrap_or_default(); h.c." & s & " = cs.as_ptr(); h._s.push(cs); }") + elif ft == "bool": + L.add(" h.c." & s & " = if self." & s & " { 1 } else { 0 };") + elif isStructT(ft, types): + L.add(" h._h_" & s & ".push(self." & s & ".to_c()); h.c." & s & " = h._h_" & s & "[0].c;") else: - L.add(" " & snake & ": self." & snake & ",") - L.add(" }") + L.add(" h.c." & s & " = self." & s & ";") + L.add(" h") 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 s = camelToSnakeCase(f.name) let ft = f.typeName.strip() - if isStringT(ft): - L.add(" " & snake & ": cstr(c." & snake & "),") + if isSeqT(ft): + let e = seqElem(ft) + var conv = "|&v| v" + if isStringT(e): conv = "|&p| cstr(p)" + elif isStructT(e, types): conv = "|e| " & capitalizeFirstLetter(e) & "::from_c(e)" + elif e == "bool": conv = "|&v| v != 0" + L.add(" " & s & ": if c." & s & ".is_null() { Vec::new() } else { unsafe { std::slice::from_raw_parts(c." & s & ", c." & s & "_len) }.iter().map(" & conv & ").collect() },") + elif isOptT(ft): + let e = optElem(ft) + var val = "c." & s + if isStringT(e): val = "cstr(c." & s & ")" + elif isStructT(e, types): val = capitalizeFirstLetter(e) & "::from_c(&c." & s & ")" + elif e == "bool": val = "c." & s & " != 0" + L.add(" " & s & ": if c." & s & "_present != 0 { Some(" & val & ") } else { None },") + elif isStringT(ft): + L.add(" " & s & ": cstr(c." & s & "),") elif ft == "bool": - L.add(" " & snake & ": c." & snake & " != 0,") + L.add(" " & s & ": c." & s & " != 0,") elif isStructT(ft, types): - L.add(" " & snake & ": " & capitalizeFirstLetter(ft) & - "::from_c(&c." & snake & "),") + L.add(" " & s & ": " & capitalizeFirstLetter(ft) & "::from_c(&c." & s & "),") else: - L.add(" " & snake & ": c." & snake & ",") + L.add(" " & s & ": c." & s & ",") L.add(" }") L.add(" }") L.add("}") @@ -257,11 +333,12 @@ proc emitApiRs( 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)) + # The holder must outlive the call (it owns the CStrings/arrays the C + # struct points into); the native ctor deep-copies before returning. + L.add(" let c_" & camelToSnakeCase(ep.name) & " = " & camelToSnakeCase(ep.name) & ".to_c();") + args.add("c_" & camelToSnakeCase(ep.name) & ".c") 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(", ") @@ -269,7 +346,6 @@ proc emitApiRs( 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("") @@ -288,12 +364,10 @@ proc emitApiRs( 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)) + conv.add(" let c_" & camelToSnakeCase(ep.name) & " = " & camelToSnakeCase(ep.name) & ".to_c();") + callArgs.add("c_" & camelToSnakeCase(ep.name) & ".c") 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) + 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(", ") & ") };") @@ -301,8 +375,6 @@ proc emitApiRs( 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("")