mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-05-06 02:09:30 +00:00
simplify ffi generation and add simple Rust example
This commit is contained in:
parent
e3eca63236
commit
26bf173e2c
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,3 +1,20 @@
|
||||
nimble.develop
|
||||
nimble.paths
|
||||
nimbledeps
|
||||
|
||||
# Nim compiler output
|
||||
*.dylib
|
||||
*.so
|
||||
*.dll
|
||||
tests/test_alloc
|
||||
tests/test_ffi_context
|
||||
tests/test_serial
|
||||
|
||||
# Generated binding crates (regenerated by `nimble genbindings_*`)
|
||||
examples/**/nim_bindings/
|
||||
|
||||
# Cargo build artifacts
|
||||
examples/**/rust_client/target/
|
||||
|
||||
# Development plan (local only)
|
||||
PLAN.md
|
||||
|
||||
50
examples/nim_timer/nim_timer.nim
Normal file
50
examples/nim_timer/nim_timer.nim
Normal file
@ -0,0 +1,50 @@
|
||||
import ffi, chronos
|
||||
|
||||
declareLibrary("nimtimer")
|
||||
|
||||
# The library's main state type. The FFI context owns one instance.
|
||||
type NimTimer = object
|
||||
name: string # set at creation time, read back in each response
|
||||
|
||||
ffiType:
|
||||
type TimerConfig = object
|
||||
name: string
|
||||
|
||||
ffiType:
|
||||
type EchoRequest = object
|
||||
message: string
|
||||
delayMs: int # how long chronos sleeps before replying
|
||||
|
||||
ffiType:
|
||||
type EchoResponse = object
|
||||
echoed: string
|
||||
timerName: string # proves that the timer's own state is accessible
|
||||
|
||||
# --- Constructor -----------------------------------------------------------
|
||||
# Called once from Rust. Creates the FFIContext + NimTimer.
|
||||
# Uses chronos (await sleepAsync) so the body is async.
|
||||
proc nimtimer_create*(
|
||||
config: TimerConfig
|
||||
): Future[Result[NimTimer, string]] {.ffiCtor.} =
|
||||
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
|
||||
return ok(NimTimer(name: config.name))
|
||||
|
||||
# --- Async method ----------------------------------------------------------
|
||||
# Waits `delayMs` milliseconds (non-blocking, on the chronos event loop)
|
||||
# then echoes the message back with a request counter.
|
||||
proc nimtimer_echo*(
|
||||
timer: NimTimer, req: EchoRequest
|
||||
): Future[Result[EchoResponse, string]] {.ffi.} =
|
||||
await sleepAsync(req.delayMs.milliseconds)
|
||||
return ok(EchoResponse(echoed: req.message, timerName: timer.name))
|
||||
|
||||
# --- Sync method -----------------------------------------------------------
|
||||
# No await — the macro detects this and fires the callback inline,
|
||||
# without going through the request channel.
|
||||
proc nimtimer_version*(
|
||||
timer: NimTimer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("nim-timer v0.1.0")
|
||||
|
||||
when defined(ffiGenBindings):
|
||||
genBindings("rust", "examples/nim_timer/nim_bindings", "../nim_timer.nim")
|
||||
115
examples/nim_timer/rust_client/Cargo.lock
generated
Normal file
115
examples/nim_timer/rust_client/Cargo.lock
generated
Normal file
@ -0,0 +1,115 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nimtimer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nimtimer",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
8
examples/nim_timer/rust_client/Cargo.toml
Normal file
8
examples/nim_timer/rust_client/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "rust_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nimtimer = { path = "../nim_bindings" }
|
||||
serde_json = "1"
|
||||
47
examples/nim_timer/rust_client/src/main.rs
Normal file
47
examples/nim_timer/rust_client/src/main.rs
Normal file
@ -0,0 +1,47 @@
|
||||
// Rust client for the nim_timer shared library built with nim-ffi + chronos.
|
||||
//
|
||||
// This file uses the generated `nimtimer` crate, which wraps all the raw FFI
|
||||
// boilerplate (extern "C" declarations, callback machinery, JSON encode/decode).
|
||||
//
|
||||
// To regenerate the `nim_bindings` crate:
|
||||
// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libnimtimer \
|
||||
// -d:ffiGenBindings examples/nim_timer/nim_timer.nim
|
||||
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
// ── 1. Create the timer service ────────────────────────────────────────
|
||||
let ctx = NimTimerCtx::create(TimerConfig { name: "demo".into() }, timeout)
|
||||
.expect("nimtimer_create failed");
|
||||
println!("[1] Context created");
|
||||
|
||||
// ── 2. Sync call: version ──────────────────────────────────────────────
|
||||
let version = ctx.version().expect("nimtimer_version failed");
|
||||
println!("[2] Version (sync call, callback fired inline): {version}");
|
||||
|
||||
// ── 3. Async call: echo (200 ms delay) ────────────────────────────────
|
||||
let echo = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "hello from Rust".into(),
|
||||
delay_ms: 200,
|
||||
})
|
||||
.expect("nimtimer_echo failed");
|
||||
println!(
|
||||
"[3] Echo (async, 200 ms chronos delay): echoed={}, timerName={}",
|
||||
echo.echoed, echo.timer_name
|
||||
);
|
||||
|
||||
// ── 4. A second echo ──────────────────────────────────────────────────
|
||||
let echo2 = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "second request".into(),
|
||||
delay_ms: 50,
|
||||
})
|
||||
.expect("second nimtimer_echo failed");
|
||||
println!("[4] Echo: echoed={}, timerName={}", echo2.echoed, echo2.timer_name);
|
||||
|
||||
println!("\nDone. The Nim FFI thread and watchdog are still running.");
|
||||
println!("(In a real app, call nimtimer_destroy to join them gracefully.)");
|
||||
}
|
||||
4
ffi.nim
4
ffi.nim
@ -2,9 +2,9 @@ import std/[atomics, tables]
|
||||
import chronos, chronicles
|
||||
import
|
||||
ffi/internal/[ffi_library, ffi_macro],
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_thread_request]
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_thread_request, serial]
|
||||
|
||||
export atomics, tables
|
||||
export chronos, chronicles
|
||||
export
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request, serial
|
||||
|
||||
@ -19,6 +19,7 @@ task buildffi, "Compile the library":
|
||||
|
||||
task test, "Run all tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_alloc.nim"
|
||||
exec "nim c -r " & nimFlags & " tests/test_serial.nim"
|
||||
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
|
||||
|
||||
task test_alloc, "Run alloc unit tests":
|
||||
@ -26,3 +27,9 @@ task test_alloc, "Run alloc unit tests":
|
||||
|
||||
task test_ffi, "Run FFI context integration tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
|
||||
|
||||
task test_serial, "Run serial unit tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_serial.nim"
|
||||
|
||||
task genbindings_example, "Generate Rust bindings for the nim_timer example":
|
||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
|
||||
35
ffi/codegen/meta.nim
Normal file
35
ffi/codegen/meta.nim
Normal file
@ -0,0 +1,35 @@
|
||||
## Compile-time metadata types for FFI binding generation.
|
||||
## Populated by the {.ffiCtor.} and {.ffi.} macros and consumed by codegen.
|
||||
|
||||
type
|
||||
FFIParamMeta* = object
|
||||
name*: string # Nim param name, e.g. "req"
|
||||
typeName*: string # Nim type name, e.g. "EchoRequest"
|
||||
isPtr*: bool # true if the type is `ptr T`
|
||||
|
||||
FFIProcKind* = enum
|
||||
ffiCtorKind
|
||||
ffiFfiKind
|
||||
|
||||
FFIProcMeta* = object
|
||||
procName*: string # e.g. "nimtimer_echo"
|
||||
libName*: string # library name, e.g. "nimtimer"
|
||||
kind*: FFIProcKind
|
||||
libTypeName*: string # e.g. "NimTimer"
|
||||
extraParams*: seq[FFIParamMeta] # all params except the lib param
|
||||
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
|
||||
returnIsPtr*: bool # true if return type is ptr T
|
||||
isAsync*: bool
|
||||
|
||||
FFIFieldMeta* = object
|
||||
name*: string # e.g. "delayMs"
|
||||
typeName*: string # e.g. "int"
|
||||
|
||||
FFITypeMeta* = object
|
||||
name*: string
|
||||
fields*: seq[FFIFieldMeta]
|
||||
|
||||
# Compile-time registries populated by the macros
|
||||
var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
|
||||
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
||||
var currentLibName* {.compileTime.}: string
|
||||
417
ffi/codegen/rust.nim
Normal file
417
ffi/codegen/rust.nim
Normal file
@ -0,0 +1,417 @@
|
||||
## Rust binding generator for the nim-ffi framework.
|
||||
## Generates a complete Rust crate from compile-time FFI metadata.
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Name conversion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc toSnakeCase*(s: string): string =
|
||||
## Converts camelCase to snake_case.
|
||||
## e.g. "delayMs" → "delay_ms", "timerName" → "timer_name"
|
||||
result = ""
|
||||
for i, c in s:
|
||||
if c.isUpperAscii() and i > 0:
|
||||
result.add('_')
|
||||
result.add(c.toLowerAscii())
|
||||
|
||||
proc toPascalCase*(s: string): string =
|
||||
## Converts the first letter to uppercase.
|
||||
if s.len == 0: return s
|
||||
result = s
|
||||
result[0] = s[0].toUpperAscii()
|
||||
|
||||
proc nimTypeToRust*(typeName: string): string =
|
||||
## Maps Nim type names to Rust type names.
|
||||
case typeName
|
||||
of "string", "cstring": "String"
|
||||
of "int", "int64": "i64"
|
||||
of "int32": "i32"
|
||||
of "bool": "bool"
|
||||
of "float", "float64": "f64"
|
||||
of "pointer": "usize"
|
||||
else: toPascalCase(typeName)
|
||||
|
||||
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
||||
## Extracts the common prefix before the first `_` from proc names.
|
||||
## e.g. ["nimtimer_create", "nimtimer_echo"] → "nimtimer"
|
||||
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 from a proc name.
|
||||
## e.g. "nimtimer_echo", "nimtimer" → "echo"
|
||||
let prefix = libName & "_"
|
||||
if procName.startsWith(prefix):
|
||||
return procName[prefix.len .. ^1]
|
||||
return procName
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File generators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc generateCargoToml*(libName: string): string =
|
||||
result = """[package]
|
||||
name = "$1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
""" % [libName]
|
||||
|
||||
proc generateBuildRs*(libName: string, nimSrcRelPath: string): string =
|
||||
## Generates build.rs that compiles the Nim library.
|
||||
## nimSrcRelPath is relative to the output (crate) directory.
|
||||
let escapedSrc = nimSrcRelPath.replace("\\", "\\\\")
|
||||
result = """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 =
|
||||
result = """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 for all procs.
|
||||
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("")
|
||||
|
||||
# Collect unique lib names for #[link(...)]
|
||||
var libNames: seq[string] = @[]
|
||||
for p in procs:
|
||||
if p.libName notin libNames:
|
||||
libNames.add(p.libName)
|
||||
|
||||
# Derive lib name from proc names if not set
|
||||
var linkLibName = ""
|
||||
if libNames.len > 0 and libNames[0].len > 0:
|
||||
linkLibName = libNames[0]
|
||||
else:
|
||||
# derive from first proc name
|
||||
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] = @[]
|
||||
if p.kind == ffiFfiKind:
|
||||
# Method: ctx comes first
|
||||
params.add("ctx: *mut c_void")
|
||||
params.add("callback: FfiCallback")
|
||||
params.add("user_data: *mut c_void")
|
||||
for ep in p.extraParams:
|
||||
params.add("$1_json: *const c_char" % [toSnakeCase(ep.name)])
|
||||
else:
|
||||
# Constructor: no ctx
|
||||
for ep in p.extraParams:
|
||||
params.add("$1_json: *const c_char" % [toSnakeCase(ep.name)])
|
||||
params.add("callback: FfiCallback")
|
||||
params.add("user_data: *mut c_void")
|
||||
|
||||
let paramStr = params.join(", ")
|
||||
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, paramStr])
|
||||
|
||||
lines.add("}")
|
||||
result = lines.join("\n") & "\n"
|
||||
|
||||
proc generateTypesRs*(types: seq[FFITypeMeta]): string =
|
||||
## Generates types.rs with Rust structs for all FFI types.
|
||||
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 = toSnakeCase(f.name)
|
||||
let rustType = nimTypeToRust(f.typeName)
|
||||
# Add serde rename if camelCase name 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("")
|
||||
|
||||
result = lines.join("\n")
|
||||
|
||||
proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
||||
## Generates api.rs with the high-level Rust API.
|
||||
var lines: seq[string] = @[]
|
||||
|
||||
# Find ctor and method procs
|
||||
var ctors: seq[FFIProcMeta] = @[]
|
||||
var methods: seq[FFIProcMeta] = @[]
|
||||
for p in procs:
|
||||
if p.kind == ffiCtorKind:
|
||||
ctors.add(p)
|
||||
else:
|
||||
methods.add(p)
|
||||
|
||||
# Derive the lib type name from ctor or from libName
|
||||
var libTypeName = ""
|
||||
if ctors.len > 0:
|
||||
libTypeName = ctors[0].libTypeName
|
||||
else:
|
||||
# Fallback: PascalCase of libName
|
||||
libTypeName = toPascalCase(libName)
|
||||
|
||||
let ctxTypeName = libTypeName & "Ctx"
|
||||
|
||||
# Imports
|
||||
lines.add("use std::ffi::{CStr, CString};")
|
||||
lines.add("use std::os::raw::{c_char, c_int, c_void};")
|
||||
lines.add("use std::sync::{Arc, Condvar, Mutex};")
|
||||
lines.add("use std::time::Duration;")
|
||||
lines.add("use super::ffi;")
|
||||
lines.add("use super::types::*;")
|
||||
lines.add("")
|
||||
|
||||
# FfiCallbackResult struct
|
||||
lines.add("#[derive(Default)]")
|
||||
lines.add("struct FfiCallbackResult {")
|
||||
lines.add(" payload: Option<Result<String, String>>,")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;")
|
||||
lines.add("")
|
||||
|
||||
# on_result callback
|
||||
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(" let pair = Arc::from_raw(user_data as *const (Mutex<FfiCallbackResult>, Condvar));")
|
||||
lines.add(" {")
|
||||
lines.add(" let (lock, cvar) = &*pair;")
|
||||
lines.add(" let mut state = lock.lock().unwrap();")
|
||||
lines.add(" state.payload = Some(if ret == 0 {")
|
||||
lines.add(" Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" } else {")
|
||||
lines.add(" Err(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" });")
|
||||
lines.add(" cvar.notify_one();")
|
||||
lines.add(" }")
|
||||
lines.add(" std::mem::forget(pair);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ffi_call helper
|
||||
lines.add("fn ffi_call<F>(timeout: Duration, f: F) -> Result<String, String>")
|
||||
lines.add("where")
|
||||
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
|
||||
lines.add("{")
|
||||
lines.add(" let pair: Pair = Arc::new((Mutex::new(FfiCallbackResult::default()), Condvar::new()));")
|
||||
lines.add(" let raw = Arc::into_raw(pair.clone()) as *mut c_void;")
|
||||
lines.add(" let ret = f(on_result, raw);")
|
||||
lines.add(" if ret == 2 {")
|
||||
lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());")
|
||||
lines.add(" }")
|
||||
lines.add(" let (lock, cvar) = &*pair;")
|
||||
lines.add(" let guard = lock.lock().unwrap();")
|
||||
lines.add(" let (guard, timed_out) = cvar")
|
||||
lines.add(" .wait_timeout_while(guard, timeout, |s| s.payload.is_none())")
|
||||
lines.add(" .unwrap();")
|
||||
lines.add(" if timed_out.timed_out() {")
|
||||
lines.add(" return Err(format!(\"timed out after {:?}\", timeout));")
|
||||
lines.add(" }")
|
||||
lines.add(" guard.payload.clone().unwrap()")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# Ctx struct
|
||||
lines.add("/// High-level context for `$1`." % [libTypeName])
|
||||
lines.add("pub struct $1 {" % [ctxTypeName])
|
||||
lines.add(" ptr: *mut c_void,")
|
||||
lines.add(" timeout: Duration,")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("unsafe impl Send for $1 {}" % [ctxTypeName])
|
||||
lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName])
|
||||
lines.add("")
|
||||
|
||||
# impl block
|
||||
lines.add("impl $1 {" % [ctxTypeName])
|
||||
|
||||
# Constructor method(s)
|
||||
for ctor in ctors:
|
||||
var paramsList: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
let rustType = nimTypeToRust(ep.typeName)
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
paramsList.add("$1: $2" % [snakeName, rustType])
|
||||
paramsList.add("timeout: Duration")
|
||||
let paramsStr = paramsList.join(", ")
|
||||
|
||||
lines.add(" pub fn create($1) -> Result<Self, String> {" % [paramsStr])
|
||||
|
||||
# Serialize extra params
|
||||
for ep in ctor.extraParams:
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
let rustType = nimTypeToRust(ep.typeName)
|
||||
if rustType == "String":
|
||||
# Primitive string — wrap it in JSON
|
||||
lines.add(" let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
||||
else:
|
||||
lines.add(" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
||||
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||
|
||||
# Build the ffi_call closure
|
||||
var ffiCallArgs: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
ffiCallArgs.add("$1_c.as_ptr()" % [snakeName])
|
||||
ffiCallArgs.add("cb")
|
||||
ffiCallArgs.add("ud")
|
||||
let ffiCallArgsStr = ffiCallArgs.join(", ")
|
||||
|
||||
lines.add(" let raw = ffi_call(timeout, |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr])
|
||||
lines.add(" })?;")
|
||||
lines.add(" // ctor returns the context address as a plain decimal string")
|
||||
lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;")
|
||||
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# Method implementations
|
||||
for m in methods:
|
||||
let methodName = stripLibPrefix(m.procName, libName)
|
||||
let retRustType = nimTypeToRust(m.returnTypeName)
|
||||
|
||||
var paramsList: seq[string] = @[]
|
||||
for ep in m.extraParams:
|
||||
let rustType = nimTypeToRust(ep.typeName)
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
paramsList.add("$1: $2" % [snakeName, rustType])
|
||||
let paramsStr = if paramsList.len > 0: ", " & paramsList.join(", ") else: ""
|
||||
|
||||
lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType])
|
||||
|
||||
# Serialize extra params
|
||||
for ep in m.extraParams:
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
let rustType = nimTypeToRust(ep.typeName)
|
||||
if rustType == "String":
|
||||
lines.add(" let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
||||
else:
|
||||
lines.add(" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
||||
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||
|
||||
# Build ffi call args: ctx first, then callback/ud, then json args
|
||||
var ffiArgs: seq[string] = @["self.ptr", "cb", "ud"]
|
||||
for ep in m.extraParams:
|
||||
let snakeName = toSnakeCase(ep.name)
|
||||
ffiArgs.add("$1_c.as_ptr()" % [snakeName])
|
||||
let ffiArgsStr = ffiArgs.join(", ")
|
||||
|
||||
lines.add(" let raw = ffi_call(self.timeout, |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [m.procName, ffiArgsStr])
|
||||
lines.add(" })?;")
|
||||
|
||||
# Deserialize return value
|
||||
if retRustType == "String":
|
||||
lines.add(" serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())")
|
||||
elif retRustType == "usize":
|
||||
lines.add(" raw.parse::<usize>().map_err(|e| e.to_string())")
|
||||
else:
|
||||
lines.add(" serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType])
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
lines.add("}")
|
||||
result = lines.join("\n") & "\n"
|
||||
|
||||
proc generateRustCrate*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
) =
|
||||
## 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))
|
||||
writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName))
|
||||
@ -197,7 +197,8 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
|
||||
chronicles.error "ffi thread could not receive a request"
|
||||
continue
|
||||
|
||||
ctx.myLib = addr ffiReqHandler
|
||||
if ctx.myLib.isNil():
|
||||
ctx.myLib = addr ffiReqHandler
|
||||
|
||||
## Handle the request
|
||||
asyncSpawn processRequest(request, ctx)
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import std/[macros, atomics], strformat, chronicles, chronos
|
||||
import ../codegen/meta
|
||||
|
||||
macro declareLibrary*(libraryName: static[string]): untyped =
|
||||
# Record the library name for binding generation
|
||||
currentLibName = libraryName
|
||||
|
||||
var res = newStmtList()
|
||||
|
||||
## Generate {.pragma: exported, exportc, cdecl, raises: [].}
|
||||
|
||||
@ -1,6 +1,39 @@
|
||||
import std/[macros, tables]
|
||||
import std/[macros, tables, strutils]
|
||||
import chronos
|
||||
import ../ffi_types
|
||||
import ../codegen/meta
|
||||
when defined(ffiGenBindings):
|
||||
import ../codegen/rust
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# String helpers used by multiple macros
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc capitalizeFirstLetter(s: string): string =
|
||||
## Returns `s` with the first character uppercased.
|
||||
if s.len == 0:
|
||||
return s
|
||||
result = s
|
||||
result[0] = s[0].toUpperAscii()
|
||||
|
||||
proc toCamelCase(s: string): string =
|
||||
## Converts snake_case or mixed identifiers to CamelCase for type names.
|
||||
## e.g. "testlib_create" -> "TestlibCreate"
|
||||
var parts = s.split('_')
|
||||
result = ""
|
||||
for p in parts:
|
||||
result.add capitalizeFirstLetter(p)
|
||||
|
||||
proc bodyHasAwait(n: NimNode): bool =
|
||||
## Returns true if the AST node `n` contains any `await` or `waitFor` call.
|
||||
if n.kind in {nnkCall, nnkCommand}:
|
||||
let callee = n[0]
|
||||
if callee.kind == nnkIdent and callee.strVal in ["await", "waitFor"]:
|
||||
return true
|
||||
for child in n:
|
||||
if bodyHasAwait(child):
|
||||
return true
|
||||
false
|
||||
|
||||
proc extractFieldsFromLambda(body: NimNode): seq[NimNode] =
|
||||
## Extracts the fields (params) from the given lambda body, when using the registerReqFFI macro.
|
||||
@ -254,7 +287,7 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode
|
||||
newStmtList(procNode.body)
|
||||
|
||||
let newBody = newStmtList()
|
||||
let reqIdent = ident("req")
|
||||
let reqIdent = genSym(nskLet, "req")
|
||||
|
||||
newBody.add quote do:
|
||||
let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request)
|
||||
@ -423,55 +456,35 @@ macro processReq*(
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
macro ffi*(prc: untyped): untyped =
|
||||
macro ffiRaw*(prc: untyped): untyped =
|
||||
## Defines an FFI-exported proc that registers a request handler to be executed
|
||||
## asynchronously in the FFI thread.
|
||||
##
|
||||
## {.ffi.} implicitly implies: ...Return[Future[Result[string, string]] {.async.}
|
||||
##
|
||||
## When using {.ffi.}, the first three parameters must be:
|
||||
##
|
||||
## This is the "raw" / legacy form of the macro where the developer writes
|
||||
## the ctx, callback, and userData parameters explicitly.
|
||||
##
|
||||
## {.ffiRaw.} implicitly implies: ...Return[Future[Result[string, string]] {.async.}
|
||||
##
|
||||
## When using {.ffiRaw.}, the first three parameters must be:
|
||||
## - ctx: ptr FFIContext[T] <-- T is the type that handles the FFI requests
|
||||
## - callback: FFICallBack
|
||||
## - userData: pointer
|
||||
## Then, additional parameters may be defined as needed, after these first three, always
|
||||
## considering that only no-GC'ed (or C-like) types are allowed.
|
||||
##
|
||||
##
|
||||
## e.g.:
|
||||
## proc waku_version(
|
||||
## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
|
||||
## ) {.ffi.} =
|
||||
## ) {.ffiRaw.} =
|
||||
## return ok(WakuNodeVersionString)
|
||||
##
|
||||
## e.g2.:
|
||||
## proc waku_start(
|
||||
## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
|
||||
## ) {.ffi.} =
|
||||
## (await startWaku(ctx[].myLib)).isOkOr:
|
||||
## error "START_NODE failed", error = error
|
||||
## return err("failed to start: " & $error)
|
||||
## return ok("")
|
||||
##
|
||||
## e.g3.:
|
||||
## proc waku_peer_exchange_request(
|
||||
## ctx: ptr FFIContext[Waku],
|
||||
## callback: FFICallBack,
|
||||
## userData: pointer,
|
||||
## numPeers: uint64,
|
||||
## ) {.ffi.} =
|
||||
## let numValidPeers = (await performPeerExchangeRequestTo(numPeers, ctx.myLib[])).valueOr:
|
||||
## error "waku_peer_exchange_request failed", error = error
|
||||
## return err("failed peer exchange: " & $error)
|
||||
## return ok($numValidPeers)
|
||||
##
|
||||
## In these examples, notice that ctx.myLib is of type "ptr Waku", being Waku main library type.
|
||||
##
|
||||
##
|
||||
|
||||
let procName = prc[0]
|
||||
let formalParams = prc[3]
|
||||
let bodyNode = prc[^1]
|
||||
|
||||
if formalParams.len < 2:
|
||||
error("`.ffi.` procs require at least 1 parameter")
|
||||
error("`.ffiRaw.` procs require at least 1 parameter")
|
||||
|
||||
let firstParam = formalParams[1]
|
||||
let paramIdent = firstParam[0]
|
||||
@ -544,3 +557,847 @@ macro ffi*(prc: untyped): untyped =
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
macro ffi*(prc: untyped): untyped =
|
||||
## Simplified FFI macro — developer writes a clean Nim-idiomatic signature.
|
||||
##
|
||||
## The annotated proc must:
|
||||
## - Have a first parameter of the library type (e.g. w: Waku)
|
||||
## - Optionally have additional Nim-typed parameters
|
||||
## - Return Future[Result[RetType, string]]
|
||||
## - NOT include ctx, callback, or userData in its signature
|
||||
##
|
||||
## Example:
|
||||
## proc mylib_send*(w: MyLib, cfg: SendConfig): Future[Result[string, string]] {.ffi.} =
|
||||
## return ok("done")
|
||||
##
|
||||
## The macro generates:
|
||||
## 1. A named async helper proc (MyLibSendBody) containing the user body
|
||||
## 2. A registerReqFFI call that deserializes cstring args and calls the helper
|
||||
## 3. A C-exported proc with ctx/callback/userData + cstring params
|
||||
|
||||
let procName = prc[0]
|
||||
let formalParams = prc[3]
|
||||
let bodyNode = prc[^1]
|
||||
|
||||
# Need at least the library param
|
||||
if formalParams.len < 2:
|
||||
error("`.ffi.` procs require at least 1 parameter (the library type)")
|
||||
|
||||
# Extract LibType from the first parameter
|
||||
let firstParam = formalParams[1]
|
||||
let libParamName = firstParam[0] # e.g. `w`
|
||||
let libTypeName = firstParam[1] # e.g. `Waku`
|
||||
|
||||
# Extract the return type: Future[Result[RetType, string]]
|
||||
# RetType is used in the body helper proc signature
|
||||
let retTypeNode = formalParams[0]
|
||||
if retTypeNode.kind == nnkEmpty:
|
||||
error("`.ffi.` proc must have an explicit return type Future[Result[RetType, string]]")
|
||||
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
||||
error("`.ffi.` return type must be Future[Result[RetType, string]], got: " & retTypeNode.repr)
|
||||
let resultInner = retTypeNode[1] # Result[RetType, string]
|
||||
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
||||
error("`.ffi.` return type must be Future[Result[RetType, string]], got: " & retTypeNode.repr)
|
||||
|
||||
# Collect additional param names and types (everything after the first param)
|
||||
var extraParamNames: seq[string] = @[]
|
||||
var extraParamTypes: seq[NimNode] = @[]
|
||||
for i in 2 ..< formalParams.len:
|
||||
let p = formalParams[i]
|
||||
for j in 0 ..< p.len - 2:
|
||||
extraParamNames.add($p[j])
|
||||
extraParamTypes.add(p[^2])
|
||||
|
||||
# Generate type/proc names from proc name
|
||||
let procNameStr = block:
|
||||
let raw = $procName
|
||||
if raw.endsWith("*"): raw[0 ..^ 2] else: raw
|
||||
let camelName = toCamelCase(procNameStr)
|
||||
|
||||
# Names of generated things
|
||||
let reqTypeName = ident(camelName & "Req")
|
||||
let helperProcName = ident(camelName & "Body")
|
||||
|
||||
# Determine whether the body uses async operations
|
||||
let isAsync = bodyHasAwait(bodyNode)
|
||||
|
||||
# Strip the * from the exported proc name (needed for both branches)
|
||||
let exportedProcName =
|
||||
if procName.kind == nnkPostfix:
|
||||
procName[1]
|
||||
else:
|
||||
procName
|
||||
|
||||
# Common exported params (needed for both branches)
|
||||
let ctxType = nnkPtrTy.newTree(
|
||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
||||
)
|
||||
|
||||
if isAsync:
|
||||
# -------------------------------------------------------------------------
|
||||
# ASYNC PATH — existing behavior
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 1. Named async helper proc containing the user body
|
||||
# -------------------------------------------------------------------------
|
||||
# proc MyLibSendBody*(w: Waku, cfg: SendConfig): Future[Result[RetType, string]] {.async.} =
|
||||
# <user body>
|
||||
var helperParams = newSeq[NimNode]()
|
||||
helperParams.add(retTypeNode)
|
||||
# First param: w: LibType (by value, not pointer)
|
||||
helperParams.add(newIdentDefs(libParamName, libTypeName))
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
helperParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
|
||||
|
||||
let helperProc = newProc(
|
||||
name = postfix(helperProcName, "*"),
|
||||
params = helperParams,
|
||||
body = newStmtList(bodyNode),
|
||||
pragmas = newTree(nnkPragma, ident("async")),
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 2. registerReqFFI call
|
||||
# -------------------------------------------------------------------------
|
||||
let futStrStr = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")),
|
||||
)
|
||||
|
||||
let ctxHandlerName = ident("ffiCtxHandler")
|
||||
let ptrFfiCtx = nnkPtrTy.newTree(
|
||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
||||
)
|
||||
|
||||
var lambdaParams = newSeq[NimNode]()
|
||||
lambdaParams.add(futStrStr)
|
||||
for name in extraParamNames:
|
||||
lambdaParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
|
||||
|
||||
let lambdaBody = newStmtList()
|
||||
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
let jsonIdent = ident(extraParamNames[i] & "Json")
|
||||
let paramIdent = ident(extraParamNames[i])
|
||||
let ptype = extraParamTypes[i]
|
||||
lambdaBody.add quote do:
|
||||
let `paramIdent` = ffiDeserialize(`jsonIdent`, `ptype`).valueOr:
|
||||
return err($error)
|
||||
|
||||
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
|
||||
let libValDeref = newTree(nnkDerefExpr, ctxMyLib)
|
||||
let helperCall = newTree(nnkCall, helperProcName, libValDeref)
|
||||
for name in extraParamNames:
|
||||
helperCall.add(ident(name))
|
||||
|
||||
let retValIdent = ident("retVal")
|
||||
lambdaBody.add quote do:
|
||||
let `retValIdent` = (await `helperCall`).valueOr:
|
||||
return err($error)
|
||||
lambdaBody.add quote do:
|
||||
return ok(ffiSerialize(`retValIdent`))
|
||||
|
||||
let lambdaNode = newProc(
|
||||
name = newEmptyNode(),
|
||||
params = lambdaParams,
|
||||
body = lambdaBody,
|
||||
pragmas = newTree(nnkPragma, ident("async")),
|
||||
)
|
||||
|
||||
let registerReq = quote:
|
||||
registerReqFFI(`reqTypeName`, `ctxHandlerName`: `ptrFfiCtx`):
|
||||
`lambdaNode`
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 3. C-exported proc (async path)
|
||||
# -------------------------------------------------------------------------
|
||||
var exportedParams = newSeq[NimNode]()
|
||||
exportedParams.add(ident("cint"))
|
||||
exportedParams.add(newIdentDefs(ident("ctx"), ctxType))
|
||||
exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||
exportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||
for name in extraParamNames:
|
||||
exportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
|
||||
|
||||
let ffiBody = newStmtList()
|
||||
|
||||
ffiBody.add quote do:
|
||||
if callback.isNil:
|
||||
return RET_MISSING_CALLBACK
|
||||
|
||||
ffiBody.add quote do:
|
||||
if ctx.isNil or ctx[].myLib.isNil:
|
||||
let errStr = "context not initialized"
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
let newReqCall = newTree(nnkCall, ident("ffiNewReq"))
|
||||
newReqCall.add(reqTypeName)
|
||||
newReqCall.add(ident("callback"))
|
||||
newReqCall.add(ident("userData"))
|
||||
for name in extraParamNames:
|
||||
newReqCall.add(ident(name & "Json"))
|
||||
|
||||
let sendCall = newCall(
|
||||
newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")),
|
||||
ident("ctx"),
|
||||
newReqCall,
|
||||
)
|
||||
|
||||
let sendResIdent = genSym(nskLet, "sendRes")
|
||||
ffiBody.add quote do:
|
||||
let `sendResIdent` =
|
||||
try:
|
||||
`sendCall`
|
||||
except Exception as exc:
|
||||
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
||||
if `sendResIdent`.isErr():
|
||||
let errStr = "error in sendRequestToFFIThread: " & `sendResIdent`.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
return RET_OK
|
||||
|
||||
let ffiProc = newProc(
|
||||
name = exportedProcName,
|
||||
params = exportedParams,
|
||||
body = ffiBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
ident("exportc"),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
)
|
||||
|
||||
# Register proc metadata for binding generation
|
||||
block:
|
||||
var ffiExtraParams: seq[FFIParamMeta] = @[]
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
let ptype = extraParamTypes[i]
|
||||
var isPtr = false
|
||||
var tn = ""
|
||||
if ptype.kind == nnkPtrTy:
|
||||
isPtr = true
|
||||
tn = $ptype[0]
|
||||
else:
|
||||
tn = $ptype
|
||||
ffiExtraParams.add(FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr))
|
||||
let retTypeInner = resultInner[1] # RetType from Result[RetType, string]
|
||||
var retIsPtr = false
|
||||
var retTn = ""
|
||||
if retTypeInner.kind == nnkPtrTy:
|
||||
retIsPtr = true
|
||||
retTn = $retTypeInner[0]
|
||||
else:
|
||||
retTn = $retTypeInner
|
||||
ffiProcRegistry.add(FFIProcMeta(
|
||||
procName: procNameStr,
|
||||
libName: currentLibName,
|
||||
kind: ffiFfiKind,
|
||||
libTypeName: $libTypeName,
|
||||
extraParams: ffiExtraParams,
|
||||
returnTypeName: retTn,
|
||||
returnIsPtr: retIsPtr,
|
||||
isAsync: true,
|
||||
))
|
||||
|
||||
result = newStmtList(helperProc, registerReq, ffiProc)
|
||||
|
||||
else:
|
||||
# -------------------------------------------------------------------------
|
||||
# SYNC PATH — no await/waitFor in body; bypass thread-channel machinery
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 1. Named sync helper proc (no {.async.}) with Result[RetType, string] return
|
||||
# -------------------------------------------------------------------------
|
||||
# proc MyLibVersionBody*(w: LibType): Result[RetType, string] =
|
||||
# <user body>
|
||||
let syncRetType = resultInner # Result[RetType, string]
|
||||
|
||||
var syncHelperParams = newSeq[NimNode]()
|
||||
syncHelperParams.add(syncRetType)
|
||||
syncHelperParams.add(newIdentDefs(libParamName, libTypeName))
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
syncHelperParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
|
||||
|
||||
let syncHelperProc = newProc(
|
||||
name = postfix(helperProcName, "*"),
|
||||
params = syncHelperParams,
|
||||
body = newStmtList(bodyNode),
|
||||
pragmas = newEmptyNode(),
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 2. C-exported proc (sync path) — calls helper inline, fires callback inline
|
||||
# -------------------------------------------------------------------------
|
||||
var syncExportedParams = newSeq[NimNode]()
|
||||
syncExportedParams.add(ident("cint"))
|
||||
syncExportedParams.add(newIdentDefs(ident("ctx"), ctxType))
|
||||
syncExportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||
syncExportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||
for name in extraParamNames:
|
||||
syncExportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
|
||||
|
||||
let syncFfiBody = newStmtList()
|
||||
|
||||
syncFfiBody.add quote do:
|
||||
if callback.isNil:
|
||||
return RET_MISSING_CALLBACK
|
||||
|
||||
syncFfiBody.add quote do:
|
||||
if ctx.isNil or ctx[].myLib.isNil:
|
||||
let errStr = "context not initialized"
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
# Inline deserialization of each extra param
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
let jsonIdent = ident(extraParamNames[i] & "Json")
|
||||
let paramIdent = ident(extraParamNames[i])
|
||||
let ptype = extraParamTypes[i]
|
||||
syncFfiBody.add quote do:
|
||||
let `paramIdent` = ffiDeserialize(`jsonIdent`, `ptype`).valueOr:
|
||||
let errStr = "deserialization failed: " & $error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
# Build the call to the sync helper: helperProcName(ctx[].myLib[], extraParam, ...)
|
||||
let syncCtxMyLib = newDotExpr(newTree(nnkDerefExpr, ident("ctx")), ident("myLib"))
|
||||
let syncLibValDeref = newTree(nnkDerefExpr, syncCtxMyLib)
|
||||
let syncHelperCall = newTree(nnkCall, helperProcName, syncLibValDeref)
|
||||
for name in extraParamNames:
|
||||
syncHelperCall.add(ident(name))
|
||||
|
||||
let retValOrErrIdent = ident("retValOrErr")
|
||||
syncFfiBody.add quote do:
|
||||
let `retValOrErrIdent` = `syncHelperCall`
|
||||
if `retValOrErrIdent`.isErr():
|
||||
let errStr = `retValOrErrIdent`.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
let serialized = ffiSerialize(`retValOrErrIdent`.value)
|
||||
callback(RET_OK, unsafeAddr serialized[0], cast[csize_t](serialized.len), userData)
|
||||
return RET_OK
|
||||
|
||||
let syncFfiProc = newProc(
|
||||
name = exportedProcName,
|
||||
params = syncExportedParams,
|
||||
body = syncFfiBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
ident("exportc"),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
)
|
||||
|
||||
# Register proc metadata for binding generation (sync path)
|
||||
block:
|
||||
var ffiExtraParamsSync: seq[FFIParamMeta] = @[]
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
let ptype = extraParamTypes[i]
|
||||
var isPtr = false
|
||||
var tn = ""
|
||||
if ptype.kind == nnkPtrTy:
|
||||
isPtr = true
|
||||
tn = $ptype[0]
|
||||
else:
|
||||
tn = $ptype
|
||||
ffiExtraParamsSync.add(FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr))
|
||||
let retTypeInnerSync = resultInner[1]
|
||||
var retIsPtrSync = false
|
||||
var retTnSync = ""
|
||||
if retTypeInnerSync.kind == nnkPtrTy:
|
||||
retIsPtrSync = true
|
||||
retTnSync = $retTypeInnerSync[0]
|
||||
else:
|
||||
retTnSync = $retTypeInnerSync
|
||||
ffiProcRegistry.add(FFIProcMeta(
|
||||
procName: procNameStr,
|
||||
libName: currentLibName,
|
||||
kind: ffiFfiKind,
|
||||
libTypeName: $libTypeName,
|
||||
extraParams: ffiExtraParamsSync,
|
||||
returnTypeName: retTnSync,
|
||||
returnIsPtr: retIsPtrSync,
|
||||
isAsync: false,
|
||||
))
|
||||
|
||||
result = newStmtList(syncHelperProc, syncFfiProc)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ffiCtor — constructor macro
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc buildCtorRequestType(reqTypeName: NimNode, paramNames: seq[string]): NimNode =
|
||||
## Builds the request object type for a ctor request.
|
||||
## Each original Nim-typed param becomes a cstring field named <paramName>Json.
|
||||
##
|
||||
## e.g. type TestlibCreateCtorReq* = object
|
||||
## configJson: cstring
|
||||
var fields: seq[NimNode] = @[]
|
||||
for name in paramNames:
|
||||
let fieldName = ident(name & "Json")
|
||||
fields.add newTree(nnkIdentDefs, fieldName, ident("cstring"), newEmptyNode())
|
||||
|
||||
let recList =
|
||||
if fields.len > 0:
|
||||
newTree(nnkRecList, fields)
|
||||
else:
|
||||
newTree(nnkRecList, newTree(nnkIdentDefs, ident("_placeholder"), ident("pointer"), newEmptyNode()))
|
||||
|
||||
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
||||
let typeName = postfix(reqTypeName, "*")
|
||||
result = newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildCtorDeleteReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimNode =
|
||||
## Generates ffiDeleteReq for the ctor request type.
|
||||
var body = newStmtList()
|
||||
for name in paramNames:
|
||||
let fieldName = ident(name & "Json")
|
||||
body.add newCall(
|
||||
ident("deallocShared"),
|
||||
newDotExpr(newTree(nnkDerefExpr, ident("self")), fieldName),
|
||||
)
|
||||
body.add newCall(ident("deallocShared"), ident("self"))
|
||||
|
||||
let selfParam = newIdentDefs(ident("self"), newTree(nnkPtrTy, reqTypeName))
|
||||
result = newProc(
|
||||
name = postfix(ident("ffiDeleteReq"), "*"),
|
||||
params = @[newEmptyNode()] & @[selfParam],
|
||||
body = body,
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimNode =
|
||||
## Generates ffiNewReq for the ctor request type.
|
||||
## Params: T: typedesc[CtorReq], callback: FFICallBack, userData: pointer,
|
||||
## <paramName>Json: cstring, ...
|
||||
|
||||
var formalParams = newSeq[NimNode]()
|
||||
|
||||
let typedescParam = newIdentDefs(
|
||||
ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)
|
||||
)
|
||||
formalParams.add(typedescParam)
|
||||
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||
|
||||
for name in paramNames:
|
||||
formalParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
|
||||
|
||||
let retType = newTree(nnkPtrTy, ident("FFIThreadRequest"))
|
||||
formalParams = @[retType] & formalParams
|
||||
|
||||
let reqObjIdent = ident("reqObj")
|
||||
var newBody = newStmtList()
|
||||
newBody.add quote do:
|
||||
var `reqObjIdent` = createShared(T)
|
||||
|
||||
for name in paramNames:
|
||||
let fieldName = ident(name & "Json")
|
||||
newBody.add quote do:
|
||||
`reqObjIdent`[].`fieldName` = `fieldName`.alloc()
|
||||
|
||||
newBody.add quote do:
|
||||
let typeStr = $T
|
||||
var ret = FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
||||
proc destroyContent(content: pointer) {.nimcall.} =
|
||||
ffiDeleteReq(cast[ptr `reqTypeName`](content))
|
||||
ret[].deleteReqContent = destroyContent
|
||||
return ret
|
||||
|
||||
result = newProc(
|
||||
name = postfix(ident("ffiNewReq"), "*"),
|
||||
params = formalParams,
|
||||
body = newBody,
|
||||
pragmas = newEmptyNode(),
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildCtorBodyProc(
|
||||
helperName: NimNode,
|
||||
paramNames: seq[string],
|
||||
paramTypes: seq[NimNode],
|
||||
libTypeName: NimNode,
|
||||
userBody: NimNode,
|
||||
): NimNode =
|
||||
## Generates a named top-level async helper proc that contains the user body.
|
||||
## e.g.:
|
||||
## proc TestlibCreateCtorBody*(config: SimpleConfig): Future[Result[SimpleLib, string]] {.async.} =
|
||||
## return ok(SimpleLib(value: config.initialValue))
|
||||
|
||||
let innerRetType = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(ident("Result"), libTypeName, ident("string")),
|
||||
)
|
||||
var innerParams = newSeq[NimNode]()
|
||||
innerParams.add(innerRetType)
|
||||
for i in 0 ..< paramNames.len:
|
||||
innerParams.add(newIdentDefs(ident(paramNames[i]), paramTypes[i]))
|
||||
|
||||
result = newProc(
|
||||
name = postfix(helperName, "*"),
|
||||
params = innerParams,
|
||||
body = newStmtList(userBody),
|
||||
pragmas = newTree(nnkPragma, ident("async")),
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildCtorProcessFFIRequestProc(
|
||||
reqTypeName: NimNode,
|
||||
helperName: NimNode,
|
||||
paramNames: seq[string],
|
||||
paramTypes: seq[NimNode],
|
||||
libTypeName: NimNode,
|
||||
): NimNode =
|
||||
## Generates the processFFIRequest proc for the ctor.
|
||||
## The handler:
|
||||
## 1. Unpacks cstring fields from the request
|
||||
## 2. Deserializes each cstring to the Nim type
|
||||
## 3. Calls the helper async proc to get Result[LibType, string]
|
||||
## 4. Stores the result in ctx.myLib via createShared
|
||||
## 5. Returns ok($cast[ByteAddress](ctx))
|
||||
|
||||
# Build Future[Result[string, string]] return type
|
||||
let returnType = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")),
|
||||
)
|
||||
|
||||
# The ctx param type: ptr FFIContext[LibType]
|
||||
let ctxType = nnkPtrTy.newTree(
|
||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
||||
)
|
||||
|
||||
let typedescParam =
|
||||
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
||||
|
||||
var formalParams: seq[NimNode] = @[]
|
||||
formalParams.add(returnType)
|
||||
formalParams.add(typedescParam)
|
||||
formalParams.add(newIdentDefs(ident("request"), ident("pointer")))
|
||||
formalParams.add(newIdentDefs(ident("ctx"), ctxType))
|
||||
|
||||
# Build the proc body
|
||||
let newBody = newStmtList()
|
||||
let reqIdent = ident("req")
|
||||
let ctxIdent = ident("ctx")
|
||||
|
||||
# Cast the request
|
||||
newBody.add quote do:
|
||||
let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request)
|
||||
|
||||
# Unpack fields and deserialize each param
|
||||
for i in 0 ..< paramNames.len:
|
||||
let fieldName = ident(paramNames[i] & "Json")
|
||||
let paramName = ident(paramNames[i])
|
||||
let ptype = paramTypes[i]
|
||||
newBody.add quote do:
|
||||
let `fieldName` = `reqIdent`[].`fieldName`
|
||||
newBody.add quote do:
|
||||
let `paramName` = ffiDeserialize(`fieldName`, `ptype`).valueOr:
|
||||
return err($error)
|
||||
|
||||
# Call the helper proc with deserialized params
|
||||
let helperCallNode = newTree(nnkCall, helperName)
|
||||
for name in paramNames:
|
||||
helperCallNode.add(ident(name))
|
||||
|
||||
let libValIdent = ident("libVal")
|
||||
newBody.add quote do:
|
||||
let `libValIdent` = (await `helperCallNode`).valueOr:
|
||||
return err($error)
|
||||
|
||||
# Store in ctx.myLib
|
||||
let myLibIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("myLib"))
|
||||
newBody.add quote do:
|
||||
`myLibIdent` = createShared(`libTypeName`)
|
||||
`myLibIdent`[] = `libValIdent`
|
||||
|
||||
# Return context address as decimal string
|
||||
newBody.add quote do:
|
||||
return ok($cast[uint](`ctxIdent`))
|
||||
|
||||
result = newProc(
|
||||
name = postfix(ident("processFFIRequest"), "*"),
|
||||
params = formalParams,
|
||||
body = newBody,
|
||||
procType = nnkProcDef,
|
||||
pragmas = newTree(nnkPragma, ident("async")),
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode =
|
||||
## Registers the ctor request in the registeredRequests table.
|
||||
## The handler casts reqHandler to ptr FFIContext[LibType] and calls processFFIRequest.
|
||||
|
||||
let ctxType = nnkPtrTy.newTree(
|
||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
||||
)
|
||||
|
||||
let returnType = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")),
|
||||
)
|
||||
|
||||
let callExpr = newCall(
|
||||
newDotExpr(reqTypeName, ident("processFFIRequest")),
|
||||
ident("request"),
|
||||
newTree(nnkCast, ctxType, ident("reqHandler")),
|
||||
)
|
||||
|
||||
var newBody = newStmtList()
|
||||
newBody.add quote do:
|
||||
return await `callExpr`
|
||||
|
||||
let asyncProc = newProc(
|
||||
name = newEmptyNode(),
|
||||
params =
|
||||
@[
|
||||
returnType,
|
||||
newIdentDefs(ident("request"), ident("pointer")),
|
||||
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
||||
],
|
||||
body = newBody,
|
||||
pragmas = nnkPragma.newTree(ident("async")),
|
||||
)
|
||||
|
||||
let key = newLit($reqTypeName)
|
||||
result = newAssignment(
|
||||
newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
macro ffiCtor*(prc: untyped): untyped =
|
||||
## Defines a C-exported constructor that creates an FFIContext and populates
|
||||
## ctx.myLib asynchronously in the FFI thread.
|
||||
##
|
||||
## The annotated proc must:
|
||||
## - Have Nim-typed parameters (they are automatically serialized to/from JSON)
|
||||
## - Return Future[Result[LibType, string]]
|
||||
## - NOT include ctx, callback, or userData in its signature
|
||||
##
|
||||
## Example:
|
||||
## proc mylib_create*(config: SimpleConfig): Future[Result[SimpleLib, string]] {.ffiCtor.} =
|
||||
## return ok(SimpleLib(value: config.initialValue))
|
||||
##
|
||||
## The generated C-exported proc will have the signature:
|
||||
## proc mylib_create(configJson: cstring, callback: FFICallBack,
|
||||
## userData: pointer): cint {.exportc, cdecl, raises: [].}
|
||||
##
|
||||
## On success the callback receives the ctx address as a decimal string.
|
||||
## The caller should hold this pointer and pass it to subsequent .ffi. calls.
|
||||
|
||||
let procName = prc[0]
|
||||
let formalParams = prc[3]
|
||||
let bodyNode = prc[^1]
|
||||
|
||||
# Extract LibType from return type: Future[Result[LibType, string]]
|
||||
let retTypeNode = formalParams[0]
|
||||
# retTypeNode should be Future[Result[LibType, string]]
|
||||
if retTypeNode.kind == nnkEmpty:
|
||||
error("ffiCtor: proc must have an explicit return type Future[Result[LibType, string]]")
|
||||
# retTypeNode: BracketExpr(Future, BracketExpr(Result, LibType, string))
|
||||
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
||||
error("ffiCtor: return type must be Future[Result[LibType, string]], got: " & retTypeNode.repr)
|
||||
let resultInner = retTypeNode[1] # Result[LibType, string]
|
||||
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
||||
error("ffiCtor: return type must be Future[Result[LibType, string]], got: " & retTypeNode.repr)
|
||||
let libTypeName = resultInner[1] # LibType
|
||||
|
||||
# Collect param names and types (skip return type at index 0)
|
||||
var paramNames: seq[string] = @[]
|
||||
var paramTypes: seq[NimNode] = @[]
|
||||
for i in 1 ..< formalParams.len:
|
||||
let p = formalParams[i]
|
||||
# p is IdentDefs: [name, type, default]
|
||||
for j in 0 ..< p.len - 2: # handle multi-name identdefs
|
||||
paramNames.add($p[j])
|
||||
paramTypes.add(p[^2])
|
||||
|
||||
# Generate ctor request type name: <ProcNameCamelCase>CtorReq
|
||||
let procNameStr = $procName
|
||||
# Strip trailing * if exported
|
||||
let cleanName =
|
||||
if procNameStr.endsWith("*"):
|
||||
procNameStr[0 ..^ 2]
|
||||
else:
|
||||
procNameStr
|
||||
let reqTypeNameStr = toCamelCase(cleanName) & "CtorReq"
|
||||
let reqTypeName = ident(reqTypeNameStr)
|
||||
|
||||
# Build constituent parts
|
||||
let typeDef = buildCtorRequestType(reqTypeName, paramNames)
|
||||
let deleteProc = buildCtorDeleteReqProc(reqTypeName, paramNames)
|
||||
let ffiNewReqProc = buildCtorFfiNewReqProc(reqTypeName, paramNames)
|
||||
# Helper proc name: e.g., TestlibCreateCtorReq -> TestlibCreateCtorBody
|
||||
let helperProcNameStr = reqTypeNameStr[0 ..^ ("CtorReq".len + 1)] & "CtorBody"
|
||||
let helperProcName = ident(helperProcNameStr)
|
||||
let helperProc = buildCtorBodyProc(helperProcName, paramNames, paramTypes, libTypeName, bodyNode)
|
||||
let processProc =
|
||||
buildCtorProcessFFIRequestProc(reqTypeName, helperProcName, paramNames, paramTypes, libTypeName)
|
||||
let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName)
|
||||
|
||||
# Build the C-exported proc params:
|
||||
# (<paramName>Json: cstring, ..., callback: FFICallBack, userData: pointer): cint
|
||||
var exportedParams = newSeq[NimNode]()
|
||||
exportedParams.add(ident("cint")) # return type
|
||||
for name in paramNames:
|
||||
exportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring")))
|
||||
exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||
exportedParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||
|
||||
# Build the C-exported proc body
|
||||
let ffiBody = newStmtList()
|
||||
|
||||
# initializeLibrary() — only if declared
|
||||
ffiBody.add quote do:
|
||||
when declared(initializeLibrary):
|
||||
initializeLibrary()
|
||||
|
||||
# if callback.isNil: return RET_MISSING_CALLBACK
|
||||
ffiBody.add quote do:
|
||||
if callback.isNil:
|
||||
return RET_MISSING_CALLBACK
|
||||
|
||||
# Deserialize each param for early validation
|
||||
for i in 0 ..< paramNames.len:
|
||||
let jsonIdent = ident(paramNames[i] & "Json")
|
||||
let ptype = paramTypes[i]
|
||||
ffiBody.add quote do:
|
||||
block:
|
||||
let validateRes = ffiDeserialize(`jsonIdent`, `ptype`)
|
||||
if validateRes.isErr():
|
||||
let errStr = "ffiCtor: failed to deserialize param: " & $validateRes.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
# Build the ffiNewReq call with all cstring params
|
||||
var newReqArgs: seq[NimNode] = @[reqTypeName, ident("callback"), ident("userData")]
|
||||
for name in paramNames:
|
||||
newReqArgs.add(ident(name & "Json"))
|
||||
let newReqCall = newCall(ident("ffiNewReq"), newReqArgs)
|
||||
|
||||
# Use a gensym'd ctx identifier so both the let binding and usage match
|
||||
let ctxSym = genSym(nskLet, "ctx")
|
||||
|
||||
ffiBody.add quote do:
|
||||
let `ctxSym` = createFFIContext[`libTypeName`]().valueOr:
|
||||
let errStr = "ffiCtor: failed to create FFIContext: " & $error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
# sendRequestToFFIThread using the gensym'd ctx
|
||||
let sendCall = newCall(
|
||||
newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall
|
||||
)
|
||||
|
||||
let sendResIdent = genSym(nskLet, "sendRes")
|
||||
ffiBody.add quote do:
|
||||
let `sendResIdent` =
|
||||
try:
|
||||
`sendCall`
|
||||
except Exception as exc:
|
||||
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
||||
if `sendResIdent`.isErr():
|
||||
let errStr = "ffiCtor: failed to send request: " & $`sendResIdent`.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
|
||||
ffiBody.add quote do:
|
||||
return RET_OK
|
||||
|
||||
# Strip the * from proc name for the C exported version
|
||||
let exportedProcName =
|
||||
if procName.kind == nnkPostfix:
|
||||
procName[1] # the bare ident without *
|
||||
else:
|
||||
procName
|
||||
|
||||
let ffiProc = newProc(
|
||||
name = exportedProcName,
|
||||
params = exportedParams,
|
||||
body = ffiBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
ident("exportc"),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
)
|
||||
|
||||
# Register metadata for binding generation (we're inside a macro = compile-time context)
|
||||
block:
|
||||
var ctorExtraParams: seq[FFIParamMeta] = @[]
|
||||
for i in 0 ..< paramNames.len:
|
||||
let ptype = paramTypes[i]
|
||||
var isPtr = false
|
||||
var tn = ""
|
||||
if ptype.kind == nnkPtrTy:
|
||||
isPtr = true
|
||||
tn = $ptype[0]
|
||||
else:
|
||||
tn = $ptype
|
||||
ctorExtraParams.add(FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPtr))
|
||||
ffiProcRegistry.add(FFIProcMeta(
|
||||
procName: cleanName,
|
||||
libName: currentLibName,
|
||||
kind: ffiCtorKind,
|
||||
libTypeName: $libTypeName,
|
||||
extraParams: ctorExtraParams,
|
||||
returnTypeName: $libTypeName,
|
||||
returnIsPtr: false,
|
||||
isAsync: true,
|
||||
))
|
||||
|
||||
result = newStmtList(typeDef, deleteProc, ffiNewReqProc, helperProc, processProc, addToReg, ffiProc)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# genBindings — Rust crate generator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
macro genBindings*(
|
||||
lang: static[string],
|
||||
outputDir: static[string],
|
||||
nimSrcRelPath: static[string] = "",
|
||||
): untyped =
|
||||
## Generates binding files for the specified target language.
|
||||
## Call at the END of your library file, after all {.ffiCtor.} and {.ffi.} procs.
|
||||
##
|
||||
## Example:
|
||||
## genBindings("rust", "examples/nim_timer/nim_bindings", "examples/nim_timer/nim_timer.nim")
|
||||
##
|
||||
## Activate with: nim c -d:ffiGenBindings mylib.nim
|
||||
|
||||
when defined(ffiGenBindings):
|
||||
if lang == "rust":
|
||||
let libName = deriveLibName(ffiProcRegistry)
|
||||
generateRustCrate(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath)
|
||||
|
||||
result = newEmptyNode()
|
||||
|
||||
130
ffi/serial.nim
Normal file
130
ffi/serial.nim
Normal file
@ -0,0 +1,130 @@
|
||||
import std/[json, macros]
|
||||
import results
|
||||
import ./codegen/meta
|
||||
|
||||
proc ffiSerialize*(x: string): string =
|
||||
$(%* x)
|
||||
|
||||
proc ffiSerialize*(x: cstring): string =
|
||||
if x.isNil: "null"
|
||||
else: ffiSerialize($x)
|
||||
|
||||
proc ffiSerialize*(x: int): string =
|
||||
$x
|
||||
|
||||
proc ffiSerialize*(x: int32): string =
|
||||
$x
|
||||
|
||||
proc ffiSerialize*(x: bool): string =
|
||||
if x: "true" else: "false"
|
||||
|
||||
proc ffiSerialize*(x: float): string =
|
||||
$(%* x)
|
||||
|
||||
proc ffiSerialize*(x: pointer): string =
|
||||
$cast[uint](x)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[string]): Result[string, string] =
|
||||
try:
|
||||
let node = parseJson($s)
|
||||
if node.kind != JString:
|
||||
return err("expected JSON string")
|
||||
ok(node.getStr())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[int]): Result[int, string] =
|
||||
try:
|
||||
ok(int(parseJson($s).getBiggestInt()))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[int32]): Result[int32, string] =
|
||||
try:
|
||||
ok(int32(parseJson($s).getBiggestInt()))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[bool]): Result[bool, string] =
|
||||
try:
|
||||
ok(parseJson($s).getBool())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[float]): Result[float, string] =
|
||||
try:
|
||||
ok(parseJson($s).getFloat())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[pointer]): Result[pointer, string] =
|
||||
try:
|
||||
let address = cast[pointer](uint(parseJson($s).getBiggestInt()))
|
||||
ok(address)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiSerialize*[T](x: ptr T): string =
|
||||
$cast[uint](x)
|
||||
|
||||
proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
|
||||
try:
|
||||
let address = cast[ptr T](uint(parseJson($s).getBiggestInt()))
|
||||
ok(address)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
macro ffiType*(body: untyped): untyped =
|
||||
## Statement macro applied to a type declaration block.
|
||||
## Generates ffiSerialize and ffiDeserialize overloads for each type,
|
||||
## and registers the type in ffiTypeRegistry for binding generation.
|
||||
## Usage:
|
||||
## ffiType:
|
||||
## type Foo = object
|
||||
## field: int
|
||||
let typeSection = body[0]
|
||||
let typeDef = typeSection[0]
|
||||
let typeName =
|
||||
if typeDef[0].kind == nnkPostfix:
|
||||
typeDef[0][1]
|
||||
else:
|
||||
typeDef[0]
|
||||
|
||||
# Collect field metadata for the codegen registry
|
||||
let typeNameStr = $typeName
|
||||
var fieldMetas: seq[FFIFieldMeta] = @[]
|
||||
# typeDef layout: TypDef[name, genericParams, objectTy]
|
||||
# objectTy layout: ObjectTy[empty, empty, recList]
|
||||
let objTy = typeDef[2]
|
||||
if objTy.kind == nnkObjectTy and objTy.len >= 3:
|
||||
let recList = objTy[2]
|
||||
if recList.kind == nnkRecList:
|
||||
for identDef in recList:
|
||||
if identDef.kind == nnkIdentDefs:
|
||||
# identDef: [name1, ..., type, default]
|
||||
let fieldType = identDef[^2]
|
||||
var fieldTypeName: string
|
||||
if fieldType.kind == nnkIdent:
|
||||
fieldTypeName = $fieldType
|
||||
elif fieldType.kind == nnkPtrTy:
|
||||
fieldTypeName = "ptr " & $fieldType[0]
|
||||
else:
|
||||
fieldTypeName = fieldType.repr
|
||||
for i in 0 ..< identDef.len - 2:
|
||||
let fname = $identDef[i]
|
||||
fieldMetas.add(FFIFieldMeta(name: fname, typeName: fieldTypeName))
|
||||
|
||||
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
|
||||
|
||||
let serializeProc = quote do:
|
||||
proc ffiSerialize*(x: `typeName`): string =
|
||||
$(%* x)
|
||||
|
||||
let deserializeProc = quote do:
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[`typeName`]): Result[`typeName`, string] =
|
||||
try:
|
||||
ok(parseJson($s).to(`typeName`))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
result = newStmtList(body, serializeProc, deserializeProc)
|
||||
@ -1,4 +1,4 @@
|
||||
import std/locks
|
||||
import std/[locks, strutils, os]
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
@ -135,3 +135,247 @@ suite "sendRequestToFFIThread":
|
||||
deinitCallbackData(d)
|
||||
check d.retCode == RET_OK
|
||||
check callbackMsg(d) == "pong:" & msg
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ffiCtor macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type SimpleLib = object
|
||||
value: int
|
||||
|
||||
ffiType:
|
||||
type SimpleConfig = object
|
||||
initialValue: int
|
||||
|
||||
proc testlib_create*(
|
||||
config: SimpleConfig
|
||||
): Future[Result[SimpleLib, string]] {.ffiCtor.} =
|
||||
return ok(SimpleLib(value: config.initialValue))
|
||||
|
||||
suite "ffiCtor macro":
|
||||
test "creates context and returns pointer via callback":
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 42))
|
||||
let ret = testlib_create(configJson.cstring, testCallback, addr d)
|
||||
|
||||
check ret == RET_OK
|
||||
|
||||
waitCallback(d)
|
||||
|
||||
check d.retCode == RET_OK
|
||||
|
||||
# The callback message is the ctx address as a decimal string
|
||||
let addrStr = callbackMsg(d)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
|
||||
# Verify the library was properly initialized
|
||||
check not ctx[].myLib.isNil
|
||||
check ctx[].myLib[].value == 42
|
||||
|
||||
check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Simplified .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ffiType:
|
||||
type SendConfig = object
|
||||
message: string
|
||||
|
||||
proc testlib_send*(
|
||||
lib: SimpleLib, cfg: SendConfig
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("echo:" & cfg.message & ":" & $lib.value)
|
||||
|
||||
suite "simplified .ffi. macro":
|
||||
test "sends request and gets serialized response via callback":
|
||||
# First create a context using ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 7))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Now call the .ffi. proc
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
|
||||
let cfgJson = ffiSerialize(SendConfig(message: "hello"))
|
||||
let ret = testlib_send(ctx, testCallback, addr d, cfgJson.cstring)
|
||||
check ret == RET_OK
|
||||
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
|
||||
let receivedMsg = callbackMsg(d)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "echo:hello:7"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# async/sync detection in .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Sync proc (no await in body) — macro detects this and bypasses thread machinery
|
||||
proc testlib_version*(
|
||||
lib: SimpleLib
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("v" & $lib.value)
|
||||
|
||||
suite "async/sync detection in .ffi.":
|
||||
test "sync proc invokes callback without thread hop":
|
||||
# Create a context using ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 3))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
var d2: CallbackData
|
||||
initCallbackData(d2)
|
||||
defer: deinitCallbackData(d2)
|
||||
|
||||
# Call sync proc — callback should fire before the proc returns (no thread hop)
|
||||
let ret = testlib_version(ctx, testCallback, addr d2)
|
||||
# No sleep needed: sync path fires callback inline before returning
|
||||
check ret == RET_OK
|
||||
check d2.called # fires synchronously — no waitCallback needed
|
||||
check d2.retCode == RET_OK
|
||||
let receivedMsg = callbackMsg(d2)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "v3"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ptr T return type in .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type Handle = object
|
||||
data: string
|
||||
|
||||
ffiType:
|
||||
type NameParam = object
|
||||
name: string
|
||||
|
||||
proc testlib_alloc_handle*(
|
||||
lib: SimpleLib, np: NameParam
|
||||
): Future[Result[ptr Handle, string]] {.ffi.} =
|
||||
let h = createShared(Handle)
|
||||
h[] = Handle(data: np.name & ":" & $lib.value)
|
||||
return ok(h)
|
||||
|
||||
proc testlib_read_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
return ok(h[].data)
|
||||
|
||||
proc testlib_free_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
deallocShared(h)
|
||||
return ok("freed")
|
||||
|
||||
suite "ptr return type in .ffi.":
|
||||
test "returns a heap-allocated handle and reads it back":
|
||||
# Create context via ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 5))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let ctxAddrStr = callbackMsg(ctorD)
|
||||
check ctxAddrStr.len > 0
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(ctxAddrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Alloc a handle
|
||||
var allocD: CallbackData
|
||||
initCallbackData(allocD)
|
||||
defer: deinitCallbackData(allocD)
|
||||
|
||||
let npJson = ffiSerialize(NameParam(name: "test"))
|
||||
let allocRet = testlib_alloc_handle(ctx, testCallback, addr allocD, npJson.cstring)
|
||||
check allocRet == RET_OK
|
||||
|
||||
waitCallback(allocD)
|
||||
check allocD.retCode == RET_OK
|
||||
|
||||
let handleAddrStr = callbackMsg(allocD)
|
||||
check handleAddrStr.len > 0
|
||||
let handleAddr = parseBiggestUInt(handleAddrStr)
|
||||
check handleAddr != 0
|
||||
|
||||
# Read the handle back
|
||||
var readD: CallbackData
|
||||
initCallbackData(readD)
|
||||
defer: deinitCallbackData(readD)
|
||||
|
||||
let handleJson = ffiSerialize(cast[pointer](handleAddr))
|
||||
let readRet = testlib_read_handle(ctx, testCallback, addr readD, handleJson.cstring)
|
||||
check readRet == RET_OK
|
||||
|
||||
waitCallback(readD)
|
||||
check readD.retCode == RET_OK
|
||||
|
||||
let readMsg = callbackMsg(readD)
|
||||
let decodedStr = ffiDeserialize(readMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decodedStr == "test:5"
|
||||
|
||||
# Free the handle
|
||||
var freeD: CallbackData
|
||||
initCallbackData(freeD)
|
||||
defer: deinitCallbackData(freeD)
|
||||
|
||||
let freeRet = testlib_free_handle(ctx, testCallback, addr freeD, handleJson.cstring)
|
||||
check freeRet == RET_OK
|
||||
|
||||
waitCallback(freeD)
|
||||
check freeD.retCode == RET_OK
|
||||
|
||||
113
tests/test_serial.nim
Normal file
113
tests/test_serial.nim
Normal file
@ -0,0 +1,113 @@
|
||||
import unittest
|
||||
import results
|
||||
import ../ffi/serial
|
||||
|
||||
ffiType:
|
||||
type Point = object
|
||||
x: int
|
||||
y: int
|
||||
|
||||
ffiType:
|
||||
type Nested = object
|
||||
label: string
|
||||
point: Point
|
||||
|
||||
suite "ffiSerialize / ffiDeserialize primitives":
|
||||
test "string round-trip":
|
||||
let s = "hello world"
|
||||
let serialized = ffiSerialize(s)
|
||||
let back = ffiDeserialize(serialized.cstring, string)
|
||||
check back.isOk()
|
||||
check back.value == s
|
||||
|
||||
test "string with special chars":
|
||||
let s = "tab\there"
|
||||
let serialized = ffiSerialize(s)
|
||||
let back = ffiDeserialize(serialized.cstring, string)
|
||||
check back.isOk()
|
||||
check back.value == s
|
||||
|
||||
test "int round-trip":
|
||||
let v = 42
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, int)
|
||||
check back.isOk()
|
||||
check back.value == v
|
||||
|
||||
test "int negative round-trip":
|
||||
let v = -100
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, int)
|
||||
check back.isOk()
|
||||
check back.value == v
|
||||
|
||||
test "bool true round-trip":
|
||||
let serialized = ffiSerialize(true)
|
||||
let back = ffiDeserialize(serialized.cstring, bool)
|
||||
check back.isOk()
|
||||
check back.value == true
|
||||
|
||||
test "bool false round-trip":
|
||||
let serialized = ffiSerialize(false)
|
||||
let back = ffiDeserialize(serialized.cstring, bool)
|
||||
check back.isOk()
|
||||
check back.value == false
|
||||
|
||||
test "float round-trip":
|
||||
let v = 3.14
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, float)
|
||||
check back.isOk()
|
||||
check abs(back.value - v) < 1e-9
|
||||
|
||||
test "float negative round-trip":
|
||||
let v = -2.718
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, float)
|
||||
check back.isOk()
|
||||
check abs(back.value - v) < 1e-9
|
||||
|
||||
suite "pointer serialization":
|
||||
test "pointer serialize and recover address":
|
||||
var x = 12345
|
||||
let p = addr x
|
||||
let serialized = ffiSerialize(cast[pointer](p))
|
||||
let back = ffiDeserialize(serialized.cstring, pointer)
|
||||
check back.isOk()
|
||||
check back.value == cast[pointer](p)
|
||||
|
||||
test "nil pointer serializes as 0":
|
||||
let p: pointer = nil
|
||||
let serialized = ffiSerialize(p)
|
||||
check serialized == "0"
|
||||
|
||||
suite "ffiType macro — object round-trip":
|
||||
test "Point round-trip":
|
||||
let pt = Point(x: 10, y: 20)
|
||||
let serialized = ffiSerialize(pt)
|
||||
let back = ffiDeserialize(serialized.cstring, Point)
|
||||
check back.isOk()
|
||||
check back.value.x == 10
|
||||
check back.value.y == 20
|
||||
|
||||
test "Nested object round-trip":
|
||||
let n = Nested(label: "origin", point: Point(x: 0, y: 0))
|
||||
let serialized = ffiSerialize(n)
|
||||
let back = ffiDeserialize(serialized.cstring, Nested)
|
||||
check back.isOk()
|
||||
check back.value.label == "origin"
|
||||
check back.value.point.x == 0
|
||||
check back.value.point.y == 0
|
||||
|
||||
suite "ffiDeserialize error handling":
|
||||
test "malformed JSON returns err":
|
||||
let back = ffiDeserialize("not json at all".cstring, int)
|
||||
check back.isErr()
|
||||
|
||||
test "wrong JSON type returns err for string":
|
||||
let back = ffiDeserialize("42".cstring, string)
|
||||
check back.isErr()
|
||||
|
||||
test "malformed JSON for object returns err":
|
||||
let back = ffiDeserialize("{bad json".cstring, Point)
|
||||
check back.isErr()
|
||||
Loading…
x
Reference in New Issue
Block a user