diff --git a/.gitignore b/.gitignore index 476ac9d..f2896a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,37 @@ nimble.develop nimble.paths nimbledeps +nimblemeta.json # Nim compiler output *.dylib *.so *.dll -tests/test_alloc -tests/test_ffi_context -tests/test_serial - -# Generated binding crates (regenerated by `nimble genbindings_*`) -examples/**/rust_bindings/target/ - -# Example build artifacts -examples/**/cpp_bindings/build/ - -# Cargo build artifacts -examples/**/rust_client/target/ - -# Development plan (local only) -PLAN.md +*.exe +*.o +*.a # Compiled test binaries (extensionless executables) tests/test_* !tests/test_*.nim + +# E2E test build artifacts (e.g. CMake build dirs under tests/e2e/cpp/build/) +tests/e2e/**/build/ + +# Generated binding crates (regenerated by `nimble genbindings_*`) +examples/**/rust_bindings/target/ + +# Example C++ build artifacts +examples/**/cpp_bindings/build/ + +# Cargo build artifacts (rust clients / harnesses) +examples/**/rust_client/target/ + +# Development plans (local only — match PLAN.md and any `*-plan.md` notes) +PLAN.md +*-plan.md + +# IDE / editor scratch +.vscode/ +.idea/ +.DS_Store diff --git a/README.md b/README.md index 4c9a0f6..ede82ed 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ 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. +`examples/timer` is now a self-contained Nimble project that imports `nim-ffi` directly. +Use `cd examples/timer && nimble install -y ../.. && nimble build` to compile the example. diff --git a/examples/nim_timer/cpp_bindings/CMakeLists.txt b/examples/nim_timer/cpp_bindings/CMakeLists.txt deleted file mode 100644 index eabfd9a..0000000 --- a/examples/nim_timer/cpp_bindings/CMakeLists.txt +++ /dev/null @@ -1,78 +0,0 @@ -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/nimtimer.hpp b/examples/nim_timer/cpp_bindings/nimtimer.hpp deleted file mode 100644 index b753a4d..0000000 --- a/examples/nim_timer/cpp_bindings/nimtimer.hpp +++ /dev/null @@ -1,238 +0,0 @@ -#pragma once -#include -#include -#include -#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); -void nimtimer_destroy(void* ctx); -} // 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) { - try { - return nlohmann::json::parse(raw).get(); - } catch (const nlohmann::json::exception& e) { - throw std::runtime_error(std::string("FFI response deserialization failed: ") + e.what()); - } -} - -template<> -inline void* deserializeFfiResult(const std::string& raw) { - try { - return reinterpret_cast(static_cast(std::stoull(raw))); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("FFI returned non-numeric address: ") + 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* sptr = static_cast*>(ud); - { - auto& s = **sptr; - 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(); - } - delete sptr; -} - -inline std::string ffi_call_(std::function f, - std::chrono::milliseconds timeout) { - auto state = std::make_shared(); - auto* cb_ref = new std::shared_ptr(state); - const int ret = f(ffi_cb_, cb_ref); - if (ret == 2) { - delete cb_ref; - throw std::runtime_error("RET_MISSING_CALLBACK (internal error)"); - } - std::unique_lock lock(state->mtx); - const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); - if (!fired) - throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms"); - 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, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - const auto config_json = serializeFfiArg(config); - const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { - return nimtimer_create(config_json.c_str(), cb, ud); - }, timeout); - try { - const auto addr = std::stoull(raw); - return NimTimerCtx(reinterpret_cast(static_cast(addr)), timeout); - } catch (const std::exception&) { - throw std::runtime_error("FFI create returned non-numeric address: " + raw); - } - } - - static std::future createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); - } - - ~NimTimerCtx() { - if (ptr_) { - nimtimer_destroy(ptr_); - ptr_ = nullptr; - } - } - - NimTimerCtx(const NimTimerCtx&) = delete; - NimTimerCtx& operator=(const NimTimerCtx&) = delete; - - NimTimerCtx(NimTimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) { - other.ptr_ = nullptr; - } - NimTimerCtx& operator=(NimTimerCtx&& other) noexcept { - if (this != &other) { - if (ptr_) nimtimer_destroy(ptr_); - ptr_ = other.ptr_; - timeout_ = other.timeout_; - other.ptr_ = nullptr; - } - return *this; - } - - 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()); - }, timeout_); - return deserializeFfiResult(raw); - } - - std::future echoAsync(const EchoRequest& req) const { - return std::async(std::launch::async, [this, req]() { return echo(req); }); - } - - std::string version() const { - const auto raw = ffi_call_([&](FfiCallback cb, void* ud) { - return nimtimer_version(ptr_, cb, ud); - }, timeout_); - return deserializeFfiResult(raw); - } - - std::future versionAsync() const { - return std::async(std::launch::async, [this]() { return version(); }); - } - - 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()); - }, timeout_); - return deserializeFfiResult(raw); - } - - std::future complexAsync(const ComplexRequest& req) const { - return std::async(std::launch::async, [this, req]() { return complex(req); }); - } - -private: - void* ptr_; - std::chrono::milliseconds timeout_; - explicit NimTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} -}; diff --git a/examples/nim_timer/nim_timer.nim b/examples/nim_timer/nim_timer.nim deleted file mode 100644 index bd4f568..0000000 --- a/examples/nim_timer/nim_timer.nim +++ /dev/null @@ -1,83 +0,0 @@ -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 - -type TimerConfig {.ffi.} = object - name: string - -type EchoRequest {.ffi.} = object - message: string - delayMs: int # how long chronos sleeps before replying - -type EchoResponse {.ffi.} = object - echoed: string - timerName: string # proves that the timer's own state is accessible - -type ComplexRequest {.ffi.} = object - messages: seq[EchoRequest] - tags: seq[string] - note: Option[string] - retries: Maybe[int] - -type ComplexResponse {.ffi.} = object - summary: string - itemCount: int - hasNote: bool - -# --- Constructor ----------------------------------------------------------- -# Called once from Rust. Creates the FFIContext + NimTimer. -# Uses chronos (await sleepAsync) so the body is async. -proc nimtimerCreate*( - 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 nimtimerEcho*( - 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 nimtimerVersion*(timer: NimTimer): Future[Result[string, string]] {.ffi.} = - return ok("nim-timer v0.1.0") - -proc nimtimerComplex*( - 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() must come AFTER every {.ffi.} / {.ffiCtor.} annotation --- -# Each pragma populates ffiProcRegistry / ffiTypeRegistry at compile time as -# the compiler processes the AST. genBindings() reads those registries to emit -# the binding files, so placing it any earlier would produce incomplete output. -# In a multi-file library, import all sub-modules first and call genBindings() -# once, at the bottom of the top-level compilation-root file. -# This call is a no-op unless -d:ffiGenBindings is passed to the compiler. -genBindings() - -proc nimtimer_destroy*(ctx: pointer) {.dynlib, exportc, cdecl, raises: [].} = - ## Tears down the FFI context created by nimtimer_create. - ## Blocks until the FFI thread and watchdog thread have joined. - try: - discard destroyFFIContext[NimTimer](cast[ptr FFIContext[NimTimer]](ctx)) - except: - discard diff --git a/examples/nim_timer/rust_bindings/Cargo.toml b/examples/nim_timer/rust_bindings/Cargo.toml deleted file mode 100644 index 838af9a..0000000 --- a/examples/nim_timer/rust_bindings/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "nimtimer" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["sync"] } diff --git a/examples/nim_timer/rust_bindings/src/api.rs b/examples/nim_timer/rust_bindings/src/api.rs deleted file mode 100644 index a9e88e8..0000000 --- a/examples/nim_timer/rust_bindings/src/api.rs +++ /dev/null @@ -1,177 +0,0 @@ -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() -} - -unsafe extern "C" fn on_result_async( - ret: c_int, - msg: *const c_char, - _len: usize, - user_data: *mut c_void, -) { - let tx = Box::from_raw( - user_data as *mut tokio::sync::oneshot::Sender>, - ); - let value = if ret == 0 { - Ok(CStr::from_ptr(msg).to_string_lossy().into_owned()) - } else { - Err(CStr::from_ptr(msg).to_string_lossy().into_owned()) - }; - let _ = tx.send(value); -} - -async fn ffi_call_async(f: F) -> Result -where - F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int, -{ - let rx = { - let (tx, rx) = tokio::sync::oneshot::channel::>(); - let raw = Box::into_raw(Box::new(tx)) as *mut c_void; - let ret = f(on_result_async, raw); - if ret == 2 { - drop(unsafe { - Box::from_raw( - raw as *mut tokio::sync::oneshot::Sender>, - ) - }); - return Err("RET_MISSING_CALLBACK (internal error)".into()); - } - rx - }; - rx.await.map_err(|_| "channel closed before callback fired".to_string())? -} - -/// 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) - })?; - let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; - Ok(Self { ptr: addr as *mut c_void, timeout }) - } - - pub async fn new_async(config: TimerConfig) -> 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_async(move |cb, ud| unsafe { - ffi::nimtimer_create(config_c.as_ptr(), cb, ud) - }).await?; - let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; - Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) }) - } - - 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 async fn echo_async(&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 ptr = self.ptr as usize; - let raw = ffi_call_async(move |cb, ud| unsafe { - ffi::nimtimer_echo(ptr as *mut c_void, cb, ud, req_c.as_ptr()) - }).await?; - 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 async fn version_async(&self) -> Result { - let ptr = self.ptr as usize; - let raw = ffi_call_async(move |cb, ud| unsafe { - ffi::nimtimer_version(ptr as *mut c_void, cb, ud) - }).await?; - 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()) - } - - pub async fn complex_async(&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 ptr = self.ptr as usize; - let raw = ffi_call_async(move |cb, ud| unsafe { - ffi::nimtimer_complex(ptr as *mut c_void, cb, ud, req_c.as_ptr()) - }).await?; - 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 deleted file mode 100644 index 2cc1172..0000000 --- a/examples/nim_timer/rust_bindings/src/ffi.rs +++ /dev/null @@ -1,16 +0,0 @@ -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/types.rs b/examples/nim_timer/rust_bindings/src/types.rs deleted file mode 100644 index 9037805..0000000 --- a/examples/nim_timer/rust_bindings/src/types.rs +++ /dev/null @@ -1,37 +0,0 @@ -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/src/main.rs b/examples/nim_timer/rust_client/src/main.rs deleted file mode 100644 index 33594cf..0000000 --- a/examples/nim_timer/rust_client/src/main.rs +++ /dev/null @@ -1,47 +0,0 @@ -// 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 `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}; -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.)"); -} diff --git a/examples/nim_timer/rust_client/src/tokio_main.rs b/examples/nim_timer/rust_client/src/tokio_main.rs deleted file mode 100644 index 5303de8..0000000 --- a/examples/nim_timer/rust_client/src/tokio_main.rs +++ /dev/null @@ -1,30 +0,0 @@ -use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig}; - -#[tokio::main(flavor = "multi_thread", worker_threads = 2)] -async fn main() -> Result<(), Box> { - let ctx = NimTimerCtx::new_async(TimerConfig { name: "tokio-demo".into() }).await?; - - let version = ctx.version_async().await?; - println!("[1] Tokio runtime started"); - println!("[2] Version: {version}"); - - let echo1 = ctx - .echo_async(EchoRequest { - message: "hello from tokio".into(), - delay_ms: 200, - }) - .await?; - - let echo2 = ctx - .echo_async(EchoRequest { - message: "second tokio request".into(), - delay_ms: 50, - }) - .await?; - - 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."); - Ok(()) -} diff --git a/examples/nim_timer/README.md b/examples/timer/README.md similarity index 71% rename from examples/nim_timer/README.md rename to examples/timer/README.md index b0d59f6..2a005eb 100644 --- a/examples/nim_timer/README.md +++ b/examples/timer/README.md @@ -1,4 +1,4 @@ -# nim_timer example +# timer example This example is a self-contained Nimble project demonstrating how to import `nim-ffi` and use the `.ffiCtor.` / `.ffi.` abstraction. @@ -6,7 +6,7 @@ This example is a self-contained Nimble project demonstrating how to import `nim 1. Change into the example directory: ```sh - cd examples/nim_timer + cd examples/timer ``` 2. Install the local `ffi` dependency: @@ -27,27 +27,27 @@ This example is a self-contained Nimble project demonstrating how to import `nim ## Rust example clients -The Rust client lives in `examples/nim_timer/rust_client`. +The Rust client lives in `examples/timer/rust_client`. - Run the sync example: ```sh - cd examples/nim_timer/rust_client + cd examples/timer/rust_client cargo run --bin rust_client ``` - Run the Tokio example: ```sh - cd examples/nim_timer/rust_client + cd examples/timer/rust_client cargo run --bin tokio_client ``` ## C++ example -The generated C++ example lives in `examples/nim_timer/cpp_bindings`. +The generated C++ example lives in `examples/timer/cpp_bindings`. Build and run it with: ```sh -cd examples/nim_timer/cpp_bindings +cd examples/timer/cpp_bindings cmake -S . -B build cmake --build build ./build/example diff --git a/examples/timer/cpp_bindings/CMakeLists.txt b/examples/timer/cpp_bindings/CMakeLists.txt new file mode 100644 index 0000000..36ca8e6 --- /dev/null +++ b/examples/timer/cpp_bindings/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.14) +project(timer_cpp_bindings CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── 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() + +get_filename_component(NIM_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/../timer.nim" + ABSOLUTE) + +find_program(NIM_EXECUTABLE nim REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.dylib") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_LIB_FILE "${REPO_ROOT}/timer.dll") +else() + set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.so") +endif() + +add_custom_command( + OUTPUT "${NIM_LIB_FILE}" + COMMAND "${NIM_EXECUTABLE}" c + --mm:orc + -d:chronicles_log_level=WARN + --app:lib + --noMain + "--nimMainPrefix:libtimer" + "-o:${NIM_LIB_FILE}" + "${NIM_SRC}" + WORKING_DIRECTORY "${REPO_ROOT}" + DEPENDS "${NIM_SRC}" + COMMENT "Compiling Nim library libtimer" + VERBATIM +) +add_custom_target(nim_lib ALL DEPENDS "${NIM_LIB_FILE}") + +add_library(timer SHARED IMPORTED GLOBAL) +set_target_properties(timer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +add_dependencies(timer nim_lib) + +# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ───────── +set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor") +add_library(tinycbor STATIC + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c" +) +target_include_directories(tinycbor PUBLIC + "${TINYCBOR_SRC_DIR}" # consumer uses #include + "${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here +) +set_property(TARGET tinycbor PROPERTY C_STANDARD 99) +set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON) + +add_library(timer_headers INTERFACE) +target_include_directories(timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(timer_headers INTERFACE timer tinycbor) + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") + add_executable(example main.cpp) + target_link_libraries(example PRIVATE timer_headers) + add_dependencies(example nim_lib) +endif() diff --git a/examples/nim_timer/cpp_bindings/README.md b/examples/timer/cpp_bindings/README.md similarity index 64% rename from examples/nim_timer/cpp_bindings/README.md rename to examples/timer/cpp_bindings/README.md index 49f94b5..b831fa4 100644 --- a/examples/nim_timer/cpp_bindings/README.md +++ b/examples/timer/cpp_bindings/README.md @@ -2,9 +2,9 @@ ## Purpose -This folder contains **auto-generated C++ bindings** for the `nim_timer` Nim library. It is generated from `../nim_timer.nim` and provides: +This folder contains **auto-generated C++ bindings** for the `timer` Nim library. It is generated from `../timer.nim` and provides: -- `nimtimer.hpp`: High-level C++ class (`NimTimerCtx`) wrapping the FFI interface +- `timer.hpp`: High-level C++ class (`TimerCtx`) 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 @@ -13,19 +13,19 @@ This folder contains **auto-generated C++ bindings** for the `nim_timer` Nim lib Generate or regenerate these bindings by running from the parent directory: ```sh -cd examples/nim_timer +cd examples/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` +2. Triggers `genBindings("examples/timer/cpp_bindings", "../timer.nim")` in `timer.nim` 3. Creates/updates the generated binding files ## Building the Example ```sh -cd examples/nim_timer/cpp_bindings +cd examples/timer/cpp_bindings cmake -S . -B build cmake --build build ./build/example diff --git a/examples/nim_timer/cpp_bindings/main.cpp b/examples/timer/cpp_bindings/main.cpp similarity index 51% rename from examples/nim_timer/cpp_bindings/main.cpp rename to examples/timer/cpp_bindings/main.cpp index 0fd57c0..5694568 100644 --- a/examples/nim_timer/cpp_bindings/main.cpp +++ b/examples/timer/cpp_bindings/main.cpp @@ -1,10 +1,10 @@ -#include "nimtimer.hpp" +#include "timer.hpp" #include #include int main() { try { - auto ctx = NimTimerCtx::create(TimerConfig{"cpp-demo"}); + auto ctx = TimerCtx::create(TimerConfig{"cpp-demo"}); std::cout << "[1] Context created\n"; auto versionFuture = ctx.versionAsync(); @@ -35,6 +35,34 @@ int main() { << ", itemCount=" << complex.itemCount << ", hasNote=" << complex.hasNote << "\n"; + // ── 6. Call with three complex parameters ───────────────────── + // Each parameter is its own generated C++ struct. The nim-ffi + // macro packs all three into one CBOR envelope on the wire — at + // the call site, this is just a typed method invocation. + auto job = JobSpec{ + /*name*/ "nightly-rollup", + /*payload*/ std::vector{"rollup", "v2"}, + /*priority*/ 10, + }; + auto retry = RetryPolicy{ + /*maxAttempts*/ 3, + /*backoffMs*/ 500, + /*retryOn*/ std::vector{"timeout", "5xx"}, + }; + auto schedule = ScheduleConfig{ + /*startAtMs*/ 1000, + /*intervalMs*/ 15000, + /*jitter*/ std::optional(250), + }; + + auto scheduleFuture = ctx.scheduleAsync(job, retry, schedule); + auto scheduleRes = scheduleFuture.get(); + std::cout << "[6] Schedule (3 complex params): jobId=" << scheduleRes.jobId + << ", willRunCount=" << scheduleRes.willRunCount + << ", firstRunAtMs=" << scheduleRes.firstRunAtMs + << ", effectiveBackoffMs=" << scheduleRes.effectiveBackoffMs + << "\n"; + std::cout << "\nDone.\n"; } catch (const std::exception& ex) { std::cerr << "Error: " << ex.what() << "\n"; diff --git a/examples/timer/cpp_bindings/timer.hpp b/examples/timer/cpp_bindings/timer.hpp new file mode 100644 index 0000000..39d9788 --- /dev/null +++ b/examples/timer/cpp_bindings/timer.hpp @@ -0,0 +1,775 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} + +// ── encode_cbor overloads (primitives + containers) ───────────────────── +// Per-struct encode_cbor / decode_cbor are emitted by cpp.nim next to each +// generated struct. These helpers cover the leaf types and container shapes +// the struct emitters defer into. + +inline CborError encode_cbor(CborEncoder& e, bool v) { + return cbor_encode_boolean(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int64_t v) { + return cbor_encode_int(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int32_t v) { + return cbor_encode_int(&e, static_cast(v)); +} +inline CborError encode_cbor(CborEncoder& e, uint64_t v) { + return cbor_encode_uint(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, double v) { + return cbor_encode_double(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, const std::string& v) { + return cbor_encode_text_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(&e, &arr, v.size()); + if (err) return err; + for (const auto& item : v) { + err = encode_cbor(arr, item); + if (err) return err; + } + return cbor_encoder_close_container(&e, &arr); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { + if (!v) return cbor_encode_null(&e); + return encode_cbor(e, *v); +} + +// ── decode_cbor overloads ─────────────────────────────────────────────── + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_boolean(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int64_t& out) { + if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_int64_checked(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int32_t& out) { + int64_t tmp = 0; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = static_cast(tmp); + return CborNoError; +} +inline CborError decode_cbor(CborValue& it, uint64_t& out) { + if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_uint64(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, double& out) { + if (cbor_value_is_double(&it)) { + CborError err = cbor_value_get_double(&it, &out); + if (err) return err; + return cbor_value_advance(&it); + } + if (cbor_value_is_float(&it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(&it, &f); + if (err) return err; + out = static_cast(f); + return cbor_value_advance(&it); + } + return CborErrorImproperValue; +} +inline CborError decode_cbor(CborValue& it, std::string& out) { + if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + +template +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_array(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(&it, &len); + if (err) return err; + out.clear(); + out.resize(len); + CborValue inner; + err = cbor_value_enter_container(&it, &inner); + if (err) return err; + for (size_t i = 0; i < len; ++i) { + err = decode_cbor(inner, out[i]); + if (err) return err; + } + return cbor_value_leave_container(&it, &inner); +} + +template +inline CborError decode_cbor(CborValue& it, std::optional& out) { + if (cbor_value_is_null(&it)) { + out = std::nullopt; + return cbor_value_advance(&it); + } + T tmp{}; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = std::move(tmp); + return CborNoError; +} + +// ── Public entry points ───────────────────────────────────────────────── + +template +inline std::vector encodeCborFFI(const T& value) { + // Start with a generous 4 KiB buffer; double on overflow until it fits. + std::vector buf(4096); + while (true) { + CborEncoder enc; + cbor_encoder_init(&enc, buf.data(), buf.size(), 0); + CborError err = encode_cbor(enc, value); + if (err == CborNoError) { + const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); + buf.resize(used); + return buf; + } + if (err == CborErrorOutOfMemory) { + const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc); + buf.resize(buf.size() + (extra > 0 ? extra : buf.size())); + continue; + } + throw std::runtime_error(std::string("FFI CBOR encode failed: ") + + cbor_error_string(err)); + } +} + +template +inline T decodeCborFFI(const std::vector& bytes) { + CborParser parser; + CborValue it; + CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); + if (err != CborNoError) { + throw std::runtime_error(std::string("FFI CBOR parse init failed: ") + + cbor_error_string(err)); + } + T out{}; + err = decode_cbor(it, out); + if (err != CborNoError) { + throw std::runtime_error(std::string("FFI CBOR decode failed: ") + + cbor_error_string(err)); + } + return out; +} + +// ============================================================ +// User-declared FFI types +// ============================================================ + +struct TimerConfig { + std::string name; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerConfig& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); if (err) return err; + err = encode_cbor(m, v.name); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerConfig& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "name", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.name); if (err) return err; + return cbor_value_advance(&it); +} + +struct EchoRequest { + std::string message; + int64_t delayMs; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoRequest& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "message"); if (err) return err; + err = encode_cbor(m, v.message); if (err) return err; + err = cbor_encode_text_stringz(&m, "delayMs"); if (err) return err; + err = encode_cbor(m, v.delayMs); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoRequest& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "message", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.message); if (err) return err; + err = cbor_value_map_find_value(&it, "delayMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.delayMs); if (err) return err; + return cbor_value_advance(&it); +} + +struct EchoResponse { + std::string echoed; + std::string timerName; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoResponse& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "echoed"); if (err) return err; + err = encode_cbor(m, v.echoed); if (err) return err; + err = cbor_encode_text_stringz(&m, "timerName"); if (err) return err; + err = encode_cbor(m, v.timerName); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoResponse& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "echoed", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.echoed); if (err) return err; + err = cbor_value_map_find_value(&it, "timerName", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.timerName); if (err) return err; + return cbor_value_advance(&it); +} + +struct ComplexRequest { + std::vector messages; + std::vector tags; + std::optional note; + std::optional retries; +}; +inline CborError encode_cbor(CborEncoder& e, const ComplexRequest& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "messages"); if (err) return err; + err = encode_cbor(m, v.messages); if (err) return err; + err = cbor_encode_text_stringz(&m, "tags"); if (err) return err; + err = encode_cbor(m, v.tags); if (err) return err; + err = cbor_encode_text_stringz(&m, "note"); if (err) return err; + err = encode_cbor(m, v.note); if (err) return err; + err = cbor_encode_text_stringz(&m, "retries"); if (err) return err; + err = encode_cbor(m, v.retries); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ComplexRequest& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "messages", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.messages); if (err) return err; + err = cbor_value_map_find_value(&it, "tags", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.tags); if (err) return err; + err = cbor_value_map_find_value(&it, "note", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.note); if (err) return err; + err = cbor_value_map_find_value(&it, "retries", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.retries); if (err) return err; + return cbor_value_advance(&it); +} + +struct ComplexResponse { + std::string summary; + int64_t itemCount; + bool hasNote; +}; +inline CborError encode_cbor(CborEncoder& e, const ComplexResponse& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "summary"); if (err) return err; + err = encode_cbor(m, v.summary); if (err) return err; + err = cbor_encode_text_stringz(&m, "itemCount"); if (err) return err; + err = encode_cbor(m, v.itemCount); if (err) return err; + err = cbor_encode_text_stringz(&m, "hasNote"); if (err) return err; + err = encode_cbor(m, v.hasNote); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ComplexResponse& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "summary", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.summary); if (err) return err; + err = cbor_value_map_find_value(&it, "itemCount", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.itemCount); if (err) return err; + err = cbor_value_map_find_value(&it, "hasNote", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.hasNote); if (err) return err; + return cbor_value_advance(&it); +} + +struct JobSpec { + std::string name; + std::vector payload; + int64_t priority; +}; +inline CborError encode_cbor(CborEncoder& e, const JobSpec& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); if (err) return err; + err = encode_cbor(m, v.name); if (err) return err; + err = cbor_encode_text_stringz(&m, "payload"); if (err) return err; + err = encode_cbor(m, v.payload); if (err) return err; + err = cbor_encode_text_stringz(&m, "priority"); if (err) return err; + err = encode_cbor(m, v.priority); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, JobSpec& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "name", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.name); if (err) return err; + err = cbor_value_map_find_value(&it, "payload", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.payload); if (err) return err; + err = cbor_value_map_find_value(&it, "priority", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.priority); if (err) return err; + return cbor_value_advance(&it); +} + +struct RetryPolicy { + int64_t maxAttempts; + int64_t backoffMs; + std::vector retryOn; +}; +inline CborError encode_cbor(CborEncoder& e, const RetryPolicy& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "maxAttempts"); if (err) return err; + err = encode_cbor(m, v.maxAttempts); if (err) return err; + err = cbor_encode_text_stringz(&m, "backoffMs"); if (err) return err; + err = encode_cbor(m, v.backoffMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "retryOn"); if (err) return err; + err = encode_cbor(m, v.retryOn); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, RetryPolicy& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "maxAttempts", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.maxAttempts); if (err) return err; + err = cbor_value_map_find_value(&it, "backoffMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.backoffMs); if (err) return err; + err = cbor_value_map_find_value(&it, "retryOn", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.retryOn); if (err) return err; + return cbor_value_advance(&it); +} + +struct ScheduleConfig { + int64_t startAtMs; + int64_t intervalMs; + std::optional jitter; +}; +inline CborError encode_cbor(CborEncoder& e, const ScheduleConfig& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "startAtMs"); if (err) return err; + err = encode_cbor(m, v.startAtMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "intervalMs"); if (err) return err; + err = encode_cbor(m, v.intervalMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "jitter"); if (err) return err; + err = encode_cbor(m, v.jitter); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ScheduleConfig& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "startAtMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.startAtMs); if (err) return err; + err = cbor_value_map_find_value(&it, "intervalMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.intervalMs); if (err) return err; + err = cbor_value_map_find_value(&it, "jitter", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.jitter); if (err) return err; + return cbor_value_advance(&it); +} + +struct ScheduleResult { + std::string jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +}; +inline CborError encode_cbor(CborEncoder& e, const ScheduleResult& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "jobId"); if (err) return err; + err = encode_cbor(m, v.jobId); if (err) return err; + err = cbor_encode_text_stringz(&m, "willRunCount"); if (err) return err; + err = encode_cbor(m, v.willRunCount); if (err) return err; + err = cbor_encode_text_stringz(&m, "firstRunAtMs"); if (err) return err; + err = encode_cbor(m, v.firstRunAtMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "effectiveBackoffMs"); if (err) return err; + err = encode_cbor(m, v.effectiveBackoffMs); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ScheduleResult& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "jobId", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.jobId); if (err) return err; + err = cbor_value_map_find_value(&it, "willRunCount", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.willRunCount); if (err) return err; + err = cbor_value_map_find_value(&it, "firstRunAtMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.firstRunAtMs); if (err) return err; + err = cbor_value_map_find_value(&it, "effectiveBackoffMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.effectiveBackoffMs); if (err) return err; + return cbor_value_advance(&it); +} + +// ============================================================ +// Per-proc request envelopes (CBOR encoded on the wire) +// ============================================================ + +struct TimerCreateCtorReq { + TimerConfig config; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerCreateCtorReq& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "config"); if (err) return err; + err = encode_cbor(m, v.config); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerCreateCtorReq& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "config", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.config); if (err) return err; + return cbor_value_advance(&it); +} + +struct TimerEchoReq { + EchoRequest req; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerEchoReq& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "req"); if (err) return err; + err = encode_cbor(m, v.req); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerEchoReq& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "req", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.req); if (err) return err; + return cbor_value_advance(&it); +} + +struct TimerVersionReq { +}; +inline CborError encode_cbor(CborEncoder& e, const TimerVersionReq&) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 0); + if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerVersionReq&) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + return cbor_value_advance(&it); +} + +struct TimerComplexReq { + ComplexRequest req; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerComplexReq& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "req"); if (err) return err; + err = encode_cbor(m, v.req); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerComplexReq& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "req", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.req); if (err) return err; + return cbor_value_advance(&it); +} + +struct TimerScheduleReq { + JobSpec job; + RetryPolicy retry; + ScheduleConfig schedule; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerScheduleReq& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "job"); if (err) return err; + err = encode_cbor(m, v.job); if (err) return err; + err = cbor_encode_text_stringz(&m, "retry"); if (err) return err; + err = encode_cbor(m, v.retry); if (err) return err; + err = cbor_encode_text_stringz(&m, "schedule"); if (err) return err; + err = encode_cbor(m, v.schedule); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerScheduleReq& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "job", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.job); if (err) return err; + err = cbor_value_map_find_value(&it, "retry", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.retry); if (err) return err; + err = cbor_value_map_find_value(&it, "schedule", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.schedule); if (err) return err; + return cbor_value_advance(&it); +} + +// ============================================================ +// C FFI declarations +// ============================================================ + +extern "C" { +typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data); + +void* timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); +int timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int timer_destroy(void* ctx); +} // extern "C" + +// ============================================================ +// Synchronous call helper +// ============================================================ + +namespace { + +struct FFICallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::vector bytes; + std::string err; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { + // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; + // take ownership here so it's freed on every exit path. + std::unique_ptr> handle( + static_cast*>(ud)); + FFICallState_& s = **handle; + + std::lock_guard lock(s.mtx); + s.ok = (ret == 0); + if (msg && len > 0) { + const auto* p = reinterpret_cast(msg); + if (s.ok) s.bytes.assign(p, p + len); + else s.err.assign(msg, len); + } + s.done = true; + s.cv.notify_one(); +} + +inline std::vector ffi_call_(std::function f, + std::chrono::milliseconds timeout) { + auto state = std::make_shared(); + auto* cb_ref = new std::shared_ptr(state); + const int ret = f(ffi_cb_, cb_ref); + if (ret == 2) { + delete cb_ref; + throw std::runtime_error("RET_MISSING_CALLBACK (internal error)"); + } + std::unique_lock lock(state->mtx); + const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); + if (!fired) + throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms"); + if (!state->ok) + throw std::runtime_error(state->err); + return state->bytes; +} + +} // anonymous namespace + +// ============================================================ +// High-level C++ context class +// ============================================================ + +class TimerCtx { +public: + static TimerCtx create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + const auto ffi_req_ = TimerCreateCtorReq{config}; + const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); + const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + (void)timer_create(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud); + return 0; + }, timeout); + const auto addr_str = decodeCborFFI(ffi_raw_); + try { + const auto addr = std::stoull(addr_str); + return TimerCtx(reinterpret_cast(static_cast(addr)), timeout); + } catch (const std::exception&) { + throw std::runtime_error("FFI create returned non-numeric address: " + addr_str); + } + } + + static std::future createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); + } + + // Rule of Five: because this class owns a raw resource (the timer + // context pointer freed in the destructor), the compiler-generated copy + // and move special members would do the wrong thing — copies would + // double-free, and a default move would leave both objects pointing at + // the same context. So we define all five special members explicitly: + // 1. destructor — releases the context. + // 2. copy constructor — deleted; contexts are not copyable. + // 3. copy assignment — deleted; same reason. + // 4. move constructor — transfers ownership, nulls the source. + // 5. move assignment — destroys the current context, then + // transfers ownership from `other`. + // See: https://en.cppreference.com/w/cpp/language/rule_of_three + ~TimerCtx() { + if (ptr_) { + timer_destroy(ptr_); + ptr_ = nullptr; + } + } + + TimerCtx(const TimerCtx&) = delete; + TimerCtx& operator=(const TimerCtx&) = delete; + + TimerCtx(TimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) { + other.ptr_ = nullptr; + } + TimerCtx& operator=(TimerCtx&& other) noexcept { + if (this != &other) { + if (ptr_) timer_destroy(ptr_); + ptr_ = other.ptr_; + timeout_ = other.timeout_; + other.ptr_ = nullptr; + } + return *this; + } + + EchoResponse echo(const EchoRequest& req) const { + const auto ffi_req_ = TimerEchoReq{req}; + const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); + const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return timer_echo(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + return decodeCborFFI(ffi_raw_); + } + + std::future echoAsync(const EchoRequest& req) const { + return std::async(std::launch::async, [this, req]() { return this->echo(req); }); + } + + std::string version() const { + const auto ffi_req_ = TimerVersionReq{}; + const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); + const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return timer_version(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + return decodeCborFFI(ffi_raw_); + } + + std::future versionAsync() const { + return std::async(std::launch::async, [this]() { return this->version(); }); + } + + ComplexResponse complex(const ComplexRequest& req) const { + const auto ffi_req_ = TimerComplexReq{req}; + const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); + const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return timer_complex(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + return decodeCborFFI(ffi_raw_); + } + + std::future complexAsync(const ComplexRequest& req) const { + return std::async(std::launch::async, [this, req]() { return this->complex(req); }); + } + + ScheduleResult schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { + const auto ffi_req_ = TimerScheduleReq{job, retry, schedule}; + const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); + const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return timer_schedule(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + return decodeCborFFI(ffi_raw_); + } + + std::future scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { + return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); }); + } + +private: + void* ptr_; + std::chrono::milliseconds timeout_; + explicit TimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} +}; diff --git a/examples/timer/rust_bindings/Cargo.lock b/examples/timer/rust_bindings/Cargo.lock new file mode 100644 index 0000000..9e6f26d --- /dev/null +++ b/examples/timer/rust_bindings/Cargo.lock @@ -0,0 +1,210 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[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" +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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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 = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[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 = "timer" +version = "0.1.0" +dependencies = [ + "ciborium", + "flume", + "serde", + "tokio", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/examples/timer/rust_bindings/Cargo.toml b/examples/timer/rust_bindings/Cargo.toml new file mode 100644 index 0000000..1e4dde1 --- /dev/null +++ b/examples/timer/rust_bindings/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "timer" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +ciborium = "0.2" +flume = { version = "0.11", default-features = false, features = ["async"] } +tokio = { version = "1", features = ["sync", "time"] } diff --git a/examples/nim_timer/rust_bindings/README.md b/examples/timer/rust_bindings/README.md similarity index 71% rename from examples/nim_timer/rust_bindings/README.md rename to examples/timer/rust_bindings/README.md index 2c7deca..8c5ba38 100644 --- a/examples/nim_timer/rust_bindings/README.md +++ b/examples/timer/rust_bindings/README.md @@ -2,13 +2,13 @@ ## 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: +This folder contains **auto-generated Rust bindings** (the `timer` crate) for the `timer` Nim library. It is generated from `../timer.nim` and provides: -- `src/lib.rs`: Main library exposing high-level Rust types and the `NimTimerCtx` API +- `src/lib.rs`: Main library exposing high-level Rust types and the `TimerCtx` 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`) +- `build.rs`: Build script that compiles the Nim library to `libtimer.dylib` (or `.so`/`.dll`) - `Cargo.toml`: Package manifest with serde and serde_json dependencies ## How It's Generated @@ -16,13 +16,13 @@ This folder contains **auto-generated Rust bindings** (the `nimtimer` crate) for Generate or regenerate these bindings by running from the parent directory: ```sh -cd examples/nim_timer +cd examples/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` +2. Triggers `genBindings("examples/timer/rust_bindings", "../timer.nim")` in `timer.nim` 3. Creates/updates the generated binding files ## Using as a Dependency @@ -31,7 +31,7 @@ The `rust_client` example consumes this crate: ```toml [dependencies] -nimtimer = { path = "../rust_bindings" } +timer = { path = "../rust_bindings" } ``` ## Do Not Edit diff --git a/examples/nim_timer/rust_bindings/build.rs b/examples/timer/rust_bindings/build.rs similarity index 85% rename from examples/nim_timer/rust_bindings/build.rs rename to examples/timer/rust_bindings/build.rs index b5b12a8..bc7a1b0 100644 --- a/examples/nim_timer/rust_bindings/build.rs +++ b/examples/timer/rust_bindings/build.rs @@ -3,8 +3,8 @@ 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")); + let nim_src = manifest.join("../timer.nim"); + let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("../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). @@ -26,7 +26,7 @@ fn main() { #[cfg(target_os = "linux")] let lib_ext = "so"; - let out_lib = repo_root.join(format!("libnimtimer.{lib_ext}")); + let out_lib = repo_root.join(format!("libtimer.{lib_ext}")); let mut cmd = Command::new("nim"); cmd.arg("c") @@ -34,7 +34,7 @@ fn main() { .arg("-d:chronicles_log_level=WARN") .arg("--app:lib") .arg("--noMain") - .arg(format!("--nimMainPrefix:libnimtimer")) + .arg(format!("--nimMainPrefix:libtimer")) .arg(format!("-o:{}", out_lib.display())); cmd.arg(&nim_src).current_dir(&repo_root); @@ -42,6 +42,6 @@ fn main() { assert!(status.success(), "Nim compilation failed"); println!("cargo:rustc-link-search={}", repo_root.display()); - println!("cargo:rustc-link-lib=nimtimer"); + println!("cargo:rustc-link-lib=timer"); println!("cargo:rerun-if-changed={}", nim_src.display()); } diff --git a/examples/timer/rust_bindings/src/api.rs b/examples/timer/rust_bindings/src/api.rs new file mode 100644 index 0000000..35cba77 --- /dev/null +++ b/examples/timer/rust_bindings/src/api.rs @@ -0,0 +1,228 @@ +use std::os::raw::{c_char, c_int, c_void}; +use std::slice; +use std::time::Duration; +use serde::de::DeserializeOwned; +use serde::Serialize; +use super::ffi; +use super::types::*; + +fn encode_cbor(value: &T) -> Result, String> { + let mut buf = Vec::new(); + ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?; + Ok(buf) +} + +fn decode_cbor(bytes: &[u8]) -> Result { + ciborium::de::from_reader(bytes).map_err(|e| e.to_string()) +} + +type FFIResult = Result, String>; +type FFISender = flume::Sender; + +// Reconstruct the (ret, msg, len) tuple delivered by the C callback +// into a Result, String>: payload on success, UTF-8 message on error. +// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the +// alternative would be to dispatch a separate Err for invalid UTF-8, but the +// codegen contract is that Nim handlers emit `string` error payloads, so +// invalid UTF-8 here would be a Nim-side bug. +unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult { + let bytes = if msg.is_null() || len == 0 { + Vec::new() + } else { + slice::from_raw_parts(msg as *const u8, len).to_vec() + }; + if ret == 0 { Ok(bytes) } + else { Err(String::from_utf8_lossy(&bytes).into_owned()) } +} + +unsafe extern "C" fn on_result( + ret: c_int, + msg: *const c_char, + len: usize, + user_data: *mut c_void, +) { + // Take ownership of the boxed Sender — dropping it at end of scope + // releases the only outstanding handle. + let tx = Box::from_raw(user_data as *mut FFISender); + + // `tx.send` returns Err only if the awaiting future was dropped (and with it + // the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch + // lost the race, or the future was dropped before being awaited. This cannot + // happen with the current rust_client demo but may occur in arbitrary + // downstream consumers, so we discard the Err safely. + // Given that this is invoked from a Nim thread, we can't propagate the error by panicking or + // returning a Result. Furthermore, an API dev may intentionally set a timeout in the await, + // in which case is also fine to discard the send error in this case because the API user will + // handle the timeout expiry in their own code. + // The important part is to ensure that the callback doesn't panic or block indefinitely if the + // receiver is gone. + let _ = tx.send(ffi_payload(ret, msg, len)); +} + +fn ffi_call_sync(timeout: Duration, f: F) -> FFIResult +where + F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int, +{ + let (tx, rx) = flume::bounded::(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let ret = f(on_result, raw); + if ret == 2 { + // Callback will never fire; reclaim the box to avoid a leak. + drop(unsafe { Box::from_raw(raw as *mut FFISender) }); + return Err("RET_MISSING_CALLBACK (internal error)".into()); + } + match rx.recv_timeout(timeout) { + Ok(payload) => payload, + Err(flume::RecvTimeoutError::Timeout) => + Err(format!("timed out after {:?}", timeout)), + Err(flume::RecvTimeoutError::Disconnected) => + Err("callback channel disconnected before delivery".into()), + } +} + +async fn ffi_call_async(timeout: Duration, f: F) -> FFIResult +where + F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int, +{ + let (tx, rx) = flume::bounded::(1); + let raw = Box::into_raw(Box::new(tx)) as *mut c_void; + let ret = f(on_result, raw); + if ret == 2 { + drop(unsafe { Box::from_raw(raw as *mut FFISender) }); + return Err("RET_MISSING_CALLBACK (internal error)".into()); + } + match tokio::time::timeout(timeout, rx.recv_async()).await { + Ok(Ok(payload)) => payload, + Ok(Err(_)) => Err("callback channel disconnected before delivery".into()), + Err(_) => Err(format!("timed out after {:?}", timeout)), + } +} + +/// High-level context for `Timer`. +pub struct TimerCtx { + ptr: *mut c_void, + timeout: Duration, +} + +// SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime. +// Every call through the generated FFI proc goes through +// `sendRequestToFFIThread` on the Nim side, which serialises every request +// behind `ctx.lock` and dispatches handlers on a single FFI thread, so the +// pointer is never accessed concurrently from Rust. The Nim-side reentrancy +// guard (`onFFIThread` threadvar) prevents handlers from re-entering the +// dispatcher and self-deadlocking. These invariants make it sound to mark +// the wrapper as Send + Sync. +unsafe impl Send for TimerCtx {} +unsafe impl Sync for TimerCtx {} + +impl Drop for TimerCtx { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { ffi::timer_destroy(self.ptr); } + self.ptr = std::ptr::null_mut(); + } + } +} + +impl TimerCtx { + pub fn create(config: TimerConfig, timeout: Duration) -> Result { + let req = TimerCreateCtorReq { config }; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe { + let _ = ffi::timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); + 0 + })?; + let addr_str: String = decode_cbor(&raw_bytes)?; + let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?; + Ok(Self { ptr: addr as *mut c_void, timeout }) + } + + pub async fn new_async(config: TimerConfig, timeout: Duration) -> Result { + let req = TimerCreateCtorReq { config }; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe { + let _ = ffi::timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); + 0 + }).await?; + let addr_str: String = decode_cbor(&raw_bytes)?; + let addr: usize = addr_str.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 = TimerEchoReq { req }; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { + ffi::timer_echo(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + })?; + decode_cbor::(&raw_bytes) + } + + pub async fn echo_async(&self, req: EchoRequest) -> Result { + let req = TimerEchoReq { req }; + let req_bytes = encode_cbor(&req)?; + let ptr = self.ptr as usize; + let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { + ffi::timer_echo(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + }).await?; + decode_cbor::(&raw_bytes) + } + + pub fn version(&self) -> Result { + let req = TimerVersionReq {}; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { + ffi::timer_version(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + })?; + decode_cbor::(&raw_bytes) + } + + pub async fn version_async(&self) -> Result { + let req = TimerVersionReq {}; + let req_bytes = encode_cbor(&req)?; + let ptr = self.ptr as usize; + let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { + ffi::timer_version(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + }).await?; + decode_cbor::(&raw_bytes) + } + + pub fn complex(&self, req: ComplexRequest) -> Result { + let req = TimerComplexReq { req }; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { + ffi::timer_complex(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + })?; + decode_cbor::(&raw_bytes) + } + + pub async fn complex_async(&self, req: ComplexRequest) -> Result { + let req = TimerComplexReq { req }; + let req_bytes = encode_cbor(&req)?; + let ptr = self.ptr as usize; + let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { + ffi::timer_complex(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + }).await?; + decode_cbor::(&raw_bytes) + } + + pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result { + let req = TimerScheduleReq { job, retry, schedule }; + let req_bytes = encode_cbor(&req)?; + let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { + ffi::timer_schedule(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + })?; + decode_cbor::(&raw_bytes) + } + + pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result { + let req = TimerScheduleReq { job, retry, schedule }; + let req_bytes = encode_cbor(&req)?; + let ptr = self.ptr as usize; + let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { + ffi::timer_schedule(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + }).await?; + decode_cbor::(&raw_bytes) + } + +} diff --git a/examples/timer/rust_bindings/src/ffi.rs b/examples/timer/rust_bindings/src/ffi.rs new file mode 100644 index 0000000..9b6f4f7 --- /dev/null +++ b/examples/timer/rust_bindings/src/ffi.rs @@ -0,0 +1,18 @@ +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 = "timer")] +extern "C" { + pub fn timer_create(req_cbor: *const u8, req_cbor_len: usize, callback: FFICallback, user_data: *mut c_void) -> *mut c_void; + pub fn timer_echo(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn timer_destroy(ctx: *mut c_void) -> c_int; +} diff --git a/examples/nim_timer/rust_bindings/src/lib.rs b/examples/timer/rust_bindings/src/lib.rs similarity index 100% rename from examples/nim_timer/rust_bindings/src/lib.rs rename to examples/timer/rust_bindings/src/lib.rs diff --git a/examples/timer/rust_bindings/src/types.rs b/examples/timer/rust_bindings/src/types.rs new file mode 100644 index 0000000..c5383af --- /dev/null +++ b/examples/timer/rust_bindings/src/types.rs @@ -0,0 +1,100 @@ +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, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JobSpec { + pub name: String, + pub payload: Vec, + pub priority: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetryPolicy { + #[serde(rename = "maxAttempts")] + pub max_attempts: i64, + #[serde(rename = "backoffMs")] + pub backoff_ms: i64, + #[serde(rename = "retryOn")] + pub retry_on: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScheduleConfig { + #[serde(rename = "startAtMs")] + pub start_at_ms: i64, + #[serde(rename = "intervalMs")] + pub interval_ms: i64, + pub jitter: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScheduleResult { + #[serde(rename = "jobId")] + pub job_id: String, + #[serde(rename = "willRunCount")] + pub will_run_count: i64, + #[serde(rename = "firstRunAtMs")] + pub first_run_at_ms: i64, + #[serde(rename = "effectiveBackoffMs")] + pub effective_backoff_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerCreateCtorReq { + pub config: TimerConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerEchoReq { + pub req: EchoRequest, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerVersionReq {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerComplexReq { + pub req: ComplexRequest, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimerScheduleReq { + pub job: JobSpec, + pub retry: RetryPolicy, + pub schedule: ScheduleConfig, +} diff --git a/examples/nim_timer/rust_client/Cargo.lock b/examples/timer/rust_client/Cargo.lock similarity index 52% rename from examples/nim_timer/rust_client/Cargo.lock rename to examples/timer/rust_client/Cargo.lock index 5bbc31e..9e0ca6f 100644 --- a/examples/nim_timer/rust_client/Cargo.lock +++ b/examples/timer/rust_client/Cargo.lock @@ -2,27 +2,100 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[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", - "tokio", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -51,11 +124,17 @@ dependencies = [ name = "rust_client" version = "0.1.0" dependencies = [ - "nimtimer", "serde_json", + "timer", "tokio", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -99,6 +178,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "syn" version = "2.0.117" @@ -110,6 +198,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "timer" +version = "0.1.0" +dependencies = [ + "ciborium", + "flume", + "serde", + "tokio", +] + [[package]] name = "tokio" version = "1.52.1" @@ -137,6 +235,26 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/examples/nim_timer/rust_client/Cargo.toml b/examples/timer/rust_client/Cargo.toml similarity index 87% rename from examples/nim_timer/rust_client/Cargo.toml rename to examples/timer/rust_client/Cargo.toml index cd21745..08c3865 100644 --- a/examples/nim_timer/rust_client/Cargo.toml +++ b/examples/timer/rust_client/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nimtimer = { path = "../rust_bindings" } +timer = { path = "../rust_bindings" } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/examples/nim_timer/rust_client/README.md b/examples/timer/rust_client/README.md similarity index 75% rename from examples/nim_timer/rust_client/README.md rename to examples/timer/rust_client/README.md index bf818af..7cb5dde 100644 --- a/examples/nim_timer/rust_client/README.md +++ b/examples/timer/rust_client/README.md @@ -2,7 +2,7 @@ ## Purpose -This folder contains **example Rust applications** that demonstrate how to use the auto-generated `nimtimer` crate (from `../rust_bindings`). +This folder contains **example Rust applications** that demonstrate how to use the auto-generated `timer` crate (from `../rust_bindings`). ## What's Included @@ -21,7 +21,7 @@ Two executable examples: ## Building ```sh -cd examples/nim_timer/rust_client +cd examples/timer/rust_client cargo build ``` @@ -37,7 +37,7 @@ cargo run --bin tokio_client ## Important Notes -- The `nimtimer` crate is a **local dependency** (`path = "../rust_bindings"`) +- The `timer` 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 +- To regenerate the `timer` crate, run `nimble genbindings_rust` from the parent directory diff --git a/examples/timer/rust_client/src/main.rs b/examples/timer/rust_client/src/main.rs new file mode 100644 index 0000000..42f90a8 --- /dev/null +++ b/examples/timer/rust_client/src/main.rs @@ -0,0 +1,80 @@ +// Rust client for the timer shared library built with nim-ffi + chronos. +// +// This file uses the generated `timer` crate, which wraps all the raw FFI +// boilerplate (extern "C" declarations, callback machinery, CBOR encode/decode). +// +// To regenerate the `rust_bindings` crate: +// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libtimer \ +// -d:ffiGenBindings examples/timer/timer.nim +use std::time::Duration; +use timer::{ + EchoRequest, JobSpec, RetryPolicy, ScheduleConfig, TimerConfig, TimerCtx, +}; + +fn main() { + let timeout = Duration::from_secs(5); + + // ── 1. Create the timer service ──────────────────────────────────────── + let ctx = TimerCtx::create(TimerConfig { name: "demo".into() }, timeout) + .expect("timer_create failed"); + println!("[1] Context created"); + + // ── 2. Sync call: version ────────────────────────────────────────────── + let version = ctx.version().expect("timer_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("timer_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 timer_echo failed"); + println!("[4] Echo: echoed={}, timerName={}", echo2.echoed, echo2.timer_name); + + // ── 5. Call with three complex parameters ───────────────────────────── + // Each param is its own first-class Rust struct generated by the + // bindings. The nim-ffi macro packs all three into one CBOR envelope on + // the wire — from the caller's perspective, this is just a typed call. + let schedule = ctx + .schedule( + JobSpec { + name: "nightly-rollup".into(), + payload: vec!["rollup".into(), "v2".into()], + priority: 10, + }, + RetryPolicy { + max_attempts: 3, + backoff_ms: 500, + retry_on: vec!["timeout".into(), "5xx".into()], + }, + ScheduleConfig { + start_at_ms: 1_000, + interval_ms: 15_000, + jitter: Some(250), + }, + ) + .expect("timer_schedule failed"); + println!( + "[5] Schedule (3 complex params): jobId={}, willRunCount={}, firstRunAtMs={}, effectiveBackoffMs={}", + schedule.job_id, + schedule.will_run_count, + schedule.first_run_at_ms, + schedule.effective_backoff_ms, + ); + + println!("\nDone. The Nim FFI thread and watchdog are still running."); + println!("(In a real app, call timer_destroy to join them gracefully.)"); +} diff --git a/examples/timer/rust_client/src/tokio_main.rs b/examples/timer/rust_client/src/tokio_main.rs new file mode 100644 index 0000000..e3c7c46 --- /dev/null +++ b/examples/timer/rust_client/src/tokio_main.rs @@ -0,0 +1,64 @@ +use std::time::Duration; +use timer::{ + EchoRequest, JobSpec, RetryPolicy, ScheduleConfig, TimerConfig, TimerCtx, +}; + +#[tokio::main(flavor = "multi_thread", worker_threads = 2)] +async fn main() -> Result<(), Box> { + let ctx = TimerCtx::new_async( + TimerConfig { name: "tokio-demo".into() }, + Duration::from_secs(30), + ).await?; + + let version = ctx.version_async().await?; + println!("[1] Tokio runtime started"); + println!("[2] Version: {version}"); + + let echo1 = ctx + .echo_async(EchoRequest { + message: "hello from tokio".into(), + delay_ms: 200, + }) + .await?; + + let echo2 = ctx + .echo_async(EchoRequest { + message: "second tokio request".into(), + delay_ms: 50, + }) + .await?; + + println!("[3] Echo 1: echoed={}, timerName={}", echo1.echoed, echo1.timer_name); + println!("[4] Echo 2: echoed={}, timerName={}", echo2.echoed, echo2.timer_name); + + // ── A call with three complex parameters ──────────────────────────── + // The generated `*_async` method returns a Future, so a tokio-driven + // caller just `.await`s it like any other async fn. The macro packs + // `job`, `retry`, and `schedule` into a single CBOR envelope on the wire. + let schedule = ctx + .schedule_async( + JobSpec { + name: "hourly-sync".into(), + payload: vec!["sync".into(), "users".into()], + priority: 5, + }, + RetryPolicy { + max_attempts: 5, + backoff_ms: 250, + retry_on: vec!["timeout".into()], + }, + ScheduleConfig { + start_at_ms: 500, + interval_ms: 3_600_000, + jitter: None, + }, + ) + .await?; + println!( + "[5] Schedule (3 complex params, awaited): jobId={}, willRunCount={}, firstRunAtMs={}", + schedule.job_id, schedule.will_run_count, schedule.first_run_at_ms, + ); + + println!("\nDone. Tokio runtime shut down."); + Ok(()) +} diff --git a/examples/timer/timer.nim b/examples/timer/timer.nim new file mode 100644 index 0000000..5ecf789 --- /dev/null +++ b/examples/timer/timer.nim @@ -0,0 +1,139 @@ +import ffi, chronos, options + +type Maybe[T] = Option[T] + +# The library's main state type. The FFI context owns one instance. +type Timer = object + name: string # set at creation time, read back in each response + +declareLibrary("timer", Timer) + +type TimerConfig {.ffi.} = object + name: string + +type EchoRequest {.ffi.} = object + message: string + delayMs: int # how long chronos sleeps before replying + +type EchoResponse {.ffi.} = object + echoed: string + timerName: string # proves that the timer's own state is accessible + +type ComplexRequest {.ffi.} = object + messages: seq[EchoRequest] + tags: seq[string] + note: Option[string] + retries: Maybe[int] + +type ComplexResponse {.ffi.} = object + summary: string + itemCount: int + hasNote: bool + +# --- Constructor ----------------------------------------------------------- +# Called once from Rust. Creates the FFIContext + Timer. +# Uses chronos (await sleepAsync) so the body is async. +proc timerCreate*(config: TimerConfig): Future[Result[Timer, string]] {.ffiCtor.} = + await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread + return ok(Timer(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 timerEcho*( + timer: Timer, 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 timerVersion*(timer: Timer): Future[Result[string, string]] {.ffi.} = + return ok("nim-timer v0.1.0") + +proc timerComplex*( + timer: Timer, 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)) + +# --- Multiple complex parameters ------------------------------------------- +# Demonstrates how a {.ffi.} proc handles several object-typed parameters at +# once. Each parameter is its own {.ffi.} type, so it lands in the generated +# foreign-side bindings as a first-class struct/class, and the per-proc Req +# envelope (TimerScheduleReq on the wire) carries all three under field names +# that match the Nim params. +type JobSpec {.ffi.} = object + name: string + payload: seq[string] + priority: int # higher = runs sooner + +type RetryPolicy {.ffi.} = object + maxAttempts: int + backoffMs: int + retryOn: seq[string] # error keywords that should trigger a retry + +type ScheduleConfig {.ffi.} = object + startAtMs: int + intervalMs: int # 0 means "fire once" + jitter: Option[int] + +type ScheduleResult {.ffi.} = object + jobId: string + willRunCount: int + firstRunAtMs: int + effectiveBackoffMs: int + +proc timerSchedule*( + timer: Timer, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig +): Future[Result[ScheduleResult, string]] {.ffi.} = + ## Composes three independent object-typed parameters (`job`, `retry`, + ## `schedule`) into a single scheduling decision. The macro packs them into + ## one CBOR-encoded request envelope on the wire and unpacks them back into + ## the named locals before this body runs. + await sleepAsync(1.milliseconds) + if job.name.len == 0: + return err("job name must not be empty") + if retry.maxAttempts <= 0: + return err("retry.maxAttempts must be positive") + let willRunCount = + if schedule.intervalMs > 0: + max(1, 60_000 div schedule.intervalMs) # rough "runs per minute" + else: + 1 + let jitter = if schedule.jitter.isSome: schedule.jitter.get else: 0 + return ok( + ScheduleResult( + jobId: timer.name & ":" & job.name, + willRunCount: willRunCount, + firstRunAtMs: schedule.startAtMs + jitter, + effectiveBackoffMs: retry.backoffMs, + ) + ) + +proc timer_destroy*(timer: Timer) {.ffiDtor.} = + ## Tears down the FFI context created by timer_create. + ## Blocks until the FFI thread and watchdog thread have joined. + discard + +# genBindings() must be the LAST top-level call in the FFI root file — +# after every {.ffi.}, {.ffiCtor.} and {.ffiDtor.} pragma. Each pragma +# fires at compile time and registers its proc into the compile-time +# ffiProcRegistry / ffiTypeRegistry; genBindings() then reads those +# registries to emit the language bindings. If genBindings() runs before +# a pragma, that proc is silently absent from the generated bindings. +# +# Multi-file libraries: keep all .ffi./.ffiCtor./.ffiDtor. pragmas in +# imported sub-modules and call genBindings() once at the bottom of the +# top-level file that imports them — Nim resolves imports before the +# importing file's body runs, so the registries are fully populated by +# the time genBindings() executes. +# +# genBindings() is a compile-time no-op unless -d:ffiGenBindings is set. +genBindings() diff --git a/examples/nim_timer/nim_timer.nimble b/examples/timer/timer.nimble similarity index 59% rename from examples/nim_timer/nim_timer.nimble rename to examples/timer/timer.nimble index a21b837..4f9c33c 100644 --- a/examples/nim_timer/nim_timer.nimble +++ b/examples/timer/timer.nimble @@ -1,5 +1,5 @@ version = "0.1.0" -packageName = "nimtimer" +packageName = "timer" author = "Institute of Free Technology" description = "Example Nim timer library using nim-ffi" license = "MIT or Apache License 2.0" @@ -12,16 +12,16 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.1.3" const nimFlags = "--mm:orc -d:chronicles_log_level=WARN" -task build, "Compile the nimtimer library": +task build, "Compile the timer library": exec "nim c " & nimFlags & - " --app:lib --noMain --nimMainPrefix:libnimtimer nim_timer.nim" + " --app:lib --noMain --nimMainPrefix:libtimer timer.nim" -task genbindings_rust, "Generate Rust bindings for the nimtimer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & +task genbindings_rust, "Generate Rust bindings for the timer example": + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libtimer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" & - " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" + " -d:ffiNimSrcRelPath=timer.nim" & " -o:/dev/null timer.nim" -task genbindings_cpp, "Generate C++ bindings for the nimtimer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & +task genbindings_cpp, "Generate C++ bindings for the timer example": + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libtimer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" & - " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" + " -d:ffiNimSrcRelPath=timer.nim" & " -o:/dev/null timer.nim" diff --git a/ffi.nim b/ffi.nim index 24186bb..5f38255 100644 --- a/ffi.nim +++ b/ffi.nim @@ -2,10 +2,10 @@ import std/[atomics, tables] import chronos, chronicles import ffi/internal/[ffi_library, ffi_macro], - ffi/[alloc, ffi_types, ffi_context, ffi_context_pool, ffi_thread_request, serial] + ffi/[alloc, ffi_types, ffi_context, ffi_context_pool, ffi_thread_request, cbor_serial] export atomics, tables export chronos, chronicles export atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_context_pool, - ffi_thread_request, serial + ffi_thread_request, cbor_serial diff --git a/ffi.nimble b/ffi.nimble index 1694abd..3a47b35 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -11,6 +11,7 @@ requires "nim >= 2.2.4" requires "chronos" requires "chronicles" requires "taskpools" +requires "cbor_serialization" const nimFlagsOrc = "--mm:orc -d:chronicles_log_level=WARN" const nimFlagsRefc = "--mm:refc -d:chronicles_log_level=WARN" @@ -25,6 +26,10 @@ task test, "Run all tests under --mm:orc and --mm:refc": exec "nim c -r " & flags & " tests/test_gc_compat.nim" exec "nim c -r " & flags & " tests/test_serial.nim" exec "nim c -r " & flags & " tests/test_ctx_validation.nim" + exec "nim c -r " & flags & " tests/test_nim_native_api.nim" + exec "nim c -r " & flags & " tests/test_meta.nim" + exec "nim c -r " & flags & " tests/test_string_helpers.nim" + exec "nim c -r " & flags & " tests/test_wire_compat.nim" task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc": exec "nim c -r " & nimFlagsOrc & " tests/test_alloc.nim" @@ -34,38 +39,38 @@ task test_ffi, "Run FFI context integration tests under --mm:orc and --mm:refc": exec "nim c -r " & nimFlagsOrc & " tests/test_ffi_context.nim" exec "nim c -r " & nimFlagsRefc & " tests/test_ffi_context.nim" -task test_serial, "Run serial unit tests": +task test_serial, "Run CBOR codec unit tests": exec "nim c -r " & nimFlagsOrc & " tests/test_serial.nim" exec "nim c -r " & nimFlagsRefc & " tests/test_serial.nim" -task genbindings_example, "Generate Rust bindings for the nim_timer example": - exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim" - exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim" +task genbindings_example, "Generate Rust bindings for the timer example": + exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libtimer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim" + exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libtimer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim" -task genbindings_rust, "Generate Rust bindings for the nim_timer example": +task genbindings_rust, "Generate Rust bindings for the timer example": exec "nim c " & nimFlagsOrc & - " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " --app:lib --noMain --nimMainPrefix:libtimer" & " -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" + " -d:ffiOutputDir=examples/timer/rust_bindings" & + " -d:ffiNimSrcRelPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" exec "nim c " & nimFlagsRefc & - " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " --app:lib --noMain --nimMainPrefix:libtimer" & " -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" + " -d:ffiOutputDir=examples/timer/rust_bindings" & + " -d:ffiNimSrcRelPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" -task genbindings_cpp, "Generate C++ bindings for the nim_timer example": +task genbindings_cpp, "Generate C++ bindings for the timer example": exec "nim c " & nimFlagsOrc & - " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " --app:lib --noMain --nimMainPrefix:libtimer" & " -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" + " -d:ffiOutputDir=examples/timer/cpp_bindings" & + " -d:ffiNimSrcRelPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" exec "nim c " & nimFlagsRefc & - " --app:lib --noMain --nimMainPrefix:libnimtimer" & + " --app:lib --noMain --nimMainPrefix:libtimer" & " -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" + " -d:ffiOutputDir=examples/timer/cpp_bindings" & + " -d:ffiNimSrcRelPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" diff --git a/ffi/cbor_serial.nim b/ffi/cbor_serial.nim new file mode 100644 index 0000000..a8a5621 --- /dev/null +++ b/ffi/cbor_serial.nim @@ -0,0 +1,72 @@ +## Thin wrapper around `cbor_serialization` (vacp2p/nim-cbor-serialization) that +## adapts the library's exception-based API to the `Result[T, string]` shape the +## FFI plumbing expects, and adds the few transport-only details the FFI layer +## needs on top: +## +## - `cborEncodeShared` writes into an `allocShared` buffer so the FFI thread +## can take ownership of the bytes without a second copy. +## - `CborNullByte` is the canonical "successful but no value" wire sentinel. +## +## `cborEncode` / `cborDecode` are the public API the macros and tests use. +## +## Type contract for `.ffi.` payloads: +## +## - Plain `object` types flow as value copies — fields are serialized and +## the foreign side reconstructs an independent value. +## - `ref T` is *also* a value copy: `cbor_serialization`'s default `ref T` +## writer dereferences and encodes the pointee, so the receiving side +## allocates a fresh `ref` local to its own GC heap. No object identity +## is preserved across the boundary — the two sides own independent +## copies after decode. +## - Raw `pointer` / `ptr T` are rejected at macro-expansion time (see +## `rejectRawPtrType` in `internal/ffi_macro.nim`). The only address that +## legitimately crosses the boundary is the opaque ctx handle returned by +## `.ffiCtor.`, which is validated against `FFIContextPool` on every +## re-entry. Arbitrary user pointers would lack that validation. + +import cbor_serialization, cbor_serialization/std/options, results + +export cbor_serialization, options, results + +const CborNullByte*: byte = 0xf6'u8 + ## CBOR encoding of `null` — used as the wire sentinel for empty OK payloads. + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +proc cborEncode*[T](x: T): seq[byte] = + ## CBOR-encode any cbor_serialization-supported type (plus `pointer` / `ptr T` + ## via our custom writers) into a fresh `seq[byte]`. + return Cbor.encode(x) + +proc cborEncodeShared*[T](x: T): tuple[data: ptr UncheckedArray[byte], len: int] = + ## Encodes `x` into a shared-memory buffer (`allocShared`). + ## + ## The returned `data` is owned by the caller and must be freed exactly once + ## via `deallocShared` (the FFIThreadRequest `deleteRequest` path does this + ## automatically). Empty payloads return `(nil, 0)` without allocating. + let bytes = Cbor.encode(x) + if bytes.len == 0: + return (nil, 0) + let buf = cast[ptr UncheckedArray[byte]](allocShared(bytes.len)) + copyMem(buf, unsafeAddr bytes[0], bytes.len) + return (buf, bytes.len) + +proc cborDecode*[T](data: openArray[byte], _: typedesc[T]): Result[T, string] = + ## Decode `data` into a `T`, converting any cbor_serialization exception + ## into a `Result.err` carrying the exception message. + try: + let v = Cbor.decode(data, T) + return ok(v) + except CatchableError as exc: + return err(exc.msg) + +proc cborDecodePtr*[T]( + data: ptr UncheckedArray[byte], dataLen: int, _: typedesc[T] +): Result[T, string] = + ## Convenience for ptr+len buffers (used by the macro to avoid binding an + ## openArray to a `let`). + if dataLen <= 0: + return cborDecode(default(seq[byte]), T) + cborDecode(toOpenArray(data, 0, dataLen - 1), T) diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 920901f..fca69e9 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -1,8 +1,24 @@ ## C++ binding generator for the nim-ffi framework. -## Generates a header-only C++ binding and CMakeLists.txt from compile-time FFI metadata. +## Generates a header-only C++ binding and CMakeLists.txt. Requests/responses +## travel as CBOR (encoded with vendored TinyCBOR on the C++ side, matching +## the Nim-side cbor_serial codec on the wire — both ends speak RFC 8949). import std/[os, strutils] -import ./meta +import ./meta, ./string_helpers + +## Wire-format C++ type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so +## the CBOR payload size is stable regardless of host architecture. +const CppPtrType* = "uint64_t" + +## Static template blocks live as real C++ / CMake files under templates/cpp/ +## and are slurped into the binary at compile time. Edits to those files are +## reflected in the generated bindings without touching this codegen. +const + HeaderPreludeTpl = staticRead("templates/cpp/header_prelude.hpp.tpl") + CborHelpersTpl = staticRead("templates/cpp/cbor_helpers.hpp.tpl") + SyncCallHelperTpl = staticRead("templates/cpp/sync_call_helper.hpp.tpl") + ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl") + CMakeListsTpl = staticRead("templates/cpp/CMakeLists.txt.tpl") proc genericInnerType(typeName, prefix: string): string = if typeName.startsWith(prefix) and typeName.endsWith("]"): @@ -14,7 +30,7 @@ proc genericInnerType(typeName, prefix: string): string = proc nimTypeToCpp*(typeName: string): string = let trimmed = typeName.strip() if trimmed.startsWith("ptr "): - return "void*" + return CppPtrType else: let seqInner = genericInnerType(trimmed, "seq[") if seqInner.len > 0: @@ -32,7 +48,7 @@ proc nimTypeToCpp*(typeName: string): string = of "bool": "bool" of "float": "float" of "float64": "double" - of "pointer": "void*" + of "pointer": CppPtrType else: trimmed proc stripLibPrefixCpp(procName, libName: string): string = @@ -41,47 +57,103 @@ proc stripLibPrefixCpp(procName, libName: string): string = return procName[prefix.len .. ^1] return procName +proc reqStructName(p: FFIProcMeta): string = + let camel = snakeToPascalCase(p.procName) + if p.kind == FFIKind.CTOR: + camel & "CtorReq" + else: + camel & "Req" + +proc emitStructCborCodec( + lines: var seq[string], structName: string, fields: seq[(string, string)] +) = + ## Appends per-struct TinyCBOR encode_cbor + decode_cbor free functions for + ## `structName`. `fields` is a sequence of (field-name, ignored C++ type) + ## pairs — the type is unused at the codec layer because the generic + ## encode_cbor / decode_cbor overloads in cbor_helpers.hpp.tpl dispatch on + ## the struct member's type. We emit a CBOR map with text-string keys to + ## match the wire format produced by Nim's cbor_serialization. + let n = fields.len + # ── encode ──────────────────────────────────────────────────────────────── + if n == 0: + lines.add( + "inline CborError encode_cbor(CborEncoder& e, const $1&) {" % [structName] + ) + else: + lines.add( + "inline CborError encode_cbor(CborEncoder& e, const $1& v) {" % [structName] + ) + lines.add(" CborEncoder m;") + lines.add(" CborError err = cbor_encoder_create_map(&e, &m, $1);" % [$n]) + lines.add(" if (err) return err;") + for (name, _) in fields: + lines.add( + " err = cbor_encode_text_stringz(&m, \"$1\"); if (err) return err;" % [name] + ) + lines.add( + " err = encode_cbor(m, v.$1); if (err) return err;" % [name] + ) + lines.add(" return cbor_encoder_close_container(&e, &m);") + lines.add("}") + # ── decode ──────────────────────────────────────────────────────────────── + if n == 0: + lines.add("inline CborError decode_cbor(CborValue& it, $1&) {" % [structName]) + lines.add(" if (!cbor_value_is_map(&it)) return CborErrorImproperValue;") + lines.add(" return cbor_value_advance(&it);") + lines.add("}") + return + lines.add("inline CborError decode_cbor(CborValue& it, $1& v) {" % [structName]) + lines.add(" if (!cbor_value_is_map(&it)) return CborErrorImproperValue;") + lines.add(" CborValue field;") + lines.add(" CborError err;") + for (name, _) in fields: + lines.add( + " err = cbor_value_map_find_value(&it, \"$1\", &field); if (err) return err;" % + [name] + ) + lines.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;") + lines.add(" err = decode_cbor(field, v.$1); if (err) return err;" % [name]) + lines.add(" return cbor_value_advance(&it);") + lines.add("}") + +proc cppBracedInit(structName: string, fieldNames: seq[string]): string = + ## Produces a C++ braced-init expression for a per-proc Req struct. + ## Used to construct the request value before CBOR-encoding it for the wire, + ## as in `const auto req = TimerEchoReq{message, count};` in the generated + ## header. The field order must match the struct's declaration order, which + ## in turn mirrors the user's Nim FFI signature. + ## + ## Examples: + ## cppBracedInit("TimerEchoReq", @["message", "count"]) + ## → "TimerEchoReq{message, count}" + ## cppBracedInit("TimerVersionReq", @[]) + ## → "TimerVersionReq{}" + ## cppBracedInit("TimerCreateCtorReq", @["config"]) + ## → "TimerCreateCtorReq{config}" + ## + ## Empty `fieldNames` collapses cleanly because `join` on an empty seq + ## returns "", so the result is the well-formed empty-init `Name{}`. + return structName & "{" & fieldNames.join(", ") & "}" + proc generateCppHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string ): string = var lines: seq[string] = @[] - # ── Includes ─────────────────────────────────────────────────────────────── - lines.add("#pragma once") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("") + lines.add(HeaderPreludeTpl) - # ── nlohmann optional support ────────────────────────────────────────── - 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("") + # CBOR primitive / container helpers must precede the per-struct codecs + # below, because each emitted `encode_cbor`/`decode_cbor(T)` calls the + # generic overloads for the struct's fields (std::string, std::vector, + # std::optional, primitives). The struct codecs are non-template `inline` + # functions, so name lookup happens at parse time — the overloads must be + # in scope before the struct codecs are parsed. + lines.add(CborHelpersTpl) # ── Types ────────────────────────────────────────────────────────────────── if types.len > 0: lines.add("// ============================================================") - lines.add("// Types") + lines.add("// User-declared FFI types") lines.add("// ============================================================") lines.add("") for t in types: @@ -89,14 +161,41 @@ proc generateCppHeader*( for f in t.fields: lines.add(" $1 $2;" % [nimTypeToCpp(f.typeName), f.name]) lines.add("};") - var fieldNames: seq[string] = @[] + var fields: seq[(string, string)] = @[] for f in t.fields: - fieldNames.add(f.name) - lines.add( - "NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")] - ) + fields.add((f.name, nimTypeToCpp(f.typeName))) + emitStructCborCodec(lines, t.name, fields) lines.add("") + # ── Per-proc Req structs (CBOR transport units) ─────────────────────────── + lines.add("// ============================================================") + lines.add("// Per-proc request envelopes (CBOR encoded on the wire)") + lines.add("// ============================================================") + lines.add("") + for p in procs: + if p.kind == FFIKind.DTOR: + continue + let reqName = reqStructName(p) + lines.add("struct $1 {" % [reqName]) + for ep in p.extraParams: + let cppType = + if ep.isPtr: + CppPtrType + else: + nimTypeToCpp(ep.typeName) + lines.add(" $1 $2;" % [cppType, ep.name]) + lines.add("};") + var fields: seq[(string, string)] = @[] + for ep in p.extraParams: + let cppType = + if ep.isPtr: + CppPtrType + else: + nimTypeToCpp(ep.typeName) + fields.add((ep.name, cppType)) + emitStructCborCodec(lines, reqName, fields) + lines.add("") + # ── C FFI declarations ───────────────────────────────────────────────────── lines.add("// ============================================================") lines.add("// C FFI declarations") @@ -104,135 +203,45 @@ proc generateCppHeader*( lines.add("") lines.add("extern \"C\" {") lines.add( - "typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);" + "typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);" ) lines.add("") for p in procs: - var params: seq[string] = @[] - if p.kind in {ffiFfiKind, ffiDtorKind}: - params.add("void* ctx") - params.add("FfiCallback callback") - params.add("void* user_data") - for ep in p.extraParams: - params.add("const char* $1_json" % [ep.name]) - else: # ffiCtorKind - for ep in p.extraParams: - params.add("const char* $1_json" % [ep.name]) - params.add("FfiCallback callback") - params.add("void* user_data") - lines.add("int $1($2);" % [p.procName, params.join(", ")]) + case p.kind + of FFIKind.FFI: + lines.add( + "int $1(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);" % + [p.procName] + ) + of FFIKind.CTOR: + lines.add( + "void* $1(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);" % + [p.procName] + ) + of FFIKind.DTOR: + lines.add("int $1(void* ctx);" % [p.procName]) lines.add("} // extern \"C\"") lines.add("") - # ── Serialization helpers ────────────────────────────────────────────────── - 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("") - # Wrap parse + get in a single try/catch so callers get a clear FFI error - # rather than a raw nlohmann exception with an opaque JSON pointer message. - lines.add("template") - lines.add("inline T deserializeFfiResult(const std::string& raw) {") - lines.add(" try {") - lines.add(" return nlohmann::json::parse(raw).get();") - lines.add(" } catch (const nlohmann::json::exception& e) {") - lines.add( - " throw std::runtime_error(std::string(\"FFI response deserialization failed: \") + e.what());" - ) - lines.add(" }") - lines.add("}") - lines.add("") - lines.add("template<>") - lines.add("inline void* deserializeFfiResult(const std::string& raw) {") - lines.add(" try {") - lines.add( - " return reinterpret_cast(static_cast(std::stoull(raw)));" - ) - lines.add(" } catch (const std::exception& e) {") - lines.add( - " throw std::runtime_error(std::string(\"FFI returned non-numeric address: \") + raw);" - ) - lines.add(" }") - lines.add("}") - lines.add("") - - # ── Call helper (anonymous namespace, header-only) ───────────────────────── - lines.add("// ============================================================") - lines.add("// Synchronous call helper (anonymous namespace, header-only)") - lines.add("// ============================================================") - lines.add("") - lines.add("namespace {") - lines.add("") - lines.add("struct FfiCallState_ {") - lines.add(" std::mutex mtx;") - lines.add(" std::condition_variable cv;") - lines.add(" bool done{false};") - lines.add(" bool ok{false};") - lines.add(" std::string msg;") - lines.add("};") - lines.add("") - # user_data is a heap-allocated shared_ptr. - # Ownership: ffi_call_ holds one copy; this callback holds the other. - # When ffi_call_ times out and returns before the callback fires, the - # state stays alive (refcount 1) until Nim eventually calls back and - # deletes cb_ref — eliminating the UAF that a stack-allocated state has. - lines.add("inline void ffi_cb_(int ret, const char* msg, size_t /*len*/, void* ud) {") - lines.add(" auto* sptr = static_cast*>(ud);") - lines.add(" {") - lines.add(" auto& s = **sptr;") - lines.add(" std::lock_guard lock(s.mtx);") - lines.add(" s.ok = (ret == 0);") - lines.add(" s.msg = msg ? std::string(msg) : std::string{};") - lines.add(" s.done = true;") - lines.add(" s.cv.notify_one();") - lines.add(" }") - lines.add(" delete sptr;") - lines.add("}") - lines.add("") - lines.add( - "inline std::string ffi_call_(std::function f," - ) - lines.add(" std::chrono::milliseconds timeout) {") - lines.add(" auto state = std::make_shared();") - lines.add(" auto* cb_ref = new std::shared_ptr(state);") - lines.add(" const int ret = f(ffi_cb_, cb_ref);") - lines.add(" if (ret == 2) {") - lines.add(" delete cb_ref;") - lines.add( - " throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");" - ) - lines.add(" }") - lines.add(" std::unique_lock lock(state->mtx);") - lines.add( - " const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });" - ) - lines.add(" if (!fired)") - lines.add( - " throw std::runtime_error(\"FFI call timed out after \" + std::to_string(timeout.count()) + \"ms\");" - ) - lines.add(" if (!state->ok)") - lines.add(" throw std::runtime_error(state->msg);") - lines.add(" return state->msg;") - lines.add("}") - lines.add("") - lines.add("} // anonymous namespace") - lines.add("") + lines.add(SyncCallHelperTpl) # ── High-level C++ context class ────────────────────────────────────────── var ctors: seq[FFIProcMeta] = @[] var methods: seq[FFIProcMeta] = @[] for p in procs: - if p.kind == ffiCtorKind: ctors.add(p) - else: methods.add(p) + case p.kind + of FFIKind.CTOR: + ctors.add(p) + of FFIKind.FFI: + methods.add(p) + of FFIKind.DTOR: + discard 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: + capitalizeFirstLetter(libName) let ctxTypeName = libTypeName & "Ctx" @@ -245,49 +254,69 @@ proc generateCppHeader*( # ── Constructors ──────────────────────────────────────────────────────── for ctor in ctors: + let reqName = reqStructName(ctor) var ctorParams: seq[string] = @[] var epNames: seq[string] = @[] for ep in ctor.extraParams: - ctorParams.add("const $1& $2" % [nimTypeToCpp(ep.typeName), ep.name]) + let cppType = + if ep.isPtr: + CppPtrType + else: + nimTypeToCpp(ep.typeName) + ctorParams.add("const $1& $2" % [cppType, ep.name]) epNames.add(ep.name) let timeoutParam = "std::chrono::milliseconds timeout = std::chrono::seconds{30}" let ctorParamsWithTimeout = - if ctorParams.len > 0: ctorParams.join(", ") & ", " & timeoutParam - else: timeoutParam + if ctorParams.len > 0: + ctorParams.join(", ") & ", " & timeoutParam + else: + timeoutParam - # -- create() factory -- + let reqInit = cppBracedInit(reqName, epNames) + + # Same `ffi_*_` underscore convention as instance methods so that a ctor + # parameter cannot collide with the local Req envelope name. + # + # The ctor's C symbol returns `void*` (the ctx pointer) synchronously, but + # `ffi_call_` expects an int-returning lambda — and we want the callback + # path anyway since it carries the CBOR-encoded ctx address. Discard the + # synchronous return and yield 0 from the lambda; the address comes back + # through the callback's CBOR text-string payload. lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParamsWithTimeout]) - for ep in ctor.extraParams: - lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name]) - var callArgs: seq[string] = @[] - for ep in ctor.extraParams: - callArgs.add("$1_json.c_str()" % [ep.name]) - callArgs.add("cb") - callArgs.add("ud") - lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {") - lines.add(" return $1($2);" % [ctor.procName, callArgs.join(", ")]) + lines.add(" const auto ffi_req_ = $1;" % [reqInit]) + lines.add(" const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);") + lines.add(" const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {") + lines.add( + " (void)$1(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);" % + [ctor.procName] + ) + lines.add(" return 0;") lines.add(" }, timeout);") + lines.add(" const auto addr_str = decodeCborFFI(ffi_raw_);") lines.add(" try {") - lines.add(" const auto addr = std::stoull(raw);") + lines.add(" const auto addr = std::stoull(addr_str);") lines.add( " return $1(reinterpret_cast(static_cast(addr)), timeout);" % [ctxTypeName] ) lines.add(" } catch (const std::exception&) {") lines.add( - " throw std::runtime_error(\"FFI create returned non-numeric address: \" + raw);" + " throw std::runtime_error(\"FFI create returned non-numeric address: \" + addr_str);" ) lines.add(" }") lines.add(" }") lines.add("") - # -- createAsync() factory: uses actual param types, not hardcoded -- let captureList = - if epNames.len > 0: epNames.join(", ") & ", timeout" - else: "timeout" + if epNames.len > 0: + epNames.join(", ") & ", timeout" + else: + "timeout" let callList = - if epNames.len > 0: epNames.join(", ") & ", timeout" - else: "timeout" + if epNames.len > 0: + epNames.join(", ") & ", timeout" + else: + "timeout" lines.add( " static std::future<$1> createAsync($2) {" % [ctxTypeName, ctorParamsWithTimeout] @@ -300,76 +329,67 @@ proc generateCppHeader*( lines.add("") # ── Rule of 5 ────────────────────────────────────────────────────────── - # Destructor tears down Nim threads; copies are deleted; moves transfer ownership. - lines.add(" ~$1() {" % [ctxTypeName]) - lines.add(" if (ptr_) {") - lines.add(" $1_destroy(ptr_);" % [libName]) - lines.add(" ptr_ = nullptr;") - lines.add(" }") - lines.add(" }") - lines.add("") - lines.add(" $1(const $1&) = delete;" % [ctxTypeName]) - lines.add(" $1& operator=(const $1&) = delete;" % [ctxTypeName]) - lines.add("") lines.add( - " $1($1&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) {" % - [ctxTypeName] + ContextRuleOf5Tpl.multiReplace(("{{CTX}}", ctxTypeName), ("{{LIB}}", libName)) ) - lines.add(" other.ptr_ = nullptr;") - lines.add(" }") - lines.add(" $1& operator=($1&& other) noexcept {" % [ctxTypeName]) - lines.add(" if (this != &other) {") - lines.add(" if (ptr_) $1_destroy(ptr_);" % [libName]) - lines.add(" ptr_ = other.ptr_;") - lines.add(" timeout_ = other.timeout_;") - lines.add(" other.ptr_ = nullptr;") - lines.add(" }") - lines.add(" return *this;") - lines.add(" }") - lines.add("") # ── Instance methods ──────────────────────────────────────────────────── for m in methods: let methodName = stripLibPrefixCpp(m.procName, libName) - let retCppType = nimTypeToCpp(m.returnTypeName) + let retCppType = + if m.returnIsPtr: + CppPtrType + else: + nimTypeToCpp(m.returnTypeName) + let reqName = reqStructName(m) var methParams: seq[string] = @[] var methParamNames: seq[string] = @[] for ep in m.extraParams: - methParams.add("const $1& $2" % [nimTypeToCpp(ep.typeName), ep.name]) + let cppType = + if ep.isPtr: + CppPtrType + else: + nimTypeToCpp(ep.typeName) + methParams.add("const $1& $2" % [cppType, ep.name]) methParamNames.add(ep.name) let methParamsStr = methParams.join(", ") let methParamNamesStr = methParamNames.join(", ") + let reqInit = cppBracedInit(reqName, methParamNames) + + # Use a single-underscore-suffixed local for the Req envelope so it can't + # shadow a method parameter whose name happens to be `req` (or similar). lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr]) - for ep in m.extraParams: - lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name]) - var callArgs = @["ptr_", "cb", "ud"] - for ep in m.extraParams: - callArgs.add("$1_json.c_str()" % [ep.name]) - lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {") - lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")]) + lines.add(" const auto ffi_req_ = $1;" % [reqInit]) + lines.add(" const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);") + lines.add(" const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {") + lines.add( + " return $1(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" % + [m.procName] + ) lines.add(" }, timeout_);") - if retCppType == "void*": - lines.add(" return deserializeFfiResult(raw);") - else: - lines.add(" return deserializeFfiResult<$1>(raw);" % [retCppType]) + lines.add(" return decodeCborFFI<$1>(ffi_raw_);" % [retCppType]) lines.add(" }") lines.add("") + # The async wrapper calls the sync method via `this->methodName(...)` so + # a method param that happens to share the method's name doesn't shadow + # the call target (e.g. `schedule(job, retry, schedule)` would otherwise + # parse as invoking the `schedule` parameter). if methParamsStr.len > 0: lines.add( " std::future<$1> $2Async($3) const {" % [retCppType, methodName, methParamsStr] ) lines.add( - " return std::async(std::launch::async, [this, $1]() { return $2($3); });" % + " return std::async(std::launch::async, [this, $1]() { return this->$2($3); });" % [methParamNamesStr, methodName, methParamNamesStr] ) lines.add(" }") else: lines.add(" std::future<$1> $2Async() const {" % [retCppType, methodName]) lines.add( - " return std::async(std::launch::async, [this]() { return $1(); });" % + " return std::async(std::launch::async, [this]() { return this->$1(); });" % [methodName] ) lines.add(" }") @@ -385,119 +405,11 @@ proc generateCppHeader*( lines.add("};") lines.add("") - result = lines.join("\n") + return lines.join("\n") proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = - ## Generates CMakeLists.txt for the C++ bindings directory. - ## 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}" - var L: seq[string] = @[] - L.add("cmake_minimum_required(VERSION 3.14)") - L.add("project(" & libName & "_cpp_bindings CXX)") - L.add("") - L.add("set(CMAKE_CXX_STANDARD 17)") - L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)") - L.add("") - L.add( - "# ── nlohmann/json ─────────────────────────────────────────────────────────────" - ) - L.add("include(FetchContent)") - L.add("FetchContent_Declare(") - L.add(" nlohmann_json") - L.add(" GIT_REPOSITORY https://github.com/nlohmann/json.git") - L.add(" GIT_TAG v3.11.3") - L.add(" GIT_SHALLOW TRUE") - L.add(")") - L.add("FetchContent_MakeAvailable(nlohmann_json)") - L.add("") - 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)") - L.add(" if(EXISTS \"" & sd & "/ffi.nimble\")") - L.add(" set(REPO_ROOT \"" & sd & "\")") - L.add(" break()") - L.add(" endif()") - 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("endif()") - L.add("") - 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("find_program(NIM_EXECUTABLE nim REQUIRED)") - L.add("") - L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")") - L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".dylib\")") - L.add("elseif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")") - L.add(" set(NIM_LIB_FILE \"" & rv & "/" & libName & ".dll\")") - L.add("else()") - L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".so\")") - L.add("endif()") - L.add("") - L.add("add_custom_command(") - L.add(" OUTPUT \"" & lf & "\"") - L.add(" COMMAND \"" & nm & "\" c") - L.add(" --mm:orc") - L.add(" -d:chronicles_log_level=WARN") - L.add(" --app:lib") - L.add(" --noMain") - L.add(" \"--nimMainPrefix:lib" & libName & "\"") - L.add(" \"-o:" & lf & "\"") - L.add(" \"" & ns & "\"") - L.add(" WORKING_DIRECTORY \"" & rv & "\"") - L.add(" DEPENDS \"" & ns & "\"") - L.add(" COMMENT \"Compiling Nim library lib" & libName & "\"") - L.add(" VERBATIM") - L.add(")") - 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("add_dependencies(" & libName & " nim_lib)") - L.add("") - 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("") - 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)") - L.add(" add_dependencies(example nim_lib)") - L.add("endif()") - L.add("") - result = L.join("\n") + return CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) proc generateCppBindings*( procs: seq[FFIProcMeta], diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 7cb6bb7..9fb9f89 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -7,20 +7,19 @@ type typeName*: string # Nim type name, e.g. "EchoRequest" isPtr*: bool # true if the type is `ptr T` - FFIProcKind* = enum - ffiCtorKind - ffiFfiKind - ffiDtorKind + FFIKind* {.pure.} = enum + FFI + CTOR + DTOR FFIProcMeta* = object - procName*: string # e.g. "nimtimer_echo" - libName*: string # library name, e.g. "nimtimer" - kind*: FFIProcKind - libTypeName*: string # e.g. "NimTimer" + procName*: string # e.g. "timer_echo" + libName*: string # library name, e.g. "timer" + kind*: FFIKind + libTypeName*: string # e.g. "Timer" 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" diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index b3c59d1..07c93d9 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -1,28 +1,13 @@ ## Rust binding generator for the nim-ffi framework. -## Generates a complete Rust crate from compile-time FFI metadata. +## Generates a complete Rust crate that uses CBOR (ciborium) on the wire. import std/[os, strutils] -import ./meta +import ./meta, ./string_helpers -# --------------------------------------------------------------------------- -# 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() +## Wire-format Rust type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so +## the CBOR payload size is stable regardless of host architecture (mirrors +## CppPtrType in cpp.nim). +const RustPtrType* = "u64" proc nimTypeToRust*(typeName: string): string = ## Maps Nim type names to Rust type names, including generics. @@ -34,17 +19,24 @@ proc nimTypeToRust*(typeName: string): string = 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(t) + of "string", "cstring": + "String" + of "int", "int64": + "i64" + of "int32": + "i32" + of "bool": + "bool" + of "float", "float64": + "f64" + of "pointer": + RustPtrType + else: + capitalizeFirstLetter(t) proc deriveLibName*(procs: seq[FFIProcMeta]): string = ## Extracts the common prefix before the first `_` from proc names. - ## e.g. ["nimtimer_create", "nimtimer_echo"] → "nimtimer" + ## e.g. ["timer_create", "timer_echo"] → "timer" if currentLibName.len > 0: return currentLibName if procs.len == 0: @@ -57,18 +49,32 @@ proc deriveLibName*(procs: seq[FFIProcMeta]): string = proc stripLibPrefix*(procName: string, libName: string): string = ## Strips the library prefix from a proc name. - ## e.g. "nimtimer_echo", "nimtimer" → "echo" + ## e.g. "timer_echo", "timer" → "echo" let prefix = libName & "_" if procName.startsWith(prefix): return procName[prefix.len .. ^1] return procName +proc reqStructName(p: FFIProcMeta): string = + ## Mirrors the Nim macro: Req or CtorReq for ctors. + let camel = snakeToPascalCase(p.procName) + if p.kind == FFIKind.CTOR: + camel & "CtorReq" + else: + camel & "Req" + # --------------------------------------------------------------------------- # File generators # --------------------------------------------------------------------------- proc generateCargoToml*(libName: string): string = - result = + # `flume` is the unified callback channel (PR #23 Rust review, item 8): one + # primitive that supports both `recv_timeout` (blocking trampoline) and + # `recv_async` (async trampoline). Default-features disabled to avoid + # pulling its async-std/futures shims. + # `tokio` is needed only for `tokio::time::timeout` around the async + # `recv_async`. Feature-gating tokio (item 11) is a follow-up commit. + return """[package] name = "$1" version = "0.1.0" @@ -76,8 +82,9 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["sync"] } +ciborium = "0.2" +flume = { version = "0.11", default-features = false, features = ["async"] } +tokio = { version = "1", features = ["sync", "time"] } """ % [libName] @@ -85,7 +92,7 @@ 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 = + return """use std::path::PathBuf; use std::process::Command; @@ -137,19 +144,20 @@ fn main() { [escapedSrc, libName] proc generateLibRs*(): string = - result = """mod ffi; + return """mod ffi; mod types; mod api; pub use types::*; pub use api::*; """ -proc generateFfiRs*(procs: seq[FFIProcMeta]): string = - ## Generates ffi.rs with extern "C" declarations for all procs. +proc generateFFIRs*(procs: seq[FFIProcMeta]): string = + ## Generates ffi.rs with extern "C" declarations. Each Nim FFI proc takes a + ## single CBOR buffer (ptr+len) for its request payload. var lines: seq[string] = @[] lines.add("use std::os::raw::{c_char, c_int, c_void};") lines.add("") - lines.add("pub type FfiCallback = unsafe extern \"C\" fn(") + lines.add("pub type FFICallback = unsafe extern \"C\" fn(") lines.add(" ret: c_int,") lines.add(" msg: *const c_char,") lines.add(" len: usize,") @@ -179,28 +187,32 @@ proc generateFfiRs*(procs: seq[FFIProcMeta]): string = for p in procs: var params: seq[string] = @[] - if p.kind in {ffiFfiKind, ffiDtorKind}: - # Method/destructor: ctx comes first + case p.kind + of FFIKind.FFI: + # Method/destructor-style: ctx comes first params.add("ctx: *mut c_void") - params.add("callback: FfiCallback") + 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("req_cbor: *const u8") + params.add("req_cbor_len: usize") + lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")]) + of FFIKind.CTOR: + # Constructor: no ctx; returns the freshly-allocated handle + params.add("req_cbor: *const u8") + params.add("req_cbor_len: usize") + params.add("callback: FFICallback") params.add("user_data: *mut c_void") - - let paramStr = params.join(", ") - lines.add(" pub fn $1($2) -> c_int;" % [p.procName, paramStr]) + lines.add(" pub fn $1($2) -> *mut c_void;" % [p.procName, params.join(", ")]) + of FFIKind.DTOR: + params.add("ctx: *mut c_void") + lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")]) lines.add("}") - result = lines.join("\n") & "\n" + return lines.join("\n") & "\n" -proc generateTypesRs*(types: seq[FFITypeMeta]): string = - ## Generates types.rs with Rust structs for all FFI types. +proc generateTypesRs*(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): string = + ## Generates types.rs with Rust structs for all user-declared FFI types and + ## for each per-proc Req struct (matching the Nim macro's generated types). var lines: seq[string] = @[] lines.add("use serde::{Deserialize, Serialize};") lines.add("") @@ -209,7 +221,7 @@ proc generateTypesRs*(types: seq[FFITypeMeta]): string = 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 snakeName = camelToSnakeCase(f.name) let rustType = nimTypeToRust(f.typeName) # Add serde rename if camelCase name differs from snake_case if snakeName != f.name: @@ -218,130 +230,206 @@ proc generateTypesRs*(types: seq[FFITypeMeta]): string = lines.add("}") lines.add("") - result = lines.join("\n") + # Per-proc Req structs — these wrap the typed parameters and are the unit of + # CBOR encoding sent across the FFI boundary. + for p in procs: + if p.kind == FFIKind.DTOR: + continue + let reqName = reqStructName(p) + lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]") + if p.extraParams.len == 0: + lines.add("pub struct $1 {}" % [reqName]) + else: + lines.add("pub struct $1 {" % [reqName]) + for ep in p.extraParams: + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.isPtr: + RustPtrType + else: + nimTypeToRust(ep.typeName) + if snake != ep.name: + lines.add(" #[serde(rename = \"$1\")]" % [ep.name]) + lines.add(" pub $1: $2," % [snake, rustType]) + lines.add("}") + lines.add("") + + return lines.join("\n") proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = ## Generates api.rs with both a blocking and a tokio-async high-level API. ## - ## Blocking: ctx.echo(req) — thread-blocks via Condvar + ## Blocking: ctx.echo(req) — thread-blocks via Condvar ## Async: ctx.echo_async(req).await — non-blocking via oneshot channel; ## the FFI callback fires from the Nim/chronos thread and wakes ## the awaiting task without ever blocking a thread. + ## + ## Requests/responses are CBOR (ciborium); errors are raw UTF-8 strings. var lines: seq[string] = @[] var ctors: seq[FFIProcMeta] = @[] var methods: seq[FFIProcMeta] = @[] + var dtorProcName = "" for p in procs: - if p.kind == ffiCtorKind: ctors.add(p) - else: methods.add(p) + case p.kind + of FFIKind.CTOR: + ctors.add(p) + of FFIKind.FFI: + methods.add(p) + of FFIKind.DTOR: + if dtorProcName.len == 0: + dtorProcName = p.procName var libTypeName = "" - if ctors.len > 0: libTypeName = ctors[0].libTypeName - else: libTypeName = toPascalCase(libName) + if ctors.len > 0: + libTypeName = ctors[0].libTypeName + else: + libTypeName = capitalizeFirstLetter(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::slice;") lines.add("use std::time::Duration;") + lines.add("use serde::de::DeserializeOwned;") + lines.add("use serde::Serialize;") lines.add("use super::ffi;") lines.add("use super::types::*;") lines.add("") - # ── Blocking trampoline ──────────────────────────────────────────────────── - lines.add("#[derive(Default)]") - lines.add("struct FfiCallbackResult {") - lines.add(" payload: Option>,") + # ── CBOR helpers ─────────────────────────────────────────────────────────── + lines.add("fn encode_cbor(value: &T) -> Result, String> {") + lines.add(" let mut buf = Vec::new();") + lines.add( + " ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;" + ) + lines.add(" Ok(buf)") lines.add("}") lines.add("") - lines.add("type Pair = Arc<(Mutex, Condvar)>;") + lines.add("fn decode_cbor(bytes: &[u8]) -> Result {") + lines.add(" ciborium::de::from_reader(bytes).map_err(|e| e.to_string())") + lines.add("}") + lines.add("") + + # ── Unified FFI trampoline (PR #23 Rust review, items 1, 2, 4, 8, 9) ─────── + # One callback shape used by both the blocking and async wrappers. The + # `user_data` pointer owns a single `Box, + # String>>>`; the callback reconstructs it, sends the payload, and drops + # the box (releasing the sender). The receiver side then either + # `recv_timeout` (sync) or `recv_async` under `tokio::time::timeout` + # (async). A late callback that fires after the caller has already timed + # out sends into a closed receiver, which is harmless: the Err is + # discarded and the box drops cleanly. No Arc/Condvar; no Box leak; no + # late-fire UAF; no double trampoline. + lines.add("type FFIResult = Result, String>;") + lines.add("type FFISender = flume::Sender;") + lines.add("") + lines.add("// Reconstruct the (ret, msg, len) tuple delivered by the C callback") + lines.add( + "// into a Result, String>: payload on success, UTF-8 message on error." + ) + lines.add( + "// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the" + ) + lines.add( + "// alternative would be to dispatch a separate Err for invalid UTF-8, but the" + ) + lines.add("// codegen contract is that Nim handlers emit `string` error payloads, so") + lines.add("// invalid UTF-8 here would be a Nim-side bug.") + lines.add( + "unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult {" + ) + lines.add(" let bytes = if msg.is_null() || len == 0 {") + lines.add(" Vec::new()") + lines.add(" } else {") + lines.add(" slice::from_raw_parts(msg as *const u8, len).to_vec()") + lines.add(" };") + lines.add(" if ret == 0 { Ok(bytes) }") + lines.add(" else { Err(String::from_utf8_lossy(&bytes).into_owned()) }") + lines.add("}") lines.add("") lines.add("unsafe extern \"C\" fn on_result(") lines.add(" ret: c_int,") lines.add(" msg: *const c_char,") - lines.add(" _len: usize,") + lines.add(" len: usize,") lines.add(" user_data: *mut c_void,") lines.add(") {") - lines.add(" let pair = Arc::from_raw(user_data as *const (Mutex, 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(" // Take ownership of the boxed Sender — dropping it at end of scope") + lines.add(" // releases the only outstanding handle.") + lines.add(" let tx = Box::from_raw(user_data as *mut FFISender);") + lines.add("") + lines.add( + " // `tx.send` returns Err only if the awaiting future was dropped (and with it" + ) + lines.add( + " // the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch" + ) + lines.add( + " // lost the race, or the future was dropped before being awaited. This cannot" + ) + lines.add( + " // happen with the current rust_client demo but may occur in arbitrary" + ) + lines.add(" // downstream consumers, so we discard the Err safely.") + lines.add( + " // Given that this is invoked from a Nim thread, we can't propagate the error by panicking or" + ) + lines.add( + " // returning a Result. Furthermore, an API dev may intentionally set a timeout in the await," + ) + lines.add( + " // in which case is also fine to discard the send error in this case because the API user will" + ) + lines.add(" // handle the timeout expiry in their own code.") + lines.add( + " // The important part is to ensure that the callback doesn't panic or block indefinitely if the" + ) + lines.add(" // receiver is gone.") + lines.add(" let _ = tx.send(ffi_payload(ret, msg, len));") lines.add("}") lines.add("") - lines.add("fn ffi_call(timeout: Duration, f: F) -> Result") + lines.add("fn ffi_call_sync(timeout: Duration, f: F) -> FFIResult") lines.add("where") - lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,") + 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 (tx, rx) = flume::bounded::(1);") + lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") lines.add(" let ret = f(on_result, raw);") lines.add(" if ret == 2 {") + lines.add(" // Callback will never fire; reclaim the box to avoid a leak.") + lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });") lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") lines.add(" }") - lines.add(" 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(" match rx.recv_timeout(timeout) {") + lines.add(" Ok(payload) => payload,") + lines.add(" Err(flume::RecvTimeoutError::Timeout) =>") + lines.add(" Err(format!(\"timed out after {:?}\", timeout)),") + lines.add(" Err(flume::RecvTimeoutError::Disconnected) =>") + lines.add( + " Err(\"callback channel disconnected before delivery\".into())," + ) lines.add(" }") - lines.add(" guard.payload.clone().unwrap()") lines.add("}") lines.add("") - - # ── Async (tokio oneshot) trampoline ─────────────────────────────────────── - # The callback is invoked from the Nim/chronos thread and sends the result - # through the oneshot channel, waking the awaiting tokio task without - # blocking any thread. - lines.add("unsafe extern \"C\" fn on_result_async(") - 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 tx = Box::from_raw(") - lines.add(" user_data as *mut tokio::sync::oneshot::Sender>,") - lines.add(" );") - lines.add(" let value = 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(" let _ = tx.send(value);") - lines.add("}") - lines.add("") - # Scoped block keeps raw/tx/F dead at the single await point so the - # returned future is Send regardless of whether F itself is Send. - lines.add("async fn ffi_call_async(f: F) -> Result") + lines.add("async fn ffi_call_async(timeout: Duration, f: F) -> FFIResult") lines.add("where") - lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,") + lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,") lines.add("{") - lines.add(" let rx = {") - lines.add(" let (tx, rx) = tokio::sync::oneshot::channel::>();") - lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") - lines.add(" let ret = f(on_result_async, raw);") - lines.add(" if ret == 2 {") - lines.add(" drop(unsafe {") - lines.add(" Box::from_raw(") - lines.add(" raw as *mut tokio::sync::oneshot::Sender>,") - lines.add(" )") - lines.add(" });") - lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") - lines.add(" }") - lines.add(" rx") - lines.add(" };") - lines.add(" rx.await.map_err(|_| \"channel closed before callback fired\".to_string())?") + lines.add(" let (tx, rx) = flume::bounded::(1);") + lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") + lines.add(" let ret = f(on_result, raw);") + lines.add(" if ret == 2 {") + lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });") + lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") + lines.add(" }") + lines.add(" match tokio::time::timeout(timeout, rx.recv_async()).await {") + lines.add(" Ok(Ok(payload)) => payload,") + lines.add( + " Ok(Err(_)) => Err(\"callback channel disconnected before delivery\".into())," + ) + lines.add(" Err(_) => Err(format!(\"timed out after {:?}\", timeout)),") + lines.add(" }") lines.add("}") lines.add("") @@ -352,68 +440,119 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = lines.add(" timeout: Duration,") lines.add("}") lines.add("") + # SAFETY block applies to both impls below (PR #23 Rust review, item 7). + lines.add( + "// SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime." + ) + lines.add("// Every call through the generated FFI proc goes through") + lines.add( + "// `sendRequestToFFIThread` on the Nim side, which serialises every request" + ) + lines.add( + "// behind `ctx.lock` and dispatches handlers on a single FFI thread, so the" + ) + lines.add( + "// pointer is never accessed concurrently from Rust. The Nim-side reentrancy" + ) + lines.add("// guard (`onFFIThread` threadvar) prevents handlers from re-entering the") + lines.add( + "// dispatcher and self-deadlocking. These invariants make it sound to mark" + ) + lines.add("// the wrapper as Send + Sync.") lines.add("unsafe impl Send for $1 {}" % [ctxTypeName]) lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName]) lines.add("") + + # ── Drop: tears down the Nim runtime when the ctx goes out of scope ────── + # Without this, forgetting the ctx leaks the entire Nim runtime (FFI thread, + # watchdog, chronos, lib state). Mirrors the C++ binding's `~$1()` dtor. + # PR #23 review (Rust), Critical item 3. + if dtorProcName.len > 0: + lines.add("impl Drop for $1 {" % [ctxTypeName]) + lines.add(" fn drop(&mut self) {") + lines.add(" if !self.ptr.is_null() {") + lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName]) + lines.add(" self.ptr = std::ptr::null_mut();") + lines.add(" }") + lines.add(" }") + lines.add("}") + lines.add("") + lines.add("impl $1 {" % [ctxTypeName]) # ── Constructors ─────────────────────────────────────────────────────────── for ctor in ctors: - var asyncParamsList: seq[string] = @[] + let reqName = reqStructName(ctor) + var paramsList: seq[string] = @[] + var fieldInits: seq[string] = @[] for ep in ctor.extraParams: - asyncParamsList.add( - "$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)] - ) - let asyncParamsStr = asyncParamsList.join(", ") - let blockingParamsStr = - if asyncParamsList.len > 0: asyncParamsList.join(", ") & ", timeout: Duration" - else: "timeout: Duration" - - # Helper: emit JSON serialization lines for extra params - template emitSerialize(snakeName, rustType: string) = - 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]) + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.isPtr: + RustPtrType + else: + nimTypeToRust(ep.typeName) + paramsList.add("$1: $2" % [snake, rustType]) + fieldInits.add(snake) + # Both `create` and `new_async` accept an explicit `timeout: Duration`; the + # value flows into `self.timeout` so subsequent method calls inherit it. + # (PR #23 Rust review, item 5: don't hardcode 30s for the async ctor.) + let ctorParamsStr = + if paramsList.len > 0: + paramsList.join(", ") & ", timeout: Duration" 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]) + "timeout: Duration" - # Build the ordered arg list for the raw FFI call (ctor: params, cb, ud) - var ffiCallArgs: seq[string] = @[] - for ep in ctor.extraParams: - ffiCallArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) - ffiCallArgs.add("cb") - ffiCallArgs.add("ud") - let ffiCallArgsStr = ffiCallArgs.join(", ") + let reqLit = + if fieldInits.len > 0: + reqName & " { " & fieldInits.join(", ") & " }" + else: + reqName & " {}" # -- blocking create -- - lines.add(" pub fn create($1) -> Result {" % [blockingParamsStr]) - for ep in ctor.extraParams: - emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) - lines.add(" let raw = ffi_call(timeout, |cb, ud| unsafe {") - lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr]) + lines.add(" pub fn create($1) -> Result {" % [ctorParamsStr]) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + # Ctor C ABI returns *mut c_void synchronously AND fires the callback; + # the callback carries the success/error payload, so discard the + # synchronous return value and yield RET_OK to make the trampoline wait + # on the callback. + lines.add(" let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe {") + lines.add( + " let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" % + [ctor.procName] + ) + lines.add(" 0") lines.add(" })?;") - lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;") + # The ctor success payload is a CBOR text string holding the ctx address. + lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;") + lines.add( + " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" + ) lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") lines.add(" }") lines.add("") # -- async new_async -- - # move closure: each CString is moved in (Send), no raw ptr escapes the block - lines.add(" pub async fn new_async($1) -> Result {" % [asyncParamsStr]) - for ep in ctor.extraParams: - emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) - lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {") - lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr]) + lines.add( + " pub async fn new_async($1) -> Result {" % [ctorParamsStr] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + # See `create` above: discard the ctor's *mut c_void synchronous return + # and rely on the callback to deliver the ctx address. + lines.add(" let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe {") + lines.add( + " let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" % + [ctor.procName] + ) + lines.add(" 0") lines.add(" }).await?;") - lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;") - lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) })") + lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;") + lines.add( + " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" + ) + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") lines.add(" }") lines.add("") @@ -421,72 +560,74 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string = for m in methods: let methodName = stripLibPrefix(m.procName, libName) let retRustType = nimTypeToRust(m.returnTypeName) + let reqName = reqStructName(m) var paramsList: seq[string] = @[] + var fieldInits: seq[string] = @[] for ep in m.extraParams: - paramsList.add("$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)]) - let paramsStr = if paramsList.len > 0: ", " & paramsList.join(", ") else: "" - - template emitSerialize(snakeName, rustType: string) = - 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]) + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.isPtr: + RustPtrType + else: + nimTypeToRust(ep.typeName) + paramsList.add("$1: $2" % [snake, rustType]) + fieldInits.add(snake) + let paramsStr = + if paramsList.len > 0: + ", " & paramsList.join(", ") 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]) + "" - template emitDeserialize(retRustType: string) = - if retRustType == "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())") + let reqLit = + if fieldInits.len > 0: + reqName & " { " & fieldInits.join(", ") & " }" else: - lines.add( - " serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType] - ) + reqName & " {}" + + let retTypeForApi = if m.returnIsPtr: RustPtrType else: retRustType # -- blocking method -- - lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType]) - for ep in m.extraParams: - emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) - var ffiArgs: seq[string] = @["self.ptr", "cb", "ud"] - for ep in m.extraParams: - ffiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) - 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( + " pub fn $1(&self$2) -> Result<$3, String> {" % + [methodName, paramsStr, retTypeForApi] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + lines.add(" let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {") + lines.add( + " ffi::$1(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())" % + [m.procName] + ) lines.add(" })?;") - emitDeserialize(retRustType) + lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi]) lines.add(" }") lines.add("") # -- async method -- # ptr is cast to usize (Copy + Send) so the move closure is Send, # keeping the returned future Send for multi-threaded tokio runtimes. - lines.add(" pub async fn $1_async(&self$2) -> Result<$3, String> {" % - [methodName, paramsStr, retRustType]) - for ep in m.extraParams: - emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName)) + lines.add( + " pub async fn $1_async(&self$2) -> Result<$3, String> {" % + [methodName, paramsStr, retTypeForApi] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") lines.add(" let ptr = self.ptr as usize;") - var asyncFfiArgs: seq[string] = @["ptr as *mut c_void", "cb", "ud"] - for ep in m.extraParams: - asyncFfiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)]) - let asyncFfiArgsStr = asyncFfiArgs.join(", ") - lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {") - lines.add(" ffi::$1($2)" % [m.procName, asyncFfiArgsStr]) + lines.add( + " let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {" + ) + lines.add( + " ffi::$1(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())" % + [m.procName] + ) lines.add(" }).await?;") - emitDeserialize(retRustType) + lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi]) lines.add(" }") lines.add("") lines.add("}") - result = lines.join("\n") & "\n" + return lines.join("\n") & "\n" proc generateRustCrate*( procs: seq[FFIProcMeta], @@ -502,6 +643,6 @@ proc generateRustCrate*( 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" / "ffi.rs", generateFFIRs(procs)) + writeFile(outputDir / "src" / "types.rs", generateTypesRs(types, procs)) writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName)) diff --git a/ffi/codegen/string_helpers.nim b/ffi/codegen/string_helpers.nim new file mode 100644 index 0000000..fbe73aa --- /dev/null +++ b/ffi/codegen/string_helpers.nim @@ -0,0 +1,45 @@ +## Identifier-casing helpers shared by the codegen modules and the FFI macro. +## All three operate on `Rune` via `std/unicode` so non-ASCII identifiers +## (rare in FFI symbols but possible in field names) round-trip correctly. + +import std/[strutils, unicode] + +proc toLower*(s: string): string = + ## Unicode-aware lowercase for an entire string. Wraps `std/unicode`'s + ## per-Rune `toLower` so callers don't have to iterate manually. + var buf = "" + for r in runes(s): + buf.add($r.toLower()) + return buf + +proc camelToSnakeCase*(s: string): string = + ## Converts camelCase to snake_case. Inserts `_` before each uppercase rune + ## that's not the first character and lowercases everything. + ## e.g. "delayMs" → "delay_ms", "timerName" → "timer_name" + var snake = "" + var first = true + for r in runes(s): + if r.isUpper() and not first: + snake.add('_') + snake.add($r.toLower()) + first = false + return snake + +proc capitalizeFirstLetter*(s: string): string = + ## Returns `s` with its first rune uppercased; the rest is left unchanged. + ## e.g. "abc" → "Abc", "" → "", "Abc" → "Abc" + if s.len == 0: + return s + var runesSeq = toRunes(s) + runesSeq[0] = runesSeq[0].toUpper() + return $runesSeq + +proc snakeToPascalCase*(s: string): string = + ## Converts snake_case identifiers to PascalCase: split on `_`, uppercase + ## the first rune of each part, concatenate. + ## e.g. "testlib_create" → "TestlibCreate", "hello_world" → "HelloWorld" + let parts = s.split('_') + var pascal = "" + for p in parts: + pascal.add capitalizeFirstLetter(p) + return pascal diff --git a/ffi/codegen/templates/cpp/CMakeLists.txt.tpl b/ffi/codegen/templates/cpp/CMakeLists.txt.tpl new file mode 100644 index 0000000..3cfa449 --- /dev/null +++ b/ffi/codegen/templates/cpp/CMakeLists.txt.tpl @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.14) +project({{LIB}}_cpp_bindings CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── 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() + +get_filename_component(NIM_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/{{SRC}}" + ABSOLUTE) + +find_program(NIM_EXECUTABLE nim REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.dylib") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_LIB_FILE "${REPO_ROOT}/{{LIB}}.dll") +else() + set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.so") +endif() + +add_custom_command( + OUTPUT "${NIM_LIB_FILE}" + COMMAND "${NIM_EXECUTABLE}" c + --mm:orc + -d:chronicles_log_level=WARN + --app:lib + --noMain + "--nimMainPrefix:lib{{LIB}}" + "-o:${NIM_LIB_FILE}" + "${NIM_SRC}" + WORKING_DIRECTORY "${REPO_ROOT}" + DEPENDS "${NIM_SRC}" + COMMENT "Compiling Nim library lib{{LIB}}" + VERBATIM +) +add_custom_target(nim_lib ALL DEPENDS "${NIM_LIB_FILE}") + +add_library({{LIB}} SHARED IMPORTED GLOBAL) +set_target_properties({{LIB}} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +add_dependencies({{LIB}} nim_lib) + +# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ───────── +set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor") +add_library(tinycbor STATIC + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c" +) +target_include_directories(tinycbor PUBLIC + "${TINYCBOR_SRC_DIR}" # consumer uses #include + "${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here +) +set_property(TARGET tinycbor PROPERTY C_STANDARD 99) +set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON) + +add_library({{LIB}}_headers INTERFACE) +target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor) + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") + add_executable(example main.cpp) + target_link_libraries(example PRIVATE {{LIB}}_headers) + add_dependencies(example nim_lib) +endif() diff --git a/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl b/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl new file mode 100644 index 0000000..9212775 --- /dev/null +++ b/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl @@ -0,0 +1,168 @@ +// ── encode_cbor overloads (primitives + containers) ───────────────────── +// Per-struct encode_cbor / decode_cbor are emitted by cpp.nim next to each +// generated struct. These helpers cover the leaf types and container shapes +// the struct emitters defer into. + +inline CborError encode_cbor(CborEncoder& e, bool v) { + return cbor_encode_boolean(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int64_t v) { + return cbor_encode_int(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int32_t v) { + return cbor_encode_int(&e, static_cast(v)); +} +inline CborError encode_cbor(CborEncoder& e, uint64_t v) { + return cbor_encode_uint(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, double v) { + return cbor_encode_double(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, const std::string& v) { + return cbor_encode_text_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(&e, &arr, v.size()); + if (err) return err; + for (const auto& item : v) { + err = encode_cbor(arr, item); + if (err) return err; + } + return cbor_encoder_close_container(&e, &arr); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { + if (!v) return cbor_encode_null(&e); + return encode_cbor(e, *v); +} + +// ── decode_cbor overloads ─────────────────────────────────────────────── + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_boolean(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int64_t& out) { + if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_int64_checked(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int32_t& out) { + int64_t tmp = 0; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = static_cast(tmp); + return CborNoError; +} +inline CborError decode_cbor(CborValue& it, uint64_t& out) { + if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_uint64(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, double& out) { + if (cbor_value_is_double(&it)) { + CborError err = cbor_value_get_double(&it, &out); + if (err) return err; + return cbor_value_advance(&it); + } + if (cbor_value_is_float(&it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(&it, &f); + if (err) return err; + out = static_cast(f); + return cbor_value_advance(&it); + } + return CborErrorImproperValue; +} +inline CborError decode_cbor(CborValue& it, std::string& out) { + if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + +template +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_array(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(&it, &len); + if (err) return err; + out.clear(); + out.resize(len); + CborValue inner; + err = cbor_value_enter_container(&it, &inner); + if (err) return err; + for (size_t i = 0; i < len; ++i) { + err = decode_cbor(inner, out[i]); + if (err) return err; + } + return cbor_value_leave_container(&it, &inner); +} + +template +inline CborError decode_cbor(CborValue& it, std::optional& out) { + if (cbor_value_is_null(&it)) { + out = std::nullopt; + return cbor_value_advance(&it); + } + T tmp{}; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = std::move(tmp); + return CborNoError; +} + +// ── Public entry points ───────────────────────────────────────────────── + +template +inline std::vector encodeCborFFI(const T& value) { + // Start with a generous 4 KiB buffer; double on overflow until it fits. + std::vector buf(4096); + while (true) { + CborEncoder enc; + cbor_encoder_init(&enc, buf.data(), buf.size(), 0); + CborError err = encode_cbor(enc, value); + if (err == CborNoError) { + const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); + buf.resize(used); + return buf; + } + if (err == CborErrorOutOfMemory) { + const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc); + buf.resize(buf.size() + (extra > 0 ? extra : buf.size())); + continue; + } + throw std::runtime_error(std::string("FFI CBOR encode failed: ") + + cbor_error_string(err)); + } +} + +template +inline T decodeCborFFI(const std::vector& bytes) { + CborParser parser; + CborValue it; + CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); + if (err != CborNoError) { + throw std::runtime_error(std::string("FFI CBOR parse init failed: ") + + cbor_error_string(err)); + } + T out{}; + err = decode_cbor(it, out); + if (err != CborNoError) { + throw std::runtime_error(std::string("FFI CBOR decode failed: ") + + cbor_error_string(err)); + } + return out; +} diff --git a/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl b/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl new file mode 100644 index 0000000..d09405b --- /dev/null +++ b/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl @@ -0,0 +1,34 @@ + // Rule of Five: because this class owns a raw resource (the {{LIB}} + // context pointer freed in the destructor), the compiler-generated copy + // and move special members would do the wrong thing — copies would + // double-free, and a default move would leave both objects pointing at + // the same context. So we define all five special members explicitly: + // 1. destructor — releases the context. + // 2. copy constructor — deleted; contexts are not copyable. + // 3. copy assignment — deleted; same reason. + // 4. move constructor — transfers ownership, nulls the source. + // 5. move assignment — destroys the current context, then + // transfers ownership from `other`. + // See: https://en.cppreference.com/w/cpp/language/rule_of_three + ~{{CTX}}() { + if (ptr_) { + {{LIB}}_destroy(ptr_); + ptr_ = nullptr; + } + } + + {{CTX}}(const {{CTX}}&) = delete; + {{CTX}}& operator=(const {{CTX}}&) = delete; + + {{CTX}}({{CTX}}&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) { + other.ptr_ = nullptr; + } + {{CTX}}& operator=({{CTX}}&& other) noexcept { + if (this != &other) { + if (ptr_) {{LIB}}_destroy(ptr_); + ptr_ = other.ptr_; + timeout_ = other.timeout_; + other.ptr_ = nullptr; + } + return *this; + } diff --git a/ffi/codegen/templates/cpp/header_prelude.hpp.tpl b/ffi/codegen/templates/cpp/header_prelude.hpp.tpl new file mode 100644 index 0000000..2a835a7 --- /dev/null +++ b/ffi/codegen/templates/cpp/header_prelude.hpp.tpl @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} diff --git a/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl b/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl new file mode 100644 index 0000000..574aac4 --- /dev/null +++ b/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl @@ -0,0 +1,52 @@ +// ============================================================ +// Synchronous call helper +// ============================================================ + +namespace { + +struct FFICallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::vector bytes; + std::string err; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { + // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; + // take ownership here so it's freed on every exit path. + std::unique_ptr> handle( + static_cast*>(ud)); + FFICallState_& s = **handle; + + std::lock_guard lock(s.mtx); + s.ok = (ret == 0); + if (msg && len > 0) { + const auto* p = reinterpret_cast(msg); + if (s.ok) s.bytes.assign(p, p + len); + else s.err.assign(msg, len); + } + s.done = true; + s.cv.notify_one(); +} + +inline std::vector ffi_call_(std::function f, + std::chrono::milliseconds timeout) { + auto state = std::make_shared(); + auto* cb_ref = new std::shared_ptr(state); + const int ret = f(ffi_cb_, cb_ref); + if (ret == 2) { + delete cb_ref; + throw std::runtime_error("RET_MISSING_CALLBACK (internal error)"); + } + std::unique_lock lock(state->mtx); + const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); + if (!fired) + throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms"); + if (!state->ok) + throw std::runtime_error(state->err); + return state->bytes; +} + +} // anonymous namespace diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE b/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE new file mode 100644 index 0000000..4aad977 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h b/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h new file mode 100644 index 0000000..be5bbc7 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h @@ -0,0 +1,724 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBOR_H +#define CBOR_H + +#ifndef assert +#include +#endif +#include +#include +#include +#include +#include + +#include "tinycbor-version.h" + +#define TINYCBOR_VERSION ((TINYCBOR_VERSION_MAJOR << 16) | (TINYCBOR_VERSION_MINOR << 8) | TINYCBOR_VERSION_PATCH) + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#ifndef SIZE_MAX +/* Some systems fail to define SIZE_MAX in , even though C99 requires it... + * Conversion from signed to unsigned is defined in 6.3.1.3 (Signed and unsigned integers) p2, + * which says: "the value is converted by repeatedly adding or subtracting one more than the + * maximum value that can be represented in the new type until the value is in the range of the + * new type." + * So -1 gets converted to size_t by adding SIZE_MAX + 1, which results in SIZE_MAX. + */ +# define SIZE_MAX ((size_t)-1) +#endif + +#ifndef CBOR_API +# define CBOR_API +#endif +#ifndef CBOR_PRIVATE_API +# define CBOR_PRIVATE_API +#endif +#ifndef CBOR_INLINE_API +# if defined(__cplusplus) +# define CBOR_INLINE inline +# define CBOR_INLINE_API inline +# else +# define CBOR_INLINE_API static CBOR_INLINE +# if defined(_MSC_VER) +# define CBOR_INLINE __inline +# elif defined(__GNUC__) +# define CBOR_INLINE __inline__ +# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define CBOR_INLINE inline +# else +# define CBOR_INLINE +# endif +# endif +#endif + +typedef enum CborType { + CborIntegerType = 0x00, + CborByteStringType = 0x40, + CborTextStringType = 0x60, + CborArrayType = 0x80, + CborMapType = 0xa0, + CborTagType = 0xc0, + CborSimpleType = 0xe0, + CborBooleanType = 0xf5, + CborNullType = 0xf6, + CborUndefinedType = 0xf7, + CborHalfFloatType = 0xf9, + CborFloatType = 0xfa, + CborDoubleType = 0xfb, + + CborInvalidType = 0xff /* equivalent to the break byte, so it will never be used */ +} CborType; + +typedef uint64_t CborTag; +typedef enum CborKnownTags { + CborDateTimeStringTag = 0, + CborUnixTime_tTag = 1, + CborPositiveBignumTag = 2, + CborNegativeBignumTag = 3, + CborDecimalTag = 4, + CborBigfloatTag = 5, + CborCOSE_Encrypt0Tag = 16, + CborCOSE_Mac0Tag = 17, + CborCOSE_Sign1Tag = 18, + CborExpectedBase64urlTag = 21, + CborExpectedBase64Tag = 22, + CborExpectedBase16Tag = 23, + CborEncodedCborTag = 24, + CborUrlTag = 32, + CborBase64urlTag = 33, + CborBase64Tag = 34, + CborRegularExpressionTag = 35, + CborMimeMessageTag = 36, + CborCOSE_EncryptTag = 96, + CborCOSE_MacTag = 97, + CborCOSE_SignTag = 98, + CborSignatureTag = 55799 +} CborKnownTags; + +/* #define the constants so we can check with #ifdef */ +#define CborDateTimeStringTag CborDateTimeStringTag +#define CborUnixTime_tTag CborUnixTime_tTag +#define CborPositiveBignumTag CborPositiveBignumTag +#define CborNegativeBignumTag CborNegativeBignumTag +#define CborDecimalTag CborDecimalTag +#define CborBigfloatTag CborBigfloatTag +#define CborCOSE_Encrypt0Tag CborCOSE_Encrypt0Tag +#define CborCOSE_Mac0Tag CborCOSE_Mac0Tag +#define CborCOSE_Sign1Tag CborCOSE_Sign1Tag +#define CborExpectedBase64urlTag CborExpectedBase64urlTag +#define CborExpectedBase64Tag CborExpectedBase64Tag +#define CborExpectedBase16Tag CborExpectedBase16Tag +#define CborEncodedCborTag CborEncodedCborTag +#define CborUrlTag CborUrlTag +#define CborBase64urlTag CborBase64urlTag +#define CborBase64Tag CborBase64Tag +#define CborRegularExpressionTag CborRegularExpressionTag +#define CborMimeMessageTag CborMimeMessageTag +#define CborCOSE_EncryptTag CborCOSE_EncryptTag +#define CborCOSE_MacTag CborCOSE_MacTag +#define CborCOSE_SignTag CborCOSE_SignTag +#define CborSignatureTag CborSignatureTag + +/* Error API */ + +typedef enum CborError { + CborNoError = 0, + + /* errors in all modes */ + CborUnknownError, + CborErrorUnknownLength, /* request for length in array, map, or string with indeterminate length */ + CborErrorAdvancePastEOF, + CborErrorIO, + + /* parser errors streaming errors */ + CborErrorGarbageAtEnd = 256, + CborErrorUnexpectedEOF, + CborErrorUnexpectedBreak, + CborErrorUnknownType, /* can only happen in major type 7 */ + CborErrorIllegalType, /* type not allowed here */ + CborErrorIllegalNumber, + CborErrorIllegalSimpleType, /* types of value less than 32 encoded in two bytes */ + CborErrorNoMoreStringChunks, + + /* parser errors in strict mode parsing only */ + CborErrorUnknownSimpleType = 512, + CborErrorUnknownTag, + CborErrorInappropriateTagForType, + CborErrorDuplicateObjectKeys, + CborErrorInvalidUtf8TextString, + CborErrorExcludedType, + CborErrorExcludedValue, + CborErrorImproperValue, + CborErrorOverlongEncoding, + CborErrorMapKeyNotString, + CborErrorMapNotSorted, + CborErrorMapKeysNotUnique, + + /* encoder errors */ + CborErrorTooManyItems = 768, + CborErrorTooFewItems, + + /* internal implementation errors */ + CborErrorDataTooLarge = 1024, + CborErrorNestingTooDeep, + CborErrorUnsupportedType, + CborErrorUnimplementedValidation, + + /* errors in converting to JSON */ + CborErrorJsonObjectKeyIsAggregate = 1280, + CborErrorJsonObjectKeyNotString, + CborErrorJsonNotImplemented, + + CborErrorOutOfMemory = (int) (~0U / 2 + 1), + CborErrorInternalError = (int) (~0U / 2) /* INT_MAX on two's complement machines */ +} CborError; + +CBOR_API const char *cbor_error_string(CborError error); + +/* Encoder API */ + +typedef enum CborEncoderAppendType +{ + CborEncoderAppendCborData = 0, + CborEncoderAppendStringData = 1 +} CborEncoderAppendType; + +typedef CborError (*CborEncoderWriteFunction)(void *, const void *, size_t, CborEncoderAppendType); + +enum CborEncoderFlags +{ + CborIteratorFlag_WriterFunction = 0x01, + CborIteratorFlag_ContainerIsMap_ = 0x20 +}; + +struct CborEncoder +{ + union { + uint8_t *ptr; + ptrdiff_t bytes_needed; + CborEncoderWriteFunction writer; + } data; + uint8_t *end; + size_t remaining; + int flags; +}; +typedef struct CborEncoder CborEncoder; + +static const size_t CborIndefiniteLength = SIZE_MAX; + +#ifndef CBOR_NO_ENCODER_API +CBOR_API void cbor_encoder_init(CborEncoder *encoder, uint8_t *buffer, size_t size, int flags); +CBOR_API void cbor_encoder_init_writer(CborEncoder *encoder, CborEncoderWriteFunction writer, void *); +CBOR_API CborError cbor_encode_uint(CborEncoder *encoder, uint64_t value); +CBOR_API CborError cbor_encode_int(CborEncoder *encoder, int64_t value); +CBOR_API CborError cbor_encode_negative_int(CborEncoder *encoder, uint64_t absolute_value); +CBOR_API CborError cbor_encode_simple_value(CborEncoder *encoder, uint8_t value); +CBOR_API CborError cbor_encode_tag(CborEncoder *encoder, CborTag tag); +CBOR_API CborError cbor_encode_text_string(CborEncoder *encoder, const char *string, size_t length); +CBOR_INLINE_API CborError cbor_encode_text_stringz(CborEncoder *encoder, const char *string) +{ return cbor_encode_text_string(encoder, string, strlen(string)); } +CBOR_API CborError cbor_encode_byte_string(CborEncoder *encoder, const uint8_t *string, size_t length); +CBOR_API CborError cbor_encode_floating_point(CborEncoder *encoder, CborType fpType, const void *value); + +CBOR_INLINE_API CborError cbor_encode_boolean(CborEncoder *encoder, bool value) +{ return cbor_encode_simple_value(encoder, (int)value - 1 + (CborBooleanType & 0x1f)); } +CBOR_INLINE_API CborError cbor_encode_null(CborEncoder *encoder) +{ return cbor_encode_simple_value(encoder, CborNullType & 0x1f); } +CBOR_INLINE_API CborError cbor_encode_undefined(CborEncoder *encoder) +{ return cbor_encode_simple_value(encoder, CborUndefinedType & 0x1f); } + +CBOR_INLINE_API CborError cbor_encode_half_float(CborEncoder *encoder, const void *value) +{ return cbor_encode_floating_point(encoder, CborHalfFloatType, value); } +CBOR_API CborError cbor_encode_float_as_half_float(CborEncoder *encoder, float value); +CBOR_INLINE_API CborError cbor_encode_float(CborEncoder *encoder, float value) +{ return cbor_encode_floating_point(encoder, CborFloatType, &value); } +CBOR_INLINE_API CborError cbor_encode_double(CborEncoder *encoder, double value) +{ return cbor_encode_floating_point(encoder, CborDoubleType, &value); } + +CBOR_API CborError cbor_encoder_create_array(CborEncoder *parentEncoder, CborEncoder *arrayEncoder, size_t length); +CBOR_API CborError cbor_encoder_create_map(CborEncoder *parentEncoder, CborEncoder *mapEncoder, size_t length); +CBOR_API CborError cbor_encoder_close_container(CborEncoder *parentEncoder, const CborEncoder *containerEncoder); +CBOR_API CborError cbor_encoder_close_container_checked(CborEncoder *parentEncoder, const CborEncoder *containerEncoder); + +CBOR_INLINE_API uint8_t *_cbor_encoder_get_buffer_pointer(const CborEncoder *encoder) +{ + return encoder->data.ptr; +} + +CBOR_INLINE_API size_t cbor_encoder_get_buffer_size(const CborEncoder *encoder, const uint8_t *buffer) +{ + return (size_t)(encoder->data.ptr - buffer); +} + +CBOR_INLINE_API size_t cbor_encoder_get_extra_bytes_needed(const CborEncoder *encoder) +{ + return encoder->end ? 0 : (size_t)encoder->data.bytes_needed; +} +#endif /* CBOR_NO_ENCODER_API */ + +/* Parser API */ + +enum CborParserGlobalFlags +{ + CborParserFlag_ExternalSource = 0x01 +}; + +enum CborParserIteratorFlags +{ + /* used for all types, but not during string chunk iteration + * (values are static-asserted, don't change) */ + CborIteratorFlag_IntegerValueIs64Bit = 0x01, + CborIteratorFlag_IntegerValueTooLarge = 0x02, + + /* used only for CborIntegerType */ + CborIteratorFlag_NegativeInteger = 0x04, + + /* used only during string iteration */ + CborIteratorFlag_BeforeFirstStringChunk = 0x04, + CborIteratorFlag_IteratingStringChunks = 0x08, + + /* used for arrays, maps and strings, including during chunk iteration */ + CborIteratorFlag_UnknownLength = 0x10, + + /* used for maps, but must be kept for all types + * (ContainerIsMap value must be CborMapType - CborArrayType) */ + CborIteratorFlag_ContainerIsMap = 0x20, + CborIteratorFlag_NextIsMapKey = 0x40 +}; + +struct CborValue; +struct CborParserOperations +{ + bool (*can_read_bytes)(void *token, size_t len); + void *(*read_bytes)(void *token, void *dst, size_t offset, size_t len); + void (*advance_bytes)(void *token, size_t len); + CborError (*transfer_string)(void *token, const void **userptr, size_t offset, size_t len); +}; + +struct CborParser +{ + union { + const uint8_t *end; + const struct CborParserOperations *ops; + } source; + enum CborParserGlobalFlags flags; +}; +typedef struct CborParser CborParser; + +struct CborValue +{ + const CborParser *parser; + union { + const uint8_t *ptr; + void *token; + } source; + uint32_t remaining; + uint16_t extra; + uint8_t type; + uint8_t flags; +}; +typedef struct CborValue CborValue; + +#ifndef CBOR_NO_PARSER_API +CBOR_API CborError cbor_parser_init(const uint8_t *buffer, size_t size, uint32_t flags, CborParser *parser, CborValue *it); +CBOR_API CborError cbor_parser_init_reader(const struct CborParserOperations *ops, CborParser *parser, CborValue *it, void *token); + +CBOR_API CborError cbor_value_validate_basic(const CborValue *it); + +CBOR_INLINE_API bool cbor_value_at_end(const CborValue *it) +{ return it->remaining == 0; } +CBOR_INLINE_API const uint8_t *cbor_value_get_next_byte(const CborValue *it) +{ return it->source.ptr; } +CBOR_API CborError cbor_value_reparse(CborValue *it); +CBOR_API CborError cbor_value_advance_fixed(CborValue *it); +CBOR_API CborError cbor_value_advance(CborValue *it); +CBOR_INLINE_API bool cbor_value_is_container(const CborValue *it) +{ return it->type == CborArrayType || it->type == CborMapType; } +CBOR_API CborError cbor_value_enter_container(const CborValue *it, CborValue *recursed); +CBOR_API CborError cbor_value_leave_container(CborValue *it, const CborValue *recursed); + +CBOR_PRIVATE_API uint64_t _cbor_value_decode_int64_internal(const CborValue *value); +CBOR_INLINE_API uint64_t _cbor_value_extract_int64_helper(const CborValue *value) +{ + return value->flags & CborIteratorFlag_IntegerValueTooLarge ? + _cbor_value_decode_int64_internal(value) : value->extra; +} + +CBOR_INLINE_API bool cbor_value_is_valid(const CborValue *value) +{ return value && value->type != CborInvalidType; } +CBOR_INLINE_API CborType cbor_value_get_type(const CborValue *value) +{ return (CborType)value->type; } + +/* Null & undefined type */ +CBOR_INLINE_API bool cbor_value_is_null(const CborValue *value) +{ return value->type == CborNullType; } +CBOR_INLINE_API bool cbor_value_is_undefined(const CborValue *value) +{ return value->type == CborUndefinedType; } + +/* Booleans */ +CBOR_INLINE_API bool cbor_value_is_boolean(const CborValue *value) +{ return value->type == CborBooleanType; } +CBOR_INLINE_API CborError cbor_value_get_boolean(const CborValue *value, bool *result) +{ + assert(cbor_value_is_boolean(value)); + *result = !!value->extra; + return CborNoError; +} + +/* Simple types */ +CBOR_INLINE_API bool cbor_value_is_simple_type(const CborValue *value) +{ return value->type == CborSimpleType; } +CBOR_INLINE_API CborError cbor_value_get_simple_type(const CborValue *value, uint8_t *result) +{ + assert(cbor_value_is_simple_type(value)); + *result = (uint8_t)value->extra; + return CborNoError; +} + +/* Integers */ +CBOR_INLINE_API bool cbor_value_is_integer(const CborValue *value) +{ return value->type == CborIntegerType; } +CBOR_INLINE_API bool cbor_value_is_unsigned_integer(const CborValue *value) +{ return cbor_value_is_integer(value) && (value->flags & CborIteratorFlag_NegativeInteger) == 0; } +CBOR_INLINE_API bool cbor_value_is_negative_integer(const CborValue *value) +{ return cbor_value_is_integer(value) && (value->flags & CborIteratorFlag_NegativeInteger); } + +CBOR_INLINE_API CborError cbor_value_get_raw_integer(const CborValue *value, uint64_t *result) +{ + assert(cbor_value_is_integer(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_uint64(const CborValue *value, uint64_t *result) +{ + assert(cbor_value_is_unsigned_integer(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_int64(const CborValue *value, int64_t *result) +{ + assert(cbor_value_is_integer(value)); + *result = (int64_t) _cbor_value_extract_int64_helper(value); + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_int(const CborValue *value, int *result) +{ + assert(cbor_value_is_integer(value)); + *result = (int) _cbor_value_extract_int64_helper(value); + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +CBOR_API CborError cbor_value_get_int64_checked(const CborValue *value, int64_t *result); +CBOR_API CborError cbor_value_get_int_checked(const CborValue *value, int *result); + +CBOR_INLINE_API bool cbor_value_is_length_known(const CborValue *value) +{ return (value->flags & CborIteratorFlag_UnknownLength) == 0; } + +/* Tags */ +CBOR_INLINE_API bool cbor_value_is_tag(const CborValue *value) +{ return value->type == CborTagType; } +CBOR_INLINE_API CborError cbor_value_get_tag(const CborValue *value, CborTag *result) +{ + assert(cbor_value_is_tag(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} +CBOR_API CborError cbor_value_skip_tag(CborValue *it); + +/* Strings */ +CBOR_INLINE_API bool cbor_value_is_byte_string(const CborValue *value) +{ return value->type == CborByteStringType; } +CBOR_INLINE_API bool cbor_value_is_text_string(const CborValue *value) +{ return value->type == CborTextStringType; } + +CBOR_INLINE_API CborError cbor_value_get_string_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_byte_string(value) || cbor_value_is_text_string(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_PRIVATE_API CborError _cbor_value_copy_string(const CborValue *value, void *buffer, + size_t *buflen, CborValue *next); +CBOR_PRIVATE_API CborError _cbor_value_dup_string(const CborValue *value, void **buffer, + size_t *buflen, CborValue *next); + +CBOR_API CborError cbor_value_calculate_string_length(const CborValue *value, size_t *length); + +CBOR_INLINE_API CborError cbor_value_copy_text_string(const CborValue *value, char *buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_copy_string(value, buffer, buflen, next); +} +CBOR_INLINE_API CborError cbor_value_copy_byte_string(const CborValue *value, uint8_t *buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_copy_string(value, buffer, buflen, next); +} + +CBOR_INLINE_API CborError cbor_value_dup_text_string(const CborValue *value, char **buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_dup_string(value, (void **)buffer, buflen, next); +} +CBOR_INLINE_API CborError cbor_value_dup_byte_string(const CborValue *value, uint8_t **buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_dup_string(value, (void **)buffer, buflen, next); +} + +CBOR_PRIVATE_API CborError _cbor_value_get_string_chunk_size(const CborValue *value, size_t *len); +CBOR_INLINE_API CborError cbor_value_get_string_chunk_size(const CborValue *value, size_t *len) +{ + assert(value->flags & CborIteratorFlag_IteratingStringChunks); + return _cbor_value_get_string_chunk_size(value, len); +} + +CBOR_INLINE_API bool cbor_value_string_iteration_at_end(const CborValue *value) +{ + size_t dummy; + return cbor_value_get_string_chunk_size(value, &dummy) == CborErrorNoMoreStringChunks; +} + +CBOR_PRIVATE_API CborError _cbor_value_begin_string_iteration(CborValue *value); +CBOR_INLINE_API CborError cbor_value_begin_string_iteration(CborValue *value) +{ + assert(cbor_value_is_text_string(value) || cbor_value_is_byte_string(value)); + assert(!(value->flags & CborIteratorFlag_IteratingStringChunks)); + return _cbor_value_begin_string_iteration(value); +} + +CBOR_PRIVATE_API CborError _cbor_value_finish_string_iteration(CborValue *value); +CBOR_INLINE_API CborError cbor_value_finish_string_iteration(CborValue *value) +{ + assert(cbor_value_string_iteration_at_end(value)); + return _cbor_value_finish_string_iteration(value); +} + +CBOR_PRIVATE_API CborError _cbor_value_get_string_chunk(const CborValue *value, const void **bufferptr, + size_t *len, CborValue *next); +CBOR_INLINE_API CborError cbor_value_get_text_string_chunk(const CborValue *value, const char **bufferptr, + size_t *len, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_get_string_chunk(value, (const void **)bufferptr, len, next); +} +CBOR_INLINE_API CborError cbor_value_get_byte_string_chunk(const CborValue *value, const uint8_t **bufferptr, + size_t *len, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_get_string_chunk(value, (const void **)bufferptr, len, next); +} + +CBOR_API CborError cbor_value_text_string_equals(const CborValue *value, const char *string, bool *result); + +/* Maps and arrays */ +CBOR_INLINE_API bool cbor_value_is_array(const CborValue *value) +{ return value->type == CborArrayType; } +CBOR_INLINE_API bool cbor_value_is_map(const CborValue *value) +{ return value->type == CborMapType; } + +CBOR_INLINE_API CborError cbor_value_get_array_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_array(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_map_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_map(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_API CborError cbor_value_map_find_value(const CborValue *map, const char *string, CborValue *element); + +/* Floating point */ +CBOR_INLINE_API bool cbor_value_is_half_float(const CborValue *value) +{ return value->type == CborHalfFloatType; } +CBOR_API CborError cbor_value_get_half_float_as_float(const CborValue *value, float *result); +CBOR_INLINE_API CborError cbor_value_get_half_float(const CborValue *value, void *result) +{ + assert(cbor_value_is_half_float(value)); + assert((value->flags & CborIteratorFlag_IntegerValueTooLarge) == 0); + + /* size has already been computed */ + memcpy(result, &value->extra, sizeof(value->extra)); + return CborNoError; +} + +CBOR_INLINE_API bool cbor_value_is_float(const CborValue *value) +{ return value->type == CborFloatType; } +CBOR_INLINE_API CborError cbor_value_get_float(const CborValue *value, float *result) +{ + uint32_t data; + assert(cbor_value_is_float(value)); + assert(value->flags & CborIteratorFlag_IntegerValueTooLarge); + data = (uint32_t)_cbor_value_decode_int64_internal(value); + memcpy(result, &data, sizeof(*result)); + return CborNoError; +} + +CBOR_INLINE_API bool cbor_value_is_double(const CborValue *value) +{ return value->type == CborDoubleType; } +CBOR_INLINE_API CborError cbor_value_get_double(const CborValue *value, double *result) +{ + uint64_t data; + assert(cbor_value_is_double(value)); + assert(value->flags & CborIteratorFlag_IntegerValueTooLarge); + data = _cbor_value_decode_int64_internal(value); + memcpy(result, &data, sizeof(*result)); + return CborNoError; +} + +/* Validation API */ +#ifndef CBOR_NO_VALIDATION_API + +enum CborValidationFlags { + /* Bit mapping: + * bits 0-7 (8 bits): canonical format + * bits 8-11 (4 bits): canonical format & strict mode + * bits 12-20 (8 bits): strict mode + * bits 21-31 (10 bits): other + */ + + CborValidateShortestIntegrals = 0x0001, + CborValidateShortestFloatingPoint = 0x0002, + CborValidateShortestNumbers = CborValidateShortestIntegrals | CborValidateShortestFloatingPoint, + CborValidateNoIndeterminateLength = 0x0100, + CborValidateMapIsSorted = 0x0200 | CborValidateNoIndeterminateLength, + + CborValidateCanonicalFormat = 0x0fff, + + CborValidateMapKeysAreUnique = 0x1000 | CborValidateMapIsSorted, + CborValidateTagUse = 0x2000, + CborValidateUtf8 = 0x4000, + + CborValidateStrictMode = 0xfff00, + + CborValidateMapKeysAreString = 0x100000, + CborValidateNoUndefined = 0x200000, + CborValidateNoTags = 0x400000, + CborValidateFiniteFloatingPoint = 0x800000, + /* unused = 0x1000000, */ + /* unused = 0x2000000, */ + + CborValidateNoUnknownSimpleTypesSA = 0x4000000, + CborValidateNoUnknownSimpleTypes = 0x8000000 | CborValidateNoUnknownSimpleTypesSA, + CborValidateNoUnknownTagsSA = 0x10000000, + CborValidateNoUnknownTagsSR = 0x20000000 | CborValidateNoUnknownTagsSA, + CborValidateNoUnknownTags = 0x40000000 | CborValidateNoUnknownTagsSR, + + CborValidateCompleteData = (int)0x80000000, + + CborValidateStrictest = (int)~0U, + CborValidateBasic = 0 +}; + +CBOR_API CborError cbor_value_validate(const CborValue *it, uint32_t flags); +#endif /* CBOR_NO_VALIDATION_API */ + +/* Human-readable (dump) API */ +#ifndef CBOR_NO_PRETTY_API + +enum CborPrettyFlags { + CborPrettyNumericEncodingIndicators = 0x01, + CborPrettyTextualEncodingIndicators = 0, + + CborPrettyIndicateIndeterminateLength = 0x02, + CborPrettyIndicateIndetermineLength = CborPrettyIndicateIndeterminateLength, /* deprecated */ + CborPrettyIndicateOverlongNumbers = 0x04, + + CborPrettyShowStringFragments = 0x100, + CborPrettyMergeStringFragments = 0, + + CborPrettyDefaultFlags = CborPrettyIndicateIndeterminateLength +}; + +typedef CborError (*CborStreamFunction)(void *token, const char *fmt, ...) +#ifdef __GNUC__ + __attribute__((__format__(printf, 2, 3))) +#endif +; + +CBOR_API CborError cbor_value_to_pretty_stream(CborStreamFunction streamFunction, void *token, CborValue *value, int flags); + +/* The following API requires a hosted C implementation (uses FILE*) */ +#if !defined(__STDC_HOSTED__) || __STDC_HOSTED__-0 == 1 +CBOR_API CborError cbor_value_to_pretty_advance_flags(FILE *out, CborValue *value, int flags); +CBOR_API CborError cbor_value_to_pretty_advance(FILE *out, CborValue *value); +CBOR_INLINE_API CborError cbor_value_to_pretty(FILE *out, const CborValue *value) +{ + CborValue copy = *value; + return cbor_value_to_pretty_advance_flags(out, ©, CborPrettyDefaultFlags); +} +#endif /* __STDC_HOSTED__ check */ + +#endif /* CBOR_NO_PRETTY_API */ + +#endif /* CBOR_NO_PARSER_API */ + +#ifdef __cplusplus +} +#endif + +#endif /* CBOR_H */ + diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c b/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c new file mode 100644 index 0000000..a51f445 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c @@ -0,0 +1,689 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "cborinternal_p.h" +#include "compilersupport_p.h" + +#include +#include + +/** + * \defgroup CborEncoding Encoding to CBOR + * \brief Group of functions used to encode data to CBOR. + * + * CborEncoder is used to encode data into a CBOR stream. The outermost + * CborEncoder is initialized by calling cbor_encoder_init(), with the buffer + * where the CBOR stream will be stored. The outermost CborEncoder is usually + * used to encode exactly one item, most often an array or map. It is possible + * to encode more than one item, but care must then be taken on the decoder + * side to ensure the state is reset after each item was decoded. + * + * Nested CborEncoder objects are created using cbor_encoder_create_array() and + * cbor_encoder_create_map(), later closed with cbor_encoder_close_container() + * or cbor_encoder_close_container_checked(). The pairs of creation and closing + * must be exactly matched and their parameters are always the same. + * + * CborEncoder writes directly to the user-supplied buffer, without extra + * buffering. CborEncoder does not allocate memory and CborEncoder objects are + * usually created on the stack of the encoding functions. + * + * The example below initializes a CborEncoder object with a buffer and encodes + * a single integer. + * + * \code + * uint8_t buf[16]; + * CborEncoder encoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * cbor_encode_int(&encoder, some_value); + * \endcode + * + * As explained before, usually the outermost CborEncoder object is used to add + * one array or map, which in turn contains multiple elements. The example + * below creates a CBOR map with one element: a key "foo" and a boolean value. + * + * \code + * uint8_t buf[16]; + * CborEncoder encoder, mapEncoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * cbor_encoder_create_map(&encoder, &mapEncoder, 1); + * cbor_encode_text_stringz(&mapEncoder, "foo"); + * cbor_encode_boolean(&mapEncoder, some_value); + * cbor_encoder_close_container(&encoder, &mapEncoder); + * \endcode + * + *

Error checking and buffer size

+ * + * All functions operating on CborEncoder return a condition of type CborError. + * If the encoding was successful, they return CborNoError. Some functions do + * extra checking on the input provided and may return some other error + * conditions (for example, cbor_encode_simple_value() checks that the type is + * of the correct type). + * + * In addition, all functions check whether the buffer has enough bytes to + * encode the item being appended. If that is not possible, they return + * CborErrorOutOfMemory. + * + * It is possible to continue with the encoding of data past the first function + * that returns CborErrorOutOfMemory. CborEncoder functions will not overrun + * the buffer, but will instead count how many more bytes are needed to + * complete the encoding. At the end, you can obtain that count by calling + * cbor_encoder_get_extra_bytes_needed(). + * + * \section1 Finalizing the encoding + * + * Once all items have been appended and the containers have all been properly + * closed, the user-supplied buffer will contain the CBOR stream and may be + * immediately used. To obtain the size of the buffer, call + * cbor_encoder_get_buffer_size() with the original buffer pointer. + * + * The example below illustrates how one can encode an item with error checking + * and then pass on the buffer for network sending. + * + * \code + * uint8_t buf[16]; + * CborError err; + * CborEncoder encoder, mapEncoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * err = cbor_encoder_create_map(&encoder, &mapEncoder, 1); + * if (err) + * return err; + * err = cbor_encode_text_stringz(&mapEncoder, "foo"); + * if (err) + * return err; + * err = cbor_encode_boolean(&mapEncoder, some_value); + * if (err) + * return err; + * err = cbor_encoder_close_container_checked(&encoder, &mapEncoder); + * if (err) + * return err; + * + * size_t len = cbor_encoder_get_buffer_size(&encoder, buf); + * send_payload(buf, len); + * return CborNoError; + * \endcode + * + * Finally, the example below expands on the one above and also + * deals with dynamically growing the buffer if the initial allocation wasn't + * big enough. Note the two places where the error checking was replaced with + * an cbor_assertion, showing where the author assumes no error can occur. + * + * \code + * uint8_t *encode_string_array(const char **strings, int n, size_t *bufsize) + * { + * CborError err; + * CborEncoder encoder, arrayEncoder; + * size_t size = 256; + * uint8_t *buf = NULL; + * + * while (1) { + * int i; + * size_t more_bytes; + * uint8_t *nbuf = realloc(buf, size); + * if (nbuf == NULL) + * goto error; + * buf = nbuf; + * + * cbor_encoder_init(&encoder, buf, size, 0); + * err = cbor_encoder_create_array(&encoder, &arrayEncoder, n); + * cbor_assert(!err); // can't fail, the buffer is always big enough + * + * for (i = 0; i < n; ++i) { + * err = cbor_encode_text_stringz(&arrayEncoder, strings[i]); + * if (err && err != CborErrorOutOfMemory) + * goto error; + * } + * + * err = cbor_encoder_close_container_checked(&encoder, &arrayEncoder); + * cbor_assert(!err); // shouldn't fail! + * + * more_bytes = cbor_encoder_get_extra_bytes_needed(encoder); + * if (more_size) { + * // buffer wasn't big enough, try again + * size += more_bytes; + * continue; + * } + * + * *bufsize = cbor_encoder_get_buffer_size(encoder, buf); + * return buf; + * } + * error: + * free(buf); + * return NULL; + * } + * \endcode + */ + +/** + * \addtogroup CborEncoding + * @{ + */ + +/** + * \struct CborEncoder + * Structure used to encode to CBOR. + */ + +/** + * Initializes a CborEncoder structure \a encoder by pointing it to buffer \a + * buffer of size \a size. The \a flags field is currently unused and must be + * zero. + */ +void cbor_encoder_init(CborEncoder *encoder, uint8_t *buffer, size_t size, int flags) +{ + encoder->data.ptr = buffer; + encoder->end = buffer + size; + encoder->remaining = 2; + encoder->flags = flags; +} + +void cbor_encoder_init_writer(CborEncoder *encoder, CborEncoderWriteFunction writer, void *token) +{ +#ifdef CBOR_ENCODER_WRITE_FUNCTION + (void) writer; +#else + encoder->data.writer = writer; +#endif + encoder->end = (uint8_t *)token; + encoder->remaining = 2; + encoder->flags = CborIteratorFlag_WriterFunction; +} + +static inline void put16(void *where, uint16_t v) +{ + uint16_t v_be = cbor_htons(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +/* Note: Since this is currently only used in situations where OOM is the only + * valid error, we KNOW this to be true. Thus, this function now returns just 'true', + * but if in the future, any function starts returning a non-OOM error, this will need + * to be changed to the test. At the moment, this is done to prevent more branches + * being created in the tinycbor output */ +static inline bool isOomError(CborError err) +{ + if (CBOR_ENCODER_WRITER_CONTROL < 0) + return true; + + /* CborErrorOutOfMemory is the only negative error code, intentionally + * so we can write the test like this */ + return (int)err < 0; +} + +static inline void put32(void *where, uint32_t v) +{ + uint32_t v_be = cbor_htonl(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +static inline void put64(void *where, uint64_t v) +{ + uint64_t v_be = cbor_htonll(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +static inline bool would_overflow(CborEncoder *encoder, size_t len) +{ + ptrdiff_t remaining = (ptrdiff_t)encoder->end; + remaining -= remaining ? (ptrdiff_t)encoder->data.ptr : encoder->data.bytes_needed; + remaining -= (ptrdiff_t)len; + return unlikely(remaining < 0); +} + +static inline void advance_ptr(CborEncoder *encoder, size_t n) +{ + if (encoder->end) + encoder->data.ptr += n; + else + encoder->data.bytes_needed += n; +} + +static inline CborError append_to_buffer(CborEncoder *encoder, const void *data, size_t len, + CborEncoderAppendType appendType) +{ + if (CBOR_ENCODER_WRITER_CONTROL >= 0) { + if (encoder->flags & CborIteratorFlag_WriterFunction || CBOR_ENCODER_WRITER_CONTROL != 0) { +# ifdef CBOR_ENCODER_WRITE_FUNCTION + return CBOR_ENCODER_WRITE_FUNCTION(encoder->end, data, len, appendType); +# else + return encoder->data.writer(encoder->end, data, len, appendType); +# endif + } + } + +#if CBOR_ENCODER_WRITER_CONTROL <= 0 + if (would_overflow(encoder, len)) { + if (encoder->end != NULL) { + len -= encoder->end - encoder->data.ptr; + encoder->end = NULL; + encoder->data.bytes_needed = 0; + } + + advance_ptr(encoder, len); + return CborErrorOutOfMemory; + } + + memcpy(encoder->data.ptr, data, len); + encoder->data.ptr += len; +#endif + return CborNoError; +} + +static inline CborError append_byte_to_buffer(CborEncoder *encoder, uint8_t byte) +{ + return append_to_buffer(encoder, &byte, 1, CborEncoderAppendCborData); +} + +static inline CborError encode_number_no_update(CborEncoder *encoder, uint64_t ui, uint8_t shiftedMajorType) +{ + /* Little-endian would have been so much more convenient here: + * We could just write at the beginning of buf but append_to_buffer + * only the necessary bytes. + * Since it has to be big endian, do it the other way around: + * write from the end. */ + uint64_t buf[2]; + uint8_t *const bufend = (uint8_t *)buf + sizeof(buf); + uint8_t *bufstart = bufend - 1; + put64(buf + 1, ui); /* we probably have a bunch of zeros in the beginning */ + + if (ui < Value8Bit) { + *bufstart += shiftedMajorType; + } else { + uint8_t more = 0; + if (ui > 0xffU) + ++more; + if (ui > 0xffffU) + ++more; + if (ui > 0xffffffffU) + ++more; + bufstart -= (size_t)1 << more; + *bufstart = shiftedMajorType + Value8Bit + more; + } + + return append_to_buffer(encoder, bufstart, bufend - bufstart, CborEncoderAppendCborData); +} + +static inline void saturated_decrement(CborEncoder *encoder) +{ + if (encoder->remaining) + --encoder->remaining; +} + +static inline CborError encode_number(CborEncoder *encoder, uint64_t ui, uint8_t shiftedMajorType) +{ + saturated_decrement(encoder); + return encode_number_no_update(encoder, ui, shiftedMajorType); +} + +/** + * Appends the unsigned 64-bit integer \a value to the CBOR stream provided by + * \a encoder. + * + * \sa cbor_encode_negative_int, cbor_encode_int + */ +CborError cbor_encode_uint(CborEncoder *encoder, uint64_t value) +{ + return encode_number(encoder, value, UnsignedIntegerType << MajorTypeShift); +} + +/** + * Appends the negative 64-bit integer whose absolute value is \a + * absolute_value to the CBOR stream provided by \a encoder. + * + * If the value \a absolute_value is zero, this function encodes -2^64. + * + * \sa cbor_encode_uint, cbor_encode_int + */ +CborError cbor_encode_negative_int(CborEncoder *encoder, uint64_t absolute_value) +{ + return encode_number(encoder, absolute_value - 1, NegativeIntegerType << MajorTypeShift); +} + +/** + * Appends the signed 64-bit integer \a value to the CBOR stream provided by + * \a encoder. + * + * \sa cbor_encode_negative_int, cbor_encode_uint + */ +CborError cbor_encode_int(CborEncoder *encoder, int64_t value) +{ + /* adapted from code in RFC 7049 appendix C (pseudocode) */ + uint64_t ui = value >> 63; /* extend sign to whole length */ + uint8_t majorType = ui & 0x20; /* extract major type */ + ui ^= value; /* complement negatives */ + return encode_number(encoder, ui, majorType); +} + +/** + * Appends the CBOR Simple Type of value \a value to the CBOR stream provided by + * \a encoder. + * + * This function may return error CborErrorIllegalSimpleType if the \a value + * variable contains a number that is not a valid simple type. + */ +CborError cbor_encode_simple_value(CborEncoder *encoder, uint8_t value) +{ +#ifndef CBOR_ENCODER_NO_CHECK_USER + /* check if this is a valid simple type */ + if (value >= HalfPrecisionFloat && value <= Break) + return CborErrorIllegalSimpleType; +#endif + return encode_number(encoder, value, SimpleTypesType << MajorTypeShift); +} + +/** + * Appends the floating-point value of type \a fpType and pointed to by \a + * value to the CBOR stream provided by \a encoder. The value of \a fpType must + * be one of CborHalfFloatType, CborFloatType or CborDoubleType, otherwise the + * behavior of this function is undefined. + * + * This function is useful for code that needs to pass through floating point + * values but does not wish to have the actual floating-point code. + * + * \sa cbor_encode_half_float, cbor_encode_float_as_half_float, cbor_encode_float, cbor_encode_double + */ +CborError cbor_encode_floating_point(CborEncoder *encoder, CborType fpType, const void *value) +{ + unsigned size; + uint8_t buf[1 + sizeof(uint64_t)]; + cbor_assert(fpType == CborHalfFloatType || fpType == CborFloatType || fpType == CborDoubleType); + buf[0] = fpType; + + size = 2U << (fpType - CborHalfFloatType); + if (size == 8) + put64(buf + 1, *(const uint64_t*)value); + else if (size == 4) + put32(buf + 1, *(const uint32_t*)value); + else + put16(buf + 1, *(const uint16_t*)value); + saturated_decrement(encoder); + return append_to_buffer(encoder, buf, size + 1, CborEncoderAppendCborData); +} + +/** + * Appends the CBOR tag \a tag to the CBOR stream provided by \a encoder. + * + * \sa CborTag + */ +CborError cbor_encode_tag(CborEncoder *encoder, CborTag tag) +{ + /* tags don't count towards the number of elements in an array or map */ + return encode_number_no_update(encoder, tag, TagType << MajorTypeShift); +} + +static CborError encode_string(CborEncoder *encoder, size_t length, uint8_t shiftedMajorType, const void *string) +{ + CborError err = encode_number(encoder, length, shiftedMajorType); + if (err && !isOomError(err)) + return err; + return append_to_buffer(encoder, string, length, CborEncoderAppendStringData); +} + +/** + * \fn CborError cbor_encode_text_stringz(CborEncoder *encoder, const char *string) + * + * Appends the null-terminated text string \a string to the CBOR stream + * provided by \a encoder. CBOR requires that \a string be valid UTF-8, but + * TinyCBOR makes no verification of correctness. The terminating null is not + * included in the stream. + * + * \sa cbor_encode_text_string, cbor_encode_byte_string + */ + +/** + * Appends the byte string \a string of length \a length to the CBOR stream + * provided by \a encoder. CBOR byte strings are arbitrary raw data. + * + * \sa cbor_encode_text_stringz, cbor_encode_text_string + */ +CborError cbor_encode_byte_string(CborEncoder *encoder, const uint8_t *string, size_t length) +{ + return encode_string(encoder, length, ByteStringType << MajorTypeShift, string); +} + +/** + * Appends the text string \a string of length \a length to the CBOR stream + * provided by \a encoder. CBOR requires that \a string be valid UTF-8, but + * TinyCBOR makes no verification of correctness. + * + * \sa CborError cbor_encode_text_stringz, cbor_encode_byte_string + */ +CborError cbor_encode_text_string(CborEncoder *encoder, const char *string, size_t length) +{ + return encode_string(encoder, length, TextStringType << MajorTypeShift, string); +} + +#ifdef __GNUC__ +__attribute__((noinline)) +#endif +static CborError create_container(CborEncoder *encoder, CborEncoder *container, size_t length, uint8_t shiftedMajorType) +{ + CborError err; + container->data.ptr = encoder->data.ptr; + container->end = encoder->end; + saturated_decrement(encoder); + container->remaining = length + 1; /* overflow ok on CborIndefiniteLength */ + + cbor_static_assert((int)CborIteratorFlag_ContainerIsMap_ == (int)CborIteratorFlag_ContainerIsMap); + cbor_static_assert(((MapType << MajorTypeShift) & CborIteratorFlag_ContainerIsMap) == CborIteratorFlag_ContainerIsMap); + cbor_static_assert(((ArrayType << MajorTypeShift) & CborIteratorFlag_ContainerIsMap) == 0); + container->flags = shiftedMajorType & CborIteratorFlag_ContainerIsMap; + if (CBOR_ENCODER_WRITER_CONTROL == 0) + container->flags |= encoder->flags & CborIteratorFlag_WriterFunction; + + if (length == CborIndefiniteLength) { + container->flags |= CborIteratorFlag_UnknownLength; + err = append_byte_to_buffer(container, shiftedMajorType + IndefiniteLength); + } else { + if (shiftedMajorType & CborIteratorFlag_ContainerIsMap) + container->remaining += length; + err = encode_number_no_update(container, length, shiftedMajorType); + } + return err; +} + +/** + * Creates a CBOR array in the CBOR stream provided by \a parentEncoder and + * initializes \a arrayEncoder so that items can be added to the array using + * the CborEncoder functions. The array must be terminated by calling either + * cbor_encoder_close_container() or cbor_encoder_close_container_checked() + * with the same \a encoder and \a arrayEncoder parameters. + * + * The number of items inserted into the array must be exactly \a length items, + * otherwise the stream is invalid. If the number of items is not known when + * creating the array, the constant \ref CborIndefiniteLength may be passed as + * length instead, and an indefinite length array is created. + * + * \sa cbor_encoder_create_map + */ +CborError cbor_encoder_create_array(CborEncoder *parentEncoder, CborEncoder *arrayEncoder, size_t length) +{ + return create_container(parentEncoder, arrayEncoder, length, ArrayType << MajorTypeShift); +} + +/** + * Creates a CBOR map in the CBOR stream provided by \a parentEncoder and + * initializes \a mapEncoder so that items can be added to the map using + * the CborEncoder functions. The map must be terminated by calling either + * cbor_encoder_close_container() or cbor_encoder_close_container_checked() + * with the same \a encoder and \a mapEncoder parameters. + * + * The number of pair of items inserted into the map must be exactly \a length + * items, otherwise the stream is invalid. If the number is not known + * when creating the map, the constant \ref CborIndefiniteLength may be passed as + * length instead, and an indefinite length map is created. + * + * \b{Implementation limitation:} TinyCBOR cannot encode more than SIZE_MAX/2 + * key-value pairs in the stream. If the length \a length is larger than this + * value (and is not \ref CborIndefiniteLength), this function returns error + * CborErrorDataTooLarge. + * + * \sa cbor_encoder_create_array + */ +CborError cbor_encoder_create_map(CborEncoder *parentEncoder, CborEncoder *mapEncoder, size_t length) +{ + if (length != CborIndefiniteLength && length > SIZE_MAX / 2) + return CborErrorDataTooLarge; + return create_container(parentEncoder, mapEncoder, length, MapType << MajorTypeShift); +} + +/** + * Closes the CBOR container (array or map) provided by \a containerEncoder and + * updates the CBOR stream provided by \a encoder. Both parameters must be the + * same as were passed to cbor_encoder_create_array() or + * cbor_encoder_create_map(). + * + * Since version 0.5, this function verifies that the number of items (or pairs + * of items, in the case of a map) was correct. It is no longer necessary to call + * cbor_encoder_close_container_checked() instead. + * + * \sa cbor_encoder_create_array(), cbor_encoder_create_map() + */ +CborError cbor_encoder_close_container(CborEncoder *parentEncoder, const CborEncoder *containerEncoder) +{ + // synchronise buffer state with that of the container + parentEncoder->end = containerEncoder->end; + parentEncoder->data = containerEncoder->data; + + if (containerEncoder->flags & CborIteratorFlag_UnknownLength) + return append_byte_to_buffer(parentEncoder, BreakByte); + + if (containerEncoder->remaining != 1) + return containerEncoder->remaining == 0 ? CborErrorTooManyItems : CborErrorTooFewItems; + + if (!parentEncoder->end) + return CborErrorOutOfMemory; /* keep the state */ + + return CborNoError; +} + +/** + * \fn CborError cbor_encode_boolean(CborEncoder *encoder, bool value) + * + * Appends the boolean value \a value to the CBOR stream provided by \a encoder. + */ + +/** + * \fn CborError cbor_encode_null(CborEncoder *encoder) + * + * Appends the CBOR type representing a null value to the CBOR stream provided + * by \a encoder. + * + * \sa cbor_encode_undefined() + */ + +/** + * \fn CborError cbor_encode_undefined(CborEncoder *encoder) + * + * Appends the CBOR type representing an undefined value to the CBOR stream + * provided by \a encoder. + * + * \sa cbor_encode_null() + */ + +/** + * \fn CborError cbor_encode_half_float(CborEncoder *encoder, const void *value) + * + * Appends the IEEE 754 half-precision (16-bit) floating point value pointed to + * by \a value to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_float_as_half_float(CborEncoder *encoder, float value) + * + * Convert the IEEE 754 single-precision (32-bit) floating point value \a value + * to the IEEE 754 half-precision (16-bit) floating point value and append it + * to the CBOR stream provided by \a encoder. + * The \a value should be in the range of the IEEE 754 half-precision floating point type, + * INFINITY, -INFINITY, or NAN, otherwise the behavior of this function is undefined. + * + * \sa cbor_encode_floating_point(), cbor_encode_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_float(CborEncoder *encoder, float value) + * + * Appends the IEEE 754 single-precision (32-bit) floating point value \a value + * to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_half_float(), cbor_encode_float_as_half_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_double(CborEncoder *encoder, double value) + * + * Appends the IEEE 754 double-precision (64-bit) floating point value \a value + * to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_half_float(), cbor_encode_float_as_half_float(), cbor_encode_float() + */ + +/** + * \fn size_t cbor_encoder_get_buffer_size(const CborEncoder *encoder, const uint8_t *buffer) + * + * Returns the total size of the buffer starting at \a buffer after the + * encoding finished without errors. The \a encoder and \a buffer arguments + * must be the same as supplied to cbor_encoder_init(). + * + * If the encoding process had errors, the return value of this function is + * meaningless. If the only errors were CborErrorOutOfMemory, instead use + * cbor_encoder_get_extra_bytes_needed() to find out by how much to grow the + * buffer before encoding again. + * + * See \ref CborEncoding for an example of using this function. + * + * \sa cbor_encoder_init(), cbor_encoder_get_extra_bytes_needed(), CborEncoding + */ + +/** + * \fn size_t cbor_encoder_get_extra_bytes_needed(const CborEncoder *encoder) + * + * Returns how many more bytes the original buffer supplied to + * cbor_encoder_init() needs to be extended by so that no CborErrorOutOfMemory + * condition will happen for the encoding. If the buffer was big enough, this + * function returns 0. The \a encoder must be the original argument as passed + * to cbor_encoder_init(). + * + * This function is usually called after an encoding sequence ended with one or + * more CborErrorOutOfMemory errors, but no other error. If any other error + * happened, the return value of this function is meaningless. + * + * See \ref CborEncoding for an example of using this function. + * + * \sa cbor_encoder_init(), cbor_encoder_get_buffer_size(), CborEncoding + */ + +/** @} */ diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c b/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c new file mode 100644 index 0000000..5661e4d --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#define _BSD_SOURCE 1 +#define _DEFAULT_SOURCE 1 +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" + +/** + * \addtogroup CborEncoding + * @{ + */ + +/** + * @deprecated + * + * Closes the CBOR container (array or map) provided by \a containerEncoder and + * updates the CBOR stream provided by \a encoder. Both parameters must be the + * same as were passed to cbor_encoder_create_array() or + * cbor_encoder_create_map(). + * + * Prior to version 0.5, cbor_encoder_close_container() did not check the + * number of items added. Since that version, it does and now + * cbor_encoder_close_container_checked() is no longer needed. + * + * \sa cbor_encoder_create_array(), cbor_encoder_create_map() + */ +CborError cbor_encoder_close_container_checked(CborEncoder *encoder, const CborEncoder *containerEncoder) +{ + return cbor_encoder_close_container(encoder, containerEncoder); +} + +/** @} */ diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c b/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c new file mode 100644 index 0000000..44f766a --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c @@ -0,0 +1,188 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#include "cbor.h" + +#ifndef _ +# define _(msg) msg +#endif + +/** + * \enum CborError + * \ingroup CborGlobals + * The CborError enum contains the possible error values used by the CBOR encoder and decoder. + * + * TinyCBOR functions report success by returning CborNoError, or one error + * condition by returning one of the values below. One exception is the + * out-of-memory condition (CborErrorOutOfMemory), which the functions for \ref + * CborEncoding may report in bit-wise OR with other conditions. + * + * This technique allows code to determine whether the only error condition was + * a lack of buffer space, which may not be a fatal condition if the buffer can + * be resized. Additionally, the functions for \ref CborEncoding may continue + * to be used even after CborErrorOutOfMemory is returned, and instead they + * will simply calculate the extra space needed. + * + * \value CborNoError No error occurred + * \omitvalue CborUnknownError + * \value CborErrorUnknownLength Request for the length of an array, map or string whose length is not provided in the CBOR stream + * \value CborErrorAdvancePastEOF Not enough data in the stream to decode item (decoding would advance past end of stream) + * \value CborErrorIO An I/O error occurred, probably due to an out-of-memory situation + * \value CborErrorGarbageAtEnd Bytes exist past the end of the CBOR stream + * \value CborErrorUnexpectedEOF End of stream reached unexpectedly + * \value CborErrorUnexpectedBreak A CBOR break byte was found where not expected + * \value CborErrorUnknownType An unknown type (future extension to CBOR) was found in the stream + * \value CborErrorIllegalType An invalid type was found while parsing a chunked CBOR string + * \value CborErrorIllegalNumber An illegal initial byte (encoding unspecified additional information) was found + * \value CborErrorIllegalSimpleType An illegal encoding of a CBOR Simple Type of value less than 32 was found + * \omitvalue CborErrorUnknownSimpleType + * \omitvalue CborErrorUnknownTag + * \omitvalue CborErrorInappropriateTagForType + * \omitvalue CborErrorDuplicateObjectKeys + * \value CborErrorInvalidUtf8TextString Illegal UTF-8 encoding found while parsing CBOR Text String + * \value CborErrorTooManyItems Too many items were added to CBOR map or array of pre-determined length + * \value CborErrorTooFewItems Too few items were added to CBOR map or array of pre-determined length + * \value CborErrorDataTooLarge Data item size exceeds TinyCBOR's implementation limits + * \value CborErrorNestingTooDeep Data item nesting exceeds TinyCBOR's implementation limits + * \omitvalue CborErrorUnsupportedType + * \value CborErrorJsonObjectKeyIsAggregate Conversion to JSON failed because the key in a map is a CBOR map or array + * \value CborErrorJsonObjectKeyNotString Conversion to JSON failed because the key in a map is not a text string + * \value CborErrorOutOfMemory During CBOR encoding, the buffer provided is insufficient for encoding the data item; + * in other situations, TinyCBOR failed to allocate memory + * \value CborErrorInternalError An internal error occurred in TinyCBOR + */ + +/** + * \ingroup CborGlobals + * Returns the error string corresponding to the CBOR error condition \a error. + */ +const char *cbor_error_string(CborError error) +{ + switch (error) { + case CborNoError: + return ""; + + case CborUnknownError: + return _("unknown error"); + + case CborErrorOutOfMemory: + return _("out of memory/need more memory"); + + case CborErrorUnknownLength: + return _("unknown length (attempted to get the length of a map/array/string of indeterminate length"); + + case CborErrorAdvancePastEOF: + return _("attempted to advance past EOF"); + + case CborErrorIO: + return _("I/O error"); + + case CborErrorGarbageAtEnd: + return _("garbage after the end of the content"); + + case CborErrorUnexpectedEOF: + return _("unexpected end of data"); + + case CborErrorUnexpectedBreak: + return _("unexpected 'break' byte"); + + case CborErrorUnknownType: + return _("illegal byte (encodes future extension type)"); + + case CborErrorIllegalType: + return _("mismatched string type in chunked string"); + + case CborErrorIllegalNumber: + return _("illegal initial byte (encodes unspecified additional information)"); + + case CborErrorIllegalSimpleType: + return _("illegal encoding of simple type smaller than 32"); + + case CborErrorNoMoreStringChunks: + return _("no more byte or text strings available"); + + case CborErrorUnknownSimpleType: + return _("unknown simple type"); + + case CborErrorUnknownTag: + return _("unknown tag"); + + case CborErrorInappropriateTagForType: + return _("inappropriate tag for type"); + + case CborErrorDuplicateObjectKeys: + return _("duplicate keys in object"); + + case CborErrorInvalidUtf8TextString: + return _("invalid UTF-8 content in string"); + + case CborErrorExcludedType: + return _("excluded type found"); + + case CborErrorExcludedValue: + return _("excluded value found"); + + case CborErrorImproperValue: + case CborErrorOverlongEncoding: + return _("value encoded in non-canonical form"); + + case CborErrorMapKeyNotString: + case CborErrorJsonObjectKeyNotString: + return _("key in map is not a string"); + + case CborErrorMapNotSorted: + return _("map is not sorted"); + + case CborErrorMapKeysNotUnique: + return _("map keys are not unique"); + + case CborErrorTooManyItems: + return _("too many items added to encoder"); + + case CborErrorTooFewItems: + return _("too few items added to encoder"); + + case CborErrorDataTooLarge: + return _("internal error: data too large"); + + case CborErrorNestingTooDeep: + return _("internal error: too many nested containers found in recursive function"); + + case CborErrorUnsupportedType: + return _("unsupported type"); + + case CborErrorUnimplementedValidation: + return _("validation not implemented for the current parser state"); + + case CborErrorJsonObjectKeyIsAggregate: + return _("conversion to JSON failed: key in object is an array or map"); + + case CborErrorJsonNotImplemented: + return _("conversion to JSON failed: open_memstream unavailable"); + + case CborErrorInternalError: + return _("internal error"); + } + return cbor_error_string(CborUnknownError); +} diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h b/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h new file mode 100644 index 0000000..16269e6 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h @@ -0,0 +1,316 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBORINTERNAL_P_H +#define CBORINTERNAL_P_H + +#include "compilersupport_p.h" + +#ifndef CBOR_NO_FLOATING_POINT +# include +# include +#else +# ifndef CBOR_NO_HALF_FLOAT_TYPE +# define CBOR_NO_HALF_FLOAT_TYPE 1 +# endif +#endif + +#ifndef CBOR_NO_HALF_FLOAT_TYPE +# if defined(__F16C__) || defined(__AVX2__) +# include +static inline unsigned short encode_half(float val) +{ + __m128i m = _mm_cvtps_ph(_mm_set_ss(val), _MM_FROUND_CUR_DIRECTION); + return _mm_extract_epi16(m, 0); +} +static inline float decode_half(unsigned short half) +{ + __m128i m = _mm_cvtsi32_si128(half); + return _mm_cvtss_f32(_mm_cvtph_ps(m)); +} +# else +/* software implementation of float-to-fp16 conversions */ +static inline unsigned short encode_half(double val) +{ + uint64_t v; + int sign, exp, mant; + memcpy(&v, &val, sizeof(v)); + sign = v >> 63 << 15; + exp = (v >> 52) & 0x7ff; + mant = v << 12 >> 12 >> (53-11); /* keep only the 11 most significant bits of the mantissa */ + exp -= 1023; + if (exp == 1024) { + /* infinity or NaN */ + exp = 16; + mant >>= 1; + } else if (exp >= 16) { + /* overflow, as largest number */ + exp = 15; + mant = 1023; + } else if (exp >= -14) { + /* regular normal */ + } else if (exp >= -24) { + /* subnormal */ + mant |= 1024; + mant >>= -(exp + 14); + exp = -15; + } else { + /* underflow, make zero */ + return 0; + } + + /* safe cast here as bit operations above guarantee not to overflow */ + return (unsigned short)(sign | ((exp + 15) << 10) | mant); +} + +/* this function was copied & adapted from RFC 7049 Appendix D */ +static inline double decode_half(unsigned short half) +{ + int exp = (half >> 10) & 0x1f; + int mant = half & 0x3ff; + double val; + if (exp == 0) val = ldexp(mant, -24); + else if (exp != 31) val = ldexp(mant + 1024, exp - 25); + else val = mant == 0 ? INFINITY : NAN; + return half & 0x8000 ? -val : val; +} +# endif +#endif /* CBOR_NO_HALF_FLOAT_TYPE */ + +#ifndef CBOR_INTERNAL_API +# define CBOR_INTERNAL_API +#endif + +#ifndef CBOR_PARSER_MAX_RECURSIONS +# define CBOR_PARSER_MAX_RECURSIONS 1024 +#endif + +#ifndef CBOR_ENCODER_WRITER_CONTROL +# define CBOR_ENCODER_WRITER_CONTROL 0 +#endif +#ifndef CBOR_PARSER_READER_CONTROL +# define CBOR_PARSER_READER_CONTROL 0 +#endif + +/* + * CBOR Major types + * Encoded in the high 3 bits of the descriptor byte + * See http://tools.ietf.org/html/rfc7049#section-2.1 + */ +typedef enum CborMajorTypes { + UnsignedIntegerType = 0U, + NegativeIntegerType = 1U, + ByteStringType = 2U, + TextStringType = 3U, + ArrayType = 4U, + MapType = 5U, /* a.k.a. object */ + TagType = 6U, + SimpleTypesType = 7U +} CborMajorTypes; + +/* + * CBOR simple and floating point types + * Encoded in the low 8 bits of the descriptor byte when the + * Major Type is 7. + */ +typedef enum CborSimpleTypes { + FalseValue = 20, + TrueValue = 21, + NullValue = 22, + UndefinedValue = 23, + SimpleTypeInNextByte = 24, /* not really a simple type */ + HalfPrecisionFloat = 25, /* ditto */ + SinglePrecisionFloat = 26, /* ditto */ + DoublePrecisionFloat = 27, /* ditto */ + Break = 31 +} CborSimpleTypes; + +enum { + SmallValueBitLength = 5U, + SmallValueMask = (1U << SmallValueBitLength) - 1, /* 31 */ + Value8Bit = 24U, + Value16Bit = 25U, + Value32Bit = 26U, + Value64Bit = 27U, + IndefiniteLength = 31U, + + MajorTypeShift = SmallValueBitLength, + MajorTypeMask = (int) (~0U << MajorTypeShift), + + BreakByte = (unsigned)Break | (SimpleTypesType << MajorTypeShift) +}; + +static inline void copy_current_position(CborValue *dst, const CborValue *src) +{ + /* This "if" is here for pedantry only: the two branches should perform + * the same memory operation. */ + if (src->parser->flags & CborParserFlag_ExternalSource) + dst->source.token = src->source.token; + else + dst->source.ptr = src->source.ptr; +} + +static inline bool can_read_bytes(const CborValue *it, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_CAN_READ_BYTES_FUNCTION + return CBOR_PARSER_CAN_READ_BYTES_FUNCTION(it->source.token, n); +#else + return it->parser->source.ops->can_read_bytes(it->source.token, n); +#endif + } + } + + /* Convert the pointer subtraction to size_t since end >= ptr + * (this prevents issues with (ptrdiff_t)n becoming negative). + */ + return (size_t)(it->parser->source.end - it->source.ptr) >= n; +} + +static inline void advance_bytes(CborValue *it, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_ADVANCE_BYTES_FUNCTION + CBOR_PARSER_ADVANCE_BYTES_FUNCTION(it->source.token, n); +#else + it->parser->source.ops->advance_bytes(it->source.token, n); +#endif + return; + } + } + + it->source.ptr += n; +} + +static inline CborError transfer_string(CborValue *it, const void **ptr, size_t offset, size_t len) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_TRANSFER_STRING_FUNCTION + return CBOR_PARSER_TRANSFER_STRING_FUNCTION(it->source.token, ptr, offset, len); +#else + return it->parser->source.ops->transfer_string(it->source.token, ptr, offset, len); +#endif + } + } + + it->source.ptr += offset; + if (can_read_bytes(it, len)) { + *CONST_CAST(const void **, ptr) = it->source.ptr; + it->source.ptr += len; + return CborNoError; + } + return CborErrorUnexpectedEOF; +} + +static inline void *read_bytes_unchecked(const CborValue *it, void *dst, size_t offset, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_READ_BYTES_FUNCTION + return CBOR_PARSER_READ_BYTES_FUNCTION(it->source.token, dst, offset, n); +#else + return it->parser->source.ops->read_bytes(it->source.token, dst, offset, n); +#endif + } + } + + return memcpy(dst, it->source.ptr + offset, n); +} + +#ifdef __GNUC__ +__attribute__((warn_unused_result)) +#endif +static inline void *read_bytes(const CborValue *it, void *dst, size_t offset, size_t n) +{ + if (can_read_bytes(it, offset + n)) + return read_bytes_unchecked(it, dst, offset, n); + return NULL; +} + +static inline uint16_t read_uint8(const CborValue *it, size_t offset) +{ + uint8_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return result; +} + +static inline uint16_t read_uint16(const CborValue *it, size_t offset) +{ + uint16_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohs(result); +} + +static inline uint32_t read_uint32(const CborValue *it, size_t offset) +{ + uint32_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohl(result); +} + +static inline uint64_t read_uint64(const CborValue *it, size_t offset) +{ + uint64_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohll(result); +} + +static inline CborError extract_number_checked(const CborValue *it, uint64_t *value, size_t *bytesUsed) +{ + uint8_t descriptor; + size_t bytesNeeded = 0; + + /* We've already verified that there's at least one byte to be read */ + read_bytes_unchecked(it, &descriptor, 0, 1); + descriptor &= SmallValueMask; + if (descriptor < Value8Bit) { + *value = descriptor; + } else if (unlikely(descriptor > Value64Bit)) { + return CborErrorIllegalNumber; + } else { + bytesNeeded = (size_t)(1 << (descriptor - Value8Bit)); + if (!can_read_bytes(it, 1 + bytesNeeded)) + return CborErrorUnexpectedEOF; + if (descriptor <= Value16Bit) { + if (descriptor == Value16Bit) + *value = read_uint16(it, 1); + else + *value = read_uint8(it, 1); + } else { + if (descriptor == Value32Bit) + *value = read_uint32(it, 1); + else + *value = read_uint64(it, 1); + } + } + + if (bytesUsed) + *bytesUsed = bytesNeeded; + return CborNoError; +} + +#endif /* CBORINTERNAL_P_H */ diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c b/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c new file mode 100644 index 0000000..74d91a3 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c @@ -0,0 +1,1529 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "cborinternal_p.h" +#include "compilersupport_p.h" + +#include + +/** + * \defgroup CborParsing Parsing CBOR streams + * \brief Group of functions used to parse CBOR streams. + * + * TinyCBOR provides functions for pull-based stream parsing of a CBOR-encoded + * payload. The main data type for the parsing is a CborValue, which behaves + * like an iterator and can be used to extract the encoded data. It is first + * initialized with a call to cbor_parser_init() and is usually used to extract + * exactly one item, most often an array or map. + * + * Nested CborValue objects can be parsed using cbor_value_enter_container(). + * Each call to cbor_value_enter_container() must be matched by a call to + * cbor_value_leave_container(), with the exact same parameters. + * + * The example below initializes a CborParser object, begins the parsing with a + * CborValue and decodes a single integer: + * + * \code + * int extract_int(const uint8_t *buffer, size_t len) + * { + * CborParser parser; + * CborValue value; + * int result; + * cbor_parser_init(buffer, len, 0, &parser, &value); + * cbor_value_get_int(&value, &result); + * return result; + * } + * \endcode + * + * The code above does no error checking, which means it assumes the data comes + * from a source trusted to send one properly-encoded integer. The following + * example does the exact same operation, but includes error checking and + * returns 0 on parsing failure: + * + * \code + * int extract_int(const uint8_t *buffer, size_t len) + * { + * CborParser parser; + * CborValue value; + * int result; + * if (cbor_parser_init(buffer, len, 0, &parser, &value) != CborNoError) + * return 0; + * if (!cbor_value_is_integer(&value) || + * cbor_value_get_int(&value, &result) != CborNoError) + * return 0; + * return result; + * } + * \endcode + * + * Note, in the example above, that one can't distinguish a parsing failure + * from an encoded value of zero. Reporting a parsing error is left as an + * exercise to the reader. + * + * The code above does not execute a range-check either: it is possible that + * the value decoded from the CBOR stream encodes a number larger than what can + * be represented in a variable of type \c{int}. If detecting that case is + * important, the code should call cbor_value_get_int_checked() instead. + * + *

Memory and parsing constraints

+ * + * TinyCBOR is designed to run with little memory and with minimal overhead. + * Except where otherwise noted, the parser functions always run on constant + * time (O(1)), do not recurse and never allocate memory (thus, stack usage is + * bounded and is O(1)). + * + *

Error handling and preconditions

+ * + * All functions operating on a CborValue return a CborError condition, with + * CborNoError standing for the normal situation in which no parsing error + * occurred. All functions may return parsing errors in case the stream cannot + * be decoded properly, be it due to corrupted data or due to reaching the end + * of the input buffer. + * + * Error conditions must not be ignored. All decoder functions have undefined + * behavior if called after an error has been reported, and may crash. + * + * Some functions are also documented to have preconditions, like + * cbor_value_get_int() requiring that the input be an integral value. + * Violation of preconditions also results in undefined behavior and the + * program may crash. + */ + +/** + * \addtogroup CborParsing + * @{ + */ + +/** + * \struct CborValue + * + * This type contains one value parsed from the CBOR stream. Each CborValue + * behaves as an iterator in a StAX-style parser. + * + * \if privatedocs + * Implementation details: the CborValue contains these fields: + * \list + * \li ptr: pointer to the actual data + * \li flags: flags from the decoder + * \li extra: partially decoded integer value (0, 1 or 2 bytes) + * \li remaining: remaining items in this collection after this item or UINT32_MAX if length is unknown + * \endlist + * \endif + */ + +static uint64_t extract_number_and_advance(CborValue *it) +{ + /* This function is only called after we've verified that the number + * here is valid, so we can just use _cbor_value_extract_int64_helper. */ + uint8_t descriptor; + uint64_t v = _cbor_value_extract_int64_helper(it); + + read_bytes_unchecked(it, &descriptor, 0, 1); + descriptor &= SmallValueMask; + + size_t bytesNeeded = descriptor < Value8Bit ? 0 : (1 << (descriptor - Value8Bit)); + advance_bytes(it, bytesNeeded + 1); + + return v; +} + +static bool is_fixed_type(uint8_t type) +{ + return type != CborTextStringType && type != CborByteStringType && type != CborArrayType && + type != CborMapType; +} + +static CborError preparse_value(CborValue *it) +{ + enum { + /* flags to keep */ + FlagsToKeep = CborIteratorFlag_ContainerIsMap | CborIteratorFlag_NextIsMapKey + }; + uint8_t descriptor; + + /* are we at the end? */ + it->type = CborInvalidType; + it->flags &= FlagsToKeep; + if (!read_bytes(it, &descriptor, 0, 1)) + return CborErrorUnexpectedEOF; + + uint8_t type = descriptor & MajorTypeMask; + it->type = type; + it->extra = (descriptor &= SmallValueMask); + + if (descriptor > Value64Bit) { + if (unlikely(descriptor != IndefiniteLength)) + return type == CborSimpleType ? CborErrorUnknownType : CborErrorIllegalNumber; + if (likely(!is_fixed_type(type))) { + /* special case */ + it->flags |= CborIteratorFlag_UnknownLength; + it->type = type; + return CborNoError; + } + return type == CborSimpleType ? CborErrorUnexpectedBreak : CborErrorIllegalNumber; + } + + size_t bytesNeeded = descriptor < Value8Bit ? 0 : (1 << (descriptor - Value8Bit)); + + if (bytesNeeded) { + if (!can_read_bytes(it, bytesNeeded + 1)) + return CborErrorUnexpectedEOF; + + it->extra = 0; + + /* read up to 16 bits into it->extra */ + if (bytesNeeded == 1) { + uint8_t extra; + read_bytes_unchecked(it, &extra, 1, bytesNeeded); + it->extra = extra; + } else if (bytesNeeded == 2) { + read_bytes_unchecked(it, &it->extra, 1, bytesNeeded); + it->extra = cbor_ntohs(it->extra); + } else { + cbor_static_assert(CborIteratorFlag_IntegerValueTooLarge == (Value32Bit & 3)); + cbor_static_assert((CborIteratorFlag_IntegerValueIs64Bit | + CborIteratorFlag_IntegerValueTooLarge) == (Value64Bit & 3)); + it->flags |= (descriptor & 3); + } + } + + uint8_t majortype = type >> MajorTypeShift; + if (majortype == NegativeIntegerType) { + it->flags |= CborIteratorFlag_NegativeInteger; + it->type = CborIntegerType; + } else if (majortype == SimpleTypesType) { + switch (descriptor) { + case FalseValue: + it->extra = false; + it->type = CborBooleanType; + break; + + case SinglePrecisionFloat: + case DoublePrecisionFloat: + it->flags |= CborIteratorFlag_IntegerValueTooLarge; + /* fall through */ + case TrueValue: + case NullValue: + case UndefinedValue: + case HalfPrecisionFloat: + read_bytes_unchecked(it, &it->type, 0, 1); + break; + + case SimpleTypeInNextByte: +#ifndef CBOR_PARSER_NO_STRICT_CHECKS + if (unlikely(it->extra < 32)) { + it->type = CborInvalidType; + return CborErrorIllegalSimpleType; + } +#endif + break; + + case 28: + case 29: + case 30: + case Break: + cbor_assert(false); /* these conditions can't be reached */ + return CborErrorUnexpectedBreak; + } + } + + return CborNoError; +} + +static CborError preparse_next_value_nodecrement(CborValue *it) +{ + uint8_t byte; + if (it->remaining == UINT32_MAX && read_bytes(it, &byte, 0, 1) && byte == (uint8_t)BreakByte) { + /* end of map or array */ + if ((it->flags & CborIteratorFlag_ContainerIsMap && it->flags & CborIteratorFlag_NextIsMapKey) + || it->type == CborTagType) { + /* but we weren't expecting it! */ + return CborErrorUnexpectedBreak; + } + it->type = CborInvalidType; + it->remaining = 0; + it->flags |= CborIteratorFlag_UnknownLength; /* leave_container must consume the Break */ + return CborNoError; + } + + return preparse_value(it); +} + +static CborError preparse_next_value(CborValue *it) +{ + /* tags don't count towards item totals or whether we've successfully + * read a map's key or value */ + bool itemCounts = it->type != CborTagType; + + if (it->remaining != UINT32_MAX) { + if (itemCounts && --it->remaining == 0) { + it->type = CborInvalidType; + it->flags &= ~CborIteratorFlag_UnknownLength; /* no Break to consume */ + return CborNoError; + } + } + if (itemCounts) { + /* toggle the flag indicating whether this was a map key */ + it->flags ^= CborIteratorFlag_NextIsMapKey; + } + return preparse_next_value_nodecrement(it); +} + +static CborError advance_internal(CborValue *it) +{ + uint64_t length = extract_number_and_advance(it); + + if (it->type == CborByteStringType || it->type == CborTextStringType) { + cbor_assert(length == (size_t)length); + cbor_assert((it->flags & CborIteratorFlag_UnknownLength) == 0); + advance_bytes(it, length); + } + + return preparse_next_value(it); +} + +/** \internal + * + * Decodes the CBOR integer value when it is larger than the 16 bits available + * in value->extra. This function requires that value->flags have the + * CborIteratorFlag_IntegerValueTooLarge flag set. + * + * This function is also used to extract single- and double-precision floating + * point values (SinglePrecisionFloat == Value32Bit and DoublePrecisionFloat == + * Value64Bit). + */ +uint64_t _cbor_value_decode_int64_internal(const CborValue *value) +{ + cbor_assert(value->flags & CborIteratorFlag_IntegerValueTooLarge || + value->type == CborFloatType || value->type == CborDoubleType); + if (value->flags & CborIteratorFlag_IntegerValueIs64Bit) + return read_uint64(value, 1); + + return read_uint32(value, 1); +} + +/** + * Initializes the CBOR parser for parsing \a size bytes beginning at \a + * buffer. Parsing will use flags set in \a flags. The iterator to the first + * element is returned in \a it. + * + * The \a parser structure needs to remain valid throughout the decoding + * process. It is not thread-safe to share one CborParser among multiple + * threads iterating at the same time, but the object can be copied so multiple + * threads can iterate. + */ +CborError cbor_parser_init(const uint8_t *buffer, size_t size, uint32_t flags, CborParser *parser, CborValue *it) +{ + memset(parser, 0, sizeof(*parser)); + parser->source.end = buffer + size; + parser->flags = (enum CborParserGlobalFlags)flags; + it->parser = parser; + it->source.ptr = buffer; + it->remaining = 1; /* there's one type altogether, usually an array or map */ + it->flags = 0; + return preparse_value(it); +} + +CborError cbor_parser_init_reader(const struct CborParserOperations *ops, CborParser *parser, CborValue *it, void *token) +{ + memset(parser, 0, sizeof(*parser)); + parser->source.ops = ops; + parser->flags = CborParserFlag_ExternalSource; + it->parser = parser; + it->source.token = token; + it->remaining = 1; + return preparse_value(it); +} + +/** + * \fn bool cbor_value_at_end(const CborValue *it) + * + * Returns true if \a it has reached the end of the iteration, usually when + * advancing after the last item in an array or map. + * + * In the case of the outermost CborValue object, this function returns true + * after decoding a single element. A pointer to the first byte of the + * remaining data (if any) can be obtained with cbor_value_get_next_byte(). + * + * \sa cbor_value_advance(), cbor_value_is_valid(), cbor_value_get_next_byte() + */ + +/** + * \fn const uint8_t *cbor_value_get_next_byte(const CborValue *it) + * + * Returns a pointer to the next byte that would be decoded if this CborValue + * object were advanced. + * + * This function is useful if cbor_value_at_end() returns true for the + * outermost CborValue: the pointer returned is the first byte of the data + * remaining in the buffer, if any. Code can decide whether to begin decoding a + * new CBOR data stream from this point, or parse some other data appended to + * the same buffer. + * + * This function may be used even after a parsing error. If that occurred, + * then this function returns a pointer to where the parsing error occurred. + * Note that the error recovery is not precise and the pointer may not indicate + * the exact byte containing bad data. + * + * This function makes sense only when using a linear buffer (that is, when the + * parser is initialize by cbor_parser_init()). If using an external source, + * this function may return garbage; instead, consult the external source itself + * to find out more details about the presence of more data. + * + * \sa cbor_value_at_end() + */ + +CborError cbor_value_reparse(CborValue *it) +{ + if (it->flags & CborIteratorFlag_IteratingStringChunks) + return CborNoError; + return preparse_next_value_nodecrement(it); +} + +/** + * \fn bool cbor_value_is_valid(const CborValue *it) + * + * Returns true if the iterator \a it contains a valid value. Invalid iterators + * happen when iteration reaches the end of a container (see \ref + * cbor_value_at_end()) or when a search function resulted in no matches. + * + * \sa cbor_value_advance(), cbor_value_at_end(), cbor_value_get_type() + */ + +/** + * Performs a basic validation of the CBOR stream pointed by \a it and returns + * the error it found. If no error was found, it returns CborNoError and the + * application can iterate over the items with certainty that no other errors + * will appear during parsing. + * + * A basic validation checks for: + * \list + * \li absence of undefined additional information bytes; + * \li well-formedness of all numbers, lengths, and simple values; + * \li string contents match reported sizes; + * \li arrays and maps contain the number of elements they are reported to have; + * \endlist + * + * For further checks, see cbor_value_validate(). + * + * This function has the same timing and memory requirements as + * cbor_value_advance(). + * + * \sa cbor_value_validate(), cbor_value_advance() + */ +CborError cbor_value_validate_basic(const CborValue *it) +{ + CborValue value = *it; + return cbor_value_advance(&value); +} + +/** + * Advances the CBOR value \a it by one fixed-size position. Fixed-size types + * are: integers, tags, simple types (including boolean, null and undefined + * values) and floating point types. + * + * If the type is not of fixed size, this function has undefined behavior. Code + * must be sure that the current type is one of the fixed-size types before + * calling this function. This function is provided because it can guarantee + * that it runs in constant time (O(1)). + * + * If the caller is not able to determine whether the type is fixed or not, code + * can use the cbor_value_advance() function instead. + * + * \sa cbor_value_at_end(), cbor_value_advance(), cbor_value_enter_container(), cbor_value_leave_container() + */ +CborError cbor_value_advance_fixed(CborValue *it) +{ + cbor_assert(it->type != CborInvalidType); + cbor_assert(is_fixed_type(it->type)); + if (!it->remaining) + return CborErrorAdvancePastEOF; + return advance_internal(it); +} + +static CborError advance_recursive(CborValue *it, int nestingLevel) +{ + CborError err; + CborValue recursed; + + if (is_fixed_type(it->type)) + return advance_internal(it); + + if (!cbor_value_is_container(it)) { + size_t len = SIZE_MAX; + return _cbor_value_copy_string(it, NULL, &len, it); + } + + /* map or array */ + if (nestingLevel == 0) + return CborErrorNestingTooDeep; + + err = cbor_value_enter_container(it, &recursed); + if (err) + return err; + while (!cbor_value_at_end(&recursed)) { + err = advance_recursive(&recursed, nestingLevel - 1); + if (err) + return err; + } + return cbor_value_leave_container(it, &recursed); +} + + +/** + * Advances the CBOR value \a it by one element, skipping over containers. + * Unlike cbor_value_advance_fixed(), this function can be called on a CBOR + * value of any type. However, if the type is a container (map or array) or a + * string with a chunked payload, this function will not run in constant time + * and will recurse into itself (it will run on O(n) time for the number of + * elements or chunks and will use O(n) memory for the number of nested + * containers). + * + * The number of recursions can be limited at compile time to avoid stack + * exhaustion in constrained systems. + * + * \sa cbor_value_at_end(), cbor_value_advance_fixed(), cbor_value_enter_container(), cbor_value_leave_container() + */ +CborError cbor_value_advance(CborValue *it) +{ + cbor_assert(it->type != CborInvalidType); + if (!it->remaining) + return CborErrorAdvancePastEOF; + return advance_recursive(it, CBOR_PARSER_MAX_RECURSIONS); +} + +/** + * \fn bool cbor_value_is_tag(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR tag. + * + * \sa cbor_value_get_tag(), cbor_value_skip_tag() + */ + +/** + * \fn CborError cbor_value_get_tag(const CborValue *value, CborTag *result) + * + * Retrieves the CBOR tag value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to a CBOR tag value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_tag is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_tag() + */ + +/** + * Advances the CBOR value \a it until it no longer points to a tag. If \a it is + * already not pointing to a tag, then this function returns it unchanged. + * + * This function does not run in constant time: it will run on O(n) for n being + * the number of tags. It does use constant memory (O(1) memory requirements). + * + * \sa cbor_value_advance_fixed(), cbor_value_advance() + */ +CborError cbor_value_skip_tag(CborValue *it) +{ + while (cbor_value_is_tag(it)) { + CborError err = cbor_value_advance_fixed(it); + if (err) + return err; + } + return CborNoError; +} + +/** + * \fn bool cbor_value_is_container(const CborValue *it) + * + * Returns true if the \a it value is a container and requires recursion in + * order to decode (maps and arrays), false otherwise. + */ + +/** + * Creates a CborValue iterator pointing to the first element of the container + * represented by \a it and saves it in \a recursed. The \a it container object + * needs to be kept and passed again to cbor_value_leave_container() in order + * to continue iterating past this container. + * + * The \a it CborValue iterator must point to a container. + * + * \sa cbor_value_is_container(), cbor_value_leave_container(), cbor_value_advance() + */ +CborError cbor_value_enter_container(const CborValue *it, CborValue *recursed) +{ + cbor_static_assert(CborIteratorFlag_ContainerIsMap == (CborMapType & ~CborArrayType)); + cbor_assert(cbor_value_is_container(it)); + *recursed = *it; + + if (it->flags & CborIteratorFlag_UnknownLength) { + recursed->remaining = UINT32_MAX; + advance_bytes(recursed, 1); + } else { + uint64_t len = extract_number_and_advance(recursed); + + recursed->remaining = (uint32_t)len; + if (recursed->remaining != len || len == UINT32_MAX) { + /* back track the pointer to indicate where the error occurred */ + copy_current_position(recursed, it); + return CborErrorDataTooLarge; + } + if (recursed->type == CborMapType) { + /* maps have keys and values, so we need to multiply by 2 */ + if (recursed->remaining > UINT32_MAX / 2) { + /* back track the pointer to indicate where the error occurred */ + copy_current_position(recursed, it); + return CborErrorDataTooLarge; + } + recursed->remaining *= 2; + } + if (len == 0) { + /* the case of the empty container */ + recursed->type = CborInvalidType; + return CborNoError; + } + } + recursed->flags = (recursed->type & CborIteratorFlag_ContainerIsMap); + return preparse_next_value_nodecrement(recursed); +} + +/** + * Updates \a it to point to the next element after the container. The \a + * recursed object needs to point to the element obtained either by advancing + * the last element of the container (via cbor_value_advance(), + * cbor_value_advance_fixed(), a nested cbor_value_leave_container(), or the \c + * next pointer from cbor_value_copy_string() or cbor_value_dup_string()). + * + * The \a it and \a recursed parameters must be the exact same as passed to + * cbor_value_enter_container(). + * + * \sa cbor_value_enter_container(), cbor_value_at_end() + */ +CborError cbor_value_leave_container(CborValue *it, const CborValue *recursed) +{ + cbor_assert(cbor_value_is_container(it)); + cbor_assert(recursed->type == CborInvalidType); + + copy_current_position(it, recursed); + if (recursed->flags & CborIteratorFlag_UnknownLength) + advance_bytes(it, 1); + return preparse_next_value(it); +} + + +/** + * \fn CborType cbor_value_get_type(const CborValue *value) + * + * Returns the type of the CBOR value that the iterator \a value points to. If + * \a value does not point to a valid value, this function returns \ref + * CborInvalidType. + * + * TinyCBOR also provides functions to test directly if a given CborValue object + * is of a given type, like cbor_value_is_text_string() and cbor_value_is_null(). + * + * \sa cbor_value_is_valid() + */ + +/** + * \fn bool cbor_value_is_null(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR null type. + * + * \sa cbor_value_is_valid(), cbor_value_is_undefined() + */ + +/** + * \fn bool cbor_value_is_undefined(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR undefined type. + * + * \sa cbor_value_is_valid(), cbor_value_is_null() + */ + +/** + * \fn bool cbor_value_is_boolean(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR boolean + * type (true or false). + * + * \sa cbor_value_is_valid(), cbor_value_get_boolean() + */ + +/** + * \fn CborError cbor_value_get_boolean(const CborValue *value, bool *result) + * + * Retrieves the boolean value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to a boolean value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_boolean is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_boolean() + */ + +/** + * \fn bool cbor_value_is_simple_type(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR Simple Type + * type (other than true, false, null and undefined). + * + * \sa cbor_value_is_valid(), cbor_value_get_simple_type() + */ + +/** + * \fn CborError cbor_value_get_simple_type(const CborValue *value, uint8_t *result) + * + * Retrieves the CBOR Simple Type value that \a value points to and stores it + * in \a result. If the iterator \a value does not point to a simple_type + * value, the behavior is undefined, so checking with \ref cbor_value_get_type + * or with \ref cbor_value_is_simple_type is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_simple_type() + */ + +/** + * \fn bool cbor_value_is_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR integer + * type. + * + * \sa cbor_value_is_valid(), cbor_value_get_int, cbor_value_get_int64, cbor_value_get_uint64, cbor_value_get_raw_integer + */ + +/** + * \fn bool cbor_value_is_unsigned_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR unsigned + * integer type (positive values or zero). + * + * \sa cbor_value_is_valid(), cbor_value_get_uint64() + */ + +/** + * \fn bool cbor_value_is_negative_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR negative + * integer type. + * + * \sa cbor_value_is_valid(), cbor_value_get_int, cbor_value_get_int64, cbor_value_get_raw_integer + */ + +/** + * \fn CborError cbor_value_get_int(const CborValue *value, int *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Note that this function does not do range-checking: integral values that do + * not fit in a variable of type \c{int} are silently truncated to fit. Use + * cbor_value_get_int_checked() if that is not acceptable. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * \fn CborError cbor_value_get_int64(const CborValue *value, int64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Note that this function does not do range-checking: integral values that do + * not fit in a variable of type \c{int64_t} are silently truncated to fit. Use + * cbor_value_get_int64_checked() that is not acceptable. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * \fn CborError cbor_value_get_uint64(const CborValue *value, uint64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an unsigned integer + * value, the behavior is undefined, so checking with \ref cbor_value_get_type + * or with \ref cbor_value_is_unsigned_integer is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_unsigned_integer() + */ + +/** + * \fn CborError cbor_value_get_raw_integer(const CborValue *value, uint64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * This function is provided because CBOR negative integers can assume values + * that cannot be represented with normal 64-bit integer variables. + * + * If the integer is unsigned (that is, if cbor_value_is_unsigned_integer() + * returns true), then \a result will contain the actual value. If the integer + * is negative, then \a result will contain the absolute value of that integer, + * minus one. That is, \c {actual = -result - 1}. On architectures using two's + * complement for representation of negative integers, it is equivalent to say + * that \a result will contain the bitwise negation of the actual value. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Unlike \ref cbor_value_get_int64(), this function performs a check to see if the + * stored integer fits in \a result without data loss. If the number is outside + * the valid range for the data type, this function returns the recoverable + * error CborErrorDataTooLarge. In that case, use either + * cbor_value_get_uint64() (if the number is positive) or + * cbor_value_get_raw_integer(). + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer(), cbor_value_get_int64() + */ +CborError cbor_value_get_int64_checked(const CborValue *value, int64_t *result) +{ + uint64_t v; + cbor_assert(cbor_value_is_integer(value)); + v = _cbor_value_extract_int64_helper(value); + + /* Check before converting, as the standard says (C11 6.3.1.3 paragraph 3): + * "[if] the new type is signed and the value cannot be represented in it; either the + * result is implementation-defined or an implementation-defined signal is raised." + * + * The range for int64_t is -2^63 to 2^63-1 (int64_t is required to be + * two's complement, C11 7.20.1.1 paragraph 3), which in CBOR is + * represented the same way, differing only on the "sign bit" (the major + * type). + */ + + if (unlikely(v > (uint64_t)INT64_MAX)) + return CborErrorDataTooLarge; + + *result = v; + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +/** + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Unlike \ref cbor_value_get_int(), this function performs a check to see if the + * stored integer fits in \a result without data loss. If the number is outside + * the valid range for the data type, this function returns the recoverable + * error CborErrorDataTooLarge. In that case, use one of the other integer + * functions to obtain the value. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer(), cbor_value_get_int64(), + * cbor_value_get_uint64(), cbor_value_get_int64_checked(), cbor_value_get_raw_integer() + */ +CborError cbor_value_get_int_checked(const CborValue *value, int *result) +{ + uint64_t v; + cbor_assert(cbor_value_is_integer(value)); + v = _cbor_value_extract_int64_helper(value); + + /* Check before converting, as the standard says (C11 6.3.1.3 paragraph 3): + * "[if] the new type is signed and the value cannot be represented in it; either the + * result is implementation-defined or an implementation-defined signal is raised." + * + * But we can convert from signed to unsigned without fault (paragraph 2). + * + * The range for int is implementation-defined and int is not guaranteed to use + * two's complement representation (although int32_t is). + */ + + if (value->flags & CborIteratorFlag_NegativeInteger) { + if (unlikely(v > (unsigned) -(INT_MIN + 1))) + return CborErrorDataTooLarge; + + *result = (int)v; + *result = -*result - 1; + } else { + if (unlikely(v > (uint64_t)INT_MAX)) + return CborErrorDataTooLarge; + + *result = (int)v; + } + return CborNoError; + +} + +/** + * \fn bool cbor_value_is_length_known(const CborValue *value) + * + * Returns true if the length of this type is known without calculation. That + * is, if the length of this CBOR string, map or array is encoded in the data + * stream, this function returns true. If the length is not encoded, it returns + * false. + * + * If the length is known, code can call cbor_value_get_string_length(), + * cbor_value_get_array_length() or cbor_value_get_map_length() to obtain the + * length. If the length is not known but is necessary, code can use the + * cbor_value_calculate_string_length() function (no equivalent function is + * provided for maps and arrays). + */ + +/** + * \fn bool cbor_value_is_text_string(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR text + * string. CBOR text strings are UTF-8 encoded and usually contain + * human-readable text. + * + * \sa cbor_value_is_valid(), cbor_value_get_string_length(), cbor_value_calculate_string_length(), + * cbor_value_copy_text_string(), cbor_value_dup_text_string() + */ + +/** + * \fn bool cbor_value_is_byte_string(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR byte + * string. CBOR byte strings are binary data with no specified encoding or + * format. + * + * \sa cbor_value_is_valid(), cbor_value_get_string_length(), cbor_value_calculate_string_length(), + * cbor_value_copy_byte_string(), cbor_value_dup_byte_string() + */ + +/** + * \fn CborError cbor_value_get_string_length(const CborValue *value, size_t *length) + * + * Extracts the length of the byte or text string that \a value points to and + * stores it in \a result. If the iterator \a value does not point to a text + * string or a byte string, the behaviour is undefined, so checking with \ref + * cbor_value_get_type, with \ref cbor_value_is_text_string or \ref + * cbor_value_is_byte_string is recommended. + * + * If the length of this string is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * If the length of the string is required but the length was not encoded, use + * cbor_value_calculate_string_length(), but note that that function does not + * run in constant time. + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known(), cbor_value_calculate_string_length() + */ + +/** + * Calculates the length of the byte or text string that \a value points to and + * stores it in \a len. If the iterator \a value does not point to a text + * string or a byte string, the behaviour is undefined, so checking with \ref + * cbor_value_get_type, with \ref cbor_value_is_text_string or \ref + * cbor_value_is_byte_string is recommended. + * + * This function is different from cbor_value_get_string_length() in that it + * calculates the length even for strings sent in chunks. For that reason, this + * function may not run in constant time (it will run in O(n) time on the + * number of chunks). It does use constant memory (O(1)). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_get_string_length(), cbor_value_copy_text_string(), cbor_value_copy_byte_string(), cbor_value_is_length_known() + */ +CborError cbor_value_calculate_string_length(const CborValue *value, size_t *len) +{ + *len = SIZE_MAX; + return _cbor_value_copy_string(value, NULL, len, NULL); +} + +CborError _cbor_value_begin_string_iteration(CborValue *it) +{ + it->flags |= CborIteratorFlag_IteratingStringChunks | + CborIteratorFlag_BeforeFirstStringChunk; + if (!cbor_value_is_length_known(it)) { + /* chunked string: we're before the first chunk; + * advance to the first chunk */ + advance_bytes(it, 1); + } + + return CborNoError; +} + +CborError _cbor_value_finish_string_iteration(CborValue *it) +{ + if (!cbor_value_is_length_known(it)) + advance_bytes(it, 1); /* skip the Break */ + + return preparse_next_value(it); +} + +static CborError get_string_chunk_size(const CborValue *it, size_t *offset, size_t *len) +{ + uint8_t descriptor; + size_t bytesNeeded = 1; + + if (cbor_value_is_length_known(it) && (it->flags & CborIteratorFlag_BeforeFirstStringChunk) == 0) + return CborErrorNoMoreStringChunks; + + /* are we at the end? */ + if (!read_bytes(it, &descriptor, 0, 1)) + return CborErrorUnexpectedEOF; + + if (descriptor == BreakByte) + return CborErrorNoMoreStringChunks; + if ((descriptor & MajorTypeMask) != it->type) + return CborErrorIllegalType; + + /* find the string length */ + descriptor &= SmallValueMask; + if (descriptor < Value8Bit) { + *len = descriptor; + } else if (unlikely(descriptor > Value64Bit)) { + return CborErrorIllegalNumber; + } else { + uint64_t val; + bytesNeeded = (size_t)(1 << (descriptor - Value8Bit)); + if (!can_read_bytes(it, 1 + bytesNeeded)) + return CborErrorUnexpectedEOF; + + if (descriptor <= Value16Bit) { + if (descriptor == Value16Bit) + val = read_uint16(it, 1); + else + val = read_uint8(it, 1); + } else { + if (descriptor == Value32Bit) + val = read_uint32(it, 1); + else + val = read_uint64(it, 1); + } + + *len = val; + if (*len != val) + return CborErrorDataTooLarge; + + ++bytesNeeded; + } + + *offset = bytesNeeded; + return CborNoError; +} + +CborError _cbor_value_get_string_chunk_size(const CborValue *value, size_t *len) +{ + size_t offset; + return get_string_chunk_size(value, &offset, len); +} + +static CborError get_string_chunk(CborValue *it, const void **bufferptr, size_t *len) +{ + size_t offset; + CborError err = get_string_chunk_size(it, &offset, len); + if (err) + return err; + + /* we're good, transfer the string now */ + err = transfer_string(it, bufferptr, offset, *len); + if (err) + return err; + + /* we've iterated at least once */ + it->flags &= ~CborIteratorFlag_BeforeFirstStringChunk; + return CborNoError; +} + +/** + * \fn CborError cbor_value_get_text_string_chunk(const CborValue *value, const char **bufferptr, size_t *len, CborValue *next) + * + * Extracts one text string chunk pointed to by \a value and stores a pointer + * to the data in \a buffer and the size in \a len, which must not be null. If + * no more chunks are available, then \a bufferptr will be set to null. This + * function may be used to iterate over any string without causing its contents + * to be copied to a separate buffer, like the convenience function + * cbor_value_copy_text_string() does. + * + * It is designed to be used in code like: + * + * \code + * if (cbor_value_is_text_string(value)) { + * char *ptr; + * size_t len; + * while (1) { + * err = cbor_value_get_text_string_chunk(value, &ptr, &len, &value)); + * if (err) return err; + * if (ptr == NULL) return CborNoError; + * consume(ptr, len); + * } + * } + * \endcode + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. During iteration, the pointer must only be passed back + * again to this function; passing it to any other function in this library + * results in undefined behavior. If there are no more chunks to be read from + * \a value, then \a next will be set to the next item after this string; if \a + * value points to the last item, then \a next will be invalid. + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_dup_text_string(), cbor_value_copy_text_string(), cbor_value_caculate_string_length(), cbor_value_get_byte_string_chunk() + */ + +/** + * \fn CborError cbor_value_get_byte_string_chunk(const CborValue *value, const char **bufferptr, size_t *len, CborValue *next) + * + * Extracts one byte string chunk pointed to by \a value and stores a pointer + * to the data in \a buffer and the size in \a len, which must not be null. If + * no more chunks are available, then \a bufferptr will be set to null. This + * function may be used to iterate over any string without causing its contents + * to be copied to a separate buffer, like the convenience function + * cbor_value_copy_byte_string() does. + * + * It is designed to be used in code like: + * + * \code + * if (cbor_value_is_byte_string(value)) { + * char *ptr; + * size_t len; + * while (1) { + * err = cbor_value_get_byte_string_chunk(value, &ptr, &len, &value)); + * if (err) return err; + * if (ptr == NULL) return CborNoError; + * consume(ptr, len); + * } + * } + * \endcode + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. During iteration, the pointer must only be passed back + * again to this function; passing it to any other function in this library + * results in undefined behavior. If there are no more chunks to be read from + * \a value, then \a next will be set to the next item after this string; if \a + * value points to the last item, then \a next will be invalid. + * + * \sa cbor_value_dup_byte_string(), cbor_value_copy_byte_string(), cbor_value_caculate_string_length(), cbor_value_get_text_string_chunk() + */ + +CborError _cbor_value_get_string_chunk(const CborValue *value, const void **bufferptr, + size_t *len, CborValue *next) +{ + CborValue tmp; + if (!next) + next = &tmp; + *next = *value; + return get_string_chunk(next, bufferptr, len); +} + +/* We return uintptr_t so that we can pass memcpy directly as the iteration + * function. The choice is to optimize for memcpy, which is used in the base + * parser API (cbor_value_copy_string), while memcmp is used in convenience API + * only. */ +typedef uintptr_t (*IterateFunction)(char *, const uint8_t *, size_t); + +static uintptr_t iterate_noop(char *dest, const uint8_t *src, size_t len) +{ + (void)dest; + (void)src; + (void)len; + return true; +} + +static uintptr_t iterate_memcmp(char *s1, const uint8_t *s2, size_t len) +{ + return memcmp(s1, (const char *)s2, len) == 0; +} + +static uintptr_t iterate_memcpy(char *dest, const uint8_t *src, size_t len) +{ + return (uintptr_t)memcpy(dest, src, len); +} + +static CborError iterate_string_chunks(const CborValue *value, char *buffer, size_t *buflen, + bool *result, CborValue *next, IterateFunction func) +{ + CborError err; + CborValue tmp; + size_t total = 0; + const void *ptr; + + cbor_assert(cbor_value_is_byte_string(value) || cbor_value_is_text_string(value)); + if (!next) + next = &tmp; + *next = *value; + *result = true; + + err = _cbor_value_begin_string_iteration(next); + if (err) + return err; + + while (1) { + size_t newTotal; + size_t chunkLen; + err = get_string_chunk(next, &ptr, &chunkLen); + if (err == CborErrorNoMoreStringChunks) + break; + if (err) + return err; + + if (unlikely(add_check_overflow(total, chunkLen, &newTotal))) + return CborErrorDataTooLarge; + + if (*result && *buflen >= newTotal) + *result = !!func(buffer + total, (const uint8_t *)ptr, chunkLen); + else + *result = false; + + total = newTotal; + } + + /* is there enough room for the ending NUL byte? */ + if (*result && *buflen > total) { + uint8_t nul[] = { 0 }; + *result = !!func(buffer + total, nul, 1); + } + *buflen = total; + return _cbor_value_finish_string_iteration(next); +} + +/** + * \fn CborError cbor_value_copy_text_string(const CborValue *value, char *buffer, size_t *buflen, CborValue *next) + * + * Copies the string pointed to by \a value into the buffer provided at \a buffer + * of \a buflen bytes. If \a buffer is a NULL pointer, this function will not + * copy anything and will only update the \a next value. + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * If the provided buffer length was too small, this function returns an error + * condition of \ref CborErrorOutOfMemory. If you need to calculate the length + * of the string in order to preallocate a buffer, use + * cbor_value_calculate_string_length(). + * + * On success, this function sets the number of bytes copied to \c{*buflen}. If + * the buffer is large enough, this function will insert a null byte after the + * last copied byte, to facilitate manipulation of text strings. That byte is + * not included in the returned value of \c{*buflen}. If there was no space for + * the terminating null, no error is returned, so callers must check the value + * of *buflen after the call, before relying on the '\0'; if it has not been + * changed by the call, there is no '\0'-termination on the buffer's contents. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_get_text_string_chunk() cbor_value_dup_text_string(), cbor_value_copy_byte_string(), cbor_value_get_string_length(), cbor_value_calculate_string_length() + */ + +/** + * \fn CborError cbor_value_copy_byte_string(const CborValue *value, uint8_t *buffer, size_t *buflen, CborValue *next) + * + * Copies the string pointed by \a value into the buffer provided at \a buffer + * of \a buflen bytes. If \a buffer is a NULL pointer, this function will not + * copy anything and will only update the \a next value. + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * If the provided buffer length was too small, this function returns an error + * condition of \ref CborErrorOutOfMemory. If you need to calculate the length + * of the string in order to preallocate a buffer, use + * cbor_value_calculate_string_length(). + * + * On success, this function sets the number of bytes copied to \c{*buflen}. If + * the buffer is large enough, this function will insert a null byte after the + * last copied byte, to facilitate manipulation of null-terminated strings. + * That byte is not included in the returned value of \c{*buflen}. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \sa cbor_value_get_byte_string_chunk(), cbor_value_dup_text_string(), cbor_value_copy_text_string(), cbor_value_get_string_length(), cbor_value_calculate_string_length() + */ + +CborError _cbor_value_copy_string(const CborValue *value, void *buffer, + size_t *buflen, CborValue *next) +{ + bool copied_all; + CborError err = iterate_string_chunks(value, (char*)buffer, buflen, &copied_all, next, + buffer ? iterate_memcpy : iterate_noop); + return err ? err : + copied_all ? CborNoError : CborErrorOutOfMemory; +} + +/** + * Compares the entry \a value with the string \a string and stores the result + * in \a result. If the value is different from \a string \a result will + * contain \c false. + * + * The entry at \a value may be a tagged string. If \a value is not a string or + * a tagged string, the comparison result will be false. + * + * CBOR requires text strings to be encoded in UTF-8, but this function does + * not validate either the strings in the stream or the string \a string to be + * matched. Moreover, comparison is done on strict codepoint comparison, + * without any Unicode normalization. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \sa cbor_value_skip_tag(), cbor_value_copy_text_string() + */ +CborError cbor_value_text_string_equals(const CborValue *value, const char *string, bool *result) +{ + size_t len; + CborValue copy = *value; + CborError err = cbor_value_skip_tag(©); + if (err) + return err; + if (!cbor_value_is_text_string(©)) { + *result = false; + return CborNoError; + } + + len = strlen(string); + return iterate_string_chunks(©, CONST_CAST(char *, string), &len, result, NULL, iterate_memcmp); +} + +/** + * \fn bool cbor_value_is_array(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR array. + * + * \sa cbor_value_is_valid(), cbor_value_is_map() + */ + +/** + * \fn CborError cbor_value_get_array_length(const CborValue *value, size_t *length) + * + * Extracts the length of the CBOR array that \a value points to and stores it + * in \a result. If the iterator \a value does not point to a CBOR array, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_array is recommended. + * + * If the length of this array is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known() + */ + +/** + * \fn bool cbor_value_is_map(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR map. + * + * \sa cbor_value_is_valid(), cbor_value_is_array() + */ + +/** + * \fn CborError cbor_value_get_map_length(const CborValue *value, size_t *length) + * + * Extracts the length of the CBOR map that \a value points to and stores it in + * \a result. If the iterator \a value does not point to a CBOR map, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_map is recommended. + * + * If the length of this map is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known() + */ + +/** + * Attempts to find the value in map \a map that corresponds to the text string + * entry \a string. If the iterator \a value does not point to a CBOR map, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_map is recommended. + * + * If the item is found, it is stored in \a result. If no item is found + * matching the key, then \a result will contain an element of type \ref + * CborInvalidType. Matching is performed using + * cbor_value_text_string_equals(), so tagged strings will also match. + * + * This function has a time complexity of O(n) where n is the number of + * elements in the map to be searched. In addition, this function is has O(n) + * memory requirement based on the number of nested containers (maps or arrays) + * found as elements of this map. + * + * \sa cbor_value_is_valid(), cbor_value_text_string_equals(), cbor_value_advance() + */ +CborError cbor_value_map_find_value(const CborValue *map, const char *string, CborValue *element) +{ + CborError err; + size_t len = strlen(string); + cbor_assert(cbor_value_is_map(map)); + err = cbor_value_enter_container(map, element); + if (err) + goto error; + + while (!cbor_value_at_end(element)) { + /* find the non-tag so we can compare */ + err = cbor_value_skip_tag(element); + if (err) + goto error; + if (cbor_value_is_text_string(element)) { + bool equals; + size_t dummyLen = len; + err = iterate_string_chunks(element, CONST_CAST(char *, string), &dummyLen, + &equals, element, iterate_memcmp); + if (err) + goto error; + if (equals) + return preparse_value(element); + } else { + /* skip this key */ + err = cbor_value_advance(element); + if (err) + goto error; + } + + /* skip this value */ + err = cbor_value_skip_tag(element); + if (err) + goto error; + err = cbor_value_advance(element); + if (err) + goto error; + } + + /* not found */ + element->type = CborInvalidType; + return CborNoError; + +error: + element->type = CborInvalidType; + return err; +} + +/** + * \fn bool cbor_value_is_float(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * single-precision floating point (32-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_double(), cbor_value_is_half_float() + */ + +/** + * \fn CborError cbor_value_get_float(const CborValue *value, float *result) + * + * Retrieves the CBOR single-precision floating point (32-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a single-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_float is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_float(), cbor_value_get_double() + */ + +/** + * \fn bool cbor_value_is_double(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * double-precision floating point (64-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_float(), cbor_value_is_half_float() + */ + +/** + * \fn CborError cbor_value_get_double(const CborValue *value, float *result) + * + * Retrieves the CBOR double-precision floating point (64-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a double-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_double is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_double(), cbor_value_get_float() + */ + +/** + * \fn bool cbor_value_is_half_float(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * single-precision floating point (16-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_double(), cbor_value_is_float() + */ + +/** + * \fn CborError cbor_value_get_half_float(const CborValue *value, void *result) + * + * Retrieves the CBOR half-precision floating point (16-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a half-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_half_float is recommended. + * + * Note: since the C language does not have a standard type for half-precision + * floating point, this function takes a \c{void *} as a parameter for the + * storage area, which must be at least 16 bits wide. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_half_float(), cbor_value_get_half_float_as_float(), cbor_value_get_float() + */ + +/** @} */ diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c b/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c new file mode 100644 index 0000000..061c5ac --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "compilersupport_p.h" +#include + +/** + * \fn CborError cbor_value_dup_text_string(const CborValue *value, char **buffer, size_t *buflen, CborValue *next) + * + * Allocates memory for the string pointed by \a value and copies it into this + * buffer. The pointer to the buffer is stored in \a buffer and the number of + * bytes copied is stored in \a buflen (those variables must not be NULL). + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * If \c malloc returns a NULL pointer, this function will return error + * condition \ref CborErrorOutOfMemory. + * + * On success, \c{*buffer} will contain a valid pointer that must be freed by + * calling \c{free()}. This is the case even for zero-length strings. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)) in addition to the + * malloc'ed block. + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_get_text_string_chunk(), cbor_value_copy_text_string(), cbor_value_dup_byte_string() + */ + +/** + * \fn CborError cbor_value_dup_byte_string(const CborValue *value, uint8_t **buffer, size_t *buflen, CborValue *next) + * + * Allocates memory for the string pointed by \a value and copies it into this + * buffer. The pointer to the buffer is stored in \a buffer and the number of + * bytes copied is stored in \a buflen (those variables must not be NULL). + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * If \c malloc returns a NULL pointer, this function will return error + * condition \ref CborErrorOutOfMemory. + * + * On success, \c{*buffer} will contain a valid pointer that must be freed by + * calling \c{free()}. This is the case even for zero-length strings. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)) in addition to the + * malloc'ed block. + * + * \sa cbor_value_get_text_string_chunk(), cbor_value_copy_byte_string(), cbor_value_dup_text_string() + */ +CborError _cbor_value_dup_string(const CborValue *value, void **buffer, size_t *buflen, CborValue *next) +{ + CborError err; + cbor_assert(buffer); + cbor_assert(buflen); + *buflen = SIZE_MAX; + err = _cbor_value_copy_string(value, NULL, buflen, NULL); + if (err) + return err; + + ++*buflen; + *buffer = malloc(*buflen); + if (!*buffer) { + /* out of memory */ + return CborErrorOutOfMemory; + } + err = _cbor_value_copy_string(value, *buffer, buflen, next); + if (err) { + free(*buffer); + return err; + } + return CborNoError; +} diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h b/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h new file mode 100644 index 0000000..0879801 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef COMPILERSUPPORT_H +#define COMPILERSUPPORT_H + +#include "cbor.h" + +#ifndef _BSD_SOURCE +# define _BSD_SOURCE +#endif +#ifndef _DEFAULT_SOURCE +# define _DEFAULT_SOURCE +#endif +#ifndef assert +# include +#endif +#include +#include +#include + +#ifndef __cplusplus +# include +#endif + +#if __STDC_VERSION__ >= 201112L || (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__cpp_static_assert) && __cpp_static_assert >= 200410) +# define cbor_static_assert(x) static_assert(x, #x) +#elif !defined(__cplusplus) && defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 406) && (__STDC_VERSION__ > 199901L) +# define cbor_static_assert(x) _Static_assert(x, #x) +#else +# define cbor_static_assert(x) ((void)sizeof(char[2*!!(x) - 1])) +#endif +#if __STDC_VERSION__ >= 199901L || defined(__cplusplus) +/* inline is a keyword */ +#else +/* use the definition from cbor.h */ +# define inline CBOR_INLINE +#endif + +#ifdef NDEBUG +# define cbor_assert(cond) do { if (!(cond)) unreachable(); } while (0) +#else +# define cbor_assert(cond) assert(cond) +#endif + +#ifndef STRINGIFY +#define STRINGIFY(x) STRINGIFY2(x) +#endif +#define STRINGIFY2(x) #x + +#if !defined(UINT32_MAX) || !defined(INT64_MAX) +/* C89? We can define UINT32_MAX portably, but not INT64_MAX */ +# error "Your system has stdint.h but that doesn't define UINT32_MAX or INT64_MAX" +#endif + +#ifndef DBL_DECIMAL_DIG +/* DBL_DECIMAL_DIG is C11 */ +# define DBL_DECIMAL_DIG 17 +#endif +#define DBL_DECIMAL_DIG_STR STRINGIFY(DBL_DECIMAL_DIG) + +#if defined(__GNUC__) && defined(__i386__) && !defined(__iamcu__) +# define CBOR_INTERNAL_API_CC __attribute__((regparm(3))) +#elif defined(_MSC_VER) && defined(_M_IX86) +# define CBOR_INTERNAL_API_CC __fastcall +#else +# define CBOR_INTERNAL_API_CC +#endif + +#ifndef __has_builtin +# define __has_builtin(x) 0 +#endif + +#if (defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 403)) || \ + (__has_builtin(__builtin_bswap64) && __has_builtin(__builtin_bswap32)) +# if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define cbor_ntohll __builtin_bswap64 +# define cbor_htonll __builtin_bswap64 +# define cbor_ntohl __builtin_bswap32 +# define cbor_htonl __builtin_bswap32 +# ifdef __INTEL_COMPILER +# define cbor_ntohs _bswap16 +# define cbor_htons _bswap16 +# elif (__GNUC__ * 100 + __GNUC_MINOR__ >= 608) || __has_builtin(__builtin_bswap16) +# define cbor_ntohs __builtin_bswap16 +# define cbor_htons __builtin_bswap16 +# else +# define cbor_ntohs(x) (((uint16_t)(x) >> 8) | ((uint16_t)(x) << 8)) +# define cbor_htons cbor_ntohs +# endif +# else +# define cbor_ntohll +# define cbor_htonll +# define cbor_ntohl +# define cbor_htonl +# define cbor_ntohs +# define cbor_htons +# endif +#elif defined(__sun) +# include +#elif defined(_MSC_VER) +/* MSVC, which implies Windows, which implies little-endian and sizeof(long) == 4 */ +# include +# define cbor_ntohll _byteswap_uint64 +# define cbor_htonll _byteswap_uint64 +# define cbor_ntohl _byteswap_ulong +# define cbor_htonl _byteswap_ulong +# define cbor_ntohs _byteswap_ushort +# define cbor_htons _byteswap_ushort +#endif +#ifndef cbor_ntohs +# include +# define cbor_ntohs ntohs +# define cbor_htons htons +#endif +#ifndef cbor_ntohl +# include +# define cbor_ntohl ntohl +# define cbor_htonl htonl +#endif +#ifndef cbor_ntohll +# define cbor_ntohll ntohll +# define cbor_htonll htonll +/* ntohll isn't usually defined */ +# ifndef ntohll +# if (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ + (defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && __BYTE_ORDER == __BIG_ENDIAN) || \ + (defined(BYTE_ORDER) && defined(BIG_ENDIAN) && BYTE_ORDER == BIG_ENDIAN) || \ + (defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN)) || (defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__)) || \ + defined(__ARMEB__) || defined(__MIPSEB__) || defined(__s390__) || defined(__sparc__) +# define ntohll +# define htonll +# elif (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \ + (defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN) || \ + (defined(BYTE_ORDER) && defined(LITTLE_ENDIAN) && BYTE_ORDER == LITTLE_ENDIAN) || \ + defined(_LITTLE_ENDIAN) || defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__MIPSEL__) || \ + defined(__i386) || defined(__i386__) || defined(__x86_64) || defined(__x86_64__) || defined(__amd64) +# define ntohll(x) ((ntohl((uint32_t)(x)) * UINT64_C(0x100000000)) + (ntohl((x) >> 32))) +# define htonll ntohll +# else +# error "Unable to determine byte order!" +# endif +# endif +#endif + + +#ifdef __cplusplus +# define CONST_CAST(t, v) const_cast(v) +#else +/* C-style const_cast without triggering a warning with -Wcast-qual */ +# define CONST_CAST(t, v) (t)(uintptr_t)(v) +#endif + +#ifdef __GNUC__ +#ifndef likely +# define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +# define unlikely(x) __builtin_expect(!!(x), 0) +#endif +# define unreachable() __builtin_unreachable() +#elif defined(_MSC_VER) +# define likely(x) (x) +# define unlikely(x) (x) +# define unreachable() __assume(0) +#else +# define likely(x) (x) +# define unlikely(x) (x) +# define unreachable() do {} while (0) +#endif + +static inline bool add_check_overflow(size_t v1, size_t v2, size_t *r) +{ +#if ((defined(__GNUC__) && (__GNUC__ >= 5)) && !defined(__INTEL_COMPILER)) || __has_builtin(__builtin_add_overflow) + return __builtin_add_overflow(v1, v2, r); +#else + /* unsigned additions are well-defined */ + *r = v1 + v2; + return v1 > v1 + v2; +#endif +} + +#endif /* COMPILERSUPPORT_H */ + diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h b/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h new file mode 100644 index 0000000..c26560c --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h @@ -0,0 +1,3 @@ +#define TINYCBOR_VERSION_MAJOR 0 +#define TINYCBOR_VERSION_MINOR 6 +#define TINYCBOR_VERSION_PATCH 0 diff --git a/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h b/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h new file mode 100644 index 0000000..ca43835 --- /dev/null +++ b/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBOR_UTF8_H +#define CBOR_UTF8_H + +#include "compilersupport_p.h" + +#include + +static inline uint32_t get_utf8(const uint8_t **buffer, const uint8_t *end) +{ + int charsNeeded; + uint32_t uc, min_uc; + uint8_t b; + ptrdiff_t n = end - *buffer; + if (n == 0) + return ~0U; + + uc = *(*buffer)++; + if (uc < 0x80) { + /* single-byte UTF-8 */ + return uc; + } + + /* multi-byte UTF-8, decode it */ + if (unlikely(uc <= 0xC1)) + return ~0U; + if (uc < 0xE0) { + /* two-byte UTF-8 */ + charsNeeded = 2; + min_uc = 0x80; + uc &= 0x1f; + } else if (uc < 0xF0) { + /* three-byte UTF-8 */ + charsNeeded = 3; + min_uc = 0x800; + uc &= 0x0f; + } else if (uc < 0xF5) { + /* four-byte UTF-8 */ + charsNeeded = 4; + min_uc = 0x10000; + uc &= 0x07; + } else { + return ~0U; + } + + if (n < charsNeeded) + return ~0U; + + /* first continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + + if (charsNeeded > 2) { + /* second continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + + if (charsNeeded > 3) { + /* third continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + } + } + + /* overlong sequence? surrogate pair? out or range? */ + if (uc < min_uc || uc - 0xd800U < 2048U || uc > 0x10ffff) + return ~0U; + + return uc; +} + +#endif /* CBOR_UTF8_H */ diff --git a/ffi/ffi_context.nim b/ffi/ffi_context.nim index f0a2cde..bd9477c 100644 --- a/ffi/ffi_context.nim +++ b/ffi/ffi_context.nim @@ -4,7 +4,7 @@ import std/[atomics, locks, json, tables] import chronicles, chronos, chronos/threadsync, taskpools/channels_spsc_single, results -import ./ffi_types, ./ffi_thread_request, ./internal/ffi_macro, ./logging +import ./ffi_types, ./ffi_thread_request, ./internal/ffi_macro, ./logging, ./cbor_serial type FFICallbackState* = object ## Holds the C event callback and its associated user-data pointer. @@ -37,7 +37,12 @@ type FFIContext*[T] = object # Pointer to with the registered requests at compile time var ffiCurrentCallbackState* {.threadvar.}: ptr FFICallbackState - ## Set by ffiThreadBody at thread startup; read by dispatchFfiEvent. + ## Set by ffiThreadBody at thread startup; read by dispatchFFIEvent. + +var onFFIThread* {.threadvar.}: bool + ## True while executing inside `ffiThreadBody`. Used by + ## `sendRequestToFFIThread` to detect re-entrant dispatch from a handler + ## (which would self-deadlock on `reqReceivedSignal`). const git_version* {.strdefine.} = "n/a" @@ -66,7 +71,7 @@ template callEventCallback*(ctx: ptr FFIContext, eventName: string, body: untype ctx[].callbackState.userData, ) -template dispatchFfiEvent*(eventName: string, body: untyped) = +template dispatchFFIEvent*(eventName: string, body: untyped) = ## Dispatches an FFI event to the callback registered via `{libName}_set_event_callback`. ## `body` is evaluated lazily — only when a callback is registered. ## Valid only on the FFI thread (i.e., inside {.ffi.} proc bodies and their async closures). @@ -89,11 +94,21 @@ template dispatchFfiEvent*(eventName: string, body: untyped) = proc sendRequestToFFIThread*( ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest, timeout = InfiniteDuration ): Result[void, string] = + # Reentrancy guard (PR #23 review, item 6): if a handler running on the FFI + # thread tries to dispatch back through this proc, it would wait forever on + # `reqReceivedSignal` — which only this thread can fire — and self-deadlock. + # Return an error instead so the caller can surface it. + if onFFIThread: + deleteRequest(ffiRequest) + return err( + "reentrant ffi call: a handler invoked sendRequestToFFIThread on its own context" + ) + + # All async submissions serialise on `ctx.lock` for the full + # trySend + fireSync + waitSync sequence because `reqChannel` is + # single-producer and `reqReceivedSignal` is shared across callers. + # Multi-producer redesign is tracked as PR #23 review item 7. ctx.lock.acquire() - # This lock is only necessary while we use a SP Channel and while the signalling - # between threads assumes that there aren't concurrent requests. - # Rearchitecting the signaling + migrating to a MP Channel will allow us to receive - # requests concurrently and spare us the need of locks defer: ctx.lock.release() @@ -201,13 +216,13 @@ proc processRequest[T]( ## That shouldn't happen because only registered requests should be sent to the FFI thread. nilProcess(request[].reqId) else: - ctx[].registeredRequests[][reqIdCs](request[].reqContent, ctx) + ctx[].registeredRequests[][reqIdCs](cast[pointer](request), ctx) let res = try: await retFut except AsyncError as exc: - Result[string, string].err( + Result[seq[byte], string].err( "Async error in processRequest for " & reqId & ": " & exc.msg ) @@ -222,10 +237,12 @@ proc processRequest[T]( proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = ## FFI thread body that attends library user API requests ffiCurrentCallbackState = addr ctx[].callbackState + onFFIThread = true logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) defer: + onFFIThread = false # Signal destroyFFIContext that this thread has exited, so its bounded # wait can unblock and proceed with cleanup. let fireRes = ctx.threadExitSignal.fireSync() diff --git a/ffi/ffi_thread_request.nim b/ffi/ffi_thread_request.nim index e1782e4..735e0cf 100644 --- a/ffi/ffi_thread_request.nim +++ b/ffi/ffi_thread_request.nim @@ -1,69 +1,144 @@ -## This file contains the base message request type that will be handled. -## The requests are created by the main thread and processed by -## the FFI Thread. +## Carries one CBOR-encoded request blob between the main thread and the FFI +## thread. The main thread allocates the request (in shared memory), the FFI +## thread frees it after invoking the user callback. -import std/[json, macros], results, tables -import chronos, chronos/threadsync -import ./ffi_types, ./internal/ffi_macro, ./alloc +import results +import chronos +import ./ffi_types, ./alloc, ./cbor_serial -type FFIDestroyContentProc* = proc(content: pointer) {.nimcall, gcsafe.} +const EmptyErrorMarker = "unknown error" + ## Sent verbatim on RET_ERR when the handler produced no message — keeps + ## the callback's msg ptr non-nil and gives the foreign side a recognizable + ## fallback to log. type FFIThreadRequest* = object - callback: FFICallBack - userData: pointer - reqId*: cstring - reqContent*: pointer - deleteReqContent*: FFIDestroyContentProc - ## Called by sendRequestToFFIThread on failure to free reqContent when - ## the FFI thread will never process (and thus never free) this request. + callback*: FFICallBack + userData*: pointer + reqId*: cstring ## Per-proc Req type name used to look up the handler. + data*: ptr UncheckedArray[byte] ## Owned CBOR-encoded request payload. + dataLen*: int + +proc allocBaseRequest( + callback: FFICallBack, userData: pointer, reqId: cstring +): ptr FFIThreadRequest = + ## Allocates the request envelope in shared memory and populates the + ## routing fields. Payload setup is delegated to one of the payload helpers + ## below depending on whether the bytes need to be copied or adopted. + var ret = createShared(FFIThreadRequest) + ret[].callback = callback + ret[].userData = userData + ret[].reqId = reqId.alloc() + ret[].data = nil + ret[].dataLen = 0 + return ret + +proc copySharedPayload(req: ptr FFIThreadRequest, data: ptr byte, dataLen: int) = + ## Allocates a fresh shared buffer and copies `dataLen` bytes from `data` + ## into `req`. Empty payloads (non-positive `dataLen` or nil `data`) leave + ## the request's payload fields at their zero-initialised state. + if dataLen > 0 and not data.isNil(): + req[].data = cast[ptr UncheckedArray[byte]](allocShared(dataLen)) + copyMem(req[].data, data, dataLen) + req[].dataLen = dataLen + +proc adoptOwnedSharedPayload( + req: ptr FFIThreadRequest, data: ptr UncheckedArray[byte], dataLen: int +) = + ## Embeds an already-`allocShared` buffer into `req` without copying. + ## `(nil, 0)` is the empty-payload contract; a zero-length-but-non-nil + ## buffer is treated as empty and disposed here so it doesn't leak. + if dataLen > 0 and not data.isNil(): + req[].data = data + req[].dataLen = dataLen + elif not data.isNil(): + deallocShared(data) + +proc initFromPtr*( + T: typedesc[FFIThreadRequest], + callback: FFICallBack, + userData: pointer, + reqId: cstring, + data: ptr byte, + dataLen: int, +): ptr type T = + ## Takes a raw ptr+len; the bytes are copied into a fresh shared-memory + ## buffer owned by the returned request. + var ret = allocBaseRequest(callback, userData, reqId) + copySharedPayload(ret, data, dataLen) + return ret proc init*( T: typedesc[FFIThreadRequest], callback: FFICallBack, userData: pointer, reqId: cstring, - reqContent: pointer, + data: openArray[byte], ): ptr type T = - var ret = createShared(FFIThreadRequest) - ret[].callback = callback - ret[].userData = userData - ret[].reqId = reqId.alloc() - ret[].reqContent = reqContent + ## Same contract as `initFromPtr` but accepts a Nim openArray, copying its + ## bytes into a fresh shared-memory buffer owned by the returned request. + let dataPtr = + if data.len > 0: + cast[ptr byte](unsafeAddr data[0]) + else: + nil + initFromPtr(T, callback, userData, reqId, dataPtr, data.len) + +proc initFromOwnedShared*( + T: typedesc[FFIThreadRequest], + callback: FFICallBack, + userData: pointer, + reqId: cstring, + data: ptr UncheckedArray[byte], + dataLen: int, +): ptr type T = + ## Takes ownership of an already-allocated shared-memory buffer (`data`) + ## and embeds it in the request without copying. Pair with `cborEncodeShared` + ## so the request payload travels from encoder to FFI thread with a single + ## allocation instead of seq → allocShared + copyMem. + ## + ## Ownership: `data` must have been allocated via `allocShared` / grown via + ## `reallocShared`. After this call, `deleteRequest` will `deallocShared` it. + ## Pass `(nil, 0)` for an empty payload. + var ret = allocBaseRequest(callback, userData, reqId) + adoptOwnedSharedPayload(ret, data, dataLen) return ret proc deleteRequest*(request: ptr FFIThreadRequest) = - if not request[].deleteReqContent.isNil(): - request[].deleteReqContent(request[].reqContent) - deallocShared(request[].reqId) + if not request[].data.isNil: + deallocShared(request[].data) + if not request[].reqId.isNil: + deallocShared(request[].reqId) deallocShared(request) -proc handleRes*[T: string | void]( - res: Result[T, string], request: ptr FFIThreadRequest -) = - ## Handles the Result responses, which can either be Result[string, string] or - ## Result[void, string]. - +proc handleRes*(res: Result[seq[byte], string], request: ptr FFIThreadRequest) = + ## Fires the registered callback exactly once and frees the request. + ## Success payload is CBOR bytes; error payload is the raw UTF-8 error string. defer: deleteRequest(request) if res.isErr(): foreignThreadGc: - let msg = res.error + let msg = if res.error.len > 0: res.error else: EmptyErrorMarker request[].callback( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData + RET_ERR, unsafeAddr msg[0], cast[csize_t](msg.len), request[].userData ) return foreignThreadGc: - var resStr: string - ## we need to bind the string to extend its lifetime to callback's in ARC/ORC - when T is string: - resStr = res.get() - let msg: cstring = resStr.cstring() - request[].callback( - RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData - ) - return + let bytes = res.get() + if bytes.len > 0: + request[].callback( + RET_OK, + cast[ptr cchar](unsafeAddr bytes[0]), + cast[csize_t](bytes.len), + request[].userData, + ) + else: + # Always hand the callback a real buffer; CBOR null marks "no value". + var sentinel = CborNullByte + request[].callback( + RET_OK, cast[ptr cchar](addr sentinel), 1.csize_t, request[].userData + ) -proc nilProcess*(reqId: cstring): Future[Result[string, string]] {.async.} = +proc nilProcess*(reqId: cstring): Future[Result[seq[byte], string]] {.async.} = return err("This request type is not implemented: " & $reqId) diff --git a/ffi/ffi_types.nim b/ffi/ffi_types.nim index 76ead50..0029ec2 100644 --- a/ffi/ffi_types.nim +++ b/ffi/ffi_types.nim @@ -18,8 +18,10 @@ const RET_MISSING_CALLBACK*: cint = 2 ################################################################################ ### FFI utils -type FFIRequestProc* = - proc(request: pointer, reqHandler: pointer): Future[Result[string, string]] {.async.} +type FFIRequestProc* = proc( + request: pointer, reqHandler: pointer +): Future[Result[seq[byte], string]] {.async.} + ## The OK payload is a CBOR-encoded response body. Errors are plain UTF-8. template foreignThreadGc*(body: untyped) = when declared(setupForeignThreadGc): diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index 7d7d11d..46b9b22 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -99,20 +99,18 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped = ## `libType` is the Nim type of the main library object (e.g. `Waku`). It is used ## to type the `ctx: ptr FFIContext[libType]` parameter of the generated ## `{libraryName}_set_event_callback` proc. - result = newStmtList() + var stmts = newStmtList() # Emit the base bootstrap (pragmas, linker flags, NimMain, initializeLibrary) - result.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName))) + stmts.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName))) let funcName = libraryName & "_set_event_callback" let funcIdent = ident(funcName) let errorMsg = "error: invalid context in " & funcName - let ctxType = nnkPtrTy.newTree( - nnkBracketExpr.newTree(ident("FFIContext"), libType) - ) + let ctxType = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libType)) - let procBody = quote do: + let procBody = quote: if isNil(ctx): echo `errorMsg` return @@ -137,4 +135,5 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped = ), ) - result.add(procNode) + stmts.add(procNode) + return stmts diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 45077bd..91ba21d 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1,7 +1,7 @@ import std/[macros, tables, strutils] import chronos import ../ffi_types -import ../codegen/meta +import ../codegen/[meta, string_helpers] when defined(ffiGenBindings): import ../codegen/rust import ../codegen/cpp @@ -10,20 +10,45 @@ when defined(ffiGenBindings): # String helpers used by multiple macros # --------------------------------------------------------------------------- -proc nimNameToCExport(s: string): string = - ## Converts a camelCase Nim proc name to a snake_case C export name. - ## Leaves already-snake_case names unchanged. - ## e.g. "nimtimerCreate" → "nimtimer_create", "nimtimer_echo" → "nimtimer_echo" - for i, c in s: - if c.isUpperAscii() and i > 0: - result.add('_') - result.add(c.toLowerAscii()) +proc isPtr(typ: NimNode): bool = + ## True iff `typ` is a `ptr T` type expression — i.e. an `nnkPtrTy` AST node. + ## Used by the binding-generator metadata path to flag pointer-typed params + ## and return types so the foreign side can render them as opaque addresses. + typ.kind == nnkPtrTy -proc registerFfiTypeInfo(typeDef: NimNode): NimNode {.compileTime.} = +proc rejectRawPtrType(typ: NimNode, where: string) = + ## Errors out at macro-expansion time if `typ` is `pointer` or `ptr T`. + ## Raw addresses must not cross the FFI boundary in user-declared fields, + ## parameters, or return types: the only pointer that legitimately crosses + ## the boundary is the opaque ctx handle returned by `.ffiCtor.` and passed + ## back as the first C-ABI argument, which the framework validates via + ## FFIContextPool.isValidCtx before dereferencing. Any other raw pointer + ## would hand the foreign caller an address with no way to validate its + ## memory state — see PR #23 review (discussion_r3236531712). + ## + ## `object` and `ref T` are not rejected: they flow as value copies through + ## cbor_serialization (the library's default `ref T` writer dereferences + ## and encodes the pointee, so no address crosses the boundary). + if typ.kind == nnkPtrTy: + error( + where & ": raw `ptr T` is not allowed across the FFI boundary " & + "(only the ctx handle, managed by the framework, may be a pointer)" + ) + if typ.kind == nnkIdent and $typ == "pointer": + error( + where & ": raw `pointer` is not allowed across the FFI boundary " & + "(only the ctx handle, managed by the framework, may be a pointer)" + ) + +proc registerFFITypeInfo(typeDef: NimNode): NimNode {.compileTime.} = ## Registers the type in ffiTypeRegistry for binding generation and returns - ## the clean typeDef. Serialization is handled by the generic overloads in serial.nim. + ## the clean typeDef. Serialization is handled by the generic overloads in + ## cbor_serial.nim. let typeName = - if typeDef[0].kind == nnkPostfix: typeDef[0][1] else: typeDef[0] + if typeDef[0].kind == nnkPostfix: + typeDef[0][1] + else: + typeDef[0] let typeNameStr = $typeName var fieldMetas: seq[FFIFieldMeta] = @[] @@ -34,221 +59,233 @@ proc registerFfiTypeInfo(typeDef: NimNode): NimNode {.compileTime.} = for identDef in recList: if identDef.kind == nnkIdentDefs: let fieldType = identDef[^2] + for i in 0 ..< identDef.len - 2: + rejectRawPtrType( + fieldType, "{.ffi.} type " & typeNameStr & "." & $identDef[i] + ) let fieldTypeName = - if fieldType.kind == nnkIdent: $fieldType - elif fieldType.kind == nnkPtrTy: "ptr " & $fieldType[0] - else: fieldType.repr + if fieldType.kind == nnkIdent: + $fieldType + else: + fieldType.repr for i in 0 ..< identDef.len - 2: fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName)) ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas)) - result = typeDef + return typeDef -proc cParamName(paramName: string, paramType: NimNode): string = - ## C export parameter name. string params are passed as-is from C and need - ## no Json suffix; other types carry Json to signal they require JSON encoding. - if paramType.kind == nnkIdent and $paramType == "string": - paramName +proc nimTypeNameRepr(typ: NimNode): string = + ## Stringifies a parameter or field type for the binding-generator registry. + ## `$ident` works for simple types; bracket/dot/expression types need `repr`. + case typ.kind + of nnkIdent: + $typ + of nnkPtrTy: + "ptr " & nimTypeNameRepr(typ[0]) else: - paramName & "Json" + typ.repr -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 storageType(typ: NimNode): NimNode = + ## Returns the in-Req-struct storage type for a user-declared param type. + ## `cstring` is stored as `string` for trivial CBOR transport; everything + ## else is stored as the user typed it. + if typ.kind == nnkIdent and $typ == "cstring": + return ident("string") + return typ -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 unpackReqField*(fieldIdent, userType, decodedIdent: NimNode): NimNode = + ## Emits AST for unpacking one field from a CBOR-decoded Req struct into a + ## local typed as the user's original param type. + ## + ## `cstring` params are stored as `string` in the Req (per storageType) + ## and cast back via `.cstring` on unpack — safe because `decodedIdent` + ## outlives the cstring use within the generated proc body. + ## + ## Produces one of: + ## let : cstring = (.).cstring # for cstring + ## let = . # for everything else + ## + ## Built with the runtime AST API rather than `quote do:` so the proc is + ## callable from both macro context and ordinary code (e.g. unit tests). + let storedAsString = userType.kind == nnkIdent and $userType == "cstring" + if not storedAsString: + return newLetStmt(fieldIdent, newDotExpr(decodedIdent, fieldIdent)) -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 + let fieldAccess = newDotExpr(decodedIdent, fieldIdent) + let castExpr = newDotExpr(fieldAccess, ident("cstring")) + return + nnkLetSection.newTree(nnkIdentDefs.newTree(fieldIdent, ident("cstring"), castExpr)) -proc extractFieldsFromLambda(body: NimNode): seq[NimNode] = - ## Extracts the fields (params) from the given lambda body, when using the registerReqFFI macro. - ## e.g., for: - ## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): - ## proc( - ## configJson: cstring, appCallbacks: AppCallbacks - ## ): Future[Result[string, string]] {.async.} = - ## ... - ## The extracted fields will be: - ## - configJson: cstring - ## - appCallbacks: AppCallbacks - ## +proc cExportedParams(ctxType: NimNode): seq[NimNode] = + ## Standard parameter list for the C-exported wrapper of a .ffi. proc: + ## (returns cint; ctx, callback, userData, reqCbor, reqCborLen) + ## Shared by the async and sync paths so both wrappers carry the same ABI. + var params: seq[NimNode] = @[] + params.add(ident("cint")) + params.add(newIdentDefs(ident("ctx"), ctxType)) + params.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) + params.add(newIdentDefs(ident("userData"), ident("pointer"))) + params.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + params.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) + return params - var procNode = body - if procNode.kind == nnkStmtList and procNode.len == 1: - procNode = procNode[0] - if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: - error "registerReqFFI expects a lambda proc, found: " & $procNode.kind - - let params = procNode[3] # parameters list - result = @[] - for p in params[1 .. ^1]: # skip return type - result.add newIdentDefs(p[0], p[1]) - - when defined(ffiDumpMacros): - echo result.repr - -proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode = - ## Builds: - ## type * = object - ## : - ## ... - ## e.g.: - ## type CreateNodeRequest* = object - ## configJson: cstring - ## appCallbacks: AppCallbacks - ## - - var procNode = body - if procNode.kind == nnkStmtList and procNode.len == 1: - procNode = procNode[0] - if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: - error "registerReqFFI expects a lambda proc, found: " & $procNode.kind - - let params = procNode[3] # formal params of the lambda +proc buildReqTypeFromFields( + reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] +): NimNode = + ## Builds the per-proc Req `nnkTypeSection` (exported) from explicit + ## parallel lists of parameter names and types. The result is the AST for + ## a `type Foo* = object` declaration that the codegen later emits. + ## + ## `cstring` parameter types are rewritten to `string` (via storageType) + ## so the request can ride a plain CBOR text string on the wire. Empty + ## parameter lists get a single `_placeholder: uint8` field so the object + ## type is well-formed (Nim won't accept an empty `object` body here). + ## + ## Examples (in pseudo-Nim, showing the AST this proc produces): + ## + ## buildReqTypeFromFields( + ## reqTypeName = ident("EchoReq"), + ## paramNames = @["message", "delayMs"], + ## paramTypes = @[ident("cstring"), ident("int")]) + ## # → type EchoReq* = object + ## # message: string # cstring rewritten to string + ## # delayMs: int + ## + ## buildReqTypeFromFields( + ## reqTypeName = ident("VersionReq"), + ## paramNames = @[], + ## paramTypes = @[]) + ## # → type VersionReq* = object + ## # _placeholder: uint8 # placeholder for the empty-params case + ## + ## If `reqTypeName` is already a postfix node (e.g. `EchoReq*`) it is used + ## as-is; otherwise the `*` export marker is added. var fields: seq[NimNode] = @[] - for p in params[1 .. ^1]: # skip return type at index 0 - let name = p[0] - let typ = p[1] - # Field must be nnkIdentDefs(name, type, defaultExpr) - fields.add newTree(nnkIdentDefs, name, typ, newEmptyNode()) + for i in 0 ..< paramNames.len: + let storedType = storageType(paramTypes[i]) + fields.add newTree(nnkIdentDefs, ident(paramNames[i]), storedType, newEmptyNode()) - # Wrap fields in a rec list - let recList = newTree(nnkRecList, fields) + let recList = + if fields.len > 0: + newTree(nnkRecList, fields) + else: + newTree( + nnkRecList, + newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()), + ) - # object type node: object [of?] [] [pragma?] recList let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList) - # Export the type (CreateNodeRequest*) let typeName = if reqTypeName.kind == nnkPostfix: reqTypeName else: postfix(reqTypeName, "*") - result = + return newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy)) - when defined(ffiDumpMacros): - echo result.repr - -proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode = - ## Builds the ffiNewProc in charge of creating the FFIThreadRequest in shared memory. - ## Then, a pointer to this request will be sent to the FFI thread for processing. +proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode = + ## Builds the per-proc Req object type from a registerReqFFI lambda body. + ## Field names match the lambda params; field types match the user-typed + ## param types (with `cstring` rewritten to `string` for transport). + ## + ## Builds: + ## type * = object + ## : + ## ... + ## ## e.g.: - ## proc ffiNewReq*(T: typedesc[CreateNodeRequest]; callback: FFICallBack; - ## userData: pointer; configJson: cstring; - ## appCallbacks: AppCallbacks): ptr FFIThreadRequest = - ## var reqObj = createShared(T) - ## reqObj[].configJson = configJson.alloc() - ## reqObj[].appCallbacks = appCallbacks - ## let typeStr`gensym2866 = $T - ## var ret`gensym2866 = FFIThreadRequest.init(callback, userData, - ## typeStr`gensym2866.cstring, reqObj) - ## return ret`gensym2866 - ## - ## This should be invoked by the ffi consumer thread (generally, main thread.) - ## Notice that the shared memory allocated by the main thread is freed by the FFI thread - ## after processing the request. + ## type EchoRequest* = object + ## message: string + ## delayMs: int + + var procNode = body + if procNode.kind == nnkStmtList and procNode.len == 1: + procNode = procNode[0] + if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: + error "registerReqFFI expects a lambda proc, found: " & $procNode.kind + + let params = procNode[3] + var paramNames: seq[string] = @[] + var paramTypes: seq[NimNode] = @[] + for p in params[1 .. ^1]: + paramNames.add($p[0]) + paramTypes.add(p[1]) + + let typeSection = buildReqTypeFromFields(reqTypeName, paramNames, paramTypes) + + when defined(ffiDumpMacros): + echo typeSection.repr + return typeSection + +proc buildFFINewReqProc(reqTypeName, body: NimNode): NimNode = + ## Builds ffiNewReq: takes the user's typed params, packs them into a Req + ## object, CBOR-encodes the Req into one byte buffer, and constructs the + ## FFIThreadRequest that owns the buffer. var formalParams = newSeq[NimNode]() var procNode: NimNode if body.kind == nnkStmtList and body.len == 1: - procNode = body[0] # unwrap single statement + procNode = body[0] else: procNode = body if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind - # T: typedesc[CreateNodeRequest] - let typedescParam = newIdentDefs( - ident("T"), # param name - nnkBracketExpr.newTree(ident("typedesc"), reqTypeName), # typedesc[T] - ) + # T: typedesc[XxxReq] + let typedescParam = + newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) formalParams.add(typedescParam) - - # Other fixed FFI params formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) formalParams.add(newIdentDefs(ident("userData"), ident("pointer"))) - # Add original lambda params + # User-typed lambda params (kept as-is so callers see their original signature) let procParams = procNode[3] for p in procParams[1 .. ^1]: formalParams.add(p) - # Build `ptr FFIThreadRequest` let retType = newNimNode(nnkPtrTy) retType.add(ident("FFIThreadRequest")) formalParams = @[retType] & formalParams - # Build body let reqObjIdent = ident("reqObj") var newBody = newStmtList() newBody.add( quote do: - var `reqObjIdent` = createShared(T) + var `reqObjIdent`: T ) for p in procParams[1 .. ^1]: - let fieldNameIdent = ident($p[0]) - let fieldTypeNode = p[1] - - # Extract type name as string - var typeStr: string - if fieldTypeNode.kind == nnkIdent: - typeStr = $fieldTypeNode - elif fieldTypeNode.kind == nnkBracketExpr: - typeStr = $fieldTypeNode[0] # e.g., `ptr` in `ptr[Waku]` - else: - typeStr = "" # fallback - - # Apply .alloc() only to cstrings - if typeStr == "cstring": + let fieldName = ident($p[0]) + let userType = p[1] + let storeAsString = userType.kind == nnkIdent and $userType == "cstring" + if storeAsString: newBody.add( quote do: - `reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`.alloc() + `reqObjIdent`.`fieldName` = $`fieldName` ) else: newBody.add( quote do: - `reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent` + `reqObjIdent`.`fieldName` = `fieldName` ) - # FFIThreadRequest.init using fnv1aHash32 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 + # Encode directly into shared memory and hand ownership to the request, + # avoiding the seq[byte] → allocShared+copyMem second copy. + let (sharedData, sharedLen) = cborEncodeShared(`reqObjIdent`) + return FFIThreadRequest.initFromOwnedShared( + callback, userData, typeStr.cstring, sharedData, sharedLen + ) ) - # Build the proc node - result = newProc( + let newReqProc = newProc( name = postfix(ident("ffiNewReq"), "*"), params = formalParams, body = newBody, @@ -256,45 +293,13 @@ proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode = ) when defined(ffiDumpMacros): - echo result.repr - -proc buildFfiDeleteReqProc(reqTypeName: NimNode, fields: seq[NimNode]): NimNode = - ## Generates: - ## proc ffiDeleteReq(self: ptr ) = - ## deallocShared(self[].) - ## deallocShared(self) - - # Build the body - var body = newStmtList() - for f in fields: - if $f[1] == "cstring": # only dealloc cstring fields - body.add newCall( - ident("deallocShared"), - newDotExpr(newTree(nnkDerefExpr, ident("self")), ident($f[0])), - ) - - # Always free the whole object at the end - body.add newCall(ident("deallocShared"), ident("self")) - - # Build the parameter: (self: ptr ) - let selfParam = newIdentDefs(ident("self"), newTree(nnkPtrTy, reqTypeName)) - - # Build the proc definition - result = newProc( - name = postfix(ident("ffiDeleteReq"), "*"), - params = @[newEmptyNode()] & @[selfParam], # ✅ properly wrapped in a sequence - body = body, - ) - - when defined(ffiDumpMacros): - echo result.repr + echo newReqProc.repr + return newReqProc proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode = - ## Builds, f.e.: - ## proc processFFIRequest(T: typedesc[CreateNodeRequest]; - ## configJson: cstring; - ## appCallbacks: AppCallbacks; - ## ctx: ptr FFIContext[Waku]) ... + ## Generates the FFI-thread-side processor for the Req type. + ## Decodes the CBOR payload into a Req struct, unpacks each field into a + ## local, then runs the user lambda body. if reqHandler.kind != nnkExprColonExpr: error( @@ -315,7 +320,6 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode let typedescParam = newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) - # Build formal params: (returnType, request: pointer, waku: ptr Waku) let procParams = procNode[3] var formalParams: seq[NimNode] = @[] formalParams.add(procParams[0]) # return type @@ -323,7 +327,6 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode formalParams.add(newIdentDefs(ident("request"), ident("pointer"))) formalParams.add(newIdentDefs(reqHandler[0], rhs)) # e.g. waku: ptr Waku - # Inject cast/unpack/defer into the body let bodyNode = if procNode.body.kind == nnkStmtList: procNode.body @@ -331,22 +334,25 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode newStmtList(procNode.body) let newBody = newStmtList() - let reqIdent = genSym(nskLet, "req") + let reqIdent = genSym(nskLet, "ffiReq") + let decodedIdent = genSym(nskLet, "decoded") newBody.add quote do: - let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request) + let `reqIdent`: ptr FFIThreadRequest = cast[ptr FFIThreadRequest](request) + let `decodedIdent` = cborDecodePtr( + cast[ptr UncheckedArray[byte]](`reqIdent`[].data), + `reqIdent`[].dataLen, + `reqTypeName`, + ).valueOr: + return err("CBOR decode failed for " & $T & ": " & $error) - # automatically unpack fields into locals + # Unpack each field as a local typed as the user's original param type. for p in procParams[1 ..^ 1]: - let fieldName = p[0] # Ident + newBody.add unpackReqField(p[0], p[1], decodedIdent) - newBody.add quote do: - let `fieldName` = `reqIdent`[].`fieldName` - - # Append user's lambda body newBody.add(bodyNode) - result = newProc( + let processProc = newProc( name = postfix(ident("processFFIRequest"), "*"), params = formalParams, body = newBody, @@ -359,53 +365,51 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode ) when defined(ffiDumpMacros): - echo result.repr + echo processProc.repr + return processProc proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode = - ## Adds a new request to the registeredRequests table. - ## The key is a representation of the request, e.g. "CreateNodeReq". - ## The value is a proc definition in charge of handling the request from FFI thread. + ## Generates the dispatcher that the FFI thread calls: it invokes + ## processFFIRequest (which returns the user's typed Result[T, string]) and + ## encodes a successful T value with cborEncode into the seq[byte] payload. - # Build: request[].reqContent - let reqContent = - newDotExpr(newTree(nnkDerefExpr, ident("request")), ident("reqContent")) - - # Build Future[Result[string, string]] return type let returnType = nnkBracketExpr.newTree( ident("Future"), - nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")), + nnkBracketExpr.newTree( + ident("Result"), + nnkBracketExpr.newTree(ident("seq"), ident("byte")), + ident("string"), + ), ) - # Extract the type from reqHandler (generic: ptr Waku, ptr Foo, ptr Bar, etc.) let rhsType = if reqHandler.kind == nnkExprColonExpr: - reqHandler[1] # Use the explicit type + reqHandler[1] else: error "Second argument must be a typed parameter, e.g. waku: ptr Waku" - # Build: cast[ptr Waku](reqHandler) or cast[ptr Foo](reqHandler) dynamically - let castedHandler = newTree( - nnkCast, - rhsType, # The type, e.g. ptr Waku - ident("reqHandler"), # The expression to cast - ) + let castedHandler = newTree(nnkCast, rhsType, ident("reqHandler")) let callExpr = newCall( newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler ) - var newBody = newStmtList() - newBody.add( - quote do: - return await `callExpr` - ) + let typedResIdent = genSym(nskLet, "typedRes") + + var newBody = newStmtList() + newBody.add quote do: + let `typedResIdent` = await `callExpr` + if `typedResIdent`.isErr: + return err(`typedResIdent`.error) + when typeof(`typedResIdent`.value) is seq[byte]: + return ok(`typedResIdent`.value) + elif typeof(`typedResIdent`.value) is void: + return ok(newSeq[byte]()) + else: + return ok(cborEncode(`typedResIdent`.value)) - # Build: - # proc(request: pointer, reqHandler: pointer): - # Future[Result[string, string]] {.async.} = - # CreateNodeRequest.processFFIRequest(request, reqHandler) let asyncProc = newProc( - name = newEmptyNode(), # anonymous proc + name = newEmptyNode(), params = @[ returnType, newIdentDefs(ident("request"), ident("pointer")), @@ -415,67 +419,61 @@ proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode = pragmas = nnkPragma.newTree(ident("async")), ) - let reqTypeNameStr = $reqTypeName - let key = newLit($reqTypeName) - # Generate: registeredRequests["CreateNodeRequest"] = - result = + let regAssign = newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc) when defined(ffiDumpMacros): - echo result.repr + echo regAssign.repr + return regAssign macro registerReqFFI*(reqTypeName, reqHandler, body: untyped): untyped = ## Registers a request that will be handled by the FFI/working thread. ## The request should be sent from the ffi consumer thread. - ## - ## e.g.: - ## In this example, we register a CreateNodeRequest that will be handled by a proc that contains - ## the provided lambda body and parameters, by the FFI/working thread. - ## + ## ## The lambda passed to this macro must: - ## - only have no-GC'ed types. + ## - Only have no-GC'ed types as parameters (cstring is allowed; it gets + ## transported as `string` in the per-proc Req struct). ## - Return Future[Result[string, string]] and be annotated with {.async.} - ## And notice that the returned values will be sent back to the ffi consumer thread. - ## - ## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): - ## proc( - ## configJson: cstring, appCallbacks: AppCallbacks - ## ): Future[Result[string, string]] {.async.} = - ## ctx.myLib[] = (await createWaku(configJson, cast[AppCallbacks](appCallbacks))).valueOr: - ## return err($error) - ## return ok("") - ## - ## On the other hand, the created FFI request should be dispatched from the ffi consumer thread - ## (generally, the main thread) following something like: - ## - ## ffi.sendRequestToFFIThread( - ## ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson, appCallbacks) + ## The returned values are sent back to the ffi consumer thread. + ## + ## Example: + ## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): + ## proc( + ## config: NodeConfig, appCallbacks: AppCallbacks + ## ): Future[Result[string, string]] {.async.} = + ## ctx.myLib[] = (await createWaku(config, appCallbacks)).valueOr: + ## return err($error) + ## return ok("") + ## + ## The created FFI request is then dispatched from the ffi consumer thread + ## (generally the main thread) following something like: + ## + ## ffi.sendRequestToFFIThread( + ## ctx, CreateNodeRequest.ffiNewReq(callback, userData, config, appCallbacks) ## ).isOkOr: - ## ... - ## ... - ## + ## ... # Extract lambda params to generate fields - let fields = extractFieldsFromLambda(body) - let typeDef = buildRequestType(reqTypeName, body) - let ffiNewReqProc = buildFfiNewReqProc(reqTypeName, body) + let ffiNewReqProc = buildFFINewReqProc(reqTypeName, body) let processProc = buildProcessFFIRequestProc(reqTypeName, reqHandler, body) let addNewReqToReg = addNewRequestToRegistry(reqTypeName, reqHandler) - let deleteProc = buildFfiDeleteReqProc(reqTypeName, fields) - result = newStmtList(typeDef, deleteProc, ffiNewReqProc, processProc, addNewReqToReg) + let stmts = newStmtList(typeDef, ffiNewReqProc, processProc, addNewReqToReg) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts macro processReq*( reqType, ctx, callback, userData: untyped, args: varargs[untyped] ): untyped = - ## Expands T.processReq(ctx, callback, userData, a, b, ...) + ## Expands T.processReq(ctx, callback, userData, a, b, ...) into a + ## sendRequestToFFIThread call that wraps the args in a freshly-built + ## FFIThreadRequest, with inline error reporting via `callback`. + ## ## e.g.: ## waku_dial_peerReq.processReq(ctx, callback, userData, peerMultiAddr, protocol, timeoutMs) - ## var callArgs = @[reqType, callback, userData] for a in args: @@ -487,7 +485,7 @@ macro processReq*( newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), ctx, newReqCall ) - result = quote: + let blockExpr = quote: block: let res = `sendCall` if res.isErr(): @@ -497,30 +495,32 @@ macro processReq*( return RET_OK when defined(ffiDumpMacros): - echo result.repr + echo blockExpr.repr + return blockExpr macro ffiRaw*(prc: untyped): untyped = ## Defines an FFI-exported proc that registers a request handler to be executed ## asynchronously in the FFI thread. ## ## This is the "raw" / legacy form of the macro where the developer writes - ## the ctx, callback, and userData parameters explicitly. + ## the ctx, callback, and userData parameters explicitly. Additional parameters + ## travel as one CBOR blob. ## - ## {.ffiRaw.} implicitly implies: ...Return[Future[Result[string, string]] {.async.} + ## {.ffiRaw.} implicitly implies a Future[Result[string, string]] {.async.} + ## return type. ## ## 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. + ## 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 - ## ) {.ffiRaw.} = - ## return ok(WakuNodeVersionString) - ## + ## proc waku_version( + ## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer + ## ) {.ffiRaw.} = + ## return ok(WakuNodeVersionString) let procName = prc[0] let formalParams = prc[3] @@ -533,22 +533,17 @@ macro ffiRaw*(prc: untyped): untyped = let paramIdent = firstParam[0] let paramType = firstParam[1] - # The first param of an `.ffiRaw.` proc is `ctx: ptr FFIContext[LibType]`. - # Extract LibType so we can call the module-level pool var (named - # "FFIPool", declared by `.ffiCtor.`) to validate ctx. let libTypeName = paramType[0][1] let poolIdent = ident($libTypeName & "FFIPool") let reqName = ident($procName & "Req") let returnType = ident("cint") - # Build parameter list (skip return type) var newParams = newSeq[NimNode]() newParams.add(returnType) for i in 1 ..< formalParams.len: newParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1])) - # Build Future[Result[string, string]] return type let futReturnType = quote: Future[Result[string, string]] @@ -558,20 +553,16 @@ macro ffiRaw*(prc: untyped): untyped = for i in 4 ..< formalParams.len: userParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1])) - # Build argument list for processReq var argsList = newSeq[NimNode]() for i in 1 ..< formalParams.len: argsList.add(formalParams[i][0]) - # 1. Build the dot expression. e.g.: waku_is_onlineReq.processReq let dotExpr = newTree(nnkDotExpr, reqName, ident"processReq") - # 2. Build the call node with dotExpr as callee let callNode = newTree(nnkCall, dotExpr) for arg in argsList: callNode.add(arg) - # Proc body let ffiBody = newStmtList( quote do: initializeLibrary() @@ -592,31 +583,37 @@ macro ffiRaw*(prc: untyped): untyped = ) var anonymousProcNode = newProc( - name = newEmptyNode(), # anonymous proc + name = newEmptyNode(), params = userParams, body = newStmtList(bodyNode), pragmas = newTree(nnkPragma, ident"async"), ) - # registerReqFFI wrapper let registerReq = quote: registerReqFFI(`reqName`, `paramIdent`: `paramType`): `anonymousProcNode` - result = newStmtList(registerReq, ffiProc) + let stmts = newStmtList(registerReq, ffiProc) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts + +# --------------------------------------------------------------------------- +# ffi macro — primary FFI proc / FFI type registration +# --------------------------------------------------------------------------- macro ffi*(prc: untyped): untyped = ## Simplified FFI macro — applies to procs or types. ## ## On a type: `type Foo {.ffi.} = object` registers Foo for binding generation - ## and generates ffiSerialize/ffiDeserialize overloads. + ## and lets the generic cborEncode/cborDecode overloads handle serialization. ## - ## On a proc: the annotated proc must have a first parameter of the library type, - ## optionally additional Nim-typed parameters, and return Future[Result[RetType, string]]. - ## It must NOT include ctx, callback, or userData in its signature. + ## On a proc: the annotated proc must have a first parameter of the library + ## type, optionally additional Nim-typed parameters, and return + ## Future[Result[RetType, string]]. It must NOT include ctx, callback, or + ## userData in its signature — the macro generates a C-exported wrapper that + ## takes one CBOR-encoded buffer as the call payload and fires the callback. ## ## Example (type): ## type EchoRequest {.ffi.} = object @@ -631,23 +628,19 @@ macro ffi*(prc: untyped): untyped = var cleanTypeDef = prc.copyNimTree() if cleanTypeDef[0].kind == nnkPragmaExpr: cleanTypeDef[0] = cleanTypeDef[0][0] - return registerFfiTypeInfo(cleanTypeDef) + return registerFFITypeInfo(cleanTypeDef) 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` + let libParamName = firstParam[0] + let libTypeName = firstParam[1] - # 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( @@ -658,127 +651,93 @@ macro ffi*(prc: untyped): untyped = "`.ffi.` return type must be Future[Result[RetType, string]], got: " & retTypeNode.repr ) - let resultInner = retTypeNode[1] # Result[RetType, string] + let resultInner = retTypeNode[1] 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) + let resultRetType = resultInner[1] + rejectRawPtrType(resultRetType, "`.ffi.` proc " & $procName & " return type") + 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: + rejectRawPtrType(p[^2], "`.ffi.` proc " & $procName & " parameter " & $p[j]) 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 cExportName = nimNameToCExport(procNameStr) - let camelName = toCamelCase(procNameStr) + let cExportName = camelToSnakeCase(procNameStr) + let camelName = snakeToPascalCase(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) + var userProcName = procName + if procName.kind == nnkPostfix: + userProcName = procName[1] + ## Both the user-facing Nim proc and the C-exported wrapper share the user's + ## original name; their signatures differ so Nim resolves the call by + ## overload. The C wrapper additionally carries `{.exportc.}` so the foreign + ## ABI symbol is unchanged. + let cExportProcName = userProcName - # 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.} = - # + proc buildAsyncHelperProc(): NimNode = + ## proc *(lib: LibType, extras...): Future[Result[T, string]] {.async.} = 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, "*"), + newProc( + name = postfix(userProcName, "*"), 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")), - ) + proc asyncPath(): NimNode = + ## Emits the C-exported wrapper and registers the request handler. + ## All `.ffi.` procs dispatch through the FFI thread channel and reply + ## through the callback when the future resolves — the previous "sync + ## fast-path" that ran inline on the foreign caller thread was removed + ## (PR #23 review, items 1–5) because it bypassed `foreignThreadGc`, + ## `ctx.lock`, and chronos's single-thread invariant. + let helperProc = buildAsyncHelperProc() + # registerReqFFI lambda: typed params, returns user's typed Result. let ctxHandlerName = ident("ffiCtxHandler") - let ptrFfiCtx = + let ptrFFICtx = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) var lambdaParams = newSeq[NimNode]() - lambdaParams.add(futStrStr) + lambdaParams.add(retTypeNode) # Future[Result[RetType, string]] for i in 0 ..< extraParamNames.len: - lambdaParams.add( - newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring")) - ) - - let lambdaBody = newStmtList() - - for i in 0 ..< extraParamNames.len: - let cIdent = ident(cParamName(extraParamNames[i], extraParamTypes[i])) - let paramIdent = ident(extraParamNames[i]) - let ptype = extraParamTypes[i] - if $cIdent != $paramIdent: - # Non-string param: cIdent has a Json suffix. Deserialize into paramIdent. - # buildProcessFFIRequestProc unpacks cIdent from the req struct, so no name clash. - lambdaBody.add quote do: - let `paramIdent` = ffiDeserialize(`cIdent`, `ptype`).valueOr: - return err($error) - # String params (cIdent == paramIdent): buildProcessFFIRequestProc already - # unpacks the cstring under the same name; we convert inline in the helperCall below. + lambdaParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i])) let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib")) let libValDeref = newTree(nnkDerefExpr, ctxMyLib) - let helperCall = newTree(nnkCall, helperProcName, libValDeref) - for i in 0 ..< extraParamNames.len: - let cIdent = ident(cParamName(extraParamNames[i], extraParamTypes[i])) - let paramIdent = ident(extraParamNames[i]) - if $cIdent == $paramIdent: - # String param: cIdent/paramIdent is the cstring from the req unpack; - # convert to string with $ (ffiDeserialize for string is just ok($s)). - helperCall.add(newCall(ident("$"), paramIdent)) - else: - helperCall.add(paramIdent) + let helperCall = newTree(nnkCall, userProcName, libValDeref) + for name in extraParamNames: + helperCall.add(ident(name)) + let lambdaBody = newStmtList() let retValIdent = ident("retVal") lambdaBody.add quote do: let `retValIdent` = (await `helperCall`).valueOr: return err($error) - lambdaBody.add quote do: - return ok(ffiSerialize(`retValIdent`)) + return ok(`retValIdent`) let lambdaNode = newProc( name = newEmptyNode(), @@ -788,21 +747,13 @@ macro ffi*(prc: untyped): untyped = ) let registerReq = quote: - registerReqFFI(`reqTypeName`, `ctxHandlerName`: `ptrFfiCtx`): + registerReqFFI(`reqTypeName`, `ctxHandlerName`: `ptrFFICtx`): `lambdaNode` # ------------------------------------------------------------------------- - # 3. C-exported proc (async path) + # C-exported wrapper: takes (ctx, callback, userData, reqCbor, reqCborLen) # ------------------------------------------------------------------------- - 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 i in 0 ..< extraParamNames.len: - exportedParams.add( - newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring")) - ) + let exportedParams = cExportedParams(ctxType) let ffiBody = newStmtList() @@ -817,24 +768,19 @@ macro ffi*(prc: untyped): untyped = 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 i in 0 ..< extraParamNames.len: - newReqCall.add(ident(cParamName(extraParamNames[i], extraParamTypes[i]))) - - let sendCall = newCall( - newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), - ident("ctx"), - newReqCall, - ) + # Build the FFIThreadRequest payload directly from the incoming bytes. + let reqPtrIdent = genSym(nskLet, "reqPtr") + ffiBody.add quote do: + let typeStr = $`reqTypeName` + let `reqPtrIdent` = FFIThreadRequest.initFromPtr( + callback, userData, typeStr.cstring, reqCbor, int(reqCborLen) + ) let sendResIdent = genSym(nskLet, "sendRes") ffiBody.add quote do: let `sendResIdent` = try: - `sendCall` + ffi_context.sendRequestToFFIThread(ctx, `reqPtrIdent`) except Exception as exc: Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg) if `sendResIdent`.isErr(): @@ -844,7 +790,7 @@ macro ffi*(prc: untyped): untyped = return RET_OK let ffiProc = newProc( - name = exportedProcName, + name = postfix(cExportProcName, "*"), params = exportedParams, body = ffiBody, pragmas = newTree( @@ -856,195 +802,59 @@ macro ffi*(prc: untyped): untyped = ), ) - # 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 + let isPointer = isPtr(ptype) + let tn = + if isPointer: + nimTypeNameRepr(ptype[0]) + else: + nimTypeNameRepr(ptype) ffiExtraParams.add( - FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr) + FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPointer) ) - 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 + let retTypeInner = resultInner[1] + let retIsPtr = isPtr(retTypeInner) + let retTn = + if retIsPtr: + nimTypeNameRepr(retTypeInner[0]) + else: + nimTypeNameRepr(retTypeInner) ffiProcRegistry.add( FFIProcMeta( procName: cExportName, libName: currentLibName, - kind: ffiFfiKind, + kind: FFIKind.FFI, 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 - # ------------------------------------------------------------------------- + return newStmtList(helperProc, registerReq, ffiProc) - # ------------------------------------------------------------------------- - # 1. Named sync helper proc (no {.async.}) with Result[RetType, string] return - # ------------------------------------------------------------------------- - # proc MyLibVersionBody*(w: LibType): Result[RetType, string] = - # - 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 i in 0 ..< extraParamNames.len: - syncExportedParams.add( - newIdentDefs(ident(cParamName(extraParamNames[i], extraParamTypes[i])), ident("cstring")) - ) - - let syncFfiBody = newStmtList() - - syncFfiBody.add quote do: - if callback.isNil: - return RET_MISSING_CALLBACK - - let syncPoolIdent = ident($libTypeName & "FFIPool") - syncFfiBody.add quote do: - if not `syncPoolIdent`.isValidCtx(cast[pointer](ctx)): - let errStr = "ctx is not a valid FFI context" - 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 cIdent = ident(cParamName(extraParamNames[i], extraParamTypes[i])) - let paramIdent = ident(extraParamNames[i]) - let ptype = extraParamTypes[i] - syncFfiBody.add quote do: - let `paramIdent` = ffiDeserialize(`cIdent`, `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, cast[ptr cchar](errStr.cstring), cast[csize_t](errStr.len), userData - ) - return RET_ERR - let serialized = ffiSerialize(`retValOrErrIdent`.value) - callback( - RET_OK, cast[ptr cchar](serialized.cstring), cast[csize_t](serialized.len), userData - ) - return RET_OK - - let syncFfiProc = newProc( - name = exportedProcName, - params = syncExportedParams, - body = syncFfiBody, - pragmas = newTree( - nnkPragma, - ident("dynlib"), - newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)), - 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: cExportName, - libName: currentLibName, - kind: ffiFfiKind, - libTypeName: $libTypeName, - extraParams: ffiExtraParamsSync, - returnTypeName: retTnSync, - returnIsPtr: retIsPtrSync, - isAsync: false, - ) - ) - - result = newStmtList(syncHelperProc, syncFfiProc) + let stmts = asyncPath() when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts # --------------------------------------------------------------------------- # 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 Json. - ## - ## e.g. type TestlibCreateCtorReq* = object - ## configJson: cstring +proc buildCtorRequestType( + reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] +): NimNode = + ## Builds the ctor's Req object using the user's actual Nim types. var fields: seq[NimNode] = @[] - for name in paramNames: - let fieldName = ident(name & "Json") - fields.add newTree(nnkIdentDefs, fieldName, ident("cstring"), newEmptyNode()) + for i in 0 ..< paramNames.len: + let fieldName = ident(paramNames[i]) + let storedType = storageType(paramTypes[i]) + fields.add newTree(nnkIdentDefs, fieldName, storedType, newEmptyNode()) let recList = if fields.len > 0: @@ -1052,42 +862,20 @@ proc buildCtorRequestType(reqTypeName: NimNode, paramNames: seq[string]): NimNod else: newTree( nnkRecList, - newTree(nnkIdentDefs, ident("_placeholder"), ident("pointer"), newEmptyNode()), + newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()), ) let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList) let typeName = postfix(reqTypeName, "*") - result = + let typeSection = newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy)) when defined(ffiDumpMacros): - echo result.repr + echo typeSection.repr + return typeSection -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, - ## Json: cstring, ... +proc buildCtorFFINewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimNode = + ## Wraps a CBOR byte buffer into an FFIThreadRequest for the ctor request type. var formalParams = newSeq[NimNode]() @@ -1096,33 +884,20 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN 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"))) + formalParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + formalParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) 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)) + return FFIThreadRequest.initFromPtr( + callback, userData, typeStr.cstring, reqCbor, int(reqCborLen) + ) - ret[].deleteReqContent = destroyContent - return ret - - result = newProc( + let newReqProc = newProc( name = postfix(ident("ffiNewReq"), "*"), params = formalParams, body = newBody, @@ -1130,7 +905,8 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN ) when defined(ffiDumpMacros): - echo result.repr + echo newReqProc.repr + return newReqProc proc buildCtorBodyProc( helperName: NimNode, @@ -1139,11 +915,6 @@ proc buildCtorBodyProc( 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")), @@ -1153,7 +924,7 @@ proc buildCtorBodyProc( for i in 0 ..< paramNames.len: innerParams.add(newIdentDefs(ident(paramNames[i]), paramTypes[i])) - result = newProc( + let bodyProc = newProc( name = postfix(helperName, "*"), params = innerParams, body = newStmtList(userBody), @@ -1161,7 +932,8 @@ proc buildCtorBodyProc( ) when defined(ffiDumpMacros): - echo result.repr + echo bodyProc.repr + return bodyProc proc buildCtorProcessFFIRequestProc( reqTypeName: NimNode, @@ -1170,21 +942,14 @@ proc buildCtorProcessFFIRequestProc( 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)) + ## Decodes the CBOR payload, unpacks fields, runs the user body, and stores + ## the resulting library value in ctx.myLib. - # 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)) @@ -1197,27 +962,23 @@ proc buildCtorProcessFFIRequestProc( 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") + let decodedIdent = ident("decoded") - # Cast the request newBody.add quote do: - let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request) + let `reqIdent` = cast[ptr FFIThreadRequest](request) + let `decodedIdent` = cborDecodePtr( + cast[ptr UncheckedArray[byte]](`reqIdent`[].data), + `reqIdent`[].dataLen, + `reqTypeName`, + ).valueOr: + return err("CBOR decode failed for " & $T & ": " & $error) - # 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) + newBody.add unpackReqField(ident(paramNames[i]), paramTypes[i], decodedIdent) - # Call the helper proc with deserialized params let helperCallNode = newTree(nnkCall, helperName) for name in paramNames: helperCallNode.add(ident(name)) @@ -1227,17 +988,15 @@ proc buildCtorProcessFFIRequestProc( 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( + let processProc = newProc( name = postfix(ident("processFFIRequest"), "*"), params = formalParams, body = newBody, @@ -1246,18 +1005,24 @@ proc buildCtorProcessFFIRequestProc( ) when defined(ffiDumpMacros): - echo result.repr + echo processProc.repr + return processProc 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. + ## Wraps the ctor processFFIRequest result in a seq[byte] dispatcher. + ## The ctor uniquely returns the ctx address as a decimal string; we wrap + ## it as raw UTF-8 bytes so the foreign side can read it back uniformly. let ctxType = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) let returnType = nnkBracketExpr.newTree( ident("Future"), - nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")), + nnkBracketExpr.newTree( + ident("Result"), + nnkBracketExpr.newTree(ident("seq"), ident("byte")), + ident("string"), + ), ) let callExpr = newCall( @@ -1266,9 +1031,15 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode = newTree(nnkCast, ctxType, ident("reqHandler")), ) + let resIdent = genSym(nskLet, "ctorRes") var newBody = newStmtList() newBody.add quote do: - return await `callExpr` + let `resIdent` = await `callExpr` + if `resIdent`.isErr: + return err(`resIdent`.error) + # The ctor returns the ctx address as a decimal string; encode it as CBOR text + # for uniform decoding on the foreign side. + return ok(cborEncode(`resIdent`.value)) let asyncProc = newProc( name = newEmptyNode(), @@ -1282,18 +1053,19 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode = ) let key = newLit($reqTypeName) - result = + let regAssign = newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc) when defined(ffiDumpMacros): - echo result.repr + echo regAssign.repr + return regAssign 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) + ## - Have Nim-typed parameters (carried over the wire as a single CBOR blob) ## - Return Future[Result[LibType, string]] ## - NOT include ctx, callback, or userData in its signature ## @@ -1301,100 +1073,92 @@ macro ffiCtor*(prc: untyped): untyped = ## 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): pointer {.exportc, cdecl, raises: [].} + ## The generated C-exported proc has the signature: + ## proc mylib_create(reqCbor: ptr byte, reqCborLen: csize_t, + ## callback: FFICallBack, userData: pointer): pointer + ## {.exportc, cdecl, raises: [].} ## - ## Returns the context pointer synchronously; NULL on failure. - ## The callback also fires when async initialization completes, passing the ctx - ## address as a decimal string on success. The caller should hold the returned - ## pointer and pass it to subsequent .ffi. calls. + ## Returns the context pointer synchronously, NULL on failure. The callback + ## also fires when async initialization completes, passing the ctx address as + ## a decimal string on success. The caller should hold the returned 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] + let resultInner = retTypeNode[1] 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 + let libTypeName = resultInner[1] - # 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 + for j in 0 ..< p.len - 2: + rejectRawPtrType(p[^2], "`.ffiCtor.` proc " & $procName & " parameter " & $p[j]) paramNames.add($p[j]) paramTypes.add(p[^2]) - # Generate ctor request type name: CtorReq let procNameStr = $procName - # Strip trailing * if exported let cleanName = if procNameStr.endsWith("*"): procNameStr[0 ..^ 2] else: procNameStr - let cExportName = nimNameToCExport(cleanName) - let reqTypeNameStr = toCamelCase(cleanName) & "CtorReq" + let cExportName = camelToSnakeCase(cleanName) + let reqTypeNameStr = snakeToPascalCase(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 typeDef = buildCtorRequestType(reqTypeName, paramNames, paramTypes) + let ffiNewReqProc = buildCtorFFINewReqProc(reqTypeName, paramNames) + # The user-facing Nim proc keeps the user's original name with their declared + # signature; the C-exported wrapper moves to `ExportC` and + # binds the snake_case C symbol via `{.exportc.}`. + var userProcName = procName + if procName.kind == nnkPostfix: + userProcName = procName[1] + # Both the Nim-facing async ctor and the C-exported wrapper share the user's + # name as overloads; the C wrapper's `{.exportc.}` keeps the ABI symbol. + let cExportProcName = userProcName let helperProc = - buildCtorBodyProc(helperProcName, paramNames, paramTypes, libTypeName, bodyNode) + buildCtorBodyProc(userProcName, paramNames, paramTypes, libTypeName, bodyNode) let processProc = buildCtorProcessFFIRequestProc( - reqTypeName, helperProcName, paramNames, paramTypes, libTypeName + reqTypeName, userProcName, paramNames, paramTypes, libTypeName ) let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName) - # Build the C-exported proc params: - # (Json: cstring, ..., callback: FFICallBack, userData: pointer): pointer + # C-exported proc: (reqCbor, reqCborLen, callback, userData) -> pointer var exportedParams = newSeq[NimNode]() - exportedParams.add(ident("pointer")) # return type: ctx pointer or nil on failure - for name in paramNames: - exportedParams.add(newIdentDefs(ident(name & "Json"), ident("cstring"))) + exportedParams.add(ident("pointer")) + exportedParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + exportedParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) 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() - # Use a gensym'd ctx identifier so both the let binding and usage match let ctxSym = genSym(nskLet, "ctx") - - # Module-level pool shared by ctor and dtor for this libType let poolIdent = ident($libTypeName & "FFIPool") - # Create the FFIContext synchronously; return nil on failure ffiBody.add quote do: let `ctxSym` = `poolIdent`.createFFIContext().valueOr: if not callback.isNil: @@ -1402,26 +1166,27 @@ macro ffiCtor*(prc: untyped): untyped = callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) return nil - # 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(): - if not callback.isNil: - let errStr = "ffiCtor: failed to deserialize param: " & $validateRes.error - callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) - return nil + # Early validation: decode the CBOR payload to verify it parses cleanly. + ffiBody.add quote do: + block: + let validateRes = cborDecodePtr( + cast[ptr UncheckedArray[byte]](reqCbor), int(reqCborLen), `reqTypeName` + ) + if validateRes.isErr(): + if not callback.isNil: + let errStr = "ffiCtor: failed to decode request: " & $validateRes.error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return nil - # 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) + let newReqCall = newCall( + ident("ffiNewReq"), + reqTypeName, + ident("callback"), + ident("userData"), + ident("reqCbor"), + ident("reqCborLen"), + ) - # sendRequestToFFIThread using the gensym'd ctx let sendCall = newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall) @@ -1441,15 +1206,8 @@ macro ffiCtor*(prc: untyped): untyped = ffiBody.add quote do: return cast[pointer](`ctxSym`) - # 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, + name = postfix(cExportProcName, "*"), params = exportedParams, body = ffiBody, pragmas = newTree( @@ -1461,51 +1219,50 @@ macro ffiCtor*(prc: untyped): untyped = ), ) - # 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)) + let isPointer = isPtr(ptype) + let tn = + if isPointer: + nimTypeNameRepr(ptype[0]) + else: + nimTypeNameRepr(ptype) + ctorExtraParams.add( + FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPointer) + ) ffiProcRegistry.add( FFIProcMeta( procName: cExportName, libName: currentLibName, - kind: ffiCtorKind, + kind: FFIKind.CTOR, libTypeName: $libTypeName, extraParams: ctorExtraParams, returnTypeName: $libTypeName, returnIsPtr: false, - isAsync: true, ) ) - let poolDecl = quote do: + let poolDecl = quote: when not declared(`poolIdent`): var `poolIdent`: FFIContextPool[`libTypeName`] - result = newStmtList( - typeDef, deleteProc, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, - ffiProc, + let stmts = newStmtList( + typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc ) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts # --------------------------------------------------------------------------- # ffiDtor — destructor macro # --------------------------------------------------------------------------- macro ffiDtor*(prc: untyped): untyped = - ## Defines a C-exported destructor. Works like {.ffi.} but also tears down - ## the FFIContext after the body runs. + ## Defines a C-exported destructor that tears down the FFIContext after the + ## body runs. ## ## The annotated proc must have exactly one parameter of the library type. ## The body contains any library-level cleanup to run before context teardown. @@ -1515,10 +1272,12 @@ macro ffiDtor*(prc: untyped): untyped = ## w.cleanup() ## ## The generated C-exported proc has the signature: - ## cint waku_destroy(void* ctx, FfiCallback callback, void* userData) + ## int waku_destroy(void* ctx) ## ## It extracts the library value from ctx, runs the body, then calls ## destroyFFIContext to tear down the FFI thread and free the context. + ## Returns RET_OK on success, RET_ERR on failure (null/invalid ctx, or + ## destroyFFIContext failure). let procName = prc[0] let formalParams = prc[3] @@ -1527,15 +1286,23 @@ macro ffiDtor*(prc: untyped): untyped = if formalParams.len < 2: error("ffiDtor: proc must have exactly one parameter (w: LibType)") - let libParamName = formalParams[1][0] # e.g. w - let libTypeName = formalParams[1][1] # e.g. Waku + let libParamName = formalParams[1][0] + let libTypeName = formalParams[1][1] let procNameStr = block: let raw = $procName - if raw.endsWith("*"): raw[0 ..^ 2] else: raw - let cExportName = nimNameToCExport(procNameStr) - let exportedProcName = - if procName.kind == nnkPostfix: procName[1] else: procName + if raw.endsWith("*"): + raw[0 ..^ 2] + else: + raw + let cExportName = camelToSnakeCase(procNameStr) + # The dtor only needs a C-exported wrapper; rename to a synthetic Nim ident + # so it doesn't shadow the user's chosen name (consistent with .ffi. / .ffiCtor.). + # The dtor only generates a C-exported wrapper; it uses the user's name + # directly (no overload needed — there's no Nim-facing helper here). + var cExportProcName = procName + if procName.kind == nnkPostfix: + cExportProcName = procName[1] let destroyResIdent = genSym(nskLet, "destroyRes") @@ -1547,20 +1314,16 @@ macro ffiDtor*(prc: untyped): untyped = ffiBody.add quote do: if ctx.isNil or cast[ptr FFIContext[`libTypeName`]](ctx)[].myLib.isNil: - if not callback.isNil: - let errStr = "context not initialized" - callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) return RET_ERR - # Extract the library value so the user body can reference it by name ffiBody.add quote do: let `libParamName` = cast[ptr FFIContext[`libTypeName`]](ctx)[].myLib[] - # Append the user body if it is not a bare discard let isNoop = - bodyNode.kind == nnkEmpty or - (bodyNode.kind == nnkStmtList and bodyNode.len == 1 and - bodyNode[0].kind == nnkDiscardStmt) + bodyNode.kind == nnkEmpty or ( + bodyNode.kind == nnkStmtList and bodyNode.len == 1 and + bodyNode[0].kind == nnkDiscardStmt + ) if not isNoop: ffiBody.add(bodyNode) @@ -1569,24 +1332,14 @@ macro ffiDtor*(prc: untyped): untyped = let `destroyResIdent` = `poolIdent`.destroyFFIContext(cast[ptr FFIContext[`libTypeName`]](ctx)) if `destroyResIdent`.isErr(): - if not callback.isNil: - let errStr = "destroy failed: " & $`destroyResIdent`.error - callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) return RET_ERR ffiBody.add quote do: - if not callback.isNil: - callback(RET_OK, nil, 0, userData) return RET_OK let ffiProc = newProc( - name = exportedProcName, - params = @[ - ident("cint"), - newIdentDefs(ident("ctx"), ident("pointer")), - newIdentDefs(ident("callback"), ident("FFICallBack")), - newIdentDefs(ident("userData"), ident("pointer")), - ], + name = postfix(cExportProcName, "*"), + params = @[ident("cint"), newIdentDefs(ident("ctx"), ident("pointer"))], body = ffiBody, pragmas = newTree( nnkPragma, @@ -1601,26 +1354,26 @@ macro ffiDtor*(prc: untyped): untyped = FFIProcMeta( procName: cExportName, libName: currentLibName, - kind: ffiDtorKind, + kind: FFIKind.DTOR, libTypeName: $libTypeName, extraParams: @[], returnTypeName: "", returnIsPtr: false, - isAsync: false, ) ) - let poolDecl = quote do: + let poolDecl = quote: when not declared(`poolIdent`): var `poolIdent`: FFIContextPool[`libTypeName`] - result = newStmtList(poolDecl, ffiProc) + let stmts = newStmtList(poolDecl, ffiProc) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts # --------------------------------------------------------------------------- -# genBindings — Rust crate generator +# genBindings — codegen entry point # --------------------------------------------------------------------------- macro genBindings*( @@ -1628,11 +1381,13 @@ macro genBindings*( nimSrcRelPath: static[string] = ffiNimSrcRelPath, ): untyped = ## Emits C++ or Rust binding files from the compile-time FFI registries. + ## The foreign-side wrapper encodes one CBOR buffer per request. ## - ## PLACEMENT REQUIREMENT: genBindings() must be called AFTER every {.ffi.} - ## and {.ffiCtor.} annotation in the compilation unit. Each pragma populates - ## ffiProcRegistry and ffiTypeRegistry as the compiler expands the AST; - ## calling genBindings() earlier produces incomplete bindings. + ## PLACEMENT REQUIREMENT: genBindings() must be called AFTER every {.ffi.}, + ## {.ffiCtor.} and {.ffiDtor.} annotation in the compilation unit. Each + ## pragma populates ffiProcRegistry / ffiTypeRegistry as the compiler + ## expands the AST; calling genBindings() earlier produces incomplete + ## bindings. ## ## In a single-file library, place it at the bottom of the file. ## In a multi-file library, import all sub-modules first and call @@ -1643,21 +1398,19 @@ macro genBindings*( ## -d:ffiNimSrcRelPath, or can be passed as explicit arguments. ## This macro is a no-op unless -d:ffiGenBindings is set. ## - ## This reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags. - ## ## 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 + ## # -d:ffiOutputDir=examples/timer/rust_bindings \ + ## # -d:ffiNimSrcRelPath=../timer.nim mylib.nim when defined(ffiGenBindings): if outputDir.len == 0: error( "genBindings: output directory is empty." & - " Pass it as an argument or set -d:ffiOutputDir=path/to/output" + " Pass it as an argument or set -d:ffiOutputDir=path/to/output" ) - let lang = targetLang.toLowerAscii() + let lang = string_helpers.toLower(targetLang) let libName = deriveLibName(ffiProcRegistry) case lang of "rust": @@ -1671,4 +1424,4 @@ macro genBindings*( else: error("genBindings: unknown targetLang '" & lang & "'. Use 'rust' or 'cpp'.") - result = newEmptyNode() + return newEmptyNode() diff --git a/ffi/serial.nim b/ffi/serial.nim deleted file mode 100644 index 8876431..0000000 --- a/ffi/serial.nim +++ /dev/null @@ -1,121 +0,0 @@ -import std/[json, options] -import results -import ./codegen/meta - -proc ffiSerialize*(x: string): string = - x - -proc ffiSerialize*(x: cstring): string = - if x.isNil: "" else: $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] = - ok($s) - -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 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 ffiSerialize*[T: object](x: T): string = - $(%*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) - -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) - -proc ffiDeserialize*[T: object](s: cstring, _: typedesc[T]): Result[T, string] = - try: - ok(parseJson($s).to(T)) - except Exception as e: - err(e.msg) diff --git a/tests/test_ctx_validation.nim b/tests/test_ctx_validation.nim index 5b4c151..f14f71a 100644 --- a/tests/test_ctx_validation.nim +++ b/tests/test_ctx_validation.nim @@ -42,7 +42,7 @@ suite "ctx pointer validation at the FFI entry point": var s: CallbackState initCbState(s) let nilCtx: ptr FFIContext[TestLib] = nil - let ret = ctxval_ping(nilCtx, validationCallback, addr s) + let ret = ctxval_ping(nilCtx, validationCallback, addr s, nil, 0.csize_t) check ret == RET_ERR check s.called.load() check s.retCode == RET_ERR @@ -51,7 +51,7 @@ suite "ctx pointer validation at the FFI entry point": var s: CallbackState initCbState(s) let invalidCtx = cast[ptr FFIContext[TestLib]](123) - let ret = ctxval_ping(invalidCtx, validationCallback, addr s) + let ret = ctxval_ping(invalidCtx, validationCallback, addr s, nil, 0.csize_t) check ret == RET_ERR check s.called.load() check s.retCode == RET_ERR diff --git a/tests/test_ffi_context.nim b/tests/test_ffi_context.nim index 72b39fc..9a68fbe 100644 --- a/tests/test_ffi_context.nim +++ b/tests/test_ffi_context.nim @@ -1,4 +1,4 @@ -import std/[locks, strutils, os] +import std/[locks, options, strutils, os, atomics] import unittest2 import results import ../ffi @@ -12,7 +12,7 @@ type CallbackData = object cond: Cond called: bool retCode: cint - msg: array[512, char] + msg: array[1024, byte] msgLen: int proc initCallbackData(d: var CallbackData) = @@ -29,10 +29,9 @@ proc testCallback( let d = cast[ptr CallbackData](userData) acquire(d[].lock) d[].retCode = retCode - let n = min(int(len), d[].msg.high) + let n = min(int(len), d[].msg.len) if n > 0 and not msg.isNil: copyMem(addr d[].msg[0], msg, n) - d[].msg[n] = '\0' d[].msgLen = n d[].called = true signal(d[].cond) @@ -44,10 +43,18 @@ proc waitCallback(d: var CallbackData) = wait(d.cond, d.lock) release(d.lock) -proc callbackMsg(d: var CallbackData): string = - result = newString(d.msgLen) +proc callbackBytes(d: var CallbackData): seq[byte] = + var bytes = newSeq[byte](d.msgLen) if d.msgLen > 0: - copyMem(addr result[0], addr d.msg[0], d.msgLen) + copyMem(addr bytes[0], addr d.msg[0], d.msgLen) + return bytes + +proc callbackErr(d: var CallbackData): string = + ## Reads the error payload (sent as raw UTF-8 bytes on RET_ERR). + var msg = newString(d.msgLen) + if d.msgLen > 0: + copyMem(addr msg[0], addr d.msg[0], d.msgLen) + return msg registerReqFFI(PingRequest, lib: ptr TestLib): proc(message: cstring): Future[Result[string, string]] {.async.} = @@ -66,35 +73,19 @@ registerReqFFI(SlowRequest, lib: ptr TestLib): await sleepAsync(500.milliseconds) return ok("slow-done") -# Coordination channel: the FFI handler signals the test thread the instant -# it is about to block the event loop, so the test can call destroyFFIContext -# while the event loop is truly frozen. var gSyncBlockStarted: Channel[bool] gSyncBlockStarted.open() registerReqFFI(SyncBlockingRequest, lib: ptr TestLib): proc(): Future[Result[string, string]] {.async.} = - # Yield first so that reqReceivedSignal fires and sendRequestToFFIThread - # returns on the calling thread before we start the synchronous block. await sleepAsync(0.milliseconds) - # Signal the test thread: the event loop is about to be frozen. - # Channel.send is annotated as raising under refc, so wrap. try: gSyncBlockStarted.send(true) except Exception as exc: return err("gSyncBlockStarted.send raised: " & exc.msg) - # Simulates a request that blocks the event-loop thread synchronously - # (e.g. w.stop() -> switch.stop() -> connManager.close() with blocking I/O). - # Unlike sleepAsync, os.sleep holds the OS thread and prevents Chronos from - # processing any callbacks -- including the reqSignal fired by destroyFFIContext. os.sleep(5_000) return ok("sync-blocking-done") -# Approximates the heavy ref-object workload that libwaku/libp2p performs on -# the FFI thread. The exact cell count is large enough to force several refc -# GC cycles; under refc this stresses the heap state that, when later combined -# with a chronos Selector allocation on the main thread (via close()), used to -# trip the rawNewObj → signal-handler infinite recursion. type RefCell = ref object next: RefCell payload: array[64, byte] @@ -107,16 +98,11 @@ registerReqFFI(HeavyRefAllocRequest, lib: ptr TestLib): head = n if i mod 1000 == 0: await sleepAsync(0.milliseconds) - # Break the chain iteratively before releasing head. - # ORC's =destroy for RefCell recurses through .next, so a 50k-node chain - # would produce ~50k nested =destroy calls and overflow the stack. - # Walking the list and unlinking each node first keeps destruction O(n) - # iterative instead of O(n) recursive. var node = head head = nil while not node.isNil(): let nxt = node.next - node.next = nil # unlink before the refcount of `node` can drop to zero + node.next = nil node = nxt await sleepAsync(10.milliseconds) return ok("heavy-done") @@ -135,12 +121,11 @@ suite "FFIContextPool": assert false, "createFFIContext(pool) failed: " & $error return check pool.destroyFFIContext(ctx1).isOk() - # After destroying, the same slot must be available again let ctx2 = pool.createFFIContext().valueOr: assert false, "createFFIContext(pool) failed after slot release: " & $error return check pool.destroyFFIContext(ctx2).isOk() - check ctx1 == ctx2 # same array slot reused + check ctx1 == ctx2 test "pool exhaustion returns error": var pool: FFIContextPool[TestLib] @@ -151,7 +136,6 @@ suite "FFIContextPool": discard pool.destroyFFIContext(ctxs[j]) assert false, "createFFIContext(pool) failed at slot " & $i & ": " & $error return - # Pool is now full — next create must fail check pool.createFFIContext().isErr() for i in 0 ..< MaxFFIContexts: discard pool.destroyFFIContext(ctxs[i]) @@ -175,7 +159,7 @@ suite "FFIContextPool": .isOk() waitCallback(d) check d.retCode == RET_OK - check callbackMsg(d) == "pong:pool" + check cborDecode(callbackBytes(d), string).value == "pong:pool" suite "createFFIContext / destroyFFIContext": test "create and destroy succeeds": @@ -195,10 +179,6 @@ suite "createFFIContext / destroyFFIContext": suite "destroyFFIContext does not hang": test "destroy while a slow async request is still in-flight": - ## Reproduces the race where destroyFFIContext was called while a long- - ## running async request (e.g. stop_node / w.stop()) was still executing. - ## The destroy must return well within 2 seconds; before the fix it would - ## block forever on joinThread(ffiThread). var pool: FFIContextPool[TestLib] let ctx = pool.createFFIContext().valueOr: check false @@ -206,106 +186,40 @@ suite "destroyFFIContext does not hang": var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) - # sendRequestToFFIThread returns as soon as the FFI thread ACKs receipt; - # the 500 ms work continues asynchronously on the FFI thread. - check sendRequestToFFIThread( - ctx, SlowRequest.ffiNewReq(testCallback, addr d) - ).isOk() + check sendRequestToFFIThread(ctx, SlowRequest.ffiNewReq(testCallback, addr d)).isOk() - # Destroy immediately while SlowRequest is still running. let t0 = Moment.now() check pool.destroyFFIContext(ctx).isOk() check (Moment.now() - t0) < 2.seconds suite "destroyFFIContext does not hang when event loop is blocked": test "destroy while sync-blocking request is in-flight": - ## Reproduces the hang seen in logosdelivery_example.c: - ## logosdelivery_stop_node(...) -- triggers w.stop() on the FFI thread - ## sleep(1) - ## logosdelivery_destroy(...) -- hangs forever - ## - ## Root cause: w.stop() (and similar tear-down calls) can execute a - ## synchronous blocking section that holds the OS thread, preventing - ## the Chronos event loop from processing the reqSignal fired by - ## destroyFFIContext. The result is joinThread(ffiThread) never returns. - ## - ## With the fix, destroyFFIContext must complete well within the 5 s that - ## SyncBlockingRequest holds the event loop. var pool: FFIContextPool[TestLib] let ctx = pool.createFFIContext().valueOr: check false return - # CallbackData and ctx are kept alive past destroyFFIContext: the leaked - # FFI thread is still inside os.sleep(5_000) and will eventually wake, - # run handleRes, fire testCallback, and exit normally. We wait for that - # to happen at the end of the test so the leaked thread cannot race with - # subsequent tests' createFFIContext on Linux/Windows. Heap allocation - # ensures the late callback's userData is still valid when it fires. let d = createShared(CallbackData) initCallbackData(d[]) - check sendRequestToFFIThread( - ctx, SyncBlockingRequest.ffiNewReq(testCallback, d) - ).isOk() + check sendRequestToFFIThread(ctx, SyncBlockingRequest.ffiNewReq(testCallback, d)) + .isOk() - # Block until the FFI handler has signalled that os.sleep is about to start. - # This guarantees destroyFFIContext is called while the event loop is frozen. discard gSyncBlockStarted.recv() - # Destroy must return promptly even though the event loop is frozen for 5s. - # It deliberately returns err and leaks ctx in this scenario rather than - # hanging on joinThread. let t0 = Moment.now() check pool.destroyFFIContext(ctx).isErr() check (Moment.now() - t0) < 3.seconds - # Drain the leaked thread before the test scope ends. - # 1. waitCallback blocks until os.sleep(5_000) returns and handleRes - # invokes testCallback (~3.5s after destroy returned), which proves - # the leaked thread has reached the end of processRequest. - # 2. Yield briefly so the thread can finish iterating its while loop, - # fire threadExitSignal in its defer, and return. Without this, on - # Linux/Windows the still-live thread can race with the next test's - # createFFIContext under --mm:orc and segfault. - # ctx.cleanUpResources is intentionally NOT called: destroyFFIContext - # skipped it for a reason, and the signal fds are reclaimed by the OS - # at process exit. waitCallback(d[]) os.sleep(200) deinitCallbackData(d[]) freeShared(d) suite "destroyFFIContext refc workaround": - ## Documents the refc-specific workaround in cleanUpResources. - ## - ## Background: when the FFI thread does heavy ref-object work (the workload - ## that triggered the libwaku hang in production), the refc GC heap reaches - ## a state where the very first chronos Selector allocation on the *main* - ## thread — which happens lazily inside ThreadSignalPtr.close() through - ## getThreadDispatcher() — traps in rawNewObj. The refc signal handler - ## itself re-enters the same allocator and the process never returns. - ## Captured stack: - ## close → safeUnregisterAndCloseFd → getThreadDispatcher → - ## newDispatcher → Selector.new → newObj (gc.nim:488) → rawNewObj → - ## _sigtramp → signalHandler → newObjNoInit → addNewObjToZCT (loop) - ## - ## The workaround in cleanUpResources is `when defined(gcRefc): discard`, - ## i.e. skip the close() calls under refc only. orc is unaffected and - ## still cleans up the signal fds normally. - ## - ## NOTE: this test is documentation more than regression: a synthetic - ## ref-allocation workload of ~50k cells does NOT corrupt the refc heap - ## the way the real libwaku/libp2p teardown does, so this test passes - ## even when the workaround is disabled. Reproducing the actual hang - ## requires the full libwaku workload (logosdelivery_example.c). - ## Verification of the workaround was done end-to-end against that - ## example: with `--mm:refc` and close() enabled it hangs forever in - ## the captured stack above; with `when defined(gcRefc): discard` it - ## returns immediately. Under `--mm:orc` it returns immediately either - ## way. test "destroy after heavy ref-allocation workload returns promptly": var pool: FFIContextPool[TestLib] let ctx = pool.createFFIContext().valueOr: @@ -314,11 +228,13 @@ suite "destroyFFIContext refc workaround": var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) check sendRequestToFFIThread( ctx, HeavyRefAllocRequest.ffiNewReq(testCallback, addr d) - ).isOk() + ) + .isOk() waitCallback(d) check d.retCode == RET_OK @@ -346,7 +262,7 @@ suite "sendRequestToFFIThread": .isOk() waitCallback(d) check d.retCode == RET_OK - check callbackMsg(d) == "pong:hello" + check cborDecode(callbackBytes(d), string).value == "pong:hello" test "failing request triggers RET_ERR callback": var d: CallbackData @@ -364,6 +280,8 @@ suite "sendRequestToFFIThread": check sendRequestToFFIThread(ctx, FailRequest.ffiNewReq(testCallback, addr d)).isOk() waitCallback(d) check d.retCode == RET_ERR + # Errors are raw UTF-8 — not CBOR. + check callbackErr(d) == "intentional failure" test "empty ok response delivers empty message": var d: CallbackData @@ -382,7 +300,8 @@ suite "sendRequestToFFIThread": .isOk() waitCallback(d) check d.retCode == RET_OK - check d.msgLen == 0 + # CBOR-encoded "" is a single byte (text string of length 0): 0x60 + check cborDecode(callbackBytes(d), string).value == "" test "sequential requests are all processed": var pool: FFIContextPool[TestLib] @@ -403,10 +322,10 @@ suite "sendRequestToFFIThread": waitCallback(d) deinitCallbackData(d) check d.retCode == RET_OK - check callbackMsg(d) == "pong:" & msg + check cborDecode(callbackBytes(d), string).value == "pong:" & msg # --------------------------------------------------------------------------- -# ffiCtor macro integration test +# ffiCtor / .ffi. macros — exercise the full CBOR transport # --------------------------------------------------------------------------- type SimpleLib = object @@ -420,30 +339,37 @@ proc testlib_create*( ): Future[Result[SimpleLib, string]] {.ffiCtor.} = return ok(SimpleLib(value: config.initialValue)) +proc encodedPtr(bytes: var seq[byte]): ptr byte = + if bytes.len == 0: + nil + else: + cast[ptr byte](addr bytes[0]) + +proc ctorAddrFromCbor(bytes: seq[byte]): uint = + ## The ctor success payload is a CBOR text string of the decimal address. + let addrStr = cborDecode(bytes, string).valueOr: + return 0 + cast[uint](parseBiggestUInt(addrStr)) + suite "ffiCtor macro": test "creates context and returns pointer via callback": var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) - let configJson = ffiSerialize(SimpleConfig(initialValue: 42)) - let ret = testlib_create(configJson.cstring, testCallback, addr d) + var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 42))) + let ret = testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr d) check not ret.isNil() 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)) + let ctxAddr = ctorAddrFromCbor(callbackBytes(d)) 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 @@ -463,185 +389,238 @@ proc testlib_send*( 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) + defer: + deinitCallbackData(ctorD) - let configJson = ffiSerialize(SimpleConfig(initialValue: 7)) - let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD) + var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 7))) + let ctorRet = + testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD) check not ctorRet.isNil() waitCallback(ctorD) check ctorD.retCode == RET_OK - let addrStr = callbackMsg(ctorD) - check addrStr.len > 0 - - let ctxAddr = cast[uint](parseBiggestUInt(addrStr)) + let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD)) check ctxAddr != 0 let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr) - defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() + defer: + check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() - # Now call the .ffi. proc var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) - let cfgJson = ffiSerialize(SendConfig(message: "hello")) - let ret = testlib_send(ctx, testCallback, addr d, cfgJson.cstring) + # The .ffi. macro packs all extra params into one CBOR Req struct. + var reqBytes = cborEncode(TestlibSendReq(cfg: SendConfig(message: "hello"))) + let ret = testlib_send( + ctx, testCallback, addr d, encodedPtr(reqBytes), reqBytes.len.csize_t + ) 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" + check cborDecode(callbackBytes(d), string).value == "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.} = +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 +suite "sync-body .ffi. is dispatched on FFI thread": + ## Before PR #23 (items 1–5), a `.ffi.` body without `await` was emitted as + ## an inline-on-foreign-thread fast path. That was deleted; all `.ffi.` + ## procs now go through the FFI thread. This test asserts the round-trip + ## still produces the expected payload via the callback. + test "sync body still produces correct payload via callback": var ctorD: CallbackData initCallbackData(ctorD) - defer: deinitCallbackData(ctorD) + defer: + deinitCallbackData(ctorD) - let configJson = ffiSerialize(SimpleConfig(initialValue: 3)) - let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD) + var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 3))) + let ctorRet = + testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD) check not ctorRet.isNil() waitCallback(ctorD) check ctorD.retCode == RET_OK - let addrStr = callbackMsg(ctorD) - check addrStr.len > 0 - - let ctxAddr = cast[uint](parseBiggestUInt(addrStr)) + let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD)) check ctxAddr != 0 let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr) - defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() + defer: + check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() var d2: CallbackData initCallbackData(d2) - defer: deinitCallbackData(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 + # No-extra-param .ffi. proc; pack an empty Req. + var emptyBytes = cborEncode(TestlibVersionReq()) + let ret = testlib_version( + ctx, testCallback, addr d2, encodedPtr(emptyBytes), emptyBytes.len.csize_t + ) check ret == RET_OK - check d2.called # fires synchronously — no waitCallback needed + waitCallback(d2) # always asynchronous now check d2.retCode == RET_OK - let receivedMsg = callbackMsg(d2) - let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr: - check false - "" - check decoded == "v3" + check cborDecode(callbackBytes(d2), string).value == "v3" # --------------------------------------------------------------------------- -# ptr T return type in .ffi. macro integration test +# Nim-native API (no callbacks, no CBOR buffers): the original proc name +# resolves to the user's declared async signature and is callable directly. # --------------------------------------------------------------------------- -type Handle = object - data: string +suite "Nim-native .ffi. / .ffiCtor. API": + test "user proc names retain their declared Future[Result[T,string]] shape": + let lib = SimpleLib(value: 9) + # Async {.ffi.} proc — call directly without ctx/callback dance. + let echoed = waitFor testlib_send(lib, SendConfig(message: "direct")) + check echoed.isOk + check echoed.value == "echo:direct:9" -type NameParam {.ffi.} = object - name: string + # Sync {.ffi.} body — still typed as Future[Result[T,string]] per the + # user's source-level declaration (b): completed-future wrapper. + let v = waitFor testlib_version(lib) + check v.isOk + check v.value == "v9" -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) + # The ctor body is similarly callable from Nim with its declared signature. + let ctorRes = waitFor testlib_create(SimpleConfig(initialValue: 21)) + check ctorRes.isOk + check ctorRes.value.value == 21 -proc testlib_read_handle*( - lib: SimpleLib, handle: pointer -): Future[Result[string, string]] {.ffi.} = - let h = cast[ptr Handle](handle) - return ok(h[].data) +# --------------------------------------------------------------------------- +# Regression for PR #23 review items 1–5: a `.ffi.` body without `await` +# used to be emitted as an inline-on-foreign-thread fast path, which bypassed +# `foreignThreadGc`, `ctx.lock`, and chronos's single-thread invariant. The +# sync fast-path was deleted; this test records `getThreadId()` inside a +# sync body and asserts the handler runs on the FFI thread, not on the +# caller's thread. +# --------------------------------------------------------------------------- -proc testlib_free_handle*( - lib: SimpleLib, handle: pointer -): Future[Result[string, string]] {.ffi.} = - let h = cast[ptr Handle](handle) - deallocShared(h) - return ok("freed") +var gRecordedHandlerTid: Atomic[int] -suite "ptr return type in .ffi.": - test "returns a heap-allocated handle and reads it back": - # Create context via ffiCtor +type RecordTidReq {.ffi.} = object + dummy: int + +proc testlib_record_tid*( + lib: SimpleLib, req: RecordTidReq +): Future[Result[int, string]] {.ffi.} = + ## Sync body — used to live on the inline fast-path; must now run on the + ## FFI thread. + let tid = getThreadId() + gRecordedHandlerTid.store(tid) + return ok(tid) + +suite "sync-body .ffi. runs on FFI thread (PR #23 regression)": + test "handler thread id differs from caller's": var ctorD: CallbackData initCallbackData(ctorD) - defer: deinitCallbackData(ctorD) + defer: + deinitCallbackData(ctorD) - let configJson = ffiSerialize(SimpleConfig(initialValue: 5)) - let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD) + var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 0))) + let ctorRet = + testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD) check not ctorRet.isNil() - waitCallback(ctorD) check ctorD.retCode == RET_OK - - let ctxAddrStr = callbackMsg(ctorD) - check ctxAddrStr.len > 0 - let ctxAddr = cast[uint](parseBiggestUInt(ctxAddrStr)) + let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD)) check ctxAddr != 0 let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr) - defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() + defer: + check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() - # Alloc a handle - var allocD: CallbackData - initCallbackData(allocD) - defer: deinitCallbackData(allocD) + gRecordedHandlerTid.store(0) + let callerTid = getThreadId() - let npJson = ffiSerialize(NameParam(name: "test")) - let allocRet = testlib_alloc_handle(ctx, testCallback, addr allocD, npJson.cstring) - check allocRet == RET_OK + var d: CallbackData + initCallbackData(d) + defer: + deinitCallbackData(d) - waitCallback(allocD) - check allocD.retCode == RET_OK + var reqBytes = cborEncode(TestlibRecordTidReq(req: RecordTidReq(dummy: 1))) + let ret = testlib_record_tid( + ctx, testCallback, addr d, encodedPtr(reqBytes), reqBytes.len.csize_t + ) + check ret == RET_OK + waitCallback(d) + check d.retCode == RET_OK - let handleAddrStr = callbackMsg(allocD) - check handleAddrStr.len > 0 - let handleAddr = parseBiggestUInt(handleAddrStr) - check handleAddr != 0 + let handlerTid = gRecordedHandlerTid.load() + check handlerTid != 0 + # The whole point of the fix: even a sync-body handler is dispatched off + # the caller thread. If this fails the inline fast-path is back. + check handlerTid != callerTid + # And the callback payload (the recorded tid) matches what the handler stored. + check cborDecode(callbackBytes(d), int).value == handlerTid - # Read the handle back - var readD: CallbackData - initCallbackData(readD) - defer: deinitCallbackData(readD) +# --------------------------------------------------------------------------- +# Regression for PR #23 review item 6: reentrancy guard on +# sendRequestToFFIThread. A handler running on the FFI thread that tries to +# dispatch back through sendRequestToFFIThread used to self-deadlock waiting +# on `reqReceivedSignal` (which only the FFI thread can fire). The guard now +# returns an Err immediately. +# --------------------------------------------------------------------------- - let handleJson = ffiSerialize(cast[pointer](handleAddr)) - let readRet = testlib_read_handle(ctx, testCallback, addr readD, handleJson.cstring) - check readRet == RET_OK +var gReentrantNestedRes: Channel[string] +gReentrantNestedRes.open() - waitCallback(readD) - check readD.retCode == RET_OK +# Handler runs on the FFI thread; it nests a send back into the same ctx and +# reports the outcome through gReentrantNestedRes. Carrying the ctx address +# via the request payload sidesteps the cross-thread visibility issue of +# thread-local pointers. +registerReqFFI(ReentrantTriggerReq, lib: ptr TestLib): + proc(ctxAddr: int): Future[Result[string, string]] {.async.} = + let ctx = cast[ptr FFIContext[TestLib]](cast[uint](ctxAddr)) + var nestedD: CallbackData + initCallbackData(nestedD) + defer: + deinitCallbackData(nestedD) + let res = sendRequestToFFIThread( + ctx, PingRequest.ffiNewReq(testCallback, addr nestedD, "x".cstring) + ) + if res.isErr(): + try: + gReentrantNestedRes.send("err:" & res.error) + except Exception as exc: + return err("channel.send raised: " & exc.msg) + return ok("guard-fired") + try: + gReentrantNestedRes.send("ok-unexpected") + except Exception as exc: + return err("channel.send raised: " & exc.msg) + return ok("ok-unexpected") - let readMsg = callbackMsg(readD) - let decodedStr = ffiDeserialize(readMsg.cstring, string).valueOr: +suite "reentrancy guard (PR #23 review, item 6)": + test "send from inside an FFI handler returns Err instead of deadlocking": + var pool: FFIContextPool[TestLib] + let ctx = pool.createFFIContext().valueOr: check false - "" - check decodedStr == "test:5" + return + defer: + discard pool.destroyFFIContext(ctx) - # Free the handle - var freeD: CallbackData - initCallbackData(freeD) - defer: deinitCallbackData(freeD) + var d: CallbackData + initCallbackData(d) + defer: + deinitCallbackData(d) - let freeRet = testlib_free_handle(ctx, testCallback, addr freeD, handleJson.cstring) - check freeRet == RET_OK + let ctxAddrInt = cast[int](cast[uint](ctx)) + check sendRequestToFFIThread( + ctx, ReentrantTriggerReq.ffiNewReq(testCallback, addr d, ctxAddrInt) + ) + .isOk() - waitCallback(freeD) - check freeD.retCode == RET_OK + # The outer callback only fires once the handler — including its nested + # send attempt — has finished. No polling/sleep needed. + waitCallback(d) + check d.retCode == RET_OK + check cborDecode(callbackBytes(d), string).value == "guard-fired" + + let nestedMsg = gReentrantNestedRes.recv() + check nestedMsg.startsWith("err:") + check "reentrant ffi call" in nestedMsg diff --git a/tests/test_gc_compat.nim b/tests/test_gc_compat.nim index c7cdf7a..970e11b 100644 --- a/tests/test_gc_compat.nim +++ b/tests/test_gc_compat.nim @@ -50,9 +50,27 @@ proc waitCallback(d: var CallbackData) = release(d.lock) proc callbackMsg(d: var CallbackData): string = - result = newString(d.msgLen) + var msg = newString(d.msgLen) if d.msgLen > 0: - copyMem(addr result[0], addr d.msg[0], d.msgLen) + copyMem(addr msg[0], addr d.msg[0], d.msgLen) + return msg + +proc callbackBytes(d: var CallbackData): seq[byte] = + var bytes = newSeq[byte](d.msgLen) + if d.msgLen > 0: + copyMem(addr bytes[0], addr d.msg[0], d.msgLen) + return bytes + +proc callbackOkString(d: var CallbackData): string = + ## Decodes the CBOR success payload as a string. Asserts the request + ## actually succeeded — silently treating an error payload as the empty + ## string would let a regression slip past the test that calls us. + doAssert d.retCode == RET_OK, + "callbackOkString called on non-OK retCode " & $d.retCode & " (msg=" & callbackMsg( + d + ) & ")" + cborDecode(callbackBytes(d), string).valueOr: + return "" # Concatenates GC-allocated strings so the result is not a string literal; # exercises the resStr lifetime binding inside handleRes. @@ -93,60 +111,75 @@ suite "GC safety - string lifetime across thread boundary": test "ok string result remains valid when callback fires": var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) var pool: FFIContextPool[GcTestLib] let ctx = pool.createFFIContext().valueOr: checkpoint "createFFIContext failed: " & $error check false return - defer: discard pool.destroyFFIContext(ctx) + defer: + discard pool.destroyFFIContext(ctx) check sendRequestToFFIThread( ctx, StringLifetimeRequest.ffiNewReq(testCallback, addr d, "hello".cstring) - ).isOk() + ) + .isOk() waitCallback(d) check d.retCode == RET_OK - check callbackMsg(d) == "lifetime:hello" + check callbackOkString(d) == "lifetime:hello" test "error string lifetime across thread boundary": var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) var pool: FFIContextPool[GcTestLib] let ctx = pool.createFFIContext().valueOr: check false return - defer: discard pool.destroyFFIContext(ctx) + defer: + discard pool.destroyFFIContext(ctx) check sendRequestToFFIThread( ctx, GcErrRequest.ffiNewReq(testCallback, addr d, "test".cstring) - ).isOk() + ) + .isOk() waitCallback(d) check d.retCode == RET_ERR + # Error payloads are raw UTF-8, not CBOR. check callbackMsg(d) == "gc-err:test" test "large string result is delivered without corruption": + # Round-trip check: build the same 512-char string the FFI handler is + # specified to produce, run the request through the FFI thread (which + # CBOR-encodes the result), decode the callback payload, and assert + # the decoded string is byte-for-byte identical to the original. + var expected = newString(512) + for i in 0 ..< 512: + expected[i] = char(ord('a') + (i mod 26)) + var d: CallbackData initCallbackData(d) - defer: deinitCallbackData(d) + defer: + deinitCallbackData(d) var pool: FFIContextPool[GcTestLib] let ctx = pool.createFFIContext().valueOr: check false return - defer: discard pool.destroyFFIContext(ctx) + defer: + discard pool.destroyFFIContext(ctx) check sendRequestToFFIThread( ctx, LargeStringRequest.ffiNewReq(testCallback, addr d) - ).isOk() + ) + .isOk() waitCallback(d) check d.retCode == RET_OK - check d.msgLen == 512 - check d.msg[0] == 'a' - check d.msg[25] == 'z' - check d.msg[26] == 'a' + check callbackOkString(d) == expected suite "GC stability - repeated requests": test "20 sequential requests without GC corruption": @@ -154,7 +187,8 @@ suite "GC stability - repeated requests": let ctx = pool.createFFIContext().valueOr: check false return - defer: discard pool.destroyFFIContext(ctx) + defer: + discard pool.destroyFFIContext(ctx) for i in 1 .. 20: var d: CallbackData @@ -162,8 +196,9 @@ suite "GC stability - repeated requests": let input = "iter" & $i check sendRequestToFFIThread( ctx, StringLifetimeRequest.ffiNewReq(testCallback, addr d, input.cstring) - ).isOk() + ) + .isOk() waitCallback(d) - deinitCallbackData(d) check d.retCode == RET_OK - check callbackMsg(d) == "lifetime:" & input + check callbackOkString(d) == "lifetime:" & input + deinitCallbackData(d) diff --git a/tests/test_meta.nim b/tests/test_meta.nim new file mode 100644 index 0000000..5438922 --- /dev/null +++ b/tests/test_meta.nim @@ -0,0 +1,56 @@ +## Unit tests for the AST helpers used by the FFI macro. +## The identifier-casing helpers used to live here too; they now have their +## own module and test file (`test_string_helpers.nim`). + +import unittest +import std/[macros, strutils] +import ../ffi/internal/ffi_macro + +suite "unpackReqField": + ## `unpackReqField` builds AST via `std/macros` helpers (`ident`, `newDotExpr`, + ## `newLetStmt`, etc.) which are compile-time magics. The tests therefore run + ## as `static:` blocks — a failed `doAssert` becomes a compile-time error, so + ## a broken helper aborts the build before the test binary is produced. + ## Whitespace in AST repr is normalised so the assertions are layout-stable. + proc normalise(s: string): string {.compileTime.} = + var buf = "" + var prevSpace = true + for c in s: + if c in {' ', '\t', '\n', '\r'}: + if not prevSpace: + buf.add(' ') + prevSpace = true + else: + buf.add(c) + prevSpace = false + return buf.strip() + + test "non-cstring field unpacks as plain assignment": + static: + let node = unpackReqField(ident("count"), ident("int"), ident("decoded")) + doAssert normalise(node.repr) == "let count = decoded.count" + + test "cstring field unpacks with .cstring cast": + static: + let node = unpackReqField(ident("message"), ident("cstring"), ident("decoded")) + doAssert normalise(node.repr) == "let message: cstring = decoded.message.cstring" + + test "non-cstring (string) does NOT add the .cstring cast": + static: + let node = unpackReqField(ident("name"), ident("string"), ident("decoded")) + let r = normalise(node.repr) + doAssert r == "let name = decoded.name" + doAssert ".cstring" notin r + + test "non-cstring complex type passes through unchanged": + # Generic / bracket / dot expressions are not nnkIdent, so the cstring + # branch must not fire even if the type's textual repr contains "cstring". + static: + let userType = nnkBracketExpr.newTree(ident("seq"), ident("int")) + let node = unpackReqField(ident("xs"), userType, ident("decoded")) + doAssert normalise(node.repr) == "let xs = decoded.xs" + + test "decoded identifier is used verbatim": + static: + let node = unpackReqField(ident("delayMs"), ident("int"), ident("myDecodedReq")) + doAssert normalise(node.repr) == "let delayMs = myDecodedReq.delayMs" diff --git a/tests/test_nim_native_api.nim b/tests/test_nim_native_api.nim new file mode 100644 index 0000000..184a55f --- /dev/null +++ b/tests/test_nim_native_api.nim @@ -0,0 +1,179 @@ +## Demonstrates the Nim-native side of the {.ffi.} / {.ffiCtor.} macros: +## every annotated proc remains callable from Nim with its declared signature +## (`Future[Result[T, string]]`), no callbacks or CBOR buffers involved. The +## C-exported wrapper exists in parallel as an overload distinguishable by +## arity — see `test_ffi_context.nim` for the C-shape callers. + +import std/options +import unittest2 +import results +import ../ffi + +type Counter = object + start: int + +type CounterConfig {.ffi.} = object + initial: int + +type IncRequest {.ffi.} = object + by: int + +type CounterState {.ffi.} = object + value: int + +proc counter_create*(cfg: CounterConfig): Future[Result[Counter, string]] {.ffiCtor.} = + ## Async ctor body — exercises the chronos path on the FFI thread. + await sleepAsync(1.milliseconds) + return ok(Counter(start: cfg.initial)) + +proc counter_value*(c: Counter): Future[Result[CounterState, string]] {.ffi.} = + ## Sync body (no `await`); the Nim-facing wrapper still returns + ## Future[Result[...]] so the source-level shape is preserved. + return ok(CounterState(value: c.start)) + +proc counter_add*( + c: Counter, req: IncRequest +): Future[Result[CounterState, string]] {.ffi.} = + ## Async body with a real chronos yield. + await sleepAsync(1.milliseconds) + return ok(CounterState(value: c.start + req.by)) + +proc counter_compose*(c: Counter, a: int, b: int): Future[Result[int, string]] {.ffi.} = + ## Multiple primitive params plus a non-object return type. + return ok(c.start + a + b) + +proc counter_greet*( + c: Counter, name: Option[string] +): Future[Result[string, string]] {.ffi.} = + ## Exercises Option[T] param round-trip. + let n = if name.isSome: name.get else: "anon" + return ok("hello " & n & " (start=" & $c.start & ")") + +proc counter_fail*(c: Counter, reason: string): Future[Result[string, string]] {.ffi.} = + ## Error path — the failure surfaces as Result.err on the caller side. + return err("rejected: " & reason) + +proc counter_chain*( + c: Counter, steps: int +): Future[Result[CounterState, string]] {.ffi.} = + ## Real async work: multiple awaits composing other {.ffi.} procs. + ## Shows that the Nim-facing wrapper for an {.ffi.} proc is itself + ## awaitable, so {.ffi.} procs can be composed naturally without ever + ## touching the C-export shape. + var current = c + for i in 0 ..< steps: + await sleepAsync(1.milliseconds) + let stepRes = await counter_add(current, IncRequest(by: 1)) + if stepRes.isErr: + return err(stepRes.error) + current = Counter(start: stepRes.value.value) + return ok(CounterState(value: current.start)) + +type RangeFilter {.ffi.} = object + lo: int + hi: int + +type Pagination {.ffi.} = object + offset: int + limit: int + +type Projection {.ffi.} = object + fields: seq[string] + includeTotals: bool + +type QueryReport {.ffi.} = object + matched: int + returned: int + fieldsKept: seq[string] + +proc counter_query*( + c: Counter, filter: RangeFilter, page: Pagination, projection: Projection +): Future[Result[QueryReport, string]] {.ffi.} = + ## Three independent object-typed parameters: `filter`, `page`, `projection`. + ## Verifies that the macro packs all three into one CBOR Req envelope on the + ## wire and unpacks them back into the typed locals before this body runs. + if filter.hi < filter.lo: + return err("filter range is empty") + if page.limit <= 0: + return err("page.limit must be positive") + let matched = max(0, filter.hi - filter.lo + 1) + let returned = min(matched - min(matched, page.offset), page.limit) + return ok( + QueryReport( + matched: matched + c.start, # surfaces lib state in the response + returned: returned, + fieldsKept: + if projection.includeTotals: + projection.fields & @["__totals__"] + else: + projection.fields, + ) + ) + +suite "Nim-native API for {.ffi.} / {.ffiCtor.}": + test "ffiCtor returns the user-typed lib value": + let res = waitFor counter_create(CounterConfig(initial: 7)) + check res.isOk + check res.value.start == 7 + + test "sync .ffi. body completes via Future[Result[T, string]]": + let res = waitFor counter_value(Counter(start: 5)) + check res.isOk + check res.value.value == 5 + + test "async .ffi. body with await": + let res = waitFor counter_add(Counter(start: 5), IncRequest(by: 3)) + check res.isOk + check res.value.value == 8 + + test "multiple primitive params": + let res = waitFor counter_compose(Counter(start: 1), 2, 3) + check res.isOk + check res.value == 6 + + test "Option[string] param round-trip — some": + let res = waitFor counter_greet(Counter(start: 1), some("jamon")) + check res.isOk + check res.value == "hello jamon (start=1)" + + test "Option[string] param round-trip — none": + let res = waitFor counter_greet(Counter(start: 2), none(string)) + check res.isOk + check res.value == "hello anon (start=2)" + + test "error result propagates as Result.err": + let res = waitFor counter_fail(Counter(start: 0), "out of cookies") + check res.isErr + check res.error == "rejected: out of cookies" + + test "async .ffi. body chains multiple awaits and composes other .ffi. procs": + let res = waitFor counter_chain(Counter(start: 10), 4) + check res.isOk + check res.value.value == 14 + + test "chain with 0 steps returns the input unchanged": + let res = waitFor counter_chain(Counter(start: 42), 0) + check res.isOk + check res.value.value == 42 + + test "three complex object params travel together in one CBOR envelope": + let res = waitFor counter_query( + Counter(start: 100), + RangeFilter(lo: 1, hi: 50), + Pagination(offset: 10, limit: 25), + Projection(fields: @["id", "name"], includeTotals: true), + ) + check res.isOk + check res.value.matched == 150 # filter range 50 + lib state 100 + check res.value.returned == 25 + check res.value.fieldsKept == @["id", "name", "__totals__"] + + test "three-complex-param error path": + let res = waitFor counter_query( + Counter(start: 0), + RangeFilter(lo: 10, hi: 1), # inverted range + Pagination(offset: 0, limit: 5), + Projection(fields: @[], includeTotals: false), + ) + check res.isErr + check res.error == "filter range is empty" diff --git a/tests/test_serial.nim b/tests/test_serial.nim index fed25b8..e0b3d86 100644 --- a/tests/test_serial.nim +++ b/tests/test_serial.nim @@ -1,3 +1,4 @@ +import std/options import unittest import results import ../ffi @@ -10,98 +11,232 @@ type Nested {.ffi.} = object label: string point: Point -suite "ffiSerialize / ffiDeserialize primitives": +type RefBox {.ffi.} = object + label: string + n: int + +type Color = enum + cRed + cGreen + cBlue + +suite "CBOR primitives round-trip": + test "bool true": + let bytes = cborEncode(true) + check cborDecode(bytes, bool).value == true + + test "bool false": + let bytes = cborEncode(false) + check cborDecode(bytes, bool).value == false + + test "int positive": + let v = 42 + let bytes = cborEncode(v) + check cborDecode(bytes, int).value == v + + test "int negative": + let v = -100 + let bytes = cborEncode(v) + check cborDecode(bytes, int).value == v + + test "int64 large": + let v: int64 = 1_000_000_000_000 + let bytes = cborEncode(v) + check cborDecode(bytes, int64).value == v + + test "int32 round-trip": + let v: int32 = -32_000 + let bytes = cborEncode(v) + check cborDecode(bytes, int32).value == v + + test "uint round-trip": + let v: uint64 = 0xdeadbeef'u64 + let bytes = cborEncode(v) + check cborDecode(bytes, uint64).value == v + + test "float64 round-trip": + let v = 3.141592653589793 + let bytes = cborEncode(v) + check abs(cborDecode(bytes, float64).value - v) < 1e-12 + + test "float64 negative": + let v = -2.718281828 + let bytes = cborEncode(v) + check abs(cborDecode(bytes, float64).value - v) < 1e-9 + 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 + let bytes = cborEncode(s) + check cborDecode(bytes, string).value == s + + test "empty string": + let s = "" + let bytes = cborEncode(s) + check cborDecode(bytes, string).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 + let s = "tab\there\nnewline" + let bytes = cborEncode(s) + check cborDecode(bytes, string).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 "{.ffi.} on type — object round-trip": +suite "CBOR object": test "Point round-trip": - let pt = Point(x: 10, y: 20) - let serialized = ffiSerialize(pt) - let back = ffiDeserialize(serialized.cstring, Point) - check back.isOk() + let pt = Point(x: 10, y: -20) + let bytes = cborEncode(pt) + let back = cborDecode(bytes, Point) + check back.isOk check back.value.x == 10 - check back.value.y == 20 + 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() + let n = Nested(label: "origin", point: Point(x: 1, y: 2)) + let bytes = cborEncode(n) + let back = cborDecode(bytes, Nested) + check back.isOk check back.value.label == "origin" - check back.value.point.x == 0 - check back.value.point.y == 0 + check back.value.point.x == 1 + check back.value.point.y == 2 -suite "ffiDeserialize error handling": - test "malformed JSON returns err": - let back = ffiDeserialize("not json at all".cstring, int) - check back.isErr() +suite "CBOR ref T (value-copy contract)": + ## cbor_serialization's default `ref T` writer dereferences and encodes the + ## pointee. On decode the receiving side allocates a fresh `ref` local to + ## its own GC heap — no address crosses the boundary and the two refs are + ## independent. Documented in ffi/cbor_serial.nim's module header. - test "malformed JSON for object returns err": - let back = ffiDeserialize("{bad json".cstring, Point) - check back.isErr() + test "ref RefBox round-trip produces an independent ref": + let original = (ref RefBox)(label: "hi", n: 7) + let bytes = cborEncode(original) + let back = cborDecode(bytes, ref RefBox) + check back.isOk + check back.value != nil + check back.value.label == "hi" + check back.value.n == 7 + # Mutate the decoded copy; the original must be untouched (proving no + # aliasing). If the wire format ever switched to identity-preserving + # transport, this would fail. + back.value.label = "mutated" + check original.label == "hi" + check cast[pointer](back.value) != cast[pointer](original) + + test "nil ref round-trips as nil": + let original: ref RefBox = nil + let bytes = cborEncode(original) + let back = cborDecode(bytes, ref RefBox) + check back.isOk + check back.value == nil + +suite "CBOR seq / array": + test "seq[int] round-trip": + let s = @[1, 2, 3, -4, 5] + let bytes = cborEncode(s) + check cborDecode(bytes, seq[int]).value == s + + test "empty seq": + let s: seq[int] = @[] + let bytes = cborEncode(s) + check cborDecode(bytes, seq[int]).value == s + + test "seq[string]": + let s = @["a", "bb", "ccc"] + let bytes = cborEncode(s) + check cborDecode(bytes, seq[string]).value == s + + test "seq[Point]": + let s = @[Point(x: 1, y: 2), Point(x: 3, y: 4)] + let bytes = cborEncode(s) + let back = cborDecode(bytes, seq[Point]).value + check back.len == 2 + check back[0].x == 1 + check back[1].y == 4 + +suite "CBOR Option": + test "some int": + let o = some(42) + let bytes = cborEncode(o) + check cborDecode(bytes, Option[int]).value == o + + test "none int": + let o = none(int) + let bytes = cborEncode(o) + check cborDecode(bytes, Option[int]).value == o + + test "some object": + let o = some(Point(x: 7, y: 8)) + let bytes = cborEncode(o) + let back = cborDecode(bytes, Option[Point]).value + check back.isSome + check back.get.x == 7 + +suite "CBOR enum": + test "enum round-trip": + let c = cGreen + let bytes = cborEncode(c) + check cborDecode(bytes, Color).value == c + +suite "CBOR error handling": + test "garbage input returns err": + let garbage = @[0xff'u8, 0xff'u8] + let res = cborDecode(garbage, int) + check res.isErr + + test "truncated input returns err": + let bytes = cborEncode("hello") + let truncated = bytes[0 ..< 2] + let res = cborDecode(truncated, string) + check res.isErr + +# --------------------------------------------------------------------------- +# Regression for PR #23 review item 9: cborEncodeShared writes directly into +# a shared-memory buffer (allocShared), letting the FFI thread request take +# ownership without an intermediate seq[byte] copy. The shared-encoder must +# produce byte-for-byte the same output as the seq-encoder. +# --------------------------------------------------------------------------- + +suite "cborEncodeShared": + test "object payload round-trips": + let n = Nested(label: "", point: Point(x: 0, y: 0)) + let (sd, sl) = cborEncodeShared(n) + defer: + if not sd.isNil: + deallocShared(sd) + check sl > 0 + let back = cborDecodePtr(sd, sl, Nested).value + check back.label == "" + check back.point.x == 0 + check back.point.y == 0 + + test "shared encoder is byte-for-byte equal to seq encoder": + let n = Nested(label: "hello", point: Point(x: 3, y: 4)) + let seqBytes = cborEncode(n) + let (sd, sl) = cborEncodeShared(n) + defer: + if not sd.isNil: + deallocShared(sd) + check sl == seqBytes.len + for i in 0 ..< sl: + check sd[i] == seqBytes[i] + let back = cborDecodePtr(sd, sl, Nested).value + check back.label == "hello" + check back.point.x == 3 + check back.point.y == 4 + + test "large string growth (exercises reallocShared)": + # Larger than the initial 16-byte cap so reallocShared must run several + # times; verifies the shared-mode grower handles repeated reallocations. + var big = newString(4096) + for i in 0 ..< big.len: + big[i] = char(ord('a') + (i mod 26)) + let (sd, sl) = cborEncodeShared(big) + defer: + if not sd.isNil: + deallocShared(sd) + let back = cborDecodePtr(sd, sl, string).value + check back == big + + test "empty-string payload is the single byte 0x60 in shared mode": + let (sd, sl) = cborEncodeShared("") + defer: + if not sd.isNil: + deallocShared(sd) + check sl == 1 + check sd[0] == 0x60'u8 diff --git a/tests/test_string_helpers.nim b/tests/test_string_helpers.nim new file mode 100644 index 0000000..a49be6b --- /dev/null +++ b/tests/test_string_helpers.nim @@ -0,0 +1,87 @@ +## Unit tests for the identifier-casing helpers used by the codegen. +## These names map identifier conventions between Nim (camelCase), +## Rust (snake_case) and C++ (PascalCase types), and they're load-bearing +## for binding generation, so it's worth pinning their behaviour with tests. + +import unittest +import ../ffi/codegen/string_helpers + +suite "camelToSnakeCase": + test "empty string": + check camelToSnakeCase("") == "" + + test "single lowercase character": + check camelToSnakeCase("a") == "a" + + test "single uppercase character": + check camelToSnakeCase("A") == "a" + + test "all lowercase passes through": + check camelToSnakeCase("hello") == "hello" + + test "simple camelCase": + check camelToSnakeCase("camelCase") == "camel_case" + + test "two-letter suffix": + check camelToSnakeCase("delayMs") == "delay_ms" + + test "PascalCase input — leading capital stays at start": + check camelToSnakeCase("PascalCase") == "pascal_case" + + test "consecutive uppercase letters each get their own underscore": + check camelToSnakeCase("ABC") == "a_b_c" + + test "multiple word boundaries": + check camelToSnakeCase("abcDefGhi") == "abc_def_ghi" + + test "already snake_case passes through": + check camelToSnakeCase("already_snake") == "already_snake" + +suite "capitalizeFirstLetter": + test "empty string": + check capitalizeFirstLetter("") == "" + + test "single lowercase character": + check capitalizeFirstLetter("a") == "A" + + test "single uppercase character": + check capitalizeFirstLetter("A") == "A" + + test "lowercase word": + check capitalizeFirstLetter("abc") == "Abc" + + test "already capitalised": + check capitalizeFirstLetter("Abc") == "Abc" + + test "all-caps stays unchanged except first stays cap": + check capitalizeFirstLetter("ABC") == "ABC" + + test "leading non-letter is left alone": + check capitalizeFirstLetter("_hello") == "_hello" + +suite "snakeToPascalCase": + test "empty string": + check snakeToPascalCase("") == "" + + test "single lowercase word": + check snakeToPascalCase("hello") == "Hello" + + test "two-part snake_case": + check snakeToPascalCase("hello_world") == "HelloWorld" + + test "three-part snake_case": + check snakeToPascalCase("testlib_create") == "TestlibCreate" + + test "single-letter parts each capitalised": + check snakeToPascalCase("a_b_c") == "ABC" + + test "trailing underscore yields empty trailing part": + check snakeToPascalCase("foo_") == "Foo" + + test "leading underscore yields empty leading part": + check snakeToPascalCase("_foo") == "Foo" + + test "already-mixed parts preserve their existing case after the first": + # split on '_', capitalize first letter of each part; "HasCaps" first + # letter is already 'H' so it's untouched. + check snakeToPascalCase("already_HasCaps") == "AlreadyHasCaps" diff --git a/tests/test_wire_compat.nim b/tests/test_wire_compat.nim new file mode 100644 index 0000000..6d9856d --- /dev/null +++ b/tests/test_wire_compat.nim @@ -0,0 +1,108 @@ +## Wire-format compatibility tests. +## +## The C++ side now uses vendored TinyCBOR (see +## `ffi/codegen/templates/cpp/vendor/tinycbor/`) and the Nim side uses +## `cbor_serialization`. Both implement RFC 8949, but a regression on either +## side could silently produce divergent bytes for the same logical value. +## +## These tests pin the *exact* byte sequences `cbor_serialization` emits for +## a handful of representative shapes. If a future bump to the Nim library +## ever shifts the encoding (e.g., key ordering, integer length choice, +## optional/null handling), the assertions here will fail loudly before the +## C++ side gets to discover the divergence at runtime. +## +## The same golden bytes are exercised on the C++ side by the timer +## example's end-to-end round-trip (`examples/timer/cpp_bindings/main.cpp`). + +import std/[options, strutils] +import unittest +import results +import ../ffi + +type WireSimple {.ffi.} = object + name: string + +type WireWithInt {.ffi.} = object + message: string + delayMs: int + +type WireWithOption {.ffi.} = object + label: string + note: Option[string] + +type WireWithVector {.ffi.} = object + items: seq[string] + +proc toHex(bytes: openArray[byte]): string = + var buf = "" + for b in bytes: + buf.add(b.toHex(2).toLowerAscii()) + return buf + +suite "wire format — single-field map": + test "WireSimple{name:\"abc\"} round-trips to a stable byte sequence": + let v = WireSimple(name: "abc") + let bytes = cborEncode(v) + # map(1), key "name" (text-string len 4), value "abc" (text-string len 3) + check toHex(bytes) == "a1646e616d6563616263" + let back = cborDecode(bytes, WireSimple) + check back.isOk + check back.value.name == "abc" + +suite "wire format — int field": + test "WireWithInt encodes ints as CBOR integers": + let v = WireWithInt(message: "hi", delayMs: 200) + let bytes = cborEncode(v) + # map(2), "message"->"hi", "delayMs"->200 (uint8 form: 0x18 0xc8) + check toHex(bytes) == "a2676d65737361676562686967" & "64656c61794d7318c8" + let back = cborDecode(bytes, WireWithInt) + check back.isOk + check back.value.message == "hi" + check back.value.delayMs == 200 + + test "negative int uses CBOR negative-integer major type": + let v = WireWithInt(message: "x", delayMs: -1) + let bytes = cborEncode(v) + # 0x20 is CBOR -1 + check toHex(bytes).endsWith("20") + +suite "wire format — Option[T]": + ## Nim's `cbor_serialization/std/options` import encodes `Option[T]`: + ## - `some v` → emit the key and the inner value. + ## - `none T` → **omit the field entirely** from the map (the resulting + ## map is smaller by one entry). + ## + ## The C++ TinyCBOR helper currently encodes `std::nullopt` as CBOR null + ## (0xf6). That divergence is invisible while no consumer sends + ## `std::nullopt` over the wire (the timer example only sends `Some` + ## values). If a future caller does, we'll need to align the conventions + ## — either teach the C++ codec to skip None-valued keys (mirroring Nim), + ## or switch the Nim side to emit explicit nulls. This test pins the + ## current Nim behavior so the divergence is detectable instead of + ## silent. + + test "Option.some encodes as the inner value (no wrapper)": + let v = WireWithOption(label: "x", note: some("hi")) + let bytes = cborEncode(v) + # map(2): "label"->"x", "note"->"hi" (text strings, no null/tag wrapping) + check toHex(bytes) == "a2656c6162656c6178646e6f7465626869" + + test "Option.none yields a smaller map without the optional key": + let v = WireWithOption(label: "x", note: none(string)) + let bytes = cborEncode(v) + # map(1): only "label"->"x"; the "note" key is absent. + check toHex(bytes) == "a1656c6162656c6178" + +suite "wire format — seq[T]": + test "empty seq encodes as CBOR array(0)": + let v = WireWithVector(items: @[]) + let bytes = cborEncode(v) + # a1 (map 1) 65 (text-str len 5) 69 74 65 6d 73 ("items") 80 (array 0) + check toHex(bytes) == "a1656974656d7380" + + test "three-element seq[string]": + let v = WireWithVector(items: @["a", "bb", "ccc"]) + let bytes = cborEncode(v) + # map(1), "items" -> array(3) of text strings "a", "bb", "ccc": + # 83 (array 3) 61 61 ("a") 62 62 62 ("bb") 63 63 63 63 ("ccc") + check toHex(bytes) == "a1656974656d7383616162626263636363"