mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-23 01:39:26 +00:00
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:
parent
159c9287d8
commit
ac303a707e
40
.gitignore
vendored
40
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
@ -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) {}
|
||||
};
|
||||
@ -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
|
||||
@ -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"] }
|
||||
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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.)");
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
@ -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
|
||||
80
examples/timer/cpp_bindings/CMakeLists.txt
Normal file
80
examples/timer/cpp_bindings/CMakeLists.txt
Normal 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()
|
||||
@ -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
|
||||
@ -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";
|
||||
775
examples/timer/cpp_bindings/timer.hpp
Normal file
775
examples/timer/cpp_bindings/timer.hpp
Normal 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
210
examples/timer/rust_bindings/Cargo.lock
generated
Normal 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",
|
||||
]
|
||||
10
examples/timer/rust_bindings/Cargo.toml
Normal file
10
examples/timer/rust_bindings/Cargo.toml
Normal 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"] }
|
||||
@ -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
|
||||
@ -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());
|
||||
}
|
||||
228
examples/timer/rust_bindings/src/api.rs
Normal file
228
examples/timer/rust_bindings/src/api.rs
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
18
examples/timer/rust_bindings/src/ffi.rs
Normal file
18
examples/timer/rust_bindings/src/ffi.rs
Normal 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;
|
||||
}
|
||||
100
examples/timer/rust_bindings/src/types.rs
Normal file
100
examples/timer/rust_bindings/src/types.rs
Normal 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,
|
||||
}
|
||||
@ -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"
|
||||
@ -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"] }
|
||||
|
||||
@ -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
|
||||
80
examples/timer/rust_client/src/main.rs
Normal file
80
examples/timer/rust_client/src/main.rs
Normal 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.)");
|
||||
}
|
||||
64
examples/timer/rust_client/src/tokio_main.rs
Normal file
64
examples/timer/rust_client/src/tokio_main.rs
Normal 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
139
examples/timer/timer.nim
Normal 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()
|
||||
@ -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"
|
||||
4
ffi.nim
4
ffi.nim
@ -2,10 +2,10 @@ import std/[atomics, tables]
|
||||
import chronos, chronicles
|
||||
import
|
||||
ffi/internal/[ffi_library, ffi_macro],
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_context_pool, ffi_thread_request, serial]
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_context_pool, ffi_thread_request, cbor_serial]
|
||||
|
||||
export atomics, tables
|
||||
export chronos, chronicles
|
||||
export
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_context_pool,
|
||||
ffi_thread_request, serial
|
||||
ffi_thread_request, cbor_serial
|
||||
|
||||
49
ffi.nimble
49
ffi.nimble
@ -11,6 +11,7 @@ requires "nim >= 2.2.4"
|
||||
requires "chronos"
|
||||
requires "chronicles"
|
||||
requires "taskpools"
|
||||
requires "cbor_serialization"
|
||||
|
||||
const nimFlagsOrc = "--mm:orc -d:chronicles_log_level=WARN"
|
||||
const nimFlagsRefc = "--mm:refc -d:chronicles_log_level=WARN"
|
||||
@ -25,6 +26,10 @@ task test, "Run all tests under --mm:orc and --mm:refc":
|
||||
exec "nim c -r " & flags & " tests/test_gc_compat.nim"
|
||||
exec "nim c -r " & flags & " tests/test_serial.nim"
|
||||
exec "nim c -r " & flags & " tests/test_ctx_validation.nim"
|
||||
exec "nim c -r " & flags & " tests/test_nim_native_api.nim"
|
||||
exec "nim c -r " & flags & " tests/test_meta.nim"
|
||||
exec "nim c -r " & flags & " tests/test_string_helpers.nim"
|
||||
exec "nim c -r " & flags & " tests/test_wire_compat.nim"
|
||||
|
||||
task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc":
|
||||
exec "nim c -r " & nimFlagsOrc & " tests/test_alloc.nim"
|
||||
@ -34,38 +39,38 @@ task test_ffi, "Run FFI context integration tests under --mm:orc and --mm:refc":
|
||||
exec "nim c -r " & nimFlagsOrc & " tests/test_ffi_context.nim"
|
||||
exec "nim c -r " & nimFlagsRefc & " tests/test_ffi_context.nim"
|
||||
|
||||
task test_serial, "Run serial unit tests":
|
||||
task test_serial, "Run CBOR codec unit tests":
|
||||
exec "nim c -r " & nimFlagsOrc & " tests/test_serial.nim"
|
||||
exec "nim c -r " & nimFlagsRefc & " tests/test_serial.nim"
|
||||
|
||||
task genbindings_example, "Generate Rust bindings for the nim_timer example":
|
||||
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
task genbindings_example, "Generate Rust bindings for the timer example":
|
||||
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libtimer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
|
||||
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libtimer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
|
||||
|
||||
task genbindings_rust, "Generate Rust bindings for the nim_timer example":
|
||||
task genbindings_rust, "Generate Rust bindings for the timer example":
|
||||
exec "nim c " & nimFlagsOrc &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" --app:lib --noMain --nimMainPrefix:libtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=rust" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
" -d:ffiOutputDir=examples/timer/rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
exec "nim c " & nimFlagsRefc &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" --app:lib --noMain --nimMainPrefix:libtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=rust" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
" -d:ffiOutputDir=examples/timer/rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
|
||||
task genbindings_cpp, "Generate C++ bindings for the nim_timer example":
|
||||
task genbindings_cpp, "Generate C++ bindings for the timer example":
|
||||
exec "nim c " & nimFlagsOrc &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" --app:lib --noMain --nimMainPrefix:libtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=cpp" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
" -d:ffiOutputDir=examples/timer/cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
exec "nim c " & nimFlagsRefc &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" --app:lib --noMain --nimMainPrefix:libtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=cpp" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
" -d:ffiOutputDir=examples/timer/cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../timer.nim" &
|
||||
" -o:/dev/null examples/timer/timer.nim"
|
||||
|
||||
72
ffi/cbor_serial.nim
Normal file
72
ffi/cbor_serial.nim
Normal 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)
|
||||
@ -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],
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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))
|
||||
|
||||
45
ffi/codegen/string_helpers.nim
Normal file
45
ffi/codegen/string_helpers.nim
Normal 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
|
||||
80
ffi/codegen/templates/cpp/CMakeLists.txt.tpl
Normal file
80
ffi/codegen/templates/cpp/CMakeLists.txt.tpl
Normal 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()
|
||||
168
ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl
Normal file
168
ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl
Normal 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;
|
||||
}
|
||||
34
ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl
Normal file
34
ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl
Normal 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;
|
||||
}
|
||||
17
ffi/codegen/templates/cpp/header_prelude.hpp.tpl
Normal file
17
ffi/codegen/templates/cpp/header_prelude.hpp.tpl
Normal 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>
|
||||
}
|
||||
52
ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl
Normal file
52
ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl
Normal 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
|
||||
21
ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE
vendored
Normal file
21
ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE
vendored
Normal 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.
|
||||
724
ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h
vendored
Normal file
724
ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h
vendored
Normal 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, ©, CborPrettyDefaultFlags);
|
||||
}
|
||||
#endif /* __STDC_HOSTED__ check */
|
||||
|
||||
#endif /* CBOR_NO_PRETTY_API */
|
||||
|
||||
#endif /* CBOR_NO_PARSER_API */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* CBOR_H */
|
||||
|
||||
689
ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c
vendored
Normal file
689
ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c
vendored
Normal 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
|
||||
*/
|
||||
|
||||
/** @} */
|
||||
57
ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c
vendored
Normal file
57
ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c
vendored
Normal 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);
|
||||
}
|
||||
|
||||
/** @} */
|
||||
188
ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c
vendored
Normal file
188
ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c
vendored
Normal 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);
|
||||
}
|
||||
316
ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h
vendored
Normal file
316
ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h
vendored
Normal 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 */
|
||||
1529
ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c
vendored
Normal file
1529
ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
119
ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c
vendored
Normal file
119
ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c
vendored
Normal 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;
|
||||
}
|
||||
205
ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h
vendored
Normal file
205
ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h
vendored
Normal 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 */
|
||||
|
||||
3
ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h
vendored
Normal file
3
ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
#define TINYCBOR_VERSION_MAJOR 0
|
||||
#define TINYCBOR_VERSION_MINOR 6
|
||||
#define TINYCBOR_VERSION_PATCH 0
|
||||
104
ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h
vendored
Normal file
104
ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h
vendored
Normal 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 */
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
121
ffi/serial.nim
121
ffi/serial.nim
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import std/[locks, strutils, os]
|
||||
import std/[locks, options, strutils, os, atomics]
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
@ -12,7 +12,7 @@ type CallbackData = object
|
||||
cond: Cond
|
||||
called: bool
|
||||
retCode: cint
|
||||
msg: array[512, char]
|
||||
msg: array[1024, byte]
|
||||
msgLen: int
|
||||
|
||||
proc initCallbackData(d: var CallbackData) =
|
||||
@ -29,10 +29,9 @@ proc testCallback(
|
||||
let d = cast[ptr CallbackData](userData)
|
||||
acquire(d[].lock)
|
||||
d[].retCode = retCode
|
||||
let n = min(int(len), d[].msg.high)
|
||||
let n = min(int(len), d[].msg.len)
|
||||
if n > 0 and not msg.isNil:
|
||||
copyMem(addr d[].msg[0], msg, n)
|
||||
d[].msg[n] = '\0'
|
||||
d[].msgLen = n
|
||||
d[].called = true
|
||||
signal(d[].cond)
|
||||
@ -44,10 +43,18 @@ proc waitCallback(d: var CallbackData) =
|
||||
wait(d.cond, d.lock)
|
||||
release(d.lock)
|
||||
|
||||
proc callbackMsg(d: var CallbackData): string =
|
||||
result = newString(d.msgLen)
|
||||
proc callbackBytes(d: var CallbackData): seq[byte] =
|
||||
var bytes = newSeq[byte](d.msgLen)
|
||||
if d.msgLen > 0:
|
||||
copyMem(addr result[0], addr d.msg[0], d.msgLen)
|
||||
copyMem(addr bytes[0], addr d.msg[0], d.msgLen)
|
||||
return bytes
|
||||
|
||||
proc callbackErr(d: var CallbackData): string =
|
||||
## Reads the error payload (sent as raw UTF-8 bytes on RET_ERR).
|
||||
var msg = newString(d.msgLen)
|
||||
if d.msgLen > 0:
|
||||
copyMem(addr msg[0], addr d.msg[0], d.msgLen)
|
||||
return msg
|
||||
|
||||
registerReqFFI(PingRequest, lib: ptr TestLib):
|
||||
proc(message: cstring): Future[Result[string, string]] {.async.} =
|
||||
@ -66,35 +73,19 @@ registerReqFFI(SlowRequest, lib: ptr TestLib):
|
||||
await sleepAsync(500.milliseconds)
|
||||
return ok("slow-done")
|
||||
|
||||
# Coordination channel: the FFI handler signals the test thread the instant
|
||||
# it is about to block the event loop, so the test can call destroyFFIContext
|
||||
# while the event loop is truly frozen.
|
||||
var gSyncBlockStarted: Channel[bool]
|
||||
gSyncBlockStarted.open()
|
||||
|
||||
registerReqFFI(SyncBlockingRequest, lib: ptr TestLib):
|
||||
proc(): Future[Result[string, string]] {.async.} =
|
||||
# Yield first so that reqReceivedSignal fires and sendRequestToFFIThread
|
||||
# returns on the calling thread before we start the synchronous block.
|
||||
await sleepAsync(0.milliseconds)
|
||||
# Signal the test thread: the event loop is about to be frozen.
|
||||
# Channel.send is annotated as raising under refc, so wrap.
|
||||
try:
|
||||
gSyncBlockStarted.send(true)
|
||||
except Exception as exc:
|
||||
return err("gSyncBlockStarted.send raised: " & exc.msg)
|
||||
# Simulates a request that blocks the event-loop thread synchronously
|
||||
# (e.g. w.stop() -> switch.stop() -> connManager.close() with blocking I/O).
|
||||
# Unlike sleepAsync, os.sleep holds the OS thread and prevents Chronos from
|
||||
# processing any callbacks -- including the reqSignal fired by destroyFFIContext.
|
||||
os.sleep(5_000)
|
||||
return ok("sync-blocking-done")
|
||||
|
||||
# Approximates the heavy ref-object workload that libwaku/libp2p performs on
|
||||
# the FFI thread. The exact cell count is large enough to force several refc
|
||||
# GC cycles; under refc this stresses the heap state that, when later combined
|
||||
# with a chronos Selector allocation on the main thread (via close()), used to
|
||||
# trip the rawNewObj → signal-handler infinite recursion.
|
||||
type RefCell = ref object
|
||||
next: RefCell
|
||||
payload: array[64, byte]
|
||||
@ -107,16 +98,11 @@ registerReqFFI(HeavyRefAllocRequest, lib: ptr TestLib):
|
||||
head = n
|
||||
if i mod 1000 == 0:
|
||||
await sleepAsync(0.milliseconds)
|
||||
# Break the chain iteratively before releasing head.
|
||||
# ORC's =destroy for RefCell recurses through .next, so a 50k-node chain
|
||||
# would produce ~50k nested =destroy calls and overflow the stack.
|
||||
# Walking the list and unlinking each node first keeps destruction O(n)
|
||||
# iterative instead of O(n) recursive.
|
||||
var node = head
|
||||
head = nil
|
||||
while not node.isNil():
|
||||
let nxt = node.next
|
||||
node.next = nil # unlink before the refcount of `node` can drop to zero
|
||||
node.next = nil
|
||||
node = nxt
|
||||
await sleepAsync(10.milliseconds)
|
||||
return ok("heavy-done")
|
||||
@ -135,12 +121,11 @@ suite "FFIContextPool":
|
||||
assert false, "createFFIContext(pool) failed: " & $error
|
||||
return
|
||||
check pool.destroyFFIContext(ctx1).isOk()
|
||||
# After destroying, the same slot must be available again
|
||||
let ctx2 = pool.createFFIContext().valueOr:
|
||||
assert false, "createFFIContext(pool) failed after slot release: " & $error
|
||||
return
|
||||
check pool.destroyFFIContext(ctx2).isOk()
|
||||
check ctx1 == ctx2 # same array slot reused
|
||||
check ctx1 == ctx2
|
||||
|
||||
test "pool exhaustion returns error":
|
||||
var pool: FFIContextPool[TestLib]
|
||||
@ -151,7 +136,6 @@ suite "FFIContextPool":
|
||||
discard pool.destroyFFIContext(ctxs[j])
|
||||
assert false, "createFFIContext(pool) failed at slot " & $i & ": " & $error
|
||||
return
|
||||
# Pool is now full — next create must fail
|
||||
check pool.createFFIContext().isErr()
|
||||
for i in 0 ..< MaxFFIContexts:
|
||||
discard pool.destroyFFIContext(ctxs[i])
|
||||
@ -175,7 +159,7 @@ suite "FFIContextPool":
|
||||
.isOk()
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
check callbackMsg(d) == "pong:pool"
|
||||
check cborDecode(callbackBytes(d), string).value == "pong:pool"
|
||||
|
||||
suite "createFFIContext / destroyFFIContext":
|
||||
test "create and destroy succeeds":
|
||||
@ -195,10 +179,6 @@ suite "createFFIContext / destroyFFIContext":
|
||||
|
||||
suite "destroyFFIContext does not hang":
|
||||
test "destroy while a slow async request is still in-flight":
|
||||
## Reproduces the race where destroyFFIContext was called while a long-
|
||||
## running async request (e.g. stop_node / w.stop()) was still executing.
|
||||
## The destroy must return well within 2 seconds; before the fix it would
|
||||
## block forever on joinThread(ffiThread).
|
||||
var pool: FFIContextPool[TestLib]
|
||||
let ctx = pool.createFFIContext().valueOr:
|
||||
check false
|
||||
@ -206,106 +186,40 @@ suite "destroyFFIContext does not hang":
|
||||
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
# sendRequestToFFIThread returns as soon as the FFI thread ACKs receipt;
|
||||
# the 500 ms work continues asynchronously on the FFI thread.
|
||||
check sendRequestToFFIThread(
|
||||
ctx, SlowRequest.ffiNewReq(testCallback, addr d)
|
||||
).isOk()
|
||||
check sendRequestToFFIThread(ctx, SlowRequest.ffiNewReq(testCallback, addr d)).isOk()
|
||||
|
||||
# Destroy immediately while SlowRequest is still running.
|
||||
let t0 = Moment.now()
|
||||
check pool.destroyFFIContext(ctx).isOk()
|
||||
check (Moment.now() - t0) < 2.seconds
|
||||
|
||||
suite "destroyFFIContext does not hang when event loop is blocked":
|
||||
test "destroy while sync-blocking request is in-flight":
|
||||
## Reproduces the hang seen in logosdelivery_example.c:
|
||||
## logosdelivery_stop_node(...) -- triggers w.stop() on the FFI thread
|
||||
## sleep(1)
|
||||
## logosdelivery_destroy(...) -- hangs forever
|
||||
##
|
||||
## Root cause: w.stop() (and similar tear-down calls) can execute a
|
||||
## synchronous blocking section that holds the OS thread, preventing
|
||||
## the Chronos event loop from processing the reqSignal fired by
|
||||
## destroyFFIContext. The result is joinThread(ffiThread) never returns.
|
||||
##
|
||||
## With the fix, destroyFFIContext must complete well within the 5 s that
|
||||
## SyncBlockingRequest holds the event loop.
|
||||
var pool: FFIContextPool[TestLib]
|
||||
let ctx = pool.createFFIContext().valueOr:
|
||||
check false
|
||||
return
|
||||
|
||||
# CallbackData and ctx are kept alive past destroyFFIContext: the leaked
|
||||
# FFI thread is still inside os.sleep(5_000) and will eventually wake,
|
||||
# run handleRes, fire testCallback, and exit normally. We wait for that
|
||||
# to happen at the end of the test so the leaked thread cannot race with
|
||||
# subsequent tests' createFFIContext on Linux/Windows. Heap allocation
|
||||
# ensures the late callback's userData is still valid when it fires.
|
||||
let d = createShared(CallbackData)
|
||||
initCallbackData(d[])
|
||||
|
||||
check sendRequestToFFIThread(
|
||||
ctx, SyncBlockingRequest.ffiNewReq(testCallback, d)
|
||||
).isOk()
|
||||
check sendRequestToFFIThread(ctx, SyncBlockingRequest.ffiNewReq(testCallback, d))
|
||||
.isOk()
|
||||
|
||||
# Block until the FFI handler has signalled that os.sleep is about to start.
|
||||
# This guarantees destroyFFIContext is called while the event loop is frozen.
|
||||
discard gSyncBlockStarted.recv()
|
||||
|
||||
# Destroy must return promptly even though the event loop is frozen for 5s.
|
||||
# It deliberately returns err and leaks ctx in this scenario rather than
|
||||
# hanging on joinThread.
|
||||
let t0 = Moment.now()
|
||||
check pool.destroyFFIContext(ctx).isErr()
|
||||
check (Moment.now() - t0) < 3.seconds
|
||||
|
||||
# Drain the leaked thread before the test scope ends.
|
||||
# 1. waitCallback blocks until os.sleep(5_000) returns and handleRes
|
||||
# invokes testCallback (~3.5s after destroy returned), which proves
|
||||
# the leaked thread has reached the end of processRequest.
|
||||
# 2. Yield briefly so the thread can finish iterating its while loop,
|
||||
# fire threadExitSignal in its defer, and return. Without this, on
|
||||
# Linux/Windows the still-live thread can race with the next test's
|
||||
# createFFIContext under --mm:orc and segfault.
|
||||
# ctx.cleanUpResources is intentionally NOT called: destroyFFIContext
|
||||
# skipped it for a reason, and the signal fds are reclaimed by the OS
|
||||
# at process exit.
|
||||
waitCallback(d[])
|
||||
os.sleep(200)
|
||||
deinitCallbackData(d[])
|
||||
freeShared(d)
|
||||
|
||||
suite "destroyFFIContext refc workaround":
|
||||
## Documents the refc-specific workaround in cleanUpResources.
|
||||
##
|
||||
## Background: when the FFI thread does heavy ref-object work (the workload
|
||||
## that triggered the libwaku hang in production), the refc GC heap reaches
|
||||
## a state where the very first chronos Selector allocation on the *main*
|
||||
## thread — which happens lazily inside ThreadSignalPtr.close() through
|
||||
## getThreadDispatcher() — traps in rawNewObj. The refc signal handler
|
||||
## itself re-enters the same allocator and the process never returns.
|
||||
## Captured stack:
|
||||
## close → safeUnregisterAndCloseFd → getThreadDispatcher →
|
||||
## newDispatcher → Selector.new → newObj (gc.nim:488) → rawNewObj →
|
||||
## _sigtramp → signalHandler → newObjNoInit → addNewObjToZCT (loop)
|
||||
##
|
||||
## The workaround in cleanUpResources is `when defined(gcRefc): discard`,
|
||||
## i.e. skip the close() calls under refc only. orc is unaffected and
|
||||
## still cleans up the signal fds normally.
|
||||
##
|
||||
## NOTE: this test is documentation more than regression: a synthetic
|
||||
## ref-allocation workload of ~50k cells does NOT corrupt the refc heap
|
||||
## the way the real libwaku/libp2p teardown does, so this test passes
|
||||
## even when the workaround is disabled. Reproducing the actual hang
|
||||
## requires the full libwaku workload (logosdelivery_example.c).
|
||||
## Verification of the workaround was done end-to-end against that
|
||||
## example: with `--mm:refc` and close() enabled it hangs forever in
|
||||
## the captured stack above; with `when defined(gcRefc): discard` it
|
||||
## returns immediately. Under `--mm:orc` it returns immediately either
|
||||
## way.
|
||||
test "destroy after heavy ref-allocation workload returns promptly":
|
||||
var pool: FFIContextPool[TestLib]
|
||||
let ctx = pool.createFFIContext().valueOr:
|
||||
@ -314,11 +228,13 @@ suite "destroyFFIContext refc workaround":
|
||||
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
check sendRequestToFFIThread(
|
||||
ctx, HeavyRefAllocRequest.ffiNewReq(testCallback, addr d)
|
||||
).isOk()
|
||||
)
|
||||
.isOk()
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
|
||||
@ -346,7 +262,7 @@ suite "sendRequestToFFIThread":
|
||||
.isOk()
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
check callbackMsg(d) == "pong:hello"
|
||||
check cborDecode(callbackBytes(d), string).value == "pong:hello"
|
||||
|
||||
test "failing request triggers RET_ERR callback":
|
||||
var d: CallbackData
|
||||
@ -364,6 +280,8 @@ suite "sendRequestToFFIThread":
|
||||
check sendRequestToFFIThread(ctx, FailRequest.ffiNewReq(testCallback, addr d)).isOk()
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_ERR
|
||||
# Errors are raw UTF-8 — not CBOR.
|
||||
check callbackErr(d) == "intentional failure"
|
||||
|
||||
test "empty ok response delivers empty message":
|
||||
var d: CallbackData
|
||||
@ -382,7 +300,8 @@ suite "sendRequestToFFIThread":
|
||||
.isOk()
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
check d.msgLen == 0
|
||||
# CBOR-encoded "" is a single byte (text string of length 0): 0x60
|
||||
check cborDecode(callbackBytes(d), string).value == ""
|
||||
|
||||
test "sequential requests are all processed":
|
||||
var pool: FFIContextPool[TestLib]
|
||||
@ -403,10 +322,10 @@ suite "sendRequestToFFIThread":
|
||||
waitCallback(d)
|
||||
deinitCallbackData(d)
|
||||
check d.retCode == RET_OK
|
||||
check callbackMsg(d) == "pong:" & msg
|
||||
check cborDecode(callbackBytes(d), string).value == "pong:" & msg
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ffiCtor macro integration test
|
||||
# ffiCtor / .ffi. macros — exercise the full CBOR transport
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type SimpleLib = object
|
||||
@ -420,30 +339,37 @@ proc testlib_create*(
|
||||
): Future[Result[SimpleLib, string]] {.ffiCtor.} =
|
||||
return ok(SimpleLib(value: config.initialValue))
|
||||
|
||||
proc encodedPtr(bytes: var seq[byte]): ptr byte =
|
||||
if bytes.len == 0:
|
||||
nil
|
||||
else:
|
||||
cast[ptr byte](addr bytes[0])
|
||||
|
||||
proc ctorAddrFromCbor(bytes: seq[byte]): uint =
|
||||
## The ctor success payload is a CBOR text string of the decimal address.
|
||||
let addrStr = cborDecode(bytes, string).valueOr:
|
||||
return 0
|
||||
cast[uint](parseBiggestUInt(addrStr))
|
||||
|
||||
suite "ffiCtor macro":
|
||||
test "creates context and returns pointer via callback":
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 42))
|
||||
let ret = testlib_create(configJson.cstring, testCallback, addr d)
|
||||
var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 42)))
|
||||
let ret = testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr d)
|
||||
|
||||
check not ret.isNil()
|
||||
|
||||
waitCallback(d)
|
||||
|
||||
check d.retCode == RET_OK
|
||||
|
||||
# The callback message is the ctx address as a decimal string
|
||||
let addrStr = callbackMsg(d)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
let ctxAddr = ctorAddrFromCbor(callbackBytes(d))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
|
||||
# Verify the library was properly initialized
|
||||
check not ctx[].myLib.isNil
|
||||
check ctx[].myLib[].value == 42
|
||||
|
||||
@ -463,185 +389,238 @@ proc testlib_send*(
|
||||
|
||||
suite "simplified .ffi. macro":
|
||||
test "sends request and gets serialized response via callback":
|
||||
# First create a context using ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
defer:
|
||||
deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 7))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 7)))
|
||||
let ctorRet =
|
||||
testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD)
|
||||
check not ctorRet.isNil()
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
defer:
|
||||
check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Now call the .ffi. proc
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
let cfgJson = ffiSerialize(SendConfig(message: "hello"))
|
||||
let ret = testlib_send(ctx, testCallback, addr d, cfgJson.cstring)
|
||||
# The .ffi. macro packs all extra params into one CBOR Req struct.
|
||||
var reqBytes = cborEncode(TestlibSendReq(cfg: SendConfig(message: "hello")))
|
||||
let ret = testlib_send(
|
||||
ctx, testCallback, addr d, encodedPtr(reqBytes), reqBytes.len.csize_t
|
||||
)
|
||||
check ret == RET_OK
|
||||
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
|
||||
let receivedMsg = callbackMsg(d)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "echo:hello:7"
|
||||
check cborDecode(callbackBytes(d), string).value == "echo:hello:7"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# async/sync detection in .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Sync proc (no await in body) — macro detects this and bypasses thread machinery
|
||||
proc testlib_version*(
|
||||
lib: SimpleLib
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
proc testlib_version*(lib: SimpleLib): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("v" & $lib.value)
|
||||
|
||||
suite "async/sync detection in .ffi.":
|
||||
test "sync proc invokes callback without thread hop":
|
||||
# Create a context using ffiCtor
|
||||
suite "sync-body .ffi. is dispatched on FFI thread":
|
||||
## Before PR #23 (items 1–5), a `.ffi.` body without `await` was emitted as
|
||||
## an inline-on-foreign-thread fast path. That was deleted; all `.ffi.`
|
||||
## procs now go through the FFI thread. This test asserts the round-trip
|
||||
## still produces the expected payload via the callback.
|
||||
test "sync body still produces correct payload via callback":
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
defer:
|
||||
deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 3))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 3)))
|
||||
let ctorRet =
|
||||
testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD)
|
||||
check not ctorRet.isNil()
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
defer:
|
||||
check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
|
||||
var d2: CallbackData
|
||||
initCallbackData(d2)
|
||||
defer: deinitCallbackData(d2)
|
||||
defer:
|
||||
deinitCallbackData(d2)
|
||||
|
||||
# Call sync proc — callback should fire before the proc returns (no thread hop)
|
||||
let ret = testlib_version(ctx, testCallback, addr d2)
|
||||
# No sleep needed: sync path fires callback inline before returning
|
||||
# No-extra-param .ffi. proc; pack an empty Req.
|
||||
var emptyBytes = cborEncode(TestlibVersionReq())
|
||||
let ret = testlib_version(
|
||||
ctx, testCallback, addr d2, encodedPtr(emptyBytes), emptyBytes.len.csize_t
|
||||
)
|
||||
check ret == RET_OK
|
||||
check d2.called # fires synchronously — no waitCallback needed
|
||||
waitCallback(d2) # always asynchronous now
|
||||
check d2.retCode == RET_OK
|
||||
let receivedMsg = callbackMsg(d2)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "v3"
|
||||
check cborDecode(callbackBytes(d2), string).value == "v3"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ptr T return type in .ffi. macro integration test
|
||||
# Nim-native API (no callbacks, no CBOR buffers): the original proc name
|
||||
# resolves to the user's declared async signature and is callable directly.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type Handle = object
|
||||
data: string
|
||||
suite "Nim-native .ffi. / .ffiCtor. API":
|
||||
test "user proc names retain their declared Future[Result[T,string]] shape":
|
||||
let lib = SimpleLib(value: 9)
|
||||
# Async {.ffi.} proc — call directly without ctx/callback dance.
|
||||
let echoed = waitFor testlib_send(lib, SendConfig(message: "direct"))
|
||||
check echoed.isOk
|
||||
check echoed.value == "echo:direct:9"
|
||||
|
||||
type NameParam {.ffi.} = object
|
||||
name: string
|
||||
# Sync {.ffi.} body — still typed as Future[Result[T,string]] per the
|
||||
# user's source-level declaration (b): completed-future wrapper.
|
||||
let v = waitFor testlib_version(lib)
|
||||
check v.isOk
|
||||
check v.value == "v9"
|
||||
|
||||
proc testlib_alloc_handle*(
|
||||
lib: SimpleLib, np: NameParam
|
||||
): Future[Result[ptr Handle, string]] {.ffi.} =
|
||||
let h = createShared(Handle)
|
||||
h[] = Handle(data: np.name & ":" & $lib.value)
|
||||
return ok(h)
|
||||
# The ctor body is similarly callable from Nim with its declared signature.
|
||||
let ctorRes = waitFor testlib_create(SimpleConfig(initialValue: 21))
|
||||
check ctorRes.isOk
|
||||
check ctorRes.value.value == 21
|
||||
|
||||
proc testlib_read_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
return ok(h[].data)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression for PR #23 review items 1–5: a `.ffi.` body without `await`
|
||||
# used to be emitted as an inline-on-foreign-thread fast path, which bypassed
|
||||
# `foreignThreadGc`, `ctx.lock`, and chronos's single-thread invariant. The
|
||||
# sync fast-path was deleted; this test records `getThreadId()` inside a
|
||||
# sync body and asserts the handler runs on the FFI thread, not on the
|
||||
# caller's thread.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc testlib_free_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
deallocShared(h)
|
||||
return ok("freed")
|
||||
var gRecordedHandlerTid: Atomic[int]
|
||||
|
||||
suite "ptr return type in .ffi.":
|
||||
test "returns a heap-allocated handle and reads it back":
|
||||
# Create context via ffiCtor
|
||||
type RecordTidReq {.ffi.} = object
|
||||
dummy: int
|
||||
|
||||
proc testlib_record_tid*(
|
||||
lib: SimpleLib, req: RecordTidReq
|
||||
): Future[Result[int, string]] {.ffi.} =
|
||||
## Sync body — used to live on the inline fast-path; must now run on the
|
||||
## FFI thread.
|
||||
let tid = getThreadId()
|
||||
gRecordedHandlerTid.store(tid)
|
||||
return ok(tid)
|
||||
|
||||
suite "sync-body .ffi. runs on FFI thread (PR #23 regression)":
|
||||
test "handler thread id differs from caller's":
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
defer:
|
||||
deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 5))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
var cfg = cborEncode(TestlibCreateCtorReq(config: SimpleConfig(initialValue: 0)))
|
||||
let ctorRet =
|
||||
testlib_create(encodedPtr(cfg), cfg.len.csize_t, testCallback, addr ctorD)
|
||||
check not ctorRet.isNil()
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let ctxAddrStr = callbackMsg(ctorD)
|
||||
check ctxAddrStr.len > 0
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(ctxAddrStr))
|
||||
let ctxAddr = ctorAddrFromCbor(callbackBytes(ctorD))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
defer:
|
||||
check SimpleLibFFIPool.destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Alloc a handle
|
||||
var allocD: CallbackData
|
||||
initCallbackData(allocD)
|
||||
defer: deinitCallbackData(allocD)
|
||||
gRecordedHandlerTid.store(0)
|
||||
let callerTid = getThreadId()
|
||||
|
||||
let npJson = ffiSerialize(NameParam(name: "test"))
|
||||
let allocRet = testlib_alloc_handle(ctx, testCallback, addr allocD, npJson.cstring)
|
||||
check allocRet == RET_OK
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
waitCallback(allocD)
|
||||
check allocD.retCode == RET_OK
|
||||
var reqBytes = cborEncode(TestlibRecordTidReq(req: RecordTidReq(dummy: 1)))
|
||||
let ret = testlib_record_tid(
|
||||
ctx, testCallback, addr d, encodedPtr(reqBytes), reqBytes.len.csize_t
|
||||
)
|
||||
check ret == RET_OK
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
|
||||
let handleAddrStr = callbackMsg(allocD)
|
||||
check handleAddrStr.len > 0
|
||||
let handleAddr = parseBiggestUInt(handleAddrStr)
|
||||
check handleAddr != 0
|
||||
let handlerTid = gRecordedHandlerTid.load()
|
||||
check handlerTid != 0
|
||||
# The whole point of the fix: even a sync-body handler is dispatched off
|
||||
# the caller thread. If this fails the inline fast-path is back.
|
||||
check handlerTid != callerTid
|
||||
# And the callback payload (the recorded tid) matches what the handler stored.
|
||||
check cborDecode(callbackBytes(d), int).value == handlerTid
|
||||
|
||||
# Read the handle back
|
||||
var readD: CallbackData
|
||||
initCallbackData(readD)
|
||||
defer: deinitCallbackData(readD)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression for PR #23 review item 6: reentrancy guard on
|
||||
# sendRequestToFFIThread. A handler running on the FFI thread that tries to
|
||||
# dispatch back through sendRequestToFFIThread used to self-deadlock waiting
|
||||
# on `reqReceivedSignal` (which only the FFI thread can fire). The guard now
|
||||
# returns an Err immediately.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
let handleJson = ffiSerialize(cast[pointer](handleAddr))
|
||||
let readRet = testlib_read_handle(ctx, testCallback, addr readD, handleJson.cstring)
|
||||
check readRet == RET_OK
|
||||
var gReentrantNestedRes: Channel[string]
|
||||
gReentrantNestedRes.open()
|
||||
|
||||
waitCallback(readD)
|
||||
check readD.retCode == RET_OK
|
||||
# Handler runs on the FFI thread; it nests a send back into the same ctx and
|
||||
# reports the outcome through gReentrantNestedRes. Carrying the ctx address
|
||||
# via the request payload sidesteps the cross-thread visibility issue of
|
||||
# thread-local pointers.
|
||||
registerReqFFI(ReentrantTriggerReq, lib: ptr TestLib):
|
||||
proc(ctxAddr: int): Future[Result[string, string]] {.async.} =
|
||||
let ctx = cast[ptr FFIContext[TestLib]](cast[uint](ctxAddr))
|
||||
var nestedD: CallbackData
|
||||
initCallbackData(nestedD)
|
||||
defer:
|
||||
deinitCallbackData(nestedD)
|
||||
let res = sendRequestToFFIThread(
|
||||
ctx, PingRequest.ffiNewReq(testCallback, addr nestedD, "x".cstring)
|
||||
)
|
||||
if res.isErr():
|
||||
try:
|
||||
gReentrantNestedRes.send("err:" & res.error)
|
||||
except Exception as exc:
|
||||
return err("channel.send raised: " & exc.msg)
|
||||
return ok("guard-fired")
|
||||
try:
|
||||
gReentrantNestedRes.send("ok-unexpected")
|
||||
except Exception as exc:
|
||||
return err("channel.send raised: " & exc.msg)
|
||||
return ok("ok-unexpected")
|
||||
|
||||
let readMsg = callbackMsg(readD)
|
||||
let decodedStr = ffiDeserialize(readMsg.cstring, string).valueOr:
|
||||
suite "reentrancy guard (PR #23 review, item 6)":
|
||||
test "send from inside an FFI handler returns Err instead of deadlocking":
|
||||
var pool: FFIContextPool[TestLib]
|
||||
let ctx = pool.createFFIContext().valueOr:
|
||||
check false
|
||||
""
|
||||
check decodedStr == "test:5"
|
||||
return
|
||||
defer:
|
||||
discard pool.destroyFFIContext(ctx)
|
||||
|
||||
# Free the handle
|
||||
var freeD: CallbackData
|
||||
initCallbackData(freeD)
|
||||
defer: deinitCallbackData(freeD)
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer:
|
||||
deinitCallbackData(d)
|
||||
|
||||
let freeRet = testlib_free_handle(ctx, testCallback, addr freeD, handleJson.cstring)
|
||||
check freeRet == RET_OK
|
||||
let ctxAddrInt = cast[int](cast[uint](ctx))
|
||||
check sendRequestToFFIThread(
|
||||
ctx, ReentrantTriggerReq.ffiNewReq(testCallback, addr d, ctxAddrInt)
|
||||
)
|
||||
.isOk()
|
||||
|
||||
waitCallback(freeD)
|
||||
check freeD.retCode == RET_OK
|
||||
# The outer callback only fires once the handler — including its nested
|
||||
# send attempt — has finished. No polling/sleep needed.
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
check cborDecode(callbackBytes(d), string).value == "guard-fired"
|
||||
|
||||
let nestedMsg = gReentrantNestedRes.recv()
|
||||
check nestedMsg.startsWith("err:")
|
||||
check "reentrant ffi call" in nestedMsg
|
||||
|
||||
@ -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
56
tests/test_meta.nim
Normal 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"
|
||||
179
tests/test_nim_native_api.nim
Normal file
179
tests/test_nim_native_api.nim
Normal 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"
|
||||
@ -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
|
||||
|
||||
87
tests/test_string_helpers.nim
Normal file
87
tests/test_string_helpers.nim
Normal 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
108
tests/test_wire_compat.nim
Normal 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"
|
||||
Loading…
x
Reference in New Issue
Block a user