Start using CBOR (#23)

Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com>
Co-authored-by: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com>
This commit is contained in:
Ivan FB 2026-05-16 01:08:42 +02:00 committed by GitHub
parent 159c9287d8
commit ac303a707e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 8595 additions and 2666 deletions

40
.gitignore vendored
View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -1,238 +0,0 @@
#pragma once
#include <string>
#include <cstdint>
#include <chrono>
#include <stdexcept>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <nlohmann/json.hpp>
namespace nlohmann {
template<typename T>
void to_json(json& j, const std::optional<T>& opt) {
if (opt) j = *opt;
else j = nullptr;
}
template<typename T>
void from_json(const json& j, std::optional<T>& opt) {
if (j.is_null()) opt = std::nullopt;
else opt = j.get<T>();
}
}
// ============================================================
// 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<EchoRequest> messages;
std::vector<std::string> tags;
std::optional<std::string> note;
std::optional<int64_t> 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<typename T>
inline std::string serializeFfiArg(const T& value) {
return nlohmann::json(value).dump();
}
inline std::string serializeFfiArg(void* value) {
return std::to_string(reinterpret_cast<uintptr_t>(value));
}
template<typename T>
inline T deserializeFfiResult(const std::string& raw) {
try {
return nlohmann::json::parse(raw).get<T>();
} catch (const nlohmann::json::exception& e) {
throw std::runtime_error(std::string("FFI response deserialization failed: ") + e.what());
}
}
template<>
inline void* deserializeFfiResult<void*>(const std::string& raw) {
try {
return reinterpret_cast<void*>(static_cast<uintptr_t>(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<std::shared_ptr<FfiCallState_>*>(ud);
{
auto& s = **sptr;
std::lock_guard<std::mutex> 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<int(FfiCallback, void*)> f,
std::chrono::milliseconds timeout) {
auto state = std::make_shared<FfiCallState_>();
auto* cb_ref = new std::shared_ptr<FfiCallState_>(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<std::mutex> 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<void*>(static_cast<uintptr_t>(addr)), timeout);
} catch (const std::exception&) {
throw std::runtime_error("FFI create returned non-numeric address: " + raw);
}
}
static std::future<NimTimerCtx> 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<EchoResponse>(raw);
}
std::future<EchoResponse> 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<std::string>(raw);
}
std::future<std::string> 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<ComplexResponse>(raw);
}
std::future<ComplexResponse> 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) {}
};

View File

@ -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: "<none>"
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

View File

@ -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"] }

View File

@ -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<Result<String, String>>,
}
type Pair = Arc<(Mutex<FfiCallbackResult>, 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<FfiCallbackResult>, 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<F>(timeout: Duration, f: F) -> Result<String, String>
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<Result<String, String>>,
);
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: F) -> Result<String, String>
where
F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,
{
let rx = {
let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
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<Result<String, String>>,
)
});
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<Self, String> {
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<Self, String> {
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<EchoResponse, String> {
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::<EchoResponse>(&raw).map_err(|e| e.to_string())
}
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
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::<EchoResponse>(&raw).map_err(|e| e.to_string())
}
pub fn version(&self) -> Result<String, String> {
let raw = ffi_call(self.timeout, |cb, ud| unsafe {
ffi::nimtimer_version(self.ptr, cb, ud)
})?;
serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())
}
pub async fn version_async(&self) -> Result<String, String> {
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::<String>(&raw).map_err(|e| e.to_string())
}
pub fn complex(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw).map_err(|e| e.to_string())
}
pub async fn complex_async(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw).map_err(|e| e.to_string())
}
}

View File

@ -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;
}

View File

@ -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<EchoRequest>,
pub tags: Vec<String>,
pub note: Option<String>,
pub retries: Option<i64>,
}
#[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,
}

View File

@ -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.)");
}

View File

@ -1,30 +0,0 @@
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}

View File

@ -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

View File

@ -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/cbor.h>
"${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()

View File

@ -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

View File

@ -1,10 +1,10 @@
#include "nimtimer.hpp"
#include "timer.hpp"
#include <iostream>
#include <future>
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<std::string>{"rollup", "v2"},
/*priority*/ 10,
};
auto retry = RetryPolicy{
/*maxAttempts*/ 3,
/*backoffMs*/ 500,
/*retryOn*/ std::vector<std::string>{"timeout", "5xx"},
};
auto schedule = ScheduleConfig{
/*startAtMs*/ 1000,
/*intervalMs*/ 15000,
/*jitter*/ std::optional<int64_t>(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";

View File

@ -0,0 +1,775 @@
#pragma once
#include <string>
#include <cstdint>
#include <chrono>
#include <stdexcept>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <type_traits>
#include <cstring>
extern "C" {
#include <tinycbor/cbor.h>
}
// ── 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<int64_t>(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<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& 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<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& 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<int32_t>(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<double>(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<typename T>
inline CborError decode_cbor(CborValue& it, std::vector<T>& 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<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& 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<typename T>
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
// Start with a generous 4 KiB buffer; double on overflow until it fits.
std::vector<std::uint8_t> 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<typename T>
inline T decodeCborFFI(const std::vector<std::uint8_t>& 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<EchoRequest> messages;
std::vector<std::string> tags;
std::optional<std::string> note;
std::optional<int64_t> 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<std::string> 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<std::string> 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<int64_t> 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<std::uint8_t> 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<std::shared_ptr<FFICallState_>> handle(
static_cast<std::shared_ptr<FFICallState_>*>(ud));
FFICallState_& s = **handle;
std::lock_guard<std::mutex> lock(s.mtx);
s.ok = (ret == 0);
if (msg && len > 0) {
const auto* p = reinterpret_cast<const std::uint8_t*>(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<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
std::chrono::milliseconds timeout) {
auto state = std::make_shared<FFICallState_>();
auto* cb_ref = new std::shared_ptr<FFICallState_>(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<std::mutex> 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<std::string>(ffi_raw_);
try {
const auto addr = std::stoull(addr_str);
return TimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout);
} catch (const std::exception&) {
throw std::runtime_error("FFI create returned non-numeric address: " + addr_str);
}
}
static std::future<TimerCtx> 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<EchoResponse>(ffi_raw_);
}
std::future<EchoResponse> 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<std::string>(ffi_raw_);
}
std::future<std::string> 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<ComplexResponse>(ffi_raw_);
}
std::future<ComplexResponse> 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<ScheduleResult>(ffi_raw_);
}
std::future<ScheduleResult> 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) {}
};

210
examples/timer/rust_bindings/Cargo.lock generated Normal file
View File

@ -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",
]

View File

@ -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"] }

View File

@ -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

View File

@ -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());
}

View File

@ -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<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
let mut buf = Vec::new();
ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;
Ok(buf)
}
fn decode_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
ciborium::de::from_reader(bytes).map_err(|e| e.to_string())
}
type FFIResult = Result<Vec<u8>, String>;
type FFISender = flume::Sender<FFIResult>;
// Reconstruct the (ret, msg, len) tuple delivered by the C callback
// into a Result<Vec<u8>, 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<F>(timeout: Duration, f: F) -> FFIResult
where
F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,
{
let (tx, rx) = flume::bounded::<FFIResult>(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<F>(timeout: Duration, f: F) -> FFIResult
where
F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,
{
let (tx, rx) = flume::bounded::<FFIResult>(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<Self, String> {
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<Self, String> {
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<EchoResponse, String> {
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::<EchoResponse>(&raw_bytes)
}
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
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::<EchoResponse>(&raw_bytes)
}
pub fn version(&self) -> Result<String, String> {
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::<String>(&raw_bytes)
}
pub async fn version_async(&self) -> Result<String, String> {
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::<String>(&raw_bytes)
}
pub fn complex(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw_bytes)
}
pub async fn complex_async(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw_bytes)
}
pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
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::<ScheduleResult>(&raw_bytes)
}
pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
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::<ScheduleResult>(&raw_bytes)
}
}

View File

@ -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;
}

View File

@ -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<EchoRequest>,
pub tags: Vec<String>,
pub note: Option<String>,
pub retries: Option<i64>,
}
#[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<String>,
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<String>,
}
#[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<i64>,
}
#[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,
}

View File

@ -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"

View File

@ -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"] }

View File

@ -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

View File

@ -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.)");
}

View File

@ -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<dyn std::error::Error>> {
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(())
}

139
examples/timer/timer.nim Normal file
View File

@ -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: "<none>"
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()

View File

@ -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"

View File

@ -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

View File

@ -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"

72
ffi/cbor_serial.nim Normal file
View File

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

View File

@ -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 <string>")
lines.add("#include <cstdint>")
lines.add("#include <chrono>")
lines.add("#include <stdexcept>")
lines.add("#include <mutex>")
lines.add("#include <condition_variable>")
lines.add("#include <memory>")
lines.add("#include <functional>")
lines.add("#include <future>")
lines.add("#include <vector>")
lines.add("#include <optional>")
lines.add("#include <nlohmann/json.hpp>")
lines.add("")
lines.add(HeaderPreludeTpl)
# ── nlohmann optional<T> support ──────────────────────────────────────────
lines.add("namespace nlohmann {")
lines.add(" template<typename T>")
lines.add(" void to_json(json& j, const std::optional<T>& opt) {")
lines.add(" if (opt) j = *opt;")
lines.add(" else j = nullptr;")
lines.add(" }")
lines.add("")
lines.add(" template<typename T>")
lines.add(" void from_json(const json& j, std::optional<T>& opt) {")
lines.add(" if (j.is_null()) opt = std::nullopt;")
lines.add(" else opt = j.get<T>();")
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<typename T>")
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<uintptr_t>(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<typename T>")
lines.add("inline T deserializeFfiResult(const std::string& raw) {")
lines.add(" try {")
lines.add(" return nlohmann::json::parse(raw).get<T>();")
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<void*>(const std::string& raw) {")
lines.add(" try {")
lines.add(
" return reinterpret_cast<void*>(static_cast<uintptr_t>(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<FfiCallState_>.
# 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<std::shared_ptr<FfiCallState_>*>(ud);")
lines.add(" {")
lines.add(" auto& s = **sptr;")
lines.add(" std::lock_guard<std::mutex> 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<int(FfiCallback, void*)> f,"
)
lines.add(" std::chrono::milliseconds timeout) {")
lines.add(" auto state = std::make_shared<FfiCallState_>();")
lines.add(" auto* cb_ref = new std::shared_ptr<FfiCallState_>(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<std::mutex> 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<std::string>(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<void*>(static_cast<uintptr_t>(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<void*>(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],

View File

@ -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"

View File

@ -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: <CamelCase(procName)>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<Result<String, String>>,")
# ── CBOR helpers ───────────────────────────────────────────────────────────
lines.add("fn encode_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {")
lines.add(" let mut buf = Vec::new();")
lines.add(
" ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;"
)
lines.add(" Ok(buf)")
lines.add("}")
lines.add("")
lines.add("type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;")
lines.add("fn decode_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {")
lines.add(" ciborium::de::from_reader(bytes).map_err(|e| e.to_string())")
lines.add("}")
lines.add("")
# ── 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<flume::Sender<Result<Vec<u8>,
# 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<Vec<u8>, String>;")
lines.add("type FFISender = flume::Sender<FFIResult>;")
lines.add("")
lines.add("// Reconstruct the (ret, msg, len) tuple delivered by the C callback")
lines.add(
"// into a Result<Vec<u8>, String>: payload on success, UTF-8 message on error."
)
lines.add(
"// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the"
)
lines.add(
"// alternative would be to dispatch a separate Err for invalid UTF-8, but the"
)
lines.add("// codegen contract is that Nim handlers emit `string` error payloads, so")
lines.add("// invalid UTF-8 here would be a Nim-side bug.")
lines.add(
"unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult {"
)
lines.add(" let bytes = if msg.is_null() || len == 0 {")
lines.add(" Vec::new()")
lines.add(" } else {")
lines.add(" slice::from_raw_parts(msg as *const u8, len).to_vec()")
lines.add(" };")
lines.add(" if ret == 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<FfiCallbackResult>, Condvar));")
lines.add(" {")
lines.add(" let (lock, cvar) = &*pair;")
lines.add(" let mut state = lock.lock().unwrap();")
lines.add(" state.payload = Some(if ret == 0 {")
lines.add(" Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())")
lines.add(" } else {")
lines.add(" Err(CStr::from_ptr(msg).to_string_lossy().into_owned())")
lines.add(" });")
lines.add(" cvar.notify_one();")
lines.add(" }")
lines.add(" std::mem::forget(pair);")
lines.add(" // 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<F>(timeout: Duration, f: F) -> Result<String, String>")
lines.add("fn ffi_call_sync<F>(timeout: Duration, f: F) -> FFIResult")
lines.add("where")
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
lines.add(" 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::<FFIResult>(1);")
lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;")
lines.add(" let ret = f(on_result, raw);")
lines.add(" if ret == 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<Result<String, String>>,")
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: F) -> Result<String, String>")
lines.add("async fn ffi_call_async<F>(timeout: Duration, f: F) -> FFIResult")
lines.add("where")
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,")
lines.add("{")
lines.add(" let rx = {")
lines.add(" let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();")
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<Result<String, String>>,")
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::<FFIResult>(1);")
lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;")
lines.add(" let ret = f(on_result, raw);")
lines.add(" if ret == 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<Self, String> {" % [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<Self, String> {" % [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<Self, String> {" % [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<Self, String> {" % [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::<String>(&raw).map_err(|e| e.to_string())")
elif retRustType == "usize":
lines.add(" raw.parse::<usize>().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))

View File

@ -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

View File

@ -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/cbor.h>
"${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()

View File

@ -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<int64_t>(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<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& 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<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& 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<int32_t>(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<double>(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<typename T>
inline CborError decode_cbor(CborValue& it, std::vector<T>& 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<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& 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<typename T>
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
// Start with a generous 4 KiB buffer; double on overflow until it fits.
std::vector<std::uint8_t> 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<typename T>
inline T decodeCborFFI(const std::vector<std::uint8_t>& 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;
}

View File

@ -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;
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <cstdint>
#include <chrono>
#include <stdexcept>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <type_traits>
#include <cstring>
extern "C" {
#include <tinycbor/cbor.h>
}

View File

@ -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<std::uint8_t> 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<std::shared_ptr<FFICallState_>> handle(
static_cast<std::shared_ptr<FFICallState_>*>(ud));
FFICallState_& s = **handle;
std::lock_guard<std::mutex> lock(s.mtx);
s.ok = (ret == 0);
if (msg && len > 0) {
const auto* p = reinterpret_cast<const std::uint8_t*>(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<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
std::chrono::milliseconds timeout) {
auto state = std::make_shared<FFICallState_>();
auto* cb_ref = new std::shared_ptr<FFICallState_>(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<std::mutex> 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

View File

@ -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.

View File

@ -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 <assert.h>
#endif
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include "tinycbor-version.h"
#define TINYCBOR_VERSION ((TINYCBOR_VERSION_MAJOR << 16) | (TINYCBOR_VERSION_MINOR << 8) | TINYCBOR_VERSION_PATCH)
#ifdef __cplusplus
extern "C" {
#else
#include <stdbool.h>
#endif
#ifndef SIZE_MAX
/* Some systems fail to define SIZE_MAX in <stdint.h>, 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, &copy, CborPrettyDefaultFlags);
}
#endif /* __STDC_HOSTED__ check */
#endif /* CBOR_NO_PRETTY_API */
#endif /* CBOR_NO_PARSER_API */
#ifdef __cplusplus
}
#endif
#endif /* CBOR_H */

View File

@ -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 <stdlib.h>
#include <string.h>
/**
* \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
*
* <h3 class="groupheader">Error checking and buffer size</h3>
*
* 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
*/
/** @} */

View File

@ -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);
}
/** @} */

View File

@ -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);
}

View File

@ -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 <float.h>
# include <math.h>
#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 <immintrin.h>
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 */

File diff suppressed because it is too large Load Diff

View File

@ -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 <stdlib.h>
/**
* \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;
}

View File

@ -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 <assert.h>
#endif
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#ifndef __cplusplus
# include <stdbool.h>
#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 <sys/byteorder.h>
#elif defined(_MSC_VER)
/* MSVC, which implies Windows, which implies little-endian and sizeof(long) == 4 */
# include <stdlib.h>
# 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 <arpa/inet.h>
# define cbor_ntohs ntohs
# define cbor_htons htons
#endif
#ifndef cbor_ntohl
# include <arpa/inet.h>
# 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<t>(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 */

View File

@ -0,0 +1,3 @@
#define TINYCBOR_VERSION_MAJOR 0
#define TINYCBOR_VERSION_MINOR 6
#define TINYCBOR_VERSION_PATCH 0

View File

@ -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 <stdint.h>
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 */

View File

@ -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()

View File

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

View File

@ -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):

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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

View File

@ -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 15), 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 15: 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

View File

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

56
tests/test_meta.nim Normal file
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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"

108
tests/test_wire_compat.nim Normal file
View File

@ -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"