## 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))