mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-16 13:09:24 +00:00
750 lines
28 KiB
Nim
750 lines
28 KiB
Nim
## Rust binding generator: emits a complete Rust crate using CBOR (ciborium).
|
|
|
|
import std/[os, strutils]
|
|
import ./meta, ./string_helpers, ./types_ir
|
|
|
|
## Wire-format Rust type for any Nim `ptr T`/`pointer`; fixed 64-bit for a
|
|
## host-independent CBOR payload size (mirrors CppPtrType).
|
|
const RustPtrType* = "u64"
|
|
|
|
func rustScalar(s: ScalarKind): string =
|
|
case s
|
|
of skBool: "bool"
|
|
of skI8: "i8"
|
|
of skI16: "i16"
|
|
of skI32: "i32"
|
|
of skI64: "i64"
|
|
of skU8: "u8"
|
|
of skU16: "u16"
|
|
of skU32: "u32"
|
|
of skU64: "u64"
|
|
of skF32: "f32"
|
|
of skF64: "f64"
|
|
|
|
func rustSeq(elem: string): string =
|
|
"Vec<" & elem & ">"
|
|
|
|
func rustOpt(elem: string): string =
|
|
"Option<" & elem & ">"
|
|
|
|
const rustMap = NativeTypeMap(
|
|
scalar: rustScalar,
|
|
str: "String",
|
|
bytes: "Vec<u8>",
|
|
ptrType: RustPtrType,
|
|
seqOf: rustSeq,
|
|
optOf: rustOpt,
|
|
structName: capitalizeFirstLetter,
|
|
)
|
|
|
|
proc nimTypeToRust*(typeName: string): string =
|
|
## Maps Nim type names to Rust type names, including generics.
|
|
renderNative(rustMap, parseFFIType(typeName))
|
|
|
|
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
|
## Common prefix before the first `_` in proc names, e.g. "timer_create" → "timer".
|
|
if currentLibName.len > 0:
|
|
return currentLibName
|
|
if procs.len == 0:
|
|
return "unknown"
|
|
let first = procs[0].procName
|
|
let parts = first.split('_')
|
|
if parts.len > 0:
|
|
return parts[0]
|
|
return "unknown"
|
|
|
|
proc stripLibPrefix*(procName: string, libName: string): string =
|
|
## Strips the library prefix, e.g. ("timer_echo", "timer") → "echo".
|
|
let prefix = libName & "_"
|
|
if procName.startsWith(prefix):
|
|
return procName[prefix.len .. ^1]
|
|
return procName
|
|
|
|
proc reqStructName(p: FFIProcMeta): string =
|
|
## Mirrors the Nim macro: <CamelCase(procName)>Req or CtorReq for ctors.
|
|
let camel = snakeToPascalCase(p.procName)
|
|
if p.kind == FFIKind.CTOR:
|
|
camel & "CtorReq"
|
|
else:
|
|
camel & "Req"
|
|
|
|
proc generateCargoToml*(libName: string): string =
|
|
# flume: callback channel (recv_timeout + recv_async), default-features off. tokio: only the async timeout.
|
|
return
|
|
"""[package]
|
|
name = "$1"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[dependencies]
|
|
serde = { version = "1", features = ["derive"] }
|
|
ciborium = "0.2"
|
|
flume = { version = "0.11", default-features = false, features = ["async"] }
|
|
tokio = { version = "1", features = ["sync", "time"] }
|
|
|
|
[dev-dependencies]
|
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
|
""" %
|
|
[libName]
|
|
|
|
proc generateBuildRs*(libName: string, nimSrcRelPath: string): string =
|
|
## Generates build.rs that compiles the Nim library; nimSrcRelPath is relative
|
|
## to the crate directory.
|
|
let escapedSrc = nimSrcRelPath.replace("\\", "\\\\")
|
|
return
|
|
"""use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn main() {
|
|
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
let nim_src = manifest.join("$1");
|
|
let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("$1"));
|
|
|
|
// Walk up to find the nim-ffi repo root (directory containing nim_src's library)
|
|
// The repo root is where nim c should be run from (contains config.nims).
|
|
// We assume nim_src lives somewhere under repo_root.
|
|
// Derive repo_root as the ancestor that contains the .nimble file or config.nims.
|
|
let mut repo_root = nim_src.clone();
|
|
loop {
|
|
repo_root = match repo_root.parent() {
|
|
Some(p) => p.to_path_buf(),
|
|
None => break,
|
|
};
|
|
if repo_root.join("config.nims").exists() || repo_root.join("ffi.nimble").exists() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let lib_ext = "dylib";
|
|
#[cfg(target_os = "linux")]
|
|
let lib_ext = "so";
|
|
|
|
let out_lib = repo_root.join(format!("lib$2.{lib_ext}"));
|
|
|
|
let mut cmd = Command::new("nim");
|
|
cmd.arg("c")
|
|
.arg("--mm:orc")
|
|
.arg("-d:chronicles_log_level=WARN")
|
|
.arg("--app:lib")
|
|
.arg("--noMain")
|
|
.arg(format!("--nimMainPrefix:lib$2"))
|
|
.arg(format!("-o:{}", out_lib.display()));
|
|
cmd.arg(&nim_src).current_dir(&repo_root);
|
|
|
|
let status = cmd.status().expect("failed to run nim compiler");
|
|
assert!(status.success(), "Nim compilation failed");
|
|
|
|
println!("cargo:rustc-link-search={}", repo_root.display());
|
|
println!("cargo:rustc-link-lib=$2");
|
|
println!("cargo:rerun-if-changed={}", nim_src.display());
|
|
}
|
|
""" %
|
|
[escapedSrc, libName]
|
|
|
|
proc generateLibRs*(): string =
|
|
return """mod ffi;
|
|
mod types;
|
|
mod api;
|
|
pub use types::*;
|
|
pub use api::*;
|
|
"""
|
|
|
|
proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
|
|
## Generates ffi.rs with extern "C" declarations; each proc takes one CBOR
|
|
## buffer (ptr+len) as its request payload.
|
|
var lines: seq[string] = @[]
|
|
lines.add("use std::os::raw::{c_char, c_int, c_void};")
|
|
lines.add("")
|
|
lines.add("pub type FFICallback = unsafe extern \"C\" fn(")
|
|
lines.add(" ret: c_int,")
|
|
lines.add(" msg: *const c_char,")
|
|
lines.add(" len: usize,")
|
|
lines.add(" user_data: *mut c_void,")
|
|
lines.add(");")
|
|
lines.add("")
|
|
|
|
var libNames: seq[string] = @[]
|
|
for p in procs:
|
|
if p.libName notin libNames:
|
|
libNames.add(p.libName)
|
|
|
|
var linkLibName = ""
|
|
if libNames.len > 0 and libNames[0].len > 0:
|
|
linkLibName = libNames[0]
|
|
else:
|
|
if procs.len > 0:
|
|
let parts = procs[0].procName.split('_')
|
|
if parts.len > 0:
|
|
linkLibName = parts[0]
|
|
|
|
lines.add("#[link(name = \"$1\")]" % [linkLibName])
|
|
lines.add("extern \"C\" {")
|
|
|
|
for p in procs:
|
|
var params: seq[string] = @[]
|
|
case p.kind
|
|
of FFIKind.FFI:
|
|
# Method-style: ctx first.
|
|
params.add("ctx: *mut c_void")
|
|
params.add("callback: FFICallback")
|
|
params.add("user_data: *mut c_void")
|
|
params.add("req_cbor: *const u8")
|
|
params.add("req_cbor_len: usize")
|
|
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")])
|
|
of FFIKind.CTOR:
|
|
# Ctor: no ctx; returns the freshly-allocated handle.
|
|
params.add("req_cbor: *const u8")
|
|
params.add("req_cbor_len: usize")
|
|
params.add("callback: FFICallback")
|
|
params.add("user_data: *mut c_void")
|
|
lines.add(" pub fn $1($2) -> *mut c_void;" % [p.procName, params.join(", ")])
|
|
of FFIKind.DTOR:
|
|
params.add("ctx: *mut c_void")
|
|
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")])
|
|
|
|
# Listener-registration ABI, always present in the dylib.
|
|
lines.add(
|
|
" pub fn $1_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;" %
|
|
[linkLibName]
|
|
)
|
|
lines.add(
|
|
" pub fn $1_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;" %
|
|
[linkLibName]
|
|
)
|
|
|
|
lines.add("}")
|
|
return lines.join("\n") & "\n"
|
|
|
|
proc generateTypesRs*(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): string =
|
|
## Generates types.rs: Rust structs for user FFI types and each per-proc Req.
|
|
var lines: seq[string] = @[]
|
|
lines.add("use serde::{Deserialize, Serialize};")
|
|
lines.add("")
|
|
|
|
for t in types:
|
|
lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]")
|
|
lines.add("pub struct $1 {" % [t.name])
|
|
for f in t.fields:
|
|
let snakeName = camelToSnakeCase(f.name)
|
|
let rustType = nimTypeToRust(f.typeName)
|
|
# serde rename when camelCase differs from snake_case.
|
|
if snakeName != f.name:
|
|
lines.add(" #[serde(rename = \"$1\")]" % [f.name])
|
|
lines.add(" pub $1: $2," % [snakeName, rustType])
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
# Per-proc Req structs: the unit of CBOR encoding sent across the boundary.
|
|
for p in procs:
|
|
if p.kind == FFIKind.DTOR:
|
|
continue
|
|
let reqName = reqStructName(p)
|
|
lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]")
|
|
if p.extraParams.len == 0:
|
|
lines.add("pub struct $1 {}" % [reqName])
|
|
else:
|
|
lines.add("pub struct $1 {" % [reqName])
|
|
for ep in p.extraParams:
|
|
let snake = camelToSnakeCase(ep.name)
|
|
let rustType =
|
|
if ep.ridesAsPtr():
|
|
RustPtrType
|
|
else:
|
|
nimTypeToRust(ep.typeName)
|
|
if snake != ep.name:
|
|
lines.add(" #[serde(rename = \"$1\")]" % [ep.name])
|
|
lines.add(" pub $1: $2," % [snake, rustType])
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
return lines.join("\n")
|
|
|
|
proc generateApiRs*(
|
|
procs: seq[FFIProcMeta], libName: string, events: seq[FFIEventMeta] = @[]
|
|
): string =
|
|
## Generates api.rs with a blocking and a tokio-async high-level API.
|
|
## Requests/responses are CBOR (ciborium); errors are raw UTF-8 strings.
|
|
var lines: seq[string] = @[]
|
|
|
|
var ctors: seq[FFIProcMeta] = @[]
|
|
var methods: seq[FFIProcMeta] = @[]
|
|
var dtorProcName = ""
|
|
for p in procs:
|
|
case p.kind
|
|
of FFIKind.CTOR:
|
|
ctors.add(p)
|
|
of FFIKind.FFI:
|
|
methods.add(p)
|
|
of FFIKind.DTOR:
|
|
if dtorProcName.len == 0:
|
|
dtorProcName = p.procName
|
|
|
|
var libTypeName = ""
|
|
if ctors.len > 0:
|
|
libTypeName = ctors[0].libTypeName
|
|
else:
|
|
libTypeName = capitalizeFirstLetter(libName)
|
|
|
|
let ctxTypeName = libTypeName & "Ctx"
|
|
|
|
lines.add("use std::os::raw::{c_char, c_int, c_void};")
|
|
lines.add("use std::slice;")
|
|
lines.add("use std::time::Duration;")
|
|
lines.add("use serde::de::DeserializeOwned;")
|
|
lines.add("use serde::Serialize;")
|
|
lines.add("use super::ffi;")
|
|
lines.add("use super::types::*;")
|
|
lines.add("")
|
|
|
|
lines.add("fn encode_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {")
|
|
lines.add(" let mut buf = Vec::new();")
|
|
lines.add(
|
|
" ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;"
|
|
)
|
|
lines.add(" Ok(buf)")
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add("fn decode_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {")
|
|
lines.add(" ciborium::de::from_reader(bytes).map_err(|e| e.to_string())")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
# FFI trampoline: user_data owns a Box<flume::Sender>; a late callback sends into a closed receiver, which is harmless.
|
|
lines.add("type FFIResult = Result<Vec<u8>, String>;")
|
|
lines.add("type FFISender = flume::Sender<FFIResult>;")
|
|
lines.add("")
|
|
lines.add("// Reconstruct the (ret, msg, len) tuple delivered by the C callback")
|
|
lines.add(
|
|
"// into a Result<Vec<u8>, String>: payload on success, UTF-8 message on error."
|
|
)
|
|
lines.add(
|
|
"// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the"
|
|
)
|
|
lines.add(
|
|
"// alternative would be to dispatch a separate Err for invalid UTF-8, but the"
|
|
)
|
|
lines.add("// codegen contract is that Nim handlers emit `string` error payloads, so")
|
|
lines.add("// invalid UTF-8 here would be a Nim-side bug.")
|
|
lines.add(
|
|
"unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult {"
|
|
)
|
|
lines.add(" let bytes = if msg.is_null() || len == 0 {")
|
|
lines.add(" Vec::new()")
|
|
lines.add(" } else {")
|
|
lines.add(" slice::from_raw_parts(msg as *const u8, len).to_vec()")
|
|
lines.add(" };")
|
|
lines.add(" if ret == NIMFFI_RET_OK { Ok(bytes) }")
|
|
lines.add(" else { Err(String::from_utf8_lossy(&bytes).into_owned()) }")
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add("// nim-ffi result-callback status codes (mirror ffi/ffi_types.nim).")
|
|
lines.add("const NIMFFI_RET_OK: c_int = 0;")
|
|
lines.add("const NIMFFI_RET_MISSING_CALLBACK: c_int = 2;")
|
|
lines.add("const NIMFFI_RET_STALE_WARN: c_int = 3;")
|
|
lines.add("")
|
|
lines.add("unsafe extern \"C\" fn on_result(")
|
|
lines.add(" ret: c_int,")
|
|
lines.add(" msg: *const c_char,")
|
|
lines.add(" len: usize,")
|
|
lines.add(" user_data: *mut c_void,")
|
|
lines.add(") {")
|
|
lines.add(
|
|
" // NIMFFI_RET_STALE_WARN (3) is a non-terminal progress ping: the request"
|
|
)
|
|
lines.add(
|
|
" // is still running. This wrapper only delivers the final result, so ignore"
|
|
)
|
|
lines.add(
|
|
" // it WITHOUT reclaiming the box — a terminal callback still owns the Sender."
|
|
)
|
|
lines.add(" if ret == NIMFFI_RET_STALE_WARN { return; }")
|
|
lines.add("")
|
|
lines.add(" // Take ownership of the boxed Sender — dropping it at end of scope")
|
|
lines.add(" // releases the only outstanding handle.")
|
|
lines.add(" let tx = Box::from_raw(user_data as *mut FFISender);")
|
|
lines.add("")
|
|
lines.add(
|
|
" // `tx.send` returns Err only if the awaiting future was dropped (and with it"
|
|
)
|
|
lines.add(
|
|
" // the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch"
|
|
)
|
|
lines.add(
|
|
" // lost the race, or the future was dropped before being awaited. This cannot"
|
|
)
|
|
lines.add(
|
|
" // happen with the current rust_client demo but may occur in arbitrary"
|
|
)
|
|
lines.add(" // downstream consumers, so we discard the Err safely.")
|
|
lines.add(
|
|
" // Given that this is invoked from a Nim thread, we can't propagate the error by panicking or"
|
|
)
|
|
lines.add(
|
|
" // returning a Result. Furthermore, an API dev may intentionally set a timeout in the await,"
|
|
)
|
|
lines.add(
|
|
" // in which case is also fine to discard the send error in this case because the API user will"
|
|
)
|
|
lines.add(" // handle the timeout expiry in their own code.")
|
|
lines.add(
|
|
" // The important part is to ensure that the callback doesn't panic or block indefinitely if the"
|
|
)
|
|
lines.add(" // receiver is gone.")
|
|
lines.add(" let _ = tx.send(ffi_payload(ret, msg, len));")
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add("fn ffi_call_sync<F>(timeout: Duration, f: F) -> FFIResult")
|
|
lines.add("where")
|
|
lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,")
|
|
lines.add("{")
|
|
lines.add(" let (tx, rx) = flume::bounded::<FFIResult>(1);")
|
|
lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;")
|
|
lines.add(" let ret = f(on_result, raw);")
|
|
lines.add(" if ret == NIMFFI_RET_MISSING_CALLBACK {")
|
|
lines.add(" // Callback will never fire; reclaim the box to avoid a leak.")
|
|
lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });")
|
|
lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());")
|
|
lines.add(" }")
|
|
lines.add(" match rx.recv_timeout(timeout) {")
|
|
lines.add(" Ok(payload) => payload,")
|
|
lines.add(" Err(flume::RecvTimeoutError::Timeout) =>")
|
|
lines.add(" Err(format!(\"timed out after {:?}\", timeout)),")
|
|
lines.add(" Err(flume::RecvTimeoutError::Disconnected) =>")
|
|
lines.add(
|
|
" Err(\"callback channel disconnected before delivery\".into()),"
|
|
)
|
|
lines.add(" }")
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add("async fn ffi_call_async<F>(timeout: Duration, f: F) -> FFIResult")
|
|
lines.add("where")
|
|
lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,")
|
|
lines.add("{")
|
|
lines.add(" let (tx, rx) = flume::bounded::<FFIResult>(1);")
|
|
lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;")
|
|
lines.add(" let ret = f(on_result, raw);")
|
|
lines.add(" if ret == NIMFFI_RET_MISSING_CALLBACK {")
|
|
lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });")
|
|
lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());")
|
|
lines.add(" }")
|
|
lines.add(" match tokio::time::timeout(timeout, rx.recv_async()).await {")
|
|
lines.add(" Ok(Ok(payload)) => payload,")
|
|
lines.add(
|
|
" Ok(Err(_)) => Err(\"callback channel disconnected before delivery\".into()),"
|
|
)
|
|
lines.add(" Err(_) => Err(format!(\"timed out after {:?}\", timeout)),")
|
|
lines.add(" }")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
# Per-listener handler boxes + extern "C" trampolines: the Box is kept alive in `listeners`, its raw pointer is the per-event `user_data`.
|
|
if events.len > 0:
|
|
for ev in events:
|
|
let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler"
|
|
let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline"
|
|
lines.add("struct $1 {" % [handlerStruct])
|
|
lines.add(" f: Box<dyn Fn(&$1) + Send + Sync>," % [ev.payloadTypeName])
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add("unsafe extern \"C\" fn $1(" % [trampolineName])
|
|
lines.add(" ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,")
|
|
lines.add(") {")
|
|
lines.add(" if ud.is_null() || ret != 0 || msg.is_null() || len == 0 {")
|
|
lines.add(" return;")
|
|
lines.add(" }")
|
|
lines.add(" let h = &*(ud as *const $1);" % [handlerStruct])
|
|
lines.add(" let bytes = slice::from_raw_parts(msg as *const u8, len);")
|
|
lines.add(" #[derive(serde::Deserialize)]")
|
|
lines.add(" struct Envelope { payload: $1 }" % [ev.payloadTypeName])
|
|
lines.add(
|
|
" if let Ok(env) = ciborium::de::from_reader::<Envelope, _>(bytes) {"
|
|
)
|
|
lines.add(" (h.f)(&env.payload);")
|
|
lines.add(" }")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
# Public handle returned by every add_…_listener call.
|
|
lines.add("#[derive(Debug, Clone, Copy)]")
|
|
lines.add("pub struct ListenerHandle { pub id: u64 }")
|
|
lines.add("")
|
|
|
|
lines.add("/// High-level context for `$1`." % [libTypeName])
|
|
lines.add("pub struct $1 {" % [ctxTypeName])
|
|
lines.add(" ptr: *mut c_void,")
|
|
lines.add(" timeout: Duration,")
|
|
if events.len > 0:
|
|
# Keeps each handler box alive while its listener id is live on the Nim side.
|
|
lines.add(
|
|
" listeners: std::sync::Mutex<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,"
|
|
)
|
|
lines.add("}")
|
|
lines.add("")
|
|
# SAFETY block applies to both impls below.
|
|
lines.add(
|
|
"// SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime."
|
|
)
|
|
lines.add("// Every call through the generated FFI proc goes through")
|
|
lines.add(
|
|
"// `sendRequestToFFIThread` on the Nim side, which only enqueues the request"
|
|
)
|
|
lines.add("// onto a mutex-guarded MPSC queue (sound from any number of threads) and")
|
|
lines.add(
|
|
"// wakes the single FFI thread that dispatches every handler. The context is"
|
|
)
|
|
lines.add(
|
|
"// thus never mutated non-atomically from the caller's thread. The Nim-side"
|
|
)
|
|
lines.add("// reentrancy guard (`onFFIThread` threadvar) prevents handlers from")
|
|
lines.add("// re-entering the dispatcher. These invariants make it sound to mark the")
|
|
lines.add("// wrapper as Send + Sync.")
|
|
lines.add("unsafe impl Send for $1 {}" % [ctxTypeName])
|
|
lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName])
|
|
lines.add("")
|
|
|
|
# Drop tears down the Nim runtime when the ctx goes out of scope; without it, forgetting the ctx leaks the entire runtime (FFI thread, watchdog, chronos).
|
|
if dtorProcName.len > 0:
|
|
lines.add("impl Drop for $1 {" % [ctxTypeName])
|
|
lines.add(" fn drop(&mut self) {")
|
|
lines.add(" if !self.ptr.is_null() {")
|
|
lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName])
|
|
lines.add(" self.ptr = std::ptr::null_mut();")
|
|
lines.add(" }")
|
|
# `listeners` drops after this body; the dylib has joined its threads by then, so no callback is mid-flight against the raw pointers we handed it.
|
|
lines.add(" }")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
lines.add("impl $1 {" % [ctxTypeName])
|
|
|
|
for ctor in ctors:
|
|
let reqName = reqStructName(ctor)
|
|
var paramsList: seq[string] = @[]
|
|
var fieldInits: seq[string] = @[]
|
|
for ep in ctor.extraParams:
|
|
let snake = camelToSnakeCase(ep.name)
|
|
let rustType =
|
|
if ep.ridesAsPtr():
|
|
RustPtrType
|
|
else:
|
|
nimTypeToRust(ep.typeName)
|
|
paramsList.add("$1: $2" % [snake, rustType])
|
|
fieldInits.add(snake)
|
|
# `create` and `new_async` take an explicit `timeout: Duration` that flows into `self.timeout` so subsequent method calls inherit it.
|
|
let ctorParamsStr =
|
|
if paramsList.len > 0:
|
|
paramsList.join(", ") & ", timeout: Duration"
|
|
else:
|
|
"timeout: Duration"
|
|
|
|
let reqLit =
|
|
if fieldInits.len > 0:
|
|
reqName & " { " & fieldInits.join(", ") & " }"
|
|
else:
|
|
reqName & " {}"
|
|
|
|
lines.add(" pub fn create($1) -> Result<Self, String> {" % [ctorParamsStr])
|
|
lines.add(" let req = $1;" % [reqLit])
|
|
lines.add(" let req_bytes = encode_cbor(&req)?;")
|
|
# Ctor also fires the callback carrying the payload, so discard the synchronous *mut c_void and yield RET_OK to wait on the callback.
|
|
lines.add(" let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe {")
|
|
lines.add(
|
|
" let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" %
|
|
[ctor.procName]
|
|
)
|
|
lines.add(" 0")
|
|
lines.add(" })?;")
|
|
# Ctor success payload is a CBOR text string holding the ctx address.
|
|
lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;")
|
|
lines.add(
|
|
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;"
|
|
)
|
|
if events.len > 0:
|
|
lines.add(
|
|
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })"
|
|
)
|
|
else:
|
|
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
lines.add(
|
|
" pub async fn new_async($1) -> Result<Self, String> {" % [ctorParamsStr]
|
|
)
|
|
lines.add(" let req = $1;" % [reqLit])
|
|
lines.add(" let req_bytes = encode_cbor(&req)?;")
|
|
# See `create`: discard the ctor's synchronous return; the callback delivers the ctx address.
|
|
lines.add(" let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe {")
|
|
lines.add(
|
|
" let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" %
|
|
[ctor.procName]
|
|
)
|
|
lines.add(" 0")
|
|
lines.add(" }).await?;")
|
|
lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;")
|
|
lines.add(
|
|
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;"
|
|
)
|
|
if events.len > 0:
|
|
lines.add(
|
|
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })"
|
|
)
|
|
else:
|
|
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
if events.len > 0:
|
|
# Shared by every public `add_*_listener`: caller owns the concrete-typed box, erased to `dyn Any + Send` only on hand-off.
|
|
lines.add(" fn add_listener_inner(")
|
|
lines.add(" &self,")
|
|
lines.add(" event_name: *const c_char,")
|
|
lines.add(" callback: ffi::FFICallback,")
|
|
lines.add(" raw: *mut c_void,")
|
|
lines.add(" owned: Box<dyn std::any::Any + Send>,")
|
|
lines.add(" ) -> ListenerHandle {")
|
|
lines.add(" let id = unsafe {")
|
|
lines.add(
|
|
" ffi::$1_add_event_listener(self.ptr, event_name, callback, raw)" %
|
|
[libName]
|
|
)
|
|
lines.add(" };")
|
|
lines.add(" if id != 0 {")
|
|
lines.add(" self.listeners.lock().unwrap().insert(id, owned);")
|
|
lines.add(" }")
|
|
lines.add(" ListenerHandle { id }")
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
for ev in events:
|
|
let methodName = "add_" & camelToSnakeCase(ev.nimProcName) & "_listener"
|
|
let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler"
|
|
let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline"
|
|
lines.add(
|
|
" /// Register a typed listener for `$1`. The returned handle can be" %
|
|
[ev.wireName]
|
|
)
|
|
lines.add(" /// passed to `remove_event_listener` to unregister.")
|
|
lines.add(" pub fn $1<F>(&self, handler: F) -> ListenerHandle" % [methodName])
|
|
lines.add(" where F: Fn(&$1) + Send + Sync + 'static," % [ev.payloadTypeName])
|
|
lines.add(" {")
|
|
lines.add(
|
|
" let owned: Box<$1> = Box::new($1 { f: Box::new(handler) });" %
|
|
[handlerStruct]
|
|
)
|
|
lines.add(
|
|
" let raw = &*owned as *const $1 as *mut c_void;" % [handlerStruct]
|
|
)
|
|
lines.add(
|
|
" self.add_listener_inner(b\"$1\\0\".as_ptr() as *const c_char, $2, raw, owned)" %
|
|
[ev.wireName, trampolineName]
|
|
)
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
# Remove by handle; drops the Box after the C ABI confirms unregistration.
|
|
lines.add(" /// Remove a previously-registered listener by handle. Returns true")
|
|
lines.add(" /// if the listener existed and was removed; false otherwise.")
|
|
lines.add(
|
|
" pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {"
|
|
)
|
|
lines.add(" if handle.id == 0 { return false; }")
|
|
lines.add(" let rc = unsafe {")
|
|
lines.add(
|
|
" ffi::$1_remove_event_listener(self.ptr, handle.id)" % [libName]
|
|
)
|
|
lines.add(" };")
|
|
lines.add(" self.listeners.lock().unwrap().remove(&handle.id);")
|
|
lines.add(" rc == 0")
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
for m in methods:
|
|
let methodName = stripLibPrefix(m.procName, libName)
|
|
let retRustType = nimTypeToRust(m.returnTypeName)
|
|
let reqName = reqStructName(m)
|
|
|
|
var paramsList: seq[string] = @[]
|
|
var fieldInits: seq[string] = @[]
|
|
for ep in m.extraParams:
|
|
let snake = camelToSnakeCase(ep.name)
|
|
let rustType =
|
|
if ep.ridesAsPtr():
|
|
RustPtrType
|
|
else:
|
|
nimTypeToRust(ep.typeName)
|
|
paramsList.add("$1: $2" % [snake, rustType])
|
|
fieldInits.add(snake)
|
|
let paramsStr =
|
|
if paramsList.len > 0:
|
|
", " & paramsList.join(", ")
|
|
else:
|
|
""
|
|
|
|
let reqLit =
|
|
if fieldInits.len > 0:
|
|
reqName & " { " & fieldInits.join(", ") & " }"
|
|
else:
|
|
reqName & " {}"
|
|
|
|
let retTypeForApi = if m.returnRidesAsPtr(): RustPtrType else: retRustType
|
|
|
|
lines.add(
|
|
" pub fn $1(&self$2) -> Result<$3, String> {" %
|
|
[methodName, paramsStr, retTypeForApi]
|
|
)
|
|
lines.add(" let req = $1;" % [reqLit])
|
|
lines.add(" let req_bytes = encode_cbor(&req)?;")
|
|
lines.add(" let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {")
|
|
lines.add(
|
|
" ffi::$1(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
|
|
[m.procName]
|
|
)
|
|
lines.add(" })?;")
|
|
lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi])
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
# async method: ptr cast to usize (Copy + Send) keeps the move closure and returned future Send for multi-threaded tokio runtimes.
|
|
lines.add(
|
|
" pub async fn $1_async(&self$2) -> Result<$3, String> {" %
|
|
[methodName, paramsStr, retTypeForApi]
|
|
)
|
|
lines.add(" let req = $1;" % [reqLit])
|
|
lines.add(" let req_bytes = encode_cbor(&req)?;")
|
|
lines.add(" let ptr = self.ptr as usize;")
|
|
lines.add(
|
|
" let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {"
|
|
)
|
|
lines.add(
|
|
" ffi::$1(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
|
|
[m.procName]
|
|
)
|
|
lines.add(" }).await?;")
|
|
lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi])
|
|
lines.add(" }")
|
|
lines.add("")
|
|
|
|
lines.add("}")
|
|
return lines.join("\n") & "\n"
|
|
|
|
proc generateRustCrate*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
outputDir: string,
|
|
nimSrcRelPath: string,
|
|
events: seq[FFIEventMeta] = @[],
|
|
) =
|
|
## Generates a complete Rust crate in outputDir.
|
|
createDir(outputDir)
|
|
createDir(outputDir / "src")
|
|
|
|
writeFile(outputDir / "Cargo.toml", generateCargoToml(libName))
|
|
writeFile(outputDir / "build.rs", generateBuildRs(libName, nimSrcRelPath))
|
|
writeFile(outputDir / "src" / "lib.rs", generateLibRs())
|
|
writeFile(outputDir / "src" / "ffi.rs", generateFFIRs(procs))
|
|
writeFile(outputDir / "src" / "types.rs", generateTypesRs(types, procs))
|
|
writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName, events))
|