nim-ffi/ffi/codegen/rust_native.nim
Ivan FB 23152d4fe7
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>
2026-05-31 18:51:06 +02:00

336 lines
14 KiB
Nim

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