From 95241084744204e036597d780bb7dff200dd9edf Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 3 May 2026 15:48:47 +0200 Subject: [PATCH] enhance cpp and rust tokio examples Co-authored-by: Copilot --- .gitignore | 5 +- README.md | 5 + examples/nim_timer/README.md | 54 ++++ .../nim_timer/cpp_bindings/CMakeLists.txt | 78 ++++++ examples/nim_timer/cpp_bindings/README.md | 36 +++ examples/nim_timer/cpp_bindings/main.cpp | 37 +++ examples/nim_timer/cpp_bindings/nimtimer.hpp | 174 +++++++++++++ examples/nim_timer/nim_timer.nim | 43 +++- examples/nim_timer/nim_timer.nimble | 21 ++ examples/nim_timer/rust_bindings/Cargo.toml | 8 + examples/nim_timer/rust_bindings/README.md | 39 +++ examples/nim_timer/rust_bindings/build.rs | 47 ++++ examples/nim_timer/rust_bindings/src/api.rs | 102 ++++++++ examples/nim_timer/rust_bindings/src/ffi.rs | 16 ++ examples/nim_timer/rust_bindings/src/lib.rs | 5 + examples/nim_timer/rust_bindings/src/types.rs | 37 +++ examples/nim_timer/rust_client/Cargo.lock | 28 +++ examples/nim_timer/rust_client/Cargo.toml | 11 +- examples/nim_timer/rust_client/README.md | 43 ++++ examples/nim_timer/rust_client/src/main.rs | 2 +- .../nim_timer/rust_client/src/tokio_main.rs | 71 ++++++ ffi.nimble | 16 +- ffi/codegen/cpp.nim | 159 +++++++++--- ffi/codegen/meta.nim | 33 ++- ffi/codegen/rust.nim | 86 +++++-- ffi/internal/ffi_macro.nim | 234 +++++++++++------- ffi/serial.nim | 81 +++++- 27 files changed, 1280 insertions(+), 191 deletions(-) create mode 100644 examples/nim_timer/README.md create mode 100644 examples/nim_timer/cpp_bindings/CMakeLists.txt create mode 100644 examples/nim_timer/cpp_bindings/README.md create mode 100644 examples/nim_timer/cpp_bindings/main.cpp create mode 100644 examples/nim_timer/cpp_bindings/nimtimer.hpp create mode 100644 examples/nim_timer/nim_timer.nimble create mode 100644 examples/nim_timer/rust_bindings/Cargo.toml create mode 100644 examples/nim_timer/rust_bindings/README.md create mode 100644 examples/nim_timer/rust_bindings/build.rs create mode 100644 examples/nim_timer/rust_bindings/src/api.rs create mode 100644 examples/nim_timer/rust_bindings/src/ffi.rs create mode 100644 examples/nim_timer/rust_bindings/src/lib.rs create mode 100644 examples/nim_timer/rust_bindings/src/types.rs create mode 100644 examples/nim_timer/rust_client/README.md create mode 100644 examples/nim_timer/rust_client/src/tokio_main.rs diff --git a/.gitignore b/.gitignore index e20ab27..0d37f33 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,10 @@ tests/test_ffi_context tests/test_serial # Generated binding crates (regenerated by `nimble genbindings_*`) -examples/**/nim_bindings/ +examples/**/rust_bindings/target/ + +# Example build artifacts +examples/**/cpp_bindings/build/ # Cargo build artifacts examples/**/rust_client/target/ diff --git a/README.md b/README.md index 9ef2548..4c9a0f6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # nim-ffi Allows exposing Nim projects to other languages + +## Example + +`examples/nim_timer` is now a self-contained Nimble project that imports `nim-ffi` directly. +Use `cd examples/nim_timer && nimble install -y ../.. && nimble build` to compile the example. diff --git a/examples/nim_timer/README.md b/examples/nim_timer/README.md new file mode 100644 index 0000000..b0d59f6 --- /dev/null +++ b/examples/nim_timer/README.md @@ -0,0 +1,54 @@ +# nim_timer example + +This example is a self-contained Nimble project demonstrating how to import `nim-ffi` and use the `.ffiCtor.` / `.ffi.` abstraction. + +## Usage + +1. Change into the example directory: + ```sh + cd examples/nim_timer + ``` + +2. Install the local `ffi` dependency: + ```sh + nimble install -y ../.. + ``` + +3. Build the example library: + ```sh + nimble build + ``` + +4. Generate bindings: + ```sh + nimble genbindings_rust + nimble genbindings_cpp + ``` + +## Rust example clients + +The Rust client lives in `examples/nim_timer/rust_client`. + +- Run the sync example: + ```sh + cd examples/nim_timer/rust_client + cargo run --bin rust_client + ``` + +- Run the Tokio example: + ```sh + cd examples/nim_timer/rust_client + cargo run --bin tokio_client + ``` + +## C++ example + +The generated C++ example lives in `examples/nim_timer/cpp_bindings`. + +Build and run it with: +```sh +cd examples/nim_timer/cpp_bindings +cmake -S . -B build +cmake --build build +./build/example +``` diff --git a/examples/nim_timer/cpp_bindings/CMakeLists.txt b/examples/nim_timer/cpp_bindings/CMakeLists.txt new file mode 100644 index 0000000..eabfd9a --- /dev/null +++ b/examples/nim_timer/cpp_bindings/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.14) +project(nimtimer_cpp_bindings CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── nlohmann/json ───────────────────────────────────────────────────────────── +include(FetchContent) +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(nlohmann_json) + +# ── Locate the repository root (contains ffi.nimble) ───────────────────────── +set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") +set(REPO_ROOT "") +foreach(_i RANGE 10) + if(EXISTS "${_search_dir}/ffi.nimble") + set(REPO_ROOT "${_search_dir}") + break() + endif() + get_filename_component(_search_dir "${_search_dir}" DIRECTORY) +endforeach() +if("${REPO_ROOT}" STREQUAL "") + message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)") +endif() + +# ── Nim source path ─────────────────────────────────────────────────────────── +get_filename_component(NIM_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/../nim_timer.nim" + ABSOLUTE) + +# ── Compile the Nim shared library ─────────────────────────────────────────── +find_program(NIM_EXECUTABLE nim REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NIM_LIB_FILE "${REPO_ROOT}/libnimtimer.dylib") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_LIB_FILE "${REPO_ROOT}/nimtimer.dll") +else() + set(NIM_LIB_FILE "${REPO_ROOT}/libnimtimer.so") +endif() + +add_custom_command( + OUTPUT "${NIM_LIB_FILE}" + COMMAND "${NIM_EXECUTABLE}" c + --mm:orc + -d:chronicles_log_level=WARN + --app:lib + --noMain + "--nimMainPrefix:libnimtimer" + "-o:${NIM_LIB_FILE}" + "${NIM_SRC}" + WORKING_DIRECTORY "${REPO_ROOT}" + DEPENDS "${NIM_SRC}" + COMMENT "Compiling Nim library libnimtimer" + VERBATIM +) +add_custom_target(nim_lib ALL DEPENDS "${NIM_LIB_FILE}") + +add_library(nimtimer SHARED IMPORTED GLOBAL) +set_target_properties(nimtimer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +add_dependencies(nimtimer nim_lib) + +# ── Interface target exposing the generated header ──────────────────────────── +add_library(nimtimer_headers INTERFACE) +target_include_directories(nimtimer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(nimtimer_headers INTERFACE nimtimer nlohmann_json::nlohmann_json) + +# ── Optional example executable ─────────────────────────────────────────────── +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") + add_executable(example main.cpp) + target_link_libraries(example PRIVATE nimtimer_headers) + add_dependencies(example nim_lib) +endif() diff --git a/examples/nim_timer/cpp_bindings/README.md b/examples/nim_timer/cpp_bindings/README.md new file mode 100644 index 0000000..49f94b5 --- /dev/null +++ b/examples/nim_timer/cpp_bindings/README.md @@ -0,0 +1,36 @@ +# C++ Bindings for nim-timer + +## Purpose + +This folder contains **auto-generated C++ bindings** for the `nim_timer` Nim library. It is generated from `../nim_timer.nim` and provides: + +- `nimtimer.hpp`: High-level C++ class (`NimTimerCtx`) wrapping the FFI interface +- `main.cpp`: Example executable demonstrating how to use the bindings +- `CMakeLists.txt`: Build configuration that compiles the Nim library and links the C++ example + +## How It's Generated + +Generate or regenerate these bindings by running from the parent directory: + +```sh +cd examples/nim_timer +nimble genbindings_cpp +``` + +This command: +1. Invokes the Nim compiler with `-d:targetLang:cpp` flag +2. Triggers `genBindings("examples/nim_timer/cpp_bindings", "../nim_timer.nim")` in `nim_timer.nim` +3. Creates/updates the generated binding files + +## Building the Example + +```sh +cd examples/nim_timer/cpp_bindings +cmake -S . -B build +cmake --build build +./build/example +``` + +## Do Not Edit + +The generated files in this folder are overwritten each time `nimble genbindings_cpp` runs. Any manual changes will be lost. diff --git a/examples/nim_timer/cpp_bindings/main.cpp b/examples/nim_timer/cpp_bindings/main.cpp new file mode 100644 index 0000000..68e9658 --- /dev/null +++ b/examples/nim_timer/cpp_bindings/main.cpp @@ -0,0 +1,37 @@ +#include "nimtimer.hpp" +#include + +int main() { + try { + auto ctx = NimTimerCtx::create(TimerConfig{"cpp-demo"}); + std::cout << "[1] Context created\n"; + + auto version = ctx.version(); + std::cout << "[2] Version: " << version << "\n"; + + auto echo = ctx.echo(EchoRequest{"hello from C++", 200}); + std::cout << "[3] Echo 1: echoed=" << echo.echoed + << ", timerName=" << echo.timerName << "\n"; + + auto echo2 = ctx.echo(EchoRequest{"second C++ request", 50}); + std::cout << "[4] Echo 2: echoed=" << echo2.echoed + << ", timerName=" << echo2.timerName << "\n"; + + auto complexReq = ComplexRequest{ + std::vector{EchoRequest{"one", 10}, EchoRequest{"two", 20}}, + std::vector{"fast", "async"}, + std::optional("extra note"), + std::optional(3) + }; + auto complex = ctx.complex(complexReq); + std::cout << "[5] Complex: summary=" << complex.summary + << ", itemCount=" << complex.itemCount + << ", hasNote=" << complex.hasNote << "\n"; + + std::cout << "\nDone.\n"; + } catch (const std::exception& ex) { + std::cerr << "Error: " << ex.what() << "\n"; + return 1; + } + return 0; +} diff --git a/examples/nim_timer/cpp_bindings/nimtimer.hpp b/examples/nim_timer/cpp_bindings/nimtimer.hpp new file mode 100644 index 0000000..a648d84 --- /dev/null +++ b/examples/nim_timer/cpp_bindings/nimtimer.hpp @@ -0,0 +1,174 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nlohmann { + template + void to_json(json& j, const std::optional& opt) { + if (opt) j = *opt; + else j = nullptr; + } + + template + void from_json(const json& j, std::optional& opt) { + if (j.is_null()) opt = std::nullopt; + else opt = j.get(); + } +} + +// ============================================================ +// Types +// ============================================================ + +struct TimerConfig { + std::string name; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TimerConfig, name) + +struct EchoRequest { + std::string message; + int64_t delayMs; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EchoRequest, message, delayMs) + +struct EchoResponse { + std::string echoed; + std::string timerName; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EchoResponse, echoed, timerName) + +struct ComplexRequest { + std::vector messages; + std::vector tags; + std::optional note; + std::optional retries; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ComplexRequest, messages, tags, note, retries) + +struct ComplexResponse { + std::string summary; + int64_t itemCount; + bool hasNote; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ComplexResponse, summary, itemCount, hasNote) + +// ============================================================ +// C FFI declarations +// ============================================================ + +extern "C" { +typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data); + +int nimtimer_create(const char* config_json, FfiCallback callback, void* user_data); +int nimtimer_echo(void* ctx, FfiCallback callback, void* user_data, const char* req_json); +int nimtimer_version(void* ctx, FfiCallback callback, void* user_data); +int nimtimer_complex(void* ctx, FfiCallback callback, void* user_data, const char* req_json); +} // extern "C" + + +template +inline std::string serializeFfiArg(const T& value) { + return nlohmann::json(value).dump(); +} + +inline std::string serializeFfiArg(void* value) { + return std::to_string(reinterpret_cast(value)); +} + +template +inline T deserializeFfiResult(const std::string& raw) { + return nlohmann::json::parse(raw).get(); +} + +template<> +inline void* deserializeFfiResult(const std::string& raw) { + return reinterpret_cast(static_cast(std::stoull(raw))); +} + +// ============================================================ +// Synchronous call helper (anonymous namespace, header-only) +// ============================================================ + +namespace { + +struct FfiCallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::string msg; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t /*len*/, void* ud) { + auto* s = static_cast(ud); + std::lock_guard lock(s->mtx); + s->ok = (ret == 0); + s->msg = msg ? std::string(msg) : std::string{}; + s->done = true; + s->cv.notify_one(); +} + +inline std::string ffi_call_(std::function f) { + FfiCallState_ state; + const int ret = f(ffi_cb_, &state); + if (ret == 2) + throw std::runtime_error("RET_MISSING_CALLBACK (internal error)"); + std::unique_lock lock(state.mtx); + state.cv.wait(lock, [&state]{ return state.done; }); + if (!state.ok) + throw std::runtime_error(state.msg); + return state.msg; +} + +} // anonymous namespace + +// ============================================================ +// High-level C++ context class +// ============================================================ + +class NimTimerCtx { +public: + static NimTimerCtx create(const TimerConfig& config) { + const auto config_json = serializeFfiArg(config); + const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { + return nimtimer_create(config_json.c_str(), cb, ud); + }); + // ctor returns the context address as a plain decimal string + const auto addr = std::stoull(raw); + return NimTimerCtx(reinterpret_cast(static_cast(addr))); + } + + EchoResponse echo(const EchoRequest& req) const { + const auto req_json = serializeFfiArg(req); + const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { + return nimtimer_echo(ptr_, cb, ud, req_json.c_str()); + }); + return deserializeFfiResult(raw); + } + + std::string version() const { + const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { + return nimtimer_version(ptr_, cb, ud); + }); + return deserializeFfiResult(raw); + } + + ComplexResponse complex(const ComplexRequest& req) const { + const auto req_json = serializeFfiArg(req); + const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { + return nimtimer_complex(ptr_, cb, ud, req_json.c_str()); + }); + return deserializeFfiResult(raw); + } + +private: + void* ptr_; + explicit NimTimerCtx(void* p) : ptr_(p) {} +}; diff --git a/examples/nim_timer/nim_timer.nim b/examples/nim_timer/nim_timer.nim index b9e25a9..4df4391 100644 --- a/examples/nim_timer/nim_timer.nim +++ b/examples/nim_timer/nim_timer.nim @@ -1,10 +1,12 @@ -import ffi, chronos +import ffi, chronos, options + +type Maybe[T] = Option[T] 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 + name: string # set at creation time, read back in each response ffiType: type TimerConfig = object @@ -13,12 +15,25 @@ ffiType: ffiType: type EchoRequest = object message: string - delayMs: int # how long chronos sleeps before replying + 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 + timerName: string # proves that the timer's own state is accessible + +ffiType: + type ComplexRequest = object + messages: seq[EchoRequest] + tags: seq[string] + note: Option[string] + retries: Maybe[int] + +ffiType: + type ComplexResponse = object + summary: string + itemCount: int + hasNote: bool # --- Constructor ----------------------------------------------------------- # Called once from Rust. Creates the FFIContext + NimTimer. @@ -26,7 +41,7 @@ ffiType: proc nimtimer_create*( config: TimerConfig ): Future[Result[NimTimer, string]] {.ffiCtor.} = - await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread + await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread return ok(NimTimer(name: config.name)) # --- Async method ---------------------------------------------------------- @@ -41,10 +56,18 @@ proc nimtimer_echo*( # --- 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.} = +proc nimtimer_version*(timer: NimTimer): Future[Result[string, string]] {.ffi.} = return ok("nim-timer v0.1.0") -when defined(ffiGenBindings): - genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim") +proc nimtimer_complex*( + timer: NimTimer, req: ComplexRequest +): Future[Result[ComplexResponse, string]] {.ffi.} = + let note = if req.note.isSome: req.note.get else: "" + let retries = if req.retries.isSome: req.retries.get else: 0 + let count = req.messages.len + let summary = + "received " & $count & " messages, note=" & note & ", retries=" & $retries + return + ok(ComplexResponse(summary: summary, itemCount: count, hasNote: req.note.isSome)) + +genBindings() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags diff --git a/examples/nim_timer/nim_timer.nimble b/examples/nim_timer/nim_timer.nimble new file mode 100644 index 0000000..103e922 --- /dev/null +++ b/examples/nim_timer/nim_timer.nimble @@ -0,0 +1,21 @@ +version = "0.1.0" +packageName = "nimtimer" +author = "Institute of Free Technology" +description = "Example Nim timer library using nim-ffi" +license = "MIT or Apache License 2.0" + +requires "nim >= 2.2.4" +requires "chronos" +requires "chronicles" +requires "taskpools" +requires "ffi >= 0.1.3" + +# Build the example library and optionally generate bindings. +task build, "Compile the nimtimer library": + exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim" + +task genbindings_rust, "Generate Rust bindings for the nimtimer example": + exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim" + +task genbindings_cpp, "Generate C++ bindings for the nimtimer example": + exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=cpp nim_timer.nim" diff --git a/examples/nim_timer/rust_bindings/Cargo.toml b/examples/nim_timer/rust_bindings/Cargo.toml new file mode 100644 index 0000000..68ef056 --- /dev/null +++ b/examples/nim_timer/rust_bindings/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "nimtimer" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/examples/nim_timer/rust_bindings/README.md b/examples/nim_timer/rust_bindings/README.md new file mode 100644 index 0000000..2c7deca --- /dev/null +++ b/examples/nim_timer/rust_bindings/README.md @@ -0,0 +1,39 @@ +# Rust Bindings for nim-timer + +## Purpose + +This folder contains **auto-generated Rust bindings** (the `nimtimer` crate) for the `nim_timer` Nim library. It is generated from `../nim_timer.nim` and provides: + +- `src/lib.rs`: Main library exposing high-level Rust types and the `NimTimerCtx` API +- `src/api.rs`: High-level async/sync wrapper around the FFI +- `src/ffi.rs`: Raw `extern "C"` declarations for the Nim library +- `src/types.rs`: Serializable Rust types matching the Nim FFI types +- `build.rs`: Build script that compiles the Nim library to `libnimtimer.dylib` (or `.so`/`.dll`) +- `Cargo.toml`: Package manifest with serde and serde_json dependencies + +## How It's Generated + +Generate or regenerate these bindings by running from the parent directory: + +```sh +cd examples/nim_timer +nimble genbindings_rust +``` + +This command: +1. Invokes the Nim compiler with `-d:targetLang:rust` flag +2. Triggers `genBindings("examples/nim_timer/rust_bindings", "../nim_timer.nim")` in `nim_timer.nim` +3. Creates/updates the generated binding files + +## Using as a Dependency + +The `rust_client` example consumes this crate: + +```toml +[dependencies] +nimtimer = { path = "../rust_bindings" } +``` + +## Do Not Edit + +The generated files in this folder are overwritten each time `nimble genbindings_rust` runs. Any manual changes will be lost. diff --git a/examples/nim_timer/rust_bindings/build.rs b/examples/nim_timer/rust_bindings/build.rs new file mode 100644 index 0000000..b5b12a8 --- /dev/null +++ b/examples/nim_timer/rust_bindings/build.rs @@ -0,0 +1,47 @@ +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("../nim_timer.nim"); + let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("../nim_timer.nim")); + + // 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!("libnimtimer.{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:libnimtimer")) + .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=nimtimer"); + println!("cargo:rerun-if-changed={}", nim_src.display()); +} diff --git a/examples/nim_timer/rust_bindings/src/api.rs b/examples/nim_timer/rust_bindings/src/api.rs new file mode 100644 index 0000000..7aa056f --- /dev/null +++ b/examples/nim_timer/rust_bindings/src/api.rs @@ -0,0 +1,102 @@ +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; +use super::ffi; +use super::types::*; + +#[derive(Default)] +struct FfiCallbackResult { + payload: Option>, +} + +type Pair = Arc<(Mutex, Condvar)>; + +unsafe extern "C" fn on_result( + ret: c_int, + msg: *const c_char, + _len: usize, + user_data: *mut c_void, +) { + let pair = Arc::from_raw(user_data as *const (Mutex, Condvar)); + { + let (lock, cvar) = &*pair; + let mut state = lock.lock().unwrap(); + state.payload = Some(if ret == 0 { + Ok(CStr::from_ptr(msg).to_string_lossy().into_owned()) + } else { + Err(CStr::from_ptr(msg).to_string_lossy().into_owned()) + }); + cvar.notify_one(); + } + std::mem::forget(pair); +} + +fn ffi_call(timeout: Duration, f: F) -> Result +where + F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int, +{ + let pair: Pair = Arc::new((Mutex::new(FfiCallbackResult::default()), Condvar::new())); + let raw = Arc::into_raw(pair.clone()) as *mut c_void; + let ret = f(on_result, raw); + if ret == 2 { + return Err("RET_MISSING_CALLBACK (internal error)".into()); + } + let (lock, cvar) = &*pair; + let guard = lock.lock().unwrap(); + let (guard, timed_out) = cvar + .wait_timeout_while(guard, timeout, |s| s.payload.is_none()) + .unwrap(); + if timed_out.timed_out() { + return Err(format!("timed out after {:?}", timeout)); + } + guard.payload.clone().unwrap() +} + +/// High-level context for `NimTimer`. +pub struct NimTimerCtx { + ptr: *mut c_void, + timeout: Duration, +} + +unsafe impl Send for NimTimerCtx {} +unsafe impl Sync for NimTimerCtx {} + +impl NimTimerCtx { + pub fn create(config: TimerConfig, timeout: Duration) -> Result { + let config_json = serde_json::to_string(&config).map_err(|e| e.to_string())?; + let config_c = CString::new(config_json).unwrap(); + let raw = ffi_call(timeout, |cb, ud| unsafe { + ffi::nimtimer_create(config_c.as_ptr(), cb, ud) + })?; + // ctor returns the context address as a plain decimal string + let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; + Ok(Self { ptr: addr as *mut c_void, timeout }) + } + + pub fn echo(&self, req: EchoRequest) -> Result { + let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?; + let req_c = CString::new(req_json).unwrap(); + let raw = ffi_call(self.timeout, |cb, ud| unsafe { + ffi::nimtimer_echo(self.ptr, cb, ud, req_c.as_ptr()) + })?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) + } + + pub fn version(&self) -> Result { + let raw = ffi_call(self.timeout, |cb, ud| unsafe { + ffi::nimtimer_version(self.ptr, cb, ud) + })?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) + } + + pub fn complex(&self, req: ComplexRequest) -> Result { + let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?; + let req_c = CString::new(req_json).unwrap(); + let raw = ffi_call(self.timeout, |cb, ud| unsafe { + ffi::nimtimer_complex(self.ptr, cb, ud, req_c.as_ptr()) + })?; + serde_json::from_str::(&raw).map_err(|e| e.to_string()) + } + +} diff --git a/examples/nim_timer/rust_bindings/src/ffi.rs b/examples/nim_timer/rust_bindings/src/ffi.rs new file mode 100644 index 0000000..2cc1172 --- /dev/null +++ b/examples/nim_timer/rust_bindings/src/ffi.rs @@ -0,0 +1,16 @@ +use std::os::raw::{c_char, c_int, c_void}; + +pub type FfiCallback = unsafe extern "C" fn( + ret: c_int, + msg: *const c_char, + len: usize, + user_data: *mut c_void, +); + +#[link(name = "nimtimer")] +extern "C" { + pub fn nimtimer_create(config_json: *const c_char, callback: FfiCallback, user_data: *mut c_void) -> c_int; + pub fn nimtimer_echo(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void, req_json: *const c_char) -> c_int; + pub fn nimtimer_version(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void) -> c_int; + pub fn nimtimer_complex(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void, req_json: *const c_char) -> c_int; +} diff --git a/examples/nim_timer/rust_bindings/src/lib.rs b/examples/nim_timer/rust_bindings/src/lib.rs new file mode 100644 index 0000000..29c439a --- /dev/null +++ b/examples/nim_timer/rust_bindings/src/lib.rs @@ -0,0 +1,5 @@ +mod ffi; +mod types; +mod api; +pub use types::*; +pub use api::*; diff --git a/examples/nim_timer/rust_bindings/src/types.rs b/examples/nim_timer/rust_bindings/src/types.rs new file mode 100644 index 0000000..9037805 --- /dev/null +++ b/examples/nim_timer/rust_bindings/src/types.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerConfig { + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoRequest { + pub message: String, + #[serde(rename = "delayMs")] + pub delay_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoResponse { + pub echoed: String, + #[serde(rename = "timerName")] + pub timer_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplexRequest { + pub messages: Vec, + pub tags: Vec, + pub note: Option, + pub retries: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplexResponse { + pub summary: String, + #[serde(rename = "itemCount")] + pub item_count: i64, + #[serde(rename = "hasNote")] + pub has_note: bool, +} diff --git a/examples/nim_timer/rust_client/Cargo.lock b/examples/nim_timer/rust_client/Cargo.lock index 51c3923..5c3459e 100644 --- a/examples/nim_timer/rust_client/Cargo.lock +++ b/examples/nim_timer/rust_client/Cargo.lock @@ -22,6 +22,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -46,6 +52,7 @@ version = "0.1.0" dependencies = [ "nimtimer", "serde_json", + "tokio", ] [[package]] @@ -102,6 +109,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/examples/nim_timer/rust_client/Cargo.toml b/examples/nim_timer/rust_client/Cargo.toml index 0189b2f..cd21745 100644 --- a/examples/nim_timer/rust_client/Cargo.toml +++ b/examples/nim_timer/rust_client/Cargo.toml @@ -4,5 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -nimtimer = { path = "../nim_bindings" } +nimtimer = { path = "../rust_bindings" } serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +[[bin]] +name = "rust_client" +path = "src/main.rs" + +[[bin]] +name = "tokio_client" +path = "src/tokio_main.rs" diff --git a/examples/nim_timer/rust_client/README.md b/examples/nim_timer/rust_client/README.md new file mode 100644 index 0000000..bf818af --- /dev/null +++ b/examples/nim_timer/rust_client/README.md @@ -0,0 +1,43 @@ +# Rust Client Examples + +## Purpose + +This folder contains **example Rust applications** that demonstrate how to use the auto-generated `nimtimer` crate (from `../rust_bindings`). + +## What's Included + +Two executable examples: + +- **`rust_client`** — Synchronous example + - Shows basic synchronous calls to the Nim timer API + - Uses blocking wait with condition variables + - Source: `src/main.rs` + +- **`tokio_client`** — Asynchronous example with Tokio runtime + - Demonstrates the Tokio async runtime integration + - Uses `spawn_blocking` to handle the blocking FFI callbacks on a separate thread pool + - Source: `src/tokio_main.rs` + +## Building + +```sh +cd examples/nim_timer/rust_client +cargo build +``` + +## Running + +```sh +# Sync example +cargo run --bin rust_client + +# Tokio async example +cargo run --bin tokio_client +``` + +## Important Notes + +- The `nimtimer` crate is a **local dependency** (`path = "../rust_bindings"`) +- It is **auto-generated** — do not manually edit it +- These examples are **not** part of the generated output; they are hand-written to show usage patterns +- To regenerate the `nimtimer` crate, run `nimble genbindings_rust` from the parent directory diff --git a/examples/nim_timer/rust_client/src/main.rs b/examples/nim_timer/rust_client/src/main.rs index cd7f5c2..33594cf 100644 --- a/examples/nim_timer/rust_client/src/main.rs +++ b/examples/nim_timer/rust_client/src/main.rs @@ -3,7 +3,7 @@ // 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: +// To regenerate the `rust_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}; diff --git a/examples/nim_timer/rust_client/src/tokio_main.rs b/examples/nim_timer/rust_client/src/tokio_main.rs new file mode 100644 index 0000000..abb8934 --- /dev/null +++ b/examples/nim_timer/rust_client/src/tokio_main.rs @@ -0,0 +1,71 @@ +use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::task; + +#[tokio::main(flavor = "multi_thread", worker_threads = 2)] +async fn main() { + let timeout = Duration::from_secs(5); + + let ctx = task::spawn_blocking(move || { + NimTimerCtx::create(TimerConfig { name: "tokio-demo".into() }, timeout) + }) + .await + .expect("failed to join create task") + .expect("nimtimer_create failed"); + + let ctx = Arc::new(Mutex::new(ctx)); + + let version = task::spawn_blocking({ + let ctx = Arc::clone(&ctx); + move || { + let ctx = ctx.lock().unwrap(); + ctx.version() + } + }) + .await + .expect("failed to join version task") + .expect("nimtimer_version failed"); + + println!("[1] Tokio runtime started"); + println!("[2] Version: {version}"); + + let req1 = EchoRequest { + message: "hello from tokio".into(), + delay_ms: 200, + }; + let req2 = EchoRequest { + message: "second tokio request".into(), + delay_ms: 50, + }; + + let fut1 = task::spawn_blocking({ + let ctx = Arc::clone(&ctx); + move || { + let ctx = ctx.lock().unwrap(); + ctx.echo(req1) + } + }); + + let fut2 = task::spawn_blocking({ + let ctx = Arc::clone(&ctx); + move || { + let ctx = ctx.lock().unwrap(); + ctx.echo(req2) + } + }); + + let echo1 = fut1 + .await + .expect("failed to join tokio blocking task") + .expect("nimtimer_echo failed"); + let echo2 = fut2 + .await + .expect("failed to join tokio blocking task") + .expect("nimtimer_echo failed"); + + println!("[3] Echo 1: echoed={}, timerName={}", echo1.echoed, echo1.timer_name); + println!("[4] Echo 2: echoed={}, timerName={}", echo2.echoed, echo2.timer_name); + + println!("\nDone. Tokio runtime shut down."); +} diff --git a/ffi.nimble b/ffi.nimble index c94c3dc..6a635bb 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -5,7 +5,7 @@ author = "Institute of Free Technology" description = "FFI framework with custom header generation" license = "MIT or Apache License 2.0" -packageName = "ffi" +packageName = "ffi" requires "nim >= 2.2.4" requires "chronos" @@ -32,7 +32,17 @@ task test_serial, "Run serial unit tests": exec "nim c -r " & nimFlags & " tests/test_serial.nim" task genbindings_rust, "Generate Rust bindings for the nim_timer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=rust -o:/dev/null examples/nim_timer/nim_timer.nim" + exec "nim c " & nimFlags & + " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " -d:ffiGenBindings -d:targetLang=rust" & + " -d:ffiOutputDir=examples/nim_timer/rust_bindings" & + " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -o:/dev/null examples/nim_timer/nim_timer.nim" task genbindings_cpp, "Generate C++ bindings for the nim_timer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=cpp -o:/dev/null examples/nim_timer/nim_timer.nim" + exec "nim c " & nimFlags & + " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " -d:ffiGenBindings -d:targetLang=cpp" & + " -d:ffiOutputDir=examples/nim_timer/cpp_bindings" & + " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -o:/dev/null examples/nim_timer/nim_timer.nim" diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 1cdd708..43b088c 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -4,15 +4,36 @@ import std/[os, strutils] import ./meta +proc genericInnerType(typeName, prefix: string): string = + if typeName.startsWith(prefix) and typeName.endsWith("]"): + let start = prefix.len + let lastIndex = typeName.len - 2 + return typeName[start .. lastIndex] + return "" + proc nimTypeToCpp*(typeName: string): string = - case typeName + let trimmed = typeName.strip() + if trimmed.startsWith("ptr "): + return "void*" + else: + let seqInner = genericInnerType(trimmed, "seq[") + if seqInner.len > 0: + return "std::vector<" & nimTypeToCpp(seqInner) & ">" + let optionInner = genericInnerType(trimmed, "Option[") + if optionInner.len > 0: + return "std::optional<" & nimTypeToCpp(optionInner) & ">" + let maybeInner = genericInnerType(trimmed, "Maybe[") + if maybeInner.len > 0: + return "std::optional<" & nimTypeToCpp(maybeInner) & ">" + case trimmed of "string", "cstring": "std::string" of "int", "int64": "int64_t" of "int32": "int32_t" of "bool": "bool" - of "float", "float64": "double" + of "float": "float" + of "float64": "double" of "pointer": "void*" - else: typeName + else: trimmed proc stripLibPrefixCpp(procName, libName: string): string = let prefix = libName & "_" @@ -21,9 +42,7 @@ proc stripLibPrefixCpp(procName, libName: string): string = return procName proc generateCppHeader*( - procs: seq[FFIProcMeta], - types: seq[FFITypeMeta], - libName: string, + procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string ): string = var lines: seq[string] = @[] @@ -34,8 +53,24 @@ proc generateCppHeader*( lines.add("#include ") lines.add("#include ") lines.add("#include ") + lines.add("#include ") + lines.add("#include ") lines.add("#include ") lines.add("") + lines.add("namespace nlohmann {") + lines.add(" template") + lines.add(" void to_json(json& j, const std::optional& opt) {") + lines.add(" if (opt) j = *opt;") + lines.add(" else j = nullptr;") + lines.add(" }") + lines.add("") + lines.add(" template") + lines.add(" void from_json(const json& j, std::optional& opt) {") + lines.add(" if (j.is_null()) opt = std::nullopt;") + lines.add(" else opt = j.get();") + lines.add(" }") + lines.add("}") + lines.add("") # Types if types.len > 0: @@ -52,7 +87,9 @@ proc generateCppHeader*( var fieldNames: seq[string] = @[] for f in t.fields: fieldNames.add(f.name) - lines.add("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")]) + lines.add( + "NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")] + ) lines.add("") # C extern declarations @@ -61,7 +98,9 @@ proc generateCppHeader*( lines.add("// ============================================================") lines.add("") lines.add("extern \"C\" {") - lines.add("typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);") + lines.add( + "typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);" + ) lines.add("") for p in procs: @@ -82,6 +121,30 @@ proc generateCppHeader*( lines.add("} // extern \"C\"") lines.add("") + # Transport serialization helpers + lines.add("") + lines.add("template") + lines.add("inline std::string serializeFfiArg(const T& value) {") + lines.add(" return nlohmann::json(value).dump();") + lines.add("}") + lines.add("") + lines.add("inline std::string serializeFfiArg(void* value) {") + lines.add(" return std::to_string(reinterpret_cast(value));") + lines.add("}") + lines.add("") + lines.add("template") + lines.add("inline T deserializeFfiResult(const std::string& raw) {") + lines.add(" return nlohmann::json::parse(raw).get();") + lines.add("}") + lines.add("") + lines.add("template<>") + lines.add("inline void* deserializeFfiResult(const std::string& raw) {") + lines.add( + " return reinterpret_cast(static_cast(std::stoull(raw)));" + ) + lines.add("}") + lines.add("") + # Anonymous namespace with synchronous call helper lines.add("// ============================================================") lines.add("// Synchronous call helper (anonymous namespace, header-only)") @@ -110,7 +173,9 @@ proc generateCppHeader*( lines.add(" FfiCallState_ state;") lines.add(" const int ret = f(ffi_cb_, &state);") lines.add(" if (ret == 2)") - lines.add(" throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");") + lines.add( + " throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");" + ) lines.add(" std::unique_lock lock(state.mtx);") lines.add(" state.cv.wait(lock, [&state]{ return state.done; });") lines.add(" if (!state.ok)") @@ -125,12 +190,16 @@ proc generateCppHeader*( var ctors: seq[FFIProcMeta] = @[] var methods: seq[FFIProcMeta] = @[] for p in procs: - if p.kind == ffiCtorKind: ctors.add(p) - else: methods.add(p) + if p.kind == ffiCtorKind: + ctors.add(p) + else: + methods.add(p) let libTypeName = - if ctors.len > 0: ctors[0].libTypeName - else: libName[0 .. 0].toUpperAscii() & libName[1 .. ^1] + if ctors.len > 0: + ctors[0].libTypeName + else: + libName[0 .. 0].toUpperAscii() & libName[1 .. ^1] let ctxTypeName = libTypeName & "Ctx" @@ -150,7 +219,7 @@ proc generateCppHeader*( lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParams.join(", ")]) for ep in ctor.extraParams: - lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name]) + lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name]) var callArgs: seq[string] = @[] for ep in ctor.extraParams: @@ -163,7 +232,10 @@ proc generateCppHeader*( lines.add(" });") lines.add(" // ctor returns the context address as a plain decimal string") lines.add(" const auto addr = std::stoull(raw);") - lines.add(" return $1(reinterpret_cast(static_cast(addr)));" % [ctxTypeName]) + lines.add( + " return $1(reinterpret_cast(static_cast(addr)));" % + [ctxTypeName] + ) lines.add(" }") lines.add("") @@ -180,7 +252,7 @@ proc generateCppHeader*( lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr]) for ep in m.extraParams: - lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name]) + lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name]) var callArgs = @["ptr_", "cb", "ud"] for ep in m.extraParams: @@ -190,10 +262,10 @@ proc generateCppHeader*( lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")]) lines.add(" });") - if retCppType == "std::string": - lines.add(" return nlohmann::json::parse(raw).get();") + if retCppType == "void*": + lines.add(" return deserializeFfiResult(raw);") else: - lines.add(" return nlohmann::json::parse(raw).get<$1>();" % [retCppType]) + lines.add(" return deserializeFfiResult<$1>(raw);" % [retCppType]) lines.add(" }") lines.add("") @@ -210,12 +282,12 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = ## CMake uses ${...} which would clash with Nim's % format operator, ## so we build the file line by line using string concatenation. let src = nimSrcRelPath.replace("\\", "/") - let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand - let rv = "${REPO_ROOT}" - let lf = "${NIM_LIB_FILE}" - let nm = "${NIM_EXECUTABLE}" - let ns = "${NIM_SRC}" - let sd = "${_search_dir}" + let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand + let rv = "${REPO_ROOT}" + let lf = "${NIM_LIB_FILE}" + let nm = "${NIM_EXECUTABLE}" + let ns = "${NIM_SRC}" + let sd = "${_search_dir}" var L: seq[string] = @[] L.add("cmake_minimum_required(VERSION 3.14)") L.add("project(" & libName & "_cpp_bindings CXX)") @@ -223,7 +295,9 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = L.add("set(CMAKE_CXX_STANDARD 17)") L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)") L.add("") - L.add("# ── nlohmann/json ─────────────────────────────────────────────────────────────") + L.add( + "# ── nlohmann/json ─────────────────────────────────────────────────────────────" + ) L.add("include(FetchContent)") L.add("FetchContent_Declare(") L.add(" nlohmann_json") @@ -233,7 +307,9 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = L.add(")") L.add("FetchContent_MakeAvailable(nlohmann_json)") L.add("") - L.add("# ── Locate the repository root (contains ffi.nimble) ─────────────────────────") + L.add( + "# ── Locate the repository root (contains ffi.nimble) ─────────────────────────" + ) L.add("set(_search_dir \"" & cv & "\")") L.add("set(REPO_ROOT \"\")") L.add("foreach(_i RANGE 10)") @@ -244,15 +320,21 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = L.add(" get_filename_component(_search_dir \"" & sd & "\" DIRECTORY)") L.add("endforeach()") L.add("if(\"${REPO_ROOT}\" STREQUAL \"\")") - L.add(" message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")") + L.add( + " message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")" + ) L.add("endif()") L.add("") - L.add("# ── Nim source path ───────────────────────────────────────────────────────────") + L.add( + "# ── Nim source path ───────────────────────────────────────────────────────────" + ) L.add("get_filename_component(NIM_SRC") L.add(" \"" & cv & "/" & src & "\"") L.add(" ABSOLUTE)") L.add("") - L.add("# ── Compile the Nim shared library ───────────────────────────────────────────") + L.add( + "# ── Compile the Nim shared library ───────────────────────────────────────────" + ) L.add("find_program(NIM_EXECUTABLE nim REQUIRED)") L.add("") L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")") @@ -281,15 +363,24 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = L.add("add_custom_target(nim_lib ALL DEPENDS \"" & lf & "\")") L.add("") L.add("add_library(" & libName & " SHARED IMPORTED GLOBAL)") - L.add("set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")") + L.add( + "set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")" + ) L.add("add_dependencies(" & libName & " nim_lib)") L.add("") - L.add("# ── Interface target exposing the generated header ────────────────────────────") + L.add( + "# ── Interface target exposing the generated header ────────────────────────────" + ) L.add("add_library(" & libName & "_headers INTERFACE)") L.add("target_include_directories(" & libName & "_headers INTERFACE \"" & cv & "\")") - L.add("target_link_libraries(" & libName & "_headers INTERFACE " & libName & " nlohmann_json::nlohmann_json)") + L.add( + "target_link_libraries(" & libName & "_headers INTERFACE " & libName & + " nlohmann_json::nlohmann_json)" + ) L.add("") - L.add("# ── Optional example executable ───────────────────────────────────────────────") + L.add( + "# ── Optional example executable ───────────────────────────────────────────────" + ) L.add("if(EXISTS \"" & cv & "/main.cpp\")") L.add(" add_executable(example main.cpp)") L.add(" target_link_libraries(example PRIVATE " & libName & "_headers)") diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 39bf1b9..cc11a72 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -3,27 +3,27 @@ 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` + 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" + 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 + 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" + name*: string # e.g. "delayMs" + typeName*: string # e.g. "int" FFITypeMeta* = object name*: string @@ -34,5 +34,12 @@ var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta] var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta] var currentLibName* {.compileTime.}: string -# Target language for binding generation; override with -d:ffiTargetLang=cpp -const ffiTargetLang* {.strdefine.} = "rust" +# Target language for binding generation; override with -d:targetLang=cpp +const targetLang* {.strdefine.} = "rust" + +# Output directory for generated bindings; set with -d:ffiOutputDir=path/to/dir +const ffiOutputDir* {.strdefine.} = "" + +# Nim source path (relative to outputDir) embedded in generated build files; +# set with -d:ffiNimSrcRelPath=../relative/path.nim +const ffiNimSrcRelPath* {.strdefine.} = "" diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 444c1bd..da000e3 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -19,20 +19,28 @@ proc toSnakeCase*(s: string): string = proc toPascalCase*(s: string): string = ## Converts the first letter to uppercase. - if s.len == 0: return s + 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 + ## Maps Nim type names to Rust type names, including generics. + let t = typeName.strip() + if t.startsWith("seq[") and t.endsWith("]"): + return "Vec<" & nimTypeToRust(t[4 .. ^2]) & ">" + if t.startsWith("Option[") and t.endsWith("]"): + return "Option<" & nimTypeToRust(t[7 .. ^2]) & ">" + if t.startsWith("Maybe[") and t.endsWith("]"): + return "Option<" & nimTypeToRust(t[6 .. ^2]) & ">" + case t of "string", "cstring": "String" of "int", "int64": "i64" of "int32": "i32" of "bool": "bool" of "float", "float64": "f64" of "pointer": "usize" - else: toPascalCase(typeName) + else: toPascalCase(t) proc deriveLibName*(procs: seq[FFIProcMeta]): string = ## Extracts the common prefix before the first `_` from proc names. @@ -60,7 +68,8 @@ proc stripLibPrefix*(procName: string, libName: string): string = # --------------------------------------------------------------------------- proc generateCargoToml*(libName: string): string = - result = """[package] + result = + """[package] name = "$1" version = "0.1.0" edition = "2021" @@ -68,13 +77,15 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -""" % [libName] +""" % + [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; + result = + """use std::path::PathBuf; use std::process::Command; fn main() { @@ -121,7 +132,8 @@ fn main() { println!("cargo:rustc-link-lib=$2"); println!("cargo:rerun-if-changed={}", nim_src.display()); } -""" % [escapedSrc, libName] +""" % + [escapedSrc, libName] proc generateLibRs*(): string = result = """mod ffi; @@ -239,7 +251,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("use super::types::*;") lines.add("") - # FfiCallbackResult struct + # FfiCallbackResult + Pair lines.add("#[derive(Default)]") lines.add("struct FfiCallbackResult {") lines.add(" payload: Option>,") @@ -248,7 +260,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("type Pair = Arc<(Mutex, Condvar)>;") lines.add("") - # on_result callback + # on_result callback (Arc-based, blocking) lines.add("unsafe extern \"C\" fn on_result(") lines.add(" ret: c_int,") lines.add(" msg: *const c_char,") @@ -270,7 +282,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add("}") lines.add("") - # ffi_call helper + # Blocking ffi_call helper using Condvar::wait_timeout_while lines.add("fn ffi_call(timeout: Duration, f: F) -> Result") lines.add("where") lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,") @@ -325,10 +337,18 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = 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]) + 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_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 @@ -344,7 +364,9 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = 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( + " 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("") @@ -359,19 +381,34 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = 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: "" + let paramsStr = + if paramsList.len > 0: + ", " & paramsList.join(", ") + else: + "" - lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType]) + 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]) + 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_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 @@ -387,11 +424,16 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = # Deserialize return value if retRustType == "String": - lines.add(" serde_json::from_str::(&raw).map_err(|e| e.to_string())") + lines.add( + " serde_json::from_str::(&raw).map_err(|e| e.to_string())" + ) elif retRustType == "usize": lines.add(" raw.parse::().map_err(|e| e.to_string())") else: - lines.add(" serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType]) + lines.add( + " serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % + [retRustType] + ) lines.add(" }") lines.add("") diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 3f3ad6d..9be68da 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -200,6 +200,7 @@ proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode = FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`) proc destroyContent(content: pointer) {.nimcall.} = ffiDeleteReq(cast[ptr `reqTypeName`](content)) + ret[].deleteReqContent = destroyContent return ret ) @@ -363,12 +364,11 @@ proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode = # CreateNodeRequest.processFFIRequest(request, reqHandler) let asyncProc = newProc( name = newEmptyNode(), # anonymous proc - params = - @[ - returnType, - newIdentDefs(ident("request"), ident("pointer")), - newIdentDefs(ident("reqHandler"), ident("pointer")), - ], + params = @[ + returnType, + newIdentDefs(ident("request"), ident("pointer")), + newIdentDefs(ident("reqHandler"), ident("pointer")), + ], body = newBody, pragmas = nnkPragma.newTree(ident("async")), ) @@ -587,19 +587,27 @@ macro ffi*(prc: untyped): untyped = # Extract LibType from the first parameter let firstParam = formalParams[1] - let libParamName = firstParam[0] # e.g. `w` - let libTypeName = firstParam[1] # e.g. `Waku` + 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]]") + 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) + 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) + 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] = @[] @@ -613,7 +621,10 @@ macro ffi*(prc: untyped): untyped = # Generate type/proc names from proc name let procNameStr = block: let raw = $procName - if raw.endsWith("*"): raw[0 ..^ 2] else: raw + if raw.endsWith("*"): + raw[0 ..^ 2] + else: + raw let camelName = toCamelCase(procNameStr) # Names of generated things @@ -631,9 +642,8 @@ macro ffi*(prc: untyped): untyped = procName # Common exported params (needed for both branches) - let ctxType = nnkPtrTy.newTree( - nnkBracketExpr.newTree(ident("FFIContext"), libTypeName) - ) + let ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) if isAsync: # ------------------------------------------------------------------------- @@ -668,9 +678,8 @@ macro ffi*(prc: untyped): untyped = ) let ctxHandlerName = ident("ffiCtxHandler") - let ptrFfiCtx = nnkPtrTy.newTree( - nnkBracketExpr.newTree(ident("FFIContext"), libTypeName) - ) + let ptrFfiCtx = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) var lambdaParams = newSeq[NimNode]() lambdaParams.add(futStrStr) @@ -785,8 +794,10 @@ macro ffi*(prc: untyped): untyped = 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] + 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: @@ -794,19 +805,20 @@ macro ffi*(prc: untyped): untyped = 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, - )) + 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 @@ -881,7 +893,9 @@ macro ffi*(prc: untyped): untyped = 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) + callback( + RET_OK, unsafeAddr serialized[0], cast[csize_t](serialized.len), userData + ) return RET_OK let syncFfiProc = newProc( @@ -909,7 +923,9 @@ macro ffi*(prc: untyped): untyped = tn = $ptype[0] else: tn = $ptype - ffiExtraParamsSync.add(FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr)) + ffiExtraParamsSync.add( + FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr) + ) let retTypeInnerSync = resultInner[1] var retIsPtrSync = false var retTnSync = "" @@ -918,16 +934,18 @@ macro ffi*(prc: untyped): untyped = 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, - )) + ffiProcRegistry.add( + FFIProcMeta( + procName: procNameStr, + libName: currentLibName, + kind: ffiFfiKind, + libTypeName: $libTypeName, + extraParams: ffiExtraParamsSync, + returnTypeName: retTnSync, + returnIsPtr: retIsPtrSync, + isAsync: false, + ) + ) result = newStmtList(syncHelperProc, syncFfiProc) @@ -953,11 +971,15 @@ proc buildCtorRequestType(reqTypeName: NimNode, paramNames: seq[string]): NimNod if fields.len > 0: newTree(nnkRecList, fields) else: - newTree(nnkRecList, newTree(nnkIdentDefs, ident("_placeholder"), ident("pointer"), newEmptyNode())) + 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)) + result = + newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy)) when defined(ffiDumpMacros): echo result.repr @@ -990,9 +1012,8 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN var formalParams = newSeq[NimNode]() - let typedescParam = newIdentDefs( - ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName) - ) + 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"))) @@ -1018,6 +1039,7 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN var ret = FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`) proc destroyContent(content: pointer) {.nimcall.} = ffiDeleteReq(cast[ptr `reqTypeName`](content)) + ret[].deleteReqContent = destroyContent return ret @@ -1084,9 +1106,8 @@ proc buildCtorProcessFFIRequestProc( ) # The ctx param type: ptr FFIContext[LibType] - let ctxType = nnkPtrTy.newTree( - nnkBracketExpr.newTree(ident("FFIContext"), libTypeName) - ) + let ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) let typedescParam = newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) @@ -1152,9 +1173,8 @@ 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 ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) let returnType = nnkBracketExpr.newTree( ident("Future"), @@ -1173,20 +1193,18 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode = let asyncProc = newProc( name = newEmptyNode(), - params = - @[ - returnType, - newIdentDefs(ident("request"), ident("pointer")), - newIdentDefs(ident("reqHandler"), ident("pointer")), - ], + 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 - ) + result = + newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc) when defined(ffiDumpMacros): echo result.repr @@ -1219,13 +1237,21 @@ macro ffiCtor*(prc: untyped): untyped = 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]]") + 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) + 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) + 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) @@ -1256,9 +1282,11 @@ macro ffiCtor*(prc: untyped): untyped = # 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 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: @@ -1311,9 +1339,8 @@ macro ffiCtor*(prc: untyped): untyped = return RET_ERR # sendRequestToFFIThread using the gensym'd ctx - let sendCall = newCall( - newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall - ) + let sendCall = + newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall) let sendResIdent = genSym(nskLet, "sendRes") ffiBody.add quote do: @@ -1363,18 +1390,22 @@ macro ffiCtor*(prc: untyped): untyped = 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, - )) + 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) + result = newStmtList( + typeDef, deleteProc, ffiNewReqProc, helperProc, processProc, addToReg, ffiProc + ) when defined(ffiDumpMacros): echo result.repr @@ -1384,28 +1415,39 @@ macro ffiCtor*(prc: untyped): untyped = # --------------------------------------------------------------------------- macro genBindings*( - outputDir: static[string], - nimSrcRelPath: static[string] = "", + outputDir: static[string] = ffiOutputDir, + nimSrcRelPath: static[string] = ffiNimSrcRelPath, ): untyped = - ## Generates binding files for the target language set by -d:ffiTargetLang=. + ## Generates binding files for the language set by -d:targetLang=. ## Supported values: "rust" (default), "cpp" (case-insensitive). - ## Call at the END of your library file, after all {.ffiCtor.} and {.ffi.} procs. + ## Output path and nim source path default to -d:ffiOutputDir and + ## -d:ffiNimSrcRelPath, or can be passed as explicit arguments. + ## This macro is a no-op unless -d:ffiGenBindings is set. ## - ## Example: - ## genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim") - ## - ## Activate with: nim c -d:ffiGenBindings -d:ffiTargetLang=rust mylib.nim - ## or: nim c -d:ffiGenBindings -d:ffiTargetLang=cpp mylib.nim + ## Example (all via compile flags): + ## genBindings() + ## # nim c -d:ffiGenBindings -d:targetLang=rust \ + ## # -d:ffiOutputDir=examples/nim_timer/rust_bindings \ + ## # -d:ffiNimSrcRelPath=../nim_timer.nim mylib.nim when defined(ffiGenBindings): - let lang = ffiTargetLang.toLowerAscii() + if outputDir.len == 0: + error( + "genBindings: output directory is empty." & + " Pass it as an argument or set -d:ffiOutputDir=path/to/output" + ) + let lang = targetLang.toLowerAscii() let libName = deriveLibName(ffiProcRegistry) case lang of "rust": - generateRustCrate(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath) + generateRustCrate( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath + ) of "cpp", "c++": - generateCppBindings(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath) + generateCppBindings( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath + ) else: - error("genBindings: unknown ffiTargetLang '" & lang & "'. Use 'rust' or 'cpp'.") + error("genBindings: unknown targetLang '" & lang & "'. Use 'rust' or 'cpp'.") result = newEmptyNode() diff --git a/ffi/serial.nim b/ffi/serial.nim index 7a042d4..935db4b 100644 --- a/ffi/serial.nim +++ b/ffi/serial.nim @@ -1,13 +1,15 @@ -import std/[json, macros] +import std/[json, macros, sequtils, options] import results import ./codegen/meta proc ffiSerialize*(x: string): string = - $(%* x) + $(%*x) proc ffiSerialize*(x: cstring): string = - if x.isNil: "null" - else: ffiSerialize($x) + if x.isNil: + "null" + else: + ffiSerialize($x) proc ffiSerialize*(x: int): string = $x @@ -19,7 +21,7 @@ proc ffiSerialize*(x: bool): string = if x: "true" else: "false" proc ffiSerialize*(x: float): string = - $(%* x) + $(%*x) proc ffiSerialize*(x: pointer): string = $cast[uint](x) @@ -67,6 +69,18 @@ proc ffiDeserialize*(s: cstring, _: typedesc[pointer]): Result[pointer, string] proc ffiSerialize*[T](x: ptr T): string = $cast[uint](x) +proc ffiSerialize*[T](x: seq[T]): string = + var arr = newJArray() + for item in x: + arr.add(parseJson(ffiSerialize(item))) + result = $arr + +proc ffiSerialize*[T](x: Option[T]): string = + if x.isSome: + ffiSerialize(x.get) + else: + "null" + proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] = try: let address = cast[ptr T](uint(parseJson($s).getBiggestInt())) @@ -74,6 +88,38 @@ proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] = except Exception as e: err(e.msg) +proc ffiDeserialize*[T](s: cstring, _: typedesc[seq[T]]): Result[seq[T], string] = + try: + let node = parseJson($s) + if node.kind != JArray: + return err("expected JSON array") + var resultSeq: seq[T] = @[] + for item in node: + let itemJson = $item + let parsed = ffiDeserialize(itemJson.cstring, typedesc[T]) + if parsed.isOk: + resultSeq.add(parsed.get) + else: + return err(parsed.error) + ok(resultSeq) + except Exception as e: + err(e.msg) + +proc ffiDeserialize*[T](s: cstring, _: typedesc[Option[T]]): Result[Option[T], string] = + try: + let node = parseJson($s) + if node.kind == JNull: + ok(none(T)) + else: + let itemJson = $node + let parsed = ffiDeserialize(itemJson.cstring, typedesc[T]) + if parsed.isOk: + ok(some(parsed.get)) + else: + err(parsed.error) + 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, @@ -116,15 +162,30 @@ macro ffiType*(body: untyped): untyped = ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas)) - let serializeProc = quote do: + let serializeProc = quote: proc ffiSerialize*(x: `typeName`): string = - $(%* x) + $(%*x) - let deserializeProc = quote do: - proc ffiDeserialize*(s: cstring, _: typedesc[`typeName`]): Result[`typeName`, string] = + var assignmentText = "" + for field in fieldMetas: + if assignmentText.len > 0: + assignmentText &= "\n" + assignmentText &= + " result[\"" & field.name & "\"] = parseJson(ffiSerialize(x." & field.name & "))" + let jsonProc = parseStmt( + "proc `%`*(x: " & typeNameStr & "): JsonNode =\n var result = newJObject()\n" & + assignmentText & "\n return result\n" + ) + + let importJson = quote: + import json + let deserializeProc = quote: + 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) + result = newStmtList(importJson, body, serializeProc, jsonProc, deserializeProc)