From a3e68333abefa6377795f0e12af5757e5aaf7b55 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 19:31:39 +0200 Subject: [PATCH] refactor(codegen): reconcile C++ header names with the native/cbor convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the C++ generators with the C generator and the symbol naming: the native (zero-serialization, same-process) wrapper is the bare `.hpp` and the CBOR (inter-process) wrapper carries the `_cbor` suffix — mirroring the C headers (`.h` / `_cbor.h`) and the `` / `_cbor` exports. Previously native was `_native.hpp` and CBOR was the bare `.hpp`, which is backwards from the symbol convention and would collide on the native `.h` when both ABIs emit into one dir (ffiMode=both). With the flip, a single `genbindings_cpp` run now drops `.hpp` + `_cbor.hpp` side by side, exactly like c_bindings holds both `.h` headers. Consumers updated to match: the CBOR cpp_bindings driver and the C++ e2e suite include `*_cbor.hpp`; the native example includes the bare `.hpp`. Validated: native example runs on `my_timer.hpp`; C++ e2e suite 19/19 on the `_cbor.hpp` headers; check_bindings_cpp regen is deterministic. Co-Authored-By: Claude Opus 4.8 --- examples/echo/cpp_bindings/echo.h | 66 + examples/echo/cpp_bindings/echo.hpp | 655 ++------- examples/echo/cpp_bindings/echo_cbor.hpp | 565 ++++++++ examples/timer/cpp_bindings/README.md | 9 +- examples/timer/cpp_bindings/main.cpp | 2 +- examples/timer/cpp_bindings/my_timer.h | 123 ++ examples/timer/cpp_bindings/my_timer.hpp | 1257 +++++------------ examples/timer/cpp_bindings/my_timer_cbor.hpp | 995 +++++++++++++ examples/timer/cpp_native_bindings/Makefile | 2 +- examples/timer/cpp_native_bindings/README.md | 11 +- examples/timer/cpp_native_bindings/main.cpp | 4 +- .../{my_timer_native.hpp => my_timer.hpp} | 0 ffi.nimble | 1 + ffi/codegen/cpp.nim | 13 +- ffi/codegen/cpp_native.nim | 10 +- tests/e2e/cpp/README.md | 4 +- tests/e2e/cpp/test_timer_e2e.cpp | 4 +- 17 files changed, 2255 insertions(+), 1466 deletions(-) create mode 100644 examples/echo/cpp_bindings/echo.h create mode 100644 examples/echo/cpp_bindings/echo_cbor.hpp create mode 100644 examples/timer/cpp_bindings/my_timer.h create mode 100644 examples/timer/cpp_bindings/my_timer_cbor.hpp rename examples/timer/cpp_native_bindings/{my_timer_native.hpp => my_timer.hpp} (100%) diff --git a/examples/echo/cpp_bindings/echo.h b/examples/echo/cpp_bindings/echo.h new file mode 100644 index 0000000..dc96a54 --- /dev/null +++ b/examples/echo/cpp_bindings/echo.h @@ -0,0 +1,66 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// Native (zero-serialization) C ABI. Each call delivers its result to the +// callback. On RET_OK: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// On RET_ERR, (msg, len) is the raw error text. A `_cbor` variant of each +// proc also exists for generic/cross-language callers that prefer CBOR. +#ifndef NIM_FFI_GEN_ECHO_H +#define NIM_FFI_GEN_ECHO_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + + +// --- {.ffi.}-annotated types, exposed as C structs ---------- +typedef struct { + const char* prefix; +} EchoConfig; + +typedef struct { + const char* text; +} ShoutRequest; + +typedef struct { + const char* shouted; + const char* prefix; +} ShoutResponse; + + +void *echo_create(EchoConfig config, FFICallBack callback, void *userData); + +int echo_shout(void *ctx, FFICallBack callback, void *userData, ShoutRequest req); + +int echo_version(void *ctx, FFICallBack callback, void *userData); + +int echo_destroy(void *ctx); + +uint64_t echo_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int echo_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_ECHO_H */ \ No newline at end of file diff --git a/examples/echo/cpp_bindings/echo.hpp b/examples/echo/cpp_bindings/echo.hpp index 1c26987..d14ad0d 100644 --- a/examples/echo/cpp_bindings/echo.hpp +++ b/examples/echo/cpp_bindings/echo.hpp @@ -1,565 +1,158 @@ -#pragma once -// Generated bindings require C++20 — the event-listener API uses -// std::span for the wildcard callback. -// MSVC keeps __cplusplus at 199711L unless /Zc:__cplusplus is passed, -// so consult _MSVC_LANG when present (it always reflects the active -// /std:c++XX level). -#if defined(_MSVC_LANG) -# if _MSVC_LANG < 202002L -# error "nim-ffi generated headers require C++20 or later (use /std:c++20)" -# endif -#elif !defined(__cplusplus) || __cplusplus < 202002L -# error "nim-ffi generated headers require C++20 or later" -#endif -#include +// Generated by nim-ffi native C++ codegen. Do not edit by hand. +// +// Native (zero-serialization) wrapper over the C ABI in "echo.h". Struct params/returns cross as flat C-POD structs — no CBOR. For the +// inter-process path use the CBOR header (echo_cbor.hpp). +#ifndef NIM_FFI_GEN_ECHO_NATIVE_HPP +#define NIM_FFI_GEN_ECHO_NATIVE_HPP + +#include "echo.h" #include -#include -#include -#include -#include -#include #include #include -#include +#include +#include #include -#include -#include -#include -extern "C" { -#include -} +#include +#include +#include -// ============================================================ -// Result — exception-free error channel -// ============================================================ -// The generated bindings never throw: every fallible entry point (create, -// instance methods, and their *Async futures) returns a Result. Callers -// branch on isOk()/isErr() (or the explicit bool conversion) and read -// value()/error(). This mirrors the Nim side's Result[T, string] and keeps -// us off C++23's std::expected. -#ifndef NIM_FFI_RESULT_HPP_INCLUDED -#define NIM_FFI_RESULT_HPP_INCLUDED - -template -class Result { - std::optional value_; - std::string error_; -public: - static Result ok(T value) { - Result r; - r.value_ = std::move(value); - return r; - } - static Result err(std::string message) { - Result r; - r.error_ = std::move(message); - return r; - } - bool isOk() const { return value_.has_value(); } - bool isErr() const { return !value_.has_value(); } - explicit operator bool() const { return isOk(); } - const T& value() const { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } - T& value() { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } - const T& operator*() const { assert(value_.has_value() && "Result::operator*() called on err Result — check isOk() first"); return *value_; } - const T* operator->() const { assert(value_.has_value() && "Result::operator->() called on err Result — check isOk() first"); return &*value_; } - T&& take() { assert(value_.has_value() && "Result::take() called on err Result — check isOk() first"); return std::move(*value_); } - const std::string& error() const { assert(!value_.has_value() && "Result::error() called on ok Result — check isErr() first"); return error_; } -}; - -template <> -class Result { - bool ok_ = true; - std::string error_; -public: - static Result ok() { - Result r; - r.ok_ = true; - return r; - } - static Result err(std::string message) { - Result r; - r.ok_ = false; - r.error_ = std::move(message); - return r; - } - Result() = default; - bool isOk() const { return ok_; } - bool isErr() const { return !ok_; } - explicit operator bool() const { return isOk(); } - const std::string& error() const { assert(!ok_ && "Result::error() called on ok Result — check isErr() first"); return error_; } -}; - -#endif // NIM_FFI_RESULT_HPP_INCLUDED - -// ── 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 they defer into. -// Guarded so two nim-ffi headers can share a translation unit. -#ifndef NIM_FFI_CBOR_HELPERS_HPP_INCLUDED -#define NIM_FFI_CBOR_HELPERS_HPP_INCLUDED - -inline CborError encode_cbor(CborEncoder& e, bool v) { - return cbor_encode_boolean(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, int64_t v) { - return cbor_encode_int(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, int32_t v) { - return cbor_encode_int(&e, static_cast(v)); -} -inline CborError encode_cbor(CborEncoder& e, uint64_t v) { - return cbor_encode_uint(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, double v) { - return cbor_encode_double(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, const std::string& v) { - return cbor_encode_text_string(&e, v.data(), v.size()); -} - -template -inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { - CborEncoder arr; - CborError err = cbor_encoder_create_array(&e, &arr, v.size()); - if (err) return err; - for (const auto& item : v) { - err = encode_cbor(arr, item); - if (err) return err; - } - return cbor_encoder_close_container(&e, &arr); -} - -template -inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { - if (!v) return cbor_encode_null(&e); - return encode_cbor(e, *v); -} - -// ── decode_cbor overloads ─────────────────────────────────────────────── - -inline CborError decode_cbor(CborValue& it, bool& out) { - if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_boolean(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, int64_t& out) { - if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_int64_checked(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, int32_t& out) { - int64_t tmp = 0; - CborError err = decode_cbor(it, tmp); - if (err) return err; - out = static_cast(tmp); - return CborNoError; -} -inline CborError decode_cbor(CborValue& it, uint64_t& out) { - if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_uint64(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, double& out) { - if (cbor_value_is_double(&it)) { - CborError err = cbor_value_get_double(&it, &out); - if (err) return err; - return cbor_value_advance(&it); - } - if (cbor_value_is_float(&it)) { - float f = 0.0f; - CborError err = cbor_value_get_float(&it, &f); - if (err) return err; - out = static_cast(f); - return cbor_value_advance(&it); - } - return CborErrorImproperValue; -} -inline CborError decode_cbor(CborValue& it, std::string& out) { - if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; - size_t len = 0; - CborError err = cbor_value_get_string_length(&it, &len); - if (err) return err; - out.resize(len); - err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); - if (err) return err; - return cbor_value_advance(&it); -} - -template -inline CborError decode_cbor(CborValue& it, std::vector& out) { - if (!cbor_value_is_array(&it)) return CborErrorImproperValue; - size_t len = 0; - CborError err = cbor_value_get_array_length(&it, &len); - if (err) return err; - out.clear(); - out.resize(len); - CborValue inner; - err = cbor_value_enter_container(&it, &inner); - if (err) return err; - for (size_t i = 0; i < len; ++i) { - err = decode_cbor(inner, out[i]); - if (err) return err; - } - return cbor_value_leave_container(&it, &inner); -} - -template -inline CborError decode_cbor(CborValue& it, std::optional& out) { - if (cbor_value_is_null(&it)) { - out = std::nullopt; - return cbor_value_advance(&it); - } - T tmp{}; - CborError err = decode_cbor(it, tmp); - if (err) return err; - out = std::move(tmp); - return CborNoError; -} - -// ── Public entry points ───────────────────────────────────────────────── - -template -inline Result> encodeCborFFI(const T& value) { - // Start with a generous 4 KiB buffer; double on overflow until it fits. - std::vector buf(4096); - while (true) { - CborEncoder enc; - cbor_encoder_init(&enc, buf.data(), buf.size(), 0); - CborError err = encode_cbor(enc, value); - if (err == CborNoError) { - const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); - buf.resize(used); - return Result>::ok(std::move(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; - } - return Result>::err( - std::string("FFI CBOR encode failed: ") + cbor_error_string(err)); - } -} - -template -inline Result decodeCborFFI(const std::vector& bytes) { - CborParser parser; - CborValue it; - CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); - if (err != CborNoError) { - return Result::err(std::string("FFI CBOR parse init failed: ") + - cbor_error_string(err)); - } - T out{}; - err = decode_cbor(it, out); - if (err != CborNoError) { - return Result::err(std::string("FFI CBOR decode failed: ") + - cbor_error_string(err)); - } - return Result::ok(std::move(out)); -} - -#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED - -// ============================================================ -// User-declared FFI types -// ============================================================ +namespace echo { struct EchoConfig { - std::string prefix; + std::string prefix{}; }; -inline CborError encode_cbor(CborEncoder& e, const EchoConfig& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 1); - if (err) return err; - err = cbor_encode_text_stringz(&m, "prefix"); if (err) return err; - err = encode_cbor(m, v.prefix); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct EchoConfigC { + ::EchoConfig c{}; +}; +inline EchoConfigC toC(const EchoConfig& v) { + EchoConfigC h; + h.c.prefix = v.prefix.c_str(); + return h; } -inline CborError decode_cbor(CborValue& it, EchoConfig& v) { - if (!cbor_value_is_map(&it)) return CborErrorImproperValue; - CborValue field; - CborError err; - err = cbor_value_map_find_value(&it, "prefix", &field); if (err) return err; - if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; - err = decode_cbor(field, v.prefix); if (err) return err; - return cbor_value_advance(&it); +inline EchoConfig fromC(const ::EchoConfig& c) { + EchoConfig v{}; + v.prefix = c.prefix ? std::string(c.prefix) : std::string(); + return v; } struct ShoutRequest { - std::string text; + std::string text{}; }; -inline CborError encode_cbor(CborEncoder& e, const ShoutRequest& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 1); - if (err) return err; - err = cbor_encode_text_stringz(&m, "text"); if (err) return err; - err = encode_cbor(m, v.text); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct ShoutRequestC { + ::ShoutRequest c{}; +}; +inline ShoutRequestC toC(const ShoutRequest& v) { + ShoutRequestC h; + h.c.text = v.text.c_str(); + return h; } -inline CborError decode_cbor(CborValue& it, ShoutRequest& v) { - if (!cbor_value_is_map(&it)) return CborErrorImproperValue; - CborValue field; - CborError err; - err = cbor_value_map_find_value(&it, "text", &field); if (err) return err; - if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; - err = decode_cbor(field, v.text); if (err) return err; - return cbor_value_advance(&it); +inline ShoutRequest fromC(const ::ShoutRequest& c) { + ShoutRequest v{}; + v.text = c.text ? std::string(c.text) : std::string(); + return v; } struct ShoutResponse { - std::string shouted; - std::string prefix; + std::string shouted{}; + std::string prefix{}; }; -inline CborError encode_cbor(CborEncoder& e, const ShoutResponse& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 2); - if (err) return err; - err = cbor_encode_text_stringz(&m, "shouted"); if (err) return err; - err = encode_cbor(m, v.shouted); if (err) return err; - err = cbor_encode_text_stringz(&m, "prefix"); if (err) return err; - err = encode_cbor(m, v.prefix); if (err) return err; - return cbor_encoder_close_container(&e, &m); -} -inline CborError decode_cbor(CborValue& it, ShoutResponse& v) { - if (!cbor_value_is_map(&it)) return CborErrorImproperValue; - CborValue field; - CborError err; - err = cbor_value_map_find_value(&it, "shouted", &field); if (err) return err; - if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; - err = decode_cbor(field, v.shouted); if (err) return err; - err = cbor_value_map_find_value(&it, "prefix", &field); if (err) return err; - if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; - err = decode_cbor(field, v.prefix); if (err) return err; - return cbor_value_advance(&it); -} - -// ============================================================ -// Per-proc request envelopes (CBOR encoded on the wire) -// ============================================================ - -struct EchoCreateCtorReq { - EchoConfig config; +struct ShoutResponseC { + ::ShoutResponse c{}; }; -inline CborError encode_cbor(CborEncoder& e, const EchoCreateCtorReq& 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 ShoutResponseC toC(const ShoutResponse& v) { + ShoutResponseC h; + h.c.shouted = v.shouted.c_str(); + h.c.prefix = v.prefix.c_str(); + return h; } -inline CborError decode_cbor(CborValue& it, EchoCreateCtorReq& 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); +inline ShoutResponse fromC(const ::ShoutResponse& c) { + ShoutResponse v{}; + v.shouted = c.shouted ? std::string(c.shouted) : std::string(); + v.prefix = c.prefix ? std::string(c.prefix) : std::string(); + return v; } -struct EchoShoutReq { - ShoutRequest req; +namespace detail { +template struct Capture { + int ret = RET_ERR; + T value{}; + std::string err; + std::promise done; }; -inline CborError encode_cbor(CborEncoder& e, const EchoShoutReq& 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, EchoShoutReq& 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 EchoVersionReq { +struct AckCapture { + int ret = RET_ERR; + std::string err; + std::promise done; }; -inline CborError encode_cbor(CborEncoder& e, const EchoVersionReq&) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 0); - if (err) return err; - return cbor_encoder_close_container(&e, &m); +inline std::string rawText(const char* msg, std::size_t len) { + return (msg && len) ? std::string(msg, len) : std::string(); } -inline CborError decode_cbor(CborValue& it, EchoVersionReq&) { - if (!cbor_value_is_map(&it)) return CborErrorImproperValue; - return cbor_value_advance(&it); -} - -// ============================================================ -// C FFI declarations -// ============================================================ +} // namespace detail extern "C" { -typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data); - -void* echo_create_cbor(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); -int echo_shout_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int echo_version_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int echo_destroy(void* ctx); -uint64_t echo_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); -int echo_remove_event_listener(void* ctx, uint64_t listener_id); +inline void echo_native_ack(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast(ud); + c->ret = ret; + if (ret == RET_ERR) c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void echo_native_str(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = detail::rawText(msg, len); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void echo_native_echo_shout(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg)); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} } // extern "C" -// ============================================================ -// Synchronous call helper -// ============================================================ -// Guarded so two nim-ffi headers can share a translation unit. -#ifndef NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED -#define NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED +class EchoNode { + public: + explicit EchoNode(const EchoConfig& config) { + detail::AckCapture cap; + auto fut = cap.done.get_future(); + auto c_config = toC(config); + ctx_ = echo_create(c_config.c, echo_native_ack, &cap); + if (!ctx_) throw std::runtime_error("echo_create returned null"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + } -namespace { + ShoutResponse Shout(const ShoutRequest& req) { + detail::Capture cap; + auto fut = cap.done.get_future(); + auto c_req = toC(req); + if (echo_shout(ctx_, echo_native_echo_shout, &cap, c_req.c) != RET_OK) + throw std::runtime_error("echo_shout dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } -struct FFICallState_ { - std::mutex mtx; - std::condition_variable cv; - bool done{false}; - bool ok{false}; - std::vector bytes; - std::string err; + std::string Version() { + detail::Capture cap; + auto fut = cap.done.get_future(); + if (echo_version(ctx_, echo_native_str, &cap) != RET_OK) + throw std::runtime_error("echo_version dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } + + ~EchoNode() { if (ctx_) echo_destroy(ctx_); } + EchoNode(const EchoNode&) = delete; + EchoNode& operator=(const EchoNode&) = delete; + + private: + void* ctx_ = nullptr; }; -inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { - // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; - // take ownership here so it's freed on every exit path. - std::unique_ptr> handle( - static_cast*>(ud)); - FFICallState_& s = **handle; +} // namespace echo - std::lock_guard lock(s.mtx); - s.ok = (ret == 0); - if (msg && len > 0) { - const auto* p = reinterpret_cast(msg); - if (s.ok) s.bytes.assign(p, p + len); - else s.err.assign(msg, len); - } - s.done = true; - s.cv.notify_one(); -} - -inline Result> ffi_call_( - std::function f, - std::chrono::milliseconds timeout) { - using Bytes = std::vector; - auto state = std::make_shared(); - auto* cb_ref = new std::shared_ptr(state); - const int ret = f(ffi_cb_, cb_ref); - if (ret == 2) { - delete cb_ref; - return Result::err("RET_MISSING_CALLBACK (internal error)"); - } - std::unique_lock lock(state->mtx); - const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); - if (!fired) - return Result::err("FFI call timed out after " + - std::to_string(timeout.count()) + "ms"); - if (!state->ok) - return Result::err(state->err); - return Result::ok(std::move(state->bytes)); -} - -} // anonymous namespace - -#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED - -// ============================================================ -// High-level C++ context class -// ============================================================ - -class EchoCtx { -public: - static Result> create(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - const auto ffi_req_ = EchoCreateCtorReq{config}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result>::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - (void)echo_create_cbor(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud); - return 0; - }, timeout); - if (ffi_raw_.isErr()) return Result>::err(ffi_raw_.error()); - auto ffi_addr_ = decodeCborFFI(ffi_raw_.value()); - if (ffi_addr_.isErr()) return Result>::err(ffi_addr_.error()); - const auto& addr_str = ffi_addr_.value(); - std::uint64_t addr = 0; - const char* addr_begin = addr_str.data(); - const char* addr_end = addr_begin + addr_str.size(); - const auto fc_ = std::from_chars(addr_begin, addr_end, addr); - if (fc_.ec != std::errc() || fc_.ptr != addr_end) { - return Result>::err("FFI create returned non-numeric address: " + addr_str); - } - return Result>::ok(std::unique_ptr(new EchoCtx(reinterpret_cast(static_cast(addr)), timeout))); - } - - static std::future>> createAsync(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); - } - - // Special-member policy: this class owns a echo context, which in - // turn owns the library's worker thread(s) and internal state. Moving - // such an object out from under a caller silently tears that state - // down and is easy to misuse (e.g. storing in a container that - // relocates its elements). It also has no clean analogue in the other - // binding languages we generate. So copies and moves are both - // deleted; ownership is transferred via EchoCtx::create returning a - // std::unique_ptr. The destructor still releases the - // context. - ~EchoCtx() { - if (ptr_) { - echo_destroy(ptr_); - ptr_ = nullptr; - } - } - - EchoCtx(const EchoCtx&) = delete; - EchoCtx& operator=(const EchoCtx&) = delete; - EchoCtx(EchoCtx&&) = delete; - EchoCtx& operator=(EchoCtx&&) = delete; - - Result shout(const ShoutRequest& req) const { - const auto ffi_req_ = EchoShoutReq{req}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return echo_shout_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } - - std::future> shoutAsync(const ShoutRequest& req) const { - return std::async(std::launch::async, [this, req]() { return this->shout(req); }); - } - - Result version() const { - const auto ffi_req_ = EchoVersionReq{}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return echo_version_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } - - std::future> versionAsync() const { - return std::async(std::launch::async, [this]() { return this->version(); }); - } - -private: - void* ptr_; - std::chrono::milliseconds timeout_; - explicit EchoCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} -}; +#endif // NIM_FFI_GEN_ECHO_NATIVE_HPP \ No newline at end of file diff --git a/examples/echo/cpp_bindings/echo_cbor.hpp b/examples/echo/cpp_bindings/echo_cbor.hpp new file mode 100644 index 0000000..1c26987 --- /dev/null +++ b/examples/echo/cpp_bindings/echo_cbor.hpp @@ -0,0 +1,565 @@ +#pragma once +// Generated bindings require C++20 — the event-listener API uses +// std::span for the wildcard callback. +// MSVC keeps __cplusplus at 199711L unless /Zc:__cplusplus is passed, +// so consult _MSVC_LANG when present (it always reflects the active +// /std:c++XX level). +#if defined(_MSVC_LANG) +# if _MSVC_LANG < 202002L +# error "nim-ffi generated headers require C++20 or later (use /std:c++20)" +# endif +#elif !defined(__cplusplus) || __cplusplus < 202002L +# error "nim-ffi generated headers require C++20 or later" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} + +// ============================================================ +// Result — exception-free error channel +// ============================================================ +// The generated bindings never throw: every fallible entry point (create, +// instance methods, and their *Async futures) returns a Result. Callers +// branch on isOk()/isErr() (or the explicit bool conversion) and read +// value()/error(). This mirrors the Nim side's Result[T, string] and keeps +// us off C++23's std::expected. +#ifndef NIM_FFI_RESULT_HPP_INCLUDED +#define NIM_FFI_RESULT_HPP_INCLUDED + +template +class Result { + std::optional value_; + std::string error_; +public: + static Result ok(T value) { + Result r; + r.value_ = std::move(value); + return r; + } + static Result err(std::string message) { + Result r; + r.error_ = std::move(message); + return r; + } + bool isOk() const { return value_.has_value(); } + bool isErr() const { return !value_.has_value(); } + explicit operator bool() const { return isOk(); } + const T& value() const { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + T& value() { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + const T& operator*() const { assert(value_.has_value() && "Result::operator*() called on err Result — check isOk() first"); return *value_; } + const T* operator->() const { assert(value_.has_value() && "Result::operator->() called on err Result — check isOk() first"); return &*value_; } + T&& take() { assert(value_.has_value() && "Result::take() called on err Result — check isOk() first"); return std::move(*value_); } + const std::string& error() const { assert(!value_.has_value() && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +template <> +class Result { + bool ok_ = true; + std::string error_; +public: + static Result ok() { + Result r; + r.ok_ = true; + return r; + } + static Result err(std::string message) { + Result r; + r.ok_ = false; + r.error_ = std::move(message); + return r; + } + Result() = default; + bool isOk() const { return ok_; } + bool isErr() const { return !ok_; } + explicit operator bool() const { return isOk(); } + const std::string& error() const { assert(!ok_ && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +#endif // NIM_FFI_RESULT_HPP_INCLUDED + +// ── 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 they defer into. +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_CBOR_HELPERS_HPP_INCLUDED +#define NIM_FFI_CBOR_HELPERS_HPP_INCLUDED + +inline CborError encode_cbor(CborEncoder& e, bool v) { + return cbor_encode_boolean(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int64_t v) { + return cbor_encode_int(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int32_t v) { + return cbor_encode_int(&e, static_cast(v)); +} +inline CborError encode_cbor(CborEncoder& e, uint64_t v) { + return cbor_encode_uint(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, double v) { + return cbor_encode_double(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, const std::string& v) { + return cbor_encode_text_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(&e, &arr, v.size()); + if (err) return err; + for (const auto& item : v) { + err = encode_cbor(arr, item); + if (err) return err; + } + return cbor_encoder_close_container(&e, &arr); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { + if (!v) return cbor_encode_null(&e); + return encode_cbor(e, *v); +} + +// ── decode_cbor overloads ─────────────────────────────────────────────── + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_boolean(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int64_t& out) { + if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_int64_checked(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int32_t& out) { + int64_t tmp = 0; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = static_cast(tmp); + return CborNoError; +} +inline CborError decode_cbor(CborValue& it, uint64_t& out) { + if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_uint64(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, double& out) { + if (cbor_value_is_double(&it)) { + CborError err = cbor_value_get_double(&it, &out); + if (err) return err; + return cbor_value_advance(&it); + } + if (cbor_value_is_float(&it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(&it, &f); + if (err) return err; + out = static_cast(f); + return cbor_value_advance(&it); + } + return CborErrorImproperValue; +} +inline CborError decode_cbor(CborValue& it, std::string& out) { + if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + +template +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_array(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(&it, &len); + if (err) return err; + out.clear(); + out.resize(len); + CborValue inner; + err = cbor_value_enter_container(&it, &inner); + if (err) return err; + for (size_t i = 0; i < len; ++i) { + err = decode_cbor(inner, out[i]); + if (err) return err; + } + return cbor_value_leave_container(&it, &inner); +} + +template +inline CborError decode_cbor(CborValue& it, std::optional& out) { + if (cbor_value_is_null(&it)) { + out = std::nullopt; + return cbor_value_advance(&it); + } + T tmp{}; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = std::move(tmp); + return CborNoError; +} + +// ── Public entry points ───────────────────────────────────────────────── + +template +inline Result> encodeCborFFI(const T& value) { + // Start with a generous 4 KiB buffer; double on overflow until it fits. + std::vector buf(4096); + while (true) { + CborEncoder enc; + cbor_encoder_init(&enc, buf.data(), buf.size(), 0); + CborError err = encode_cbor(enc, value); + if (err == CborNoError) { + const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); + buf.resize(used); + return Result>::ok(std::move(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; + } + return Result>::err( + std::string("FFI CBOR encode failed: ") + cbor_error_string(err)); + } +} + +template +inline Result decodeCborFFI(const std::vector& bytes) { + CborParser parser; + CborValue it; + CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR parse init failed: ") + + cbor_error_string(err)); + } + T out{}; + err = decode_cbor(it, out); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR decode failed: ") + + cbor_error_string(err)); + } + return Result::ok(std::move(out)); +} + +#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED + +// ============================================================ +// User-declared FFI types +// ============================================================ + +struct EchoConfig { + std::string prefix; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoConfig& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "prefix"); if (err) return err; + err = encode_cbor(m, v.prefix); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoConfig& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "prefix", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.prefix); if (err) return err; + return cbor_value_advance(&it); +} + +struct ShoutRequest { + std::string text; +}; +inline CborError encode_cbor(CborEncoder& e, const ShoutRequest& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "text"); if (err) return err; + err = encode_cbor(m, v.text); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ShoutRequest& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "text", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.text); if (err) return err; + return cbor_value_advance(&it); +} + +struct ShoutResponse { + std::string shouted; + std::string prefix; +}; +inline CborError encode_cbor(CborEncoder& e, const ShoutResponse& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "shouted"); if (err) return err; + err = encode_cbor(m, v.shouted); if (err) return err; + err = cbor_encode_text_stringz(&m, "prefix"); if (err) return err; + err = encode_cbor(m, v.prefix); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ShoutResponse& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "shouted", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.shouted); if (err) return err; + err = cbor_value_map_find_value(&it, "prefix", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.prefix); if (err) return err; + return cbor_value_advance(&it); +} + +// ============================================================ +// Per-proc request envelopes (CBOR encoded on the wire) +// ============================================================ + +struct EchoCreateCtorReq { + EchoConfig config; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoCreateCtorReq& 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, EchoCreateCtorReq& 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 EchoShoutReq { + ShoutRequest req; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoShoutReq& 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, EchoShoutReq& 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 EchoVersionReq { +}; +inline CborError encode_cbor(CborEncoder& e, const EchoVersionReq&) { + 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, EchoVersionReq&) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + 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* echo_create_cbor(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); +int echo_shout_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int echo_version_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int echo_destroy(void* ctx); +uint64_t echo_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); +int echo_remove_event_listener(void* ctx, uint64_t listener_id); +} // extern "C" + +// ============================================================ +// Synchronous call helper +// ============================================================ +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED +#define NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED + +namespace { + +struct FFICallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::vector bytes; + std::string err; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { + // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; + // take ownership here so it's freed on every exit path. + std::unique_ptr> handle( + static_cast*>(ud)); + FFICallState_& s = **handle; + + std::lock_guard lock(s.mtx); + s.ok = (ret == 0); + if (msg && len > 0) { + const auto* p = reinterpret_cast(msg); + if (s.ok) s.bytes.assign(p, p + len); + else s.err.assign(msg, len); + } + s.done = true; + s.cv.notify_one(); +} + +inline Result> ffi_call_( + std::function f, + std::chrono::milliseconds timeout) { + using Bytes = std::vector; + auto state = std::make_shared(); + auto* cb_ref = new std::shared_ptr(state); + const int ret = f(ffi_cb_, cb_ref); + if (ret == 2) { + delete cb_ref; + return Result::err("RET_MISSING_CALLBACK (internal error)"); + } + std::unique_lock lock(state->mtx); + const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); + if (!fired) + return Result::err("FFI call timed out after " + + std::to_string(timeout.count()) + "ms"); + if (!state->ok) + return Result::err(state->err); + return Result::ok(std::move(state->bytes)); +} + +} // anonymous namespace + +#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED + +// ============================================================ +// High-level C++ context class +// ============================================================ + +class EchoCtx { +public: + static Result> create(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + const auto ffi_req_ = EchoCreateCtorReq{config}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result>::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + (void)echo_create_cbor(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud); + return 0; + }, timeout); + if (ffi_raw_.isErr()) return Result>::err(ffi_raw_.error()); + auto ffi_addr_ = decodeCborFFI(ffi_raw_.value()); + if (ffi_addr_.isErr()) return Result>::err(ffi_addr_.error()); + const auto& addr_str = ffi_addr_.value(); + std::uint64_t addr = 0; + const char* addr_begin = addr_str.data(); + const char* addr_end = addr_begin + addr_str.size(); + const auto fc_ = std::from_chars(addr_begin, addr_end, addr); + if (fc_.ec != std::errc() || fc_.ptr != addr_end) { + return Result>::err("FFI create returned non-numeric address: " + addr_str); + } + return Result>::ok(std::unique_ptr(new EchoCtx(reinterpret_cast(static_cast(addr)), timeout))); + } + + static std::future>> createAsync(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); + } + + // Special-member policy: this class owns a echo context, which in + // turn owns the library's worker thread(s) and internal state. Moving + // such an object out from under a caller silently tears that state + // down and is easy to misuse (e.g. storing in a container that + // relocates its elements). It also has no clean analogue in the other + // binding languages we generate. So copies and moves are both + // deleted; ownership is transferred via EchoCtx::create returning a + // std::unique_ptr. The destructor still releases the + // context. + ~EchoCtx() { + if (ptr_) { + echo_destroy(ptr_); + ptr_ = nullptr; + } + } + + EchoCtx(const EchoCtx&) = delete; + EchoCtx& operator=(const EchoCtx&) = delete; + EchoCtx(EchoCtx&&) = delete; + EchoCtx& operator=(EchoCtx&&) = delete; + + Result shout(const ShoutRequest& req) const { + const auto ffi_req_ = EchoShoutReq{req}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return echo_shout_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> shoutAsync(const ShoutRequest& req) const { + return std::async(std::launch::async, [this, req]() { return this->shout(req); }); + } + + Result version() const { + const auto ffi_req_ = EchoVersionReq{}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return echo_version_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> versionAsync() const { + return std::async(std::launch::async, [this]() { return this->version(); }); + } + +private: + void* ptr_; + std::chrono::milliseconds timeout_; + explicit EchoCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} +}; diff --git a/examples/timer/cpp_bindings/README.md b/examples/timer/cpp_bindings/README.md index ba61c49..26cb46d 100644 --- a/examples/timer/cpp_bindings/README.md +++ b/examples/timer/cpp_bindings/README.md @@ -2,12 +2,15 @@ ## Purpose -This folder contains **auto-generated C++ bindings** for the `my_timer` Nim library. It is generated from `../timer.nim` and provides: +This folder contains **auto-generated C++ bindings** for the `my_timer` Nim library. It is generated from `../timer.nim` (with the default `both` ABI mode) and provides: -- `my_timer.hpp`: High-level C++ class (`MyTimerCtx`) wrapping the FFI interface -- `main.cpp`: Example executable demonstrating how to use the bindings +- `my_timer_cbor.hpp`: High-level C++ class (`MyTimerCtx`) wrapping the **CBOR** FFI interface (inter-process) +- `my_timer.hpp` + `my_timer.h`: the **native** (zero-serialization, same-process) wrapper and the C ABI it builds on +- `main.cpp`: Example executable demonstrating how to use the CBOR bindings - `CMakeLists.txt`: Build configuration that compiles the Nim library and links the C++ example +The native header is the bare `my_timer.hpp` and the CBOR header carries the `_cbor` suffix — the same convention as the C bindings (`my_timer.h` / `my_timer_cbor.h`) and the underlying symbols (`` / `_cbor`). + ## How It's Generated Generate or regenerate these bindings by running from the parent directory: diff --git a/examples/timer/cpp_bindings/main.cpp b/examples/timer/cpp_bindings/main.cpp index 3ab361d..cc056a5 100644 --- a/examples/timer/cpp_bindings/main.cpp +++ b/examples/timer/cpp_bindings/main.cpp @@ -1,4 +1,4 @@ -#include "my_timer.hpp" +#include "my_timer_cbor.hpp" #include #include #include diff --git a/examples/timer/cpp_bindings/my_timer.h b/examples/timer/cpp_bindings/my_timer.h new file mode 100644 index 0000000..d5270b0 --- /dev/null +++ b/examples/timer/cpp_bindings/my_timer.h @@ -0,0 +1,123 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// Native (zero-serialization) C ABI. Each call delivers its result to the +// callback. On RET_OK: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// On RET_ERR, (msg, len) is the raw error text. A `_cbor` variant of each +// proc also exists for generic/cross-language callers that prefer CBOR. +#ifndef NIM_FFI_GEN_MY_TIMER_H +#define NIM_FFI_GEN_MY_TIMER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + + +// --- {.ffi.}-annotated types, exposed as C structs ---------- +typedef struct { + const char* name; +} TimerConfig; + +typedef struct { + const char* message; + int64_t delayMs; +} EchoRequest; + +typedef struct { + const char* echoed; + const char* timerName; +} EchoResponse; + +typedef struct { + EchoRequest *messages; + size_t messages_len; + const char* *tags; + size_t tags_len; + int note_present; + const char* note; + int retries_present; + int64_t retries; +} ComplexRequest; + +typedef struct { + const char* summary; + int64_t itemCount; + int hasNote; +} ComplexResponse; + +typedef struct { + const char* message; + int64_t echoCount; +} EchoEvent; + +typedef struct { + const char* name; + const char* *payload; + size_t payload_len; + int64_t priority; +} JobSpec; + +typedef struct { + int64_t maxAttempts; + int64_t backoffMs; + const char* *retryOn; + size_t retryOn_len; +} RetryPolicy; + +typedef struct { + int64_t startAtMs; + int64_t intervalMs; + int jitter_present; + int64_t jitter; +} ScheduleConfig; + +typedef struct { + const char* jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +} ScheduleResult; + + +void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData); + +int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req); + +int my_timer_version(void *ctx, FFICallBack callback, void *userData); + +int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req); + +int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule); + +int my_timer_destroy(void *ctx); + +// Native event payloads — cast the callback's msg accordingly: +// "on_echo_fired" -> const EchoEvent * +uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_MY_TIMER_H */ \ No newline at end of file diff --git a/examples/timer/cpp_bindings/my_timer.hpp b/examples/timer/cpp_bindings/my_timer.hpp index 8a0591d..29e5d1b 100644 --- a/examples/timer/cpp_bindings/my_timer.hpp +++ b/examples/timer/cpp_bindings/my_timer.hpp @@ -1,995 +1,432 @@ -#pragma once -// Generated bindings require C++20 — the event-listener API uses -// std::span for the wildcard callback. -// MSVC keeps __cplusplus at 199711L unless /Zc:__cplusplus is passed, -// so consult _MSVC_LANG when present (it always reflects the active -// /std:c++XX level). -#if defined(_MSVC_LANG) -# if _MSVC_LANG < 202002L -# error "nim-ffi generated headers require C++20 or later (use /std:c++20)" -# endif -#elif !defined(__cplusplus) || __cplusplus < 202002L -# error "nim-ffi generated headers require C++20 or later" -#endif -#include +// Generated by nim-ffi native C++ codegen. Do not edit by hand. +// +// Native (zero-serialization) wrapper over the C ABI in "my_timer.h". Struct params/returns cross as flat C-POD structs — no CBOR. For the +// inter-process path use the CBOR header (my_timer_cbor.hpp). +#ifndef NIM_FFI_GEN_MY_TIMER_NATIVE_HPP +#define NIM_FFI_GEN_MY_TIMER_NATIVE_HPP + +#include "my_timer.h" #include -#include -#include -#include -#include -#include #include #include -#include +#include +#include #include -#include -#include -#include -extern "C" { -#include -} +#include +#include +#include -#include -#include -// ============================================================ -// Result — exception-free error channel -// ============================================================ -// The generated bindings never throw: every fallible entry point (create, -// instance methods, and their *Async futures) returns a Result. Callers -// branch on isOk()/isErr() (or the explicit bool conversion) and read -// value()/error(). This mirrors the Nim side's Result[T, string] and keeps -// us off C++23's std::expected. -#ifndef NIM_FFI_RESULT_HPP_INCLUDED -#define NIM_FFI_RESULT_HPP_INCLUDED - -template -class Result { - std::optional value_; - std::string error_; -public: - static Result ok(T value) { - Result r; - r.value_ = std::move(value); - return r; - } - static Result err(std::string message) { - Result r; - r.error_ = std::move(message); - return r; - } - bool isOk() const { return value_.has_value(); } - bool isErr() const { return !value_.has_value(); } - explicit operator bool() const { return isOk(); } - const T& value() const { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } - T& value() { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } - const T& operator*() const { assert(value_.has_value() && "Result::operator*() called on err Result — check isOk() first"); return *value_; } - const T* operator->() const { assert(value_.has_value() && "Result::operator->() called on err Result — check isOk() first"); return &*value_; } - T&& take() { assert(value_.has_value() && "Result::take() called on err Result — check isOk() first"); return std::move(*value_); } - const std::string& error() const { assert(!value_.has_value() && "Result::error() called on ok Result — check isErr() first"); return error_; } -}; - -template <> -class Result { - bool ok_ = true; - std::string error_; -public: - static Result ok() { - Result r; - r.ok_ = true; - return r; - } - static Result err(std::string message) { - Result r; - r.ok_ = false; - r.error_ = std::move(message); - return r; - } - Result() = default; - bool isOk() const { return ok_; } - bool isErr() const { return !ok_; } - explicit operator bool() const { return isOk(); } - const std::string& error() const { assert(!ok_ && "Result::error() called on ok Result — check isErr() first"); return error_; } -}; - -#endif // NIM_FFI_RESULT_HPP_INCLUDED - -// ── 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 they defer into. -// Guarded so two nim-ffi headers can share a translation unit. -#ifndef NIM_FFI_CBOR_HELPERS_HPP_INCLUDED -#define NIM_FFI_CBOR_HELPERS_HPP_INCLUDED - -inline CborError encode_cbor(CborEncoder& e, bool v) { - return cbor_encode_boolean(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, int64_t v) { - return cbor_encode_int(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, int32_t v) { - return cbor_encode_int(&e, static_cast(v)); -} -inline CborError encode_cbor(CborEncoder& e, uint64_t v) { - return cbor_encode_uint(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, double v) { - return cbor_encode_double(&e, v); -} -inline CborError encode_cbor(CborEncoder& e, const std::string& v) { - return cbor_encode_text_string(&e, v.data(), v.size()); -} - -template -inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { - CborEncoder arr; - CborError err = cbor_encoder_create_array(&e, &arr, v.size()); - if (err) return err; - for (const auto& item : v) { - err = encode_cbor(arr, item); - if (err) return err; - } - return cbor_encoder_close_container(&e, &arr); -} - -template -inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { - if (!v) return cbor_encode_null(&e); - return encode_cbor(e, *v); -} - -// ── decode_cbor overloads ─────────────────────────────────────────────── - -inline CborError decode_cbor(CborValue& it, bool& out) { - if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_boolean(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, int64_t& out) { - if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_int64_checked(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, int32_t& out) { - int64_t tmp = 0; - CborError err = decode_cbor(it, tmp); - if (err) return err; - out = static_cast(tmp); - return CborNoError; -} -inline CborError decode_cbor(CborValue& it, uint64_t& out) { - if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_uint64(&it, &out); - if (err) return err; - return cbor_value_advance(&it); -} -inline CborError decode_cbor(CborValue& it, double& out) { - if (cbor_value_is_double(&it)) { - CborError err = cbor_value_get_double(&it, &out); - if (err) return err; - return cbor_value_advance(&it); - } - if (cbor_value_is_float(&it)) { - float f = 0.0f; - CborError err = cbor_value_get_float(&it, &f); - if (err) return err; - out = static_cast(f); - return cbor_value_advance(&it); - } - return CborErrorImproperValue; -} -inline CborError decode_cbor(CborValue& it, std::string& out) { - if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; - size_t len = 0; - CborError err = cbor_value_get_string_length(&it, &len); - if (err) return err; - out.resize(len); - err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); - if (err) return err; - return cbor_value_advance(&it); -} - -template -inline CborError decode_cbor(CborValue& it, std::vector& out) { - if (!cbor_value_is_array(&it)) return CborErrorImproperValue; - size_t len = 0; - CborError err = cbor_value_get_array_length(&it, &len); - if (err) return err; - out.clear(); - out.resize(len); - CborValue inner; - err = cbor_value_enter_container(&it, &inner); - if (err) return err; - for (size_t i = 0; i < len; ++i) { - err = decode_cbor(inner, out[i]); - if (err) return err; - } - return cbor_value_leave_container(&it, &inner); -} - -template -inline CborError decode_cbor(CborValue& it, std::optional& out) { - if (cbor_value_is_null(&it)) { - out = std::nullopt; - return cbor_value_advance(&it); - } - T tmp{}; - CborError err = decode_cbor(it, tmp); - if (err) return err; - out = std::move(tmp); - return CborNoError; -} - -// ── Public entry points ───────────────────────────────────────────────── - -template -inline Result> encodeCborFFI(const T& value) { - // Start with a generous 4 KiB buffer; double on overflow until it fits. - std::vector buf(4096); - while (true) { - CborEncoder enc; - cbor_encoder_init(&enc, buf.data(), buf.size(), 0); - CborError err = encode_cbor(enc, value); - if (err == CborNoError) { - const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); - buf.resize(used); - return Result>::ok(std::move(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; - } - return Result>::err( - std::string("FFI CBOR encode failed: ") + cbor_error_string(err)); - } -} - -template -inline Result decodeCborFFI(const std::vector& bytes) { - CborParser parser; - CborValue it; - CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); - if (err != CborNoError) { - return Result::err(std::string("FFI CBOR parse init failed: ") + - cbor_error_string(err)); - } - T out{}; - err = decode_cbor(it, out); - if (err != CborNoError) { - return Result::err(std::string("FFI CBOR decode failed: ") + - cbor_error_string(err)); - } - return Result::ok(std::move(out)); -} - -#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED - -// ============================================================ -// User-declared FFI types -// ============================================================ +namespace my_timer { struct TimerConfig { - std::string name; + 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); +struct TimerConfigC { + ::TimerConfig c{}; +}; +inline TimerConfigC toC(const TimerConfig& v) { + TimerConfigC h; + h.c.name = v.name.c_str(); + return h; } -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); +inline TimerConfig fromC(const ::TimerConfig& c) { + TimerConfig v{}; + v.name = c.name ? std::string(c.name) : std::string(); + return v; } struct EchoRequest { - std::string message; - int64_t delayMs; + 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); +struct EchoRequestC { + ::EchoRequest c{}; +}; +inline EchoRequestC toC(const EchoRequest& v) { + EchoRequestC h; + h.c.message = v.message.c_str(); + h.c.delayMs = v.delayMs; + return h; } -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); +inline EchoRequest fromC(const ::EchoRequest& c) { + EchoRequest v{}; + v.message = c.message ? std::string(c.message) : std::string(); + v.delayMs = c.delayMs; + return v; } struct EchoResponse { - std::string echoed; - std::string timerName; + 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); +struct EchoResponseC { + ::EchoResponse c{}; +}; +inline EchoResponseC toC(const EchoResponse& v) { + EchoResponseC h; + h.c.echoed = v.echoed.c_str(); + h.c.timerName = v.timerName.c_str(); + return h; } -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); +inline EchoResponse fromC(const ::EchoResponse& c) { + EchoResponse v{}; + v.echoed = c.echoed ? std::string(c.echoed) : std::string(); + v.timerName = c.timerName ? std::string(c.timerName) : std::string(); + return v; } struct ComplexRequest { - std::vector messages; - std::vector tags; - std::optional note; - std::optional retries; + std::vector messages{}; + std::vector tags{}; + std::optional note{}; + std::optional retries{}; }; -inline CborError encode_cbor(CborEncoder& e, const ComplexRequest& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 4); - if (err) return err; - err = cbor_encode_text_stringz(&m, "messages"); if (err) return err; - err = encode_cbor(m, v.messages); if (err) return err; - err = cbor_encode_text_stringz(&m, "tags"); if (err) return err; - err = encode_cbor(m, v.tags); if (err) return err; - err = cbor_encode_text_stringz(&m, "note"); if (err) return err; - err = encode_cbor(m, v.note); if (err) return err; - err = cbor_encode_text_stringz(&m, "retries"); if (err) return err; - err = encode_cbor(m, v.retries); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct ComplexRequestC { + ::ComplexRequest c{}; + std::vector<::EchoRequest> _messages; + std::vector _messagesH; + std::vector _tags; +}; +inline ComplexRequestC toC(const ComplexRequest& v) { + ComplexRequestC h; + for (const auto& it : v.messages) { + h._messagesH.push_back(toC(it)); + } + for (const auto& hh : h._messagesH) h._messages.push_back(hh.c); + h.c.messages = h._messages.empty() ? nullptr : h._messages.data(); + h.c.messages_len = h._messages.size(); + for (const auto& it : v.tags) { + h._tags.push_back(it.c_str()); + } + h.c.tags = h._tags.empty() ? nullptr : h._tags.data(); + h.c.tags_len = h._tags.size(); + if (v.note.has_value()) { + h.c.note_present = 1; + h.c.note = (*v.note).c_str(); + } + if (v.retries.has_value()) { + h.c.retries_present = 1; + h.c.retries = (*v.retries); + } + return h; } -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); +inline ComplexRequest fromC(const ::ComplexRequest& c) { + ComplexRequest v{}; + for (std::size_t i = 0; i < c.messages_len; i++) + v.messages.push_back(fromC(c.messages[i])); + for (std::size_t i = 0; i < c.tags_len; i++) + v.tags.push_back((c.tags[i] ? std::string(c.tags[i]) : std::string())); + if (c.note_present) + v.note = (c.note ? std::string(c.note) : std::string()); + if (c.retries_present) + v.retries = c.retries; + return v; } struct ComplexResponse { - std::string summary; - int64_t itemCount; - bool hasNote; + 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); +struct ComplexResponseC { + ::ComplexResponse c{}; +}; +inline ComplexResponseC toC(const ComplexResponse& v) { + ComplexResponseC h; + h.c.summary = v.summary.c_str(); + h.c.itemCount = v.itemCount; + h.c.hasNote = v.hasNote ? 1 : 0; + return h; } -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); +inline ComplexResponse fromC(const ::ComplexResponse& c) { + ComplexResponse v{}; + v.summary = c.summary ? std::string(c.summary) : std::string(); + v.itemCount = c.itemCount; + v.hasNote = c.hasNote != 0; + return v; } struct EchoEvent { - std::string message; - int64_t echoCount; + std::string message{}; + int64_t echoCount{}; }; -inline CborError encode_cbor(CborEncoder& e, const EchoEvent& 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, "echoCount"); if (err) return err; - err = encode_cbor(m, v.echoCount); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct EchoEventC { + ::EchoEvent c{}; +}; +inline EchoEventC toC(const EchoEvent& v) { + EchoEventC h; + h.c.message = v.message.c_str(); + h.c.echoCount = v.echoCount; + return h; } -inline CborError decode_cbor(CborValue& it, EchoEvent& 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, "echoCount", &field); if (err) return err; - if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; - err = decode_cbor(field, v.echoCount); if (err) return err; - return cbor_value_advance(&it); +inline EchoEvent fromC(const ::EchoEvent& c) { + EchoEvent v{}; + v.message = c.message ? std::string(c.message) : std::string(); + v.echoCount = c.echoCount; + return v; } struct JobSpec { - std::string name; - std::vector payload; - int64_t priority; + std::string name{}; + std::vector payload{}; + int64_t priority{}; }; -inline CborError encode_cbor(CborEncoder& e, const JobSpec& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 3); - if (err) return err; - err = cbor_encode_text_stringz(&m, "name"); if (err) return err; - err = encode_cbor(m, v.name); if (err) return err; - err = cbor_encode_text_stringz(&m, "payload"); if (err) return err; - err = encode_cbor(m, v.payload); if (err) return err; - err = cbor_encode_text_stringz(&m, "priority"); if (err) return err; - err = encode_cbor(m, v.priority); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct JobSpecC { + ::JobSpec c{}; + std::vector _payload; +}; +inline JobSpecC toC(const JobSpec& v) { + JobSpecC h; + h.c.name = v.name.c_str(); + for (const auto& it : v.payload) { + h._payload.push_back(it.c_str()); + } + h.c.payload = h._payload.empty() ? nullptr : h._payload.data(); + h.c.payload_len = h._payload.size(); + h.c.priority = v.priority; + return h; } -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); +inline JobSpec fromC(const ::JobSpec& c) { + JobSpec v{}; + v.name = c.name ? std::string(c.name) : std::string(); + for (std::size_t i = 0; i < c.payload_len; i++) + v.payload.push_back((c.payload[i] ? std::string(c.payload[i]) : std::string())); + v.priority = c.priority; + return v; } struct RetryPolicy { - int64_t maxAttempts; - int64_t backoffMs; - std::vector retryOn; + int64_t maxAttempts{}; + int64_t backoffMs{}; + std::vector retryOn{}; }; -inline CborError encode_cbor(CborEncoder& e, const RetryPolicy& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 3); - if (err) return err; - err = cbor_encode_text_stringz(&m, "maxAttempts"); if (err) return err; - err = encode_cbor(m, v.maxAttempts); if (err) return err; - err = cbor_encode_text_stringz(&m, "backoffMs"); if (err) return err; - err = encode_cbor(m, v.backoffMs); if (err) return err; - err = cbor_encode_text_stringz(&m, "retryOn"); if (err) return err; - err = encode_cbor(m, v.retryOn); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct RetryPolicyC { + ::RetryPolicy c{}; + std::vector _retryOn; +}; +inline RetryPolicyC toC(const RetryPolicy& v) { + RetryPolicyC h; + h.c.maxAttempts = v.maxAttempts; + h.c.backoffMs = v.backoffMs; + for (const auto& it : v.retryOn) { + h._retryOn.push_back(it.c_str()); + } + h.c.retryOn = h._retryOn.empty() ? nullptr : h._retryOn.data(); + h.c.retryOn_len = h._retryOn.size(); + return h; } -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); +inline RetryPolicy fromC(const ::RetryPolicy& c) { + RetryPolicy v{}; + v.maxAttempts = c.maxAttempts; + v.backoffMs = c.backoffMs; + for (std::size_t i = 0; i < c.retryOn_len; i++) + v.retryOn.push_back((c.retryOn[i] ? std::string(c.retryOn[i]) : std::string())); + return v; } struct ScheduleConfig { - int64_t startAtMs; - int64_t intervalMs; - std::optional jitter; + int64_t startAtMs{}; + int64_t intervalMs{}; + std::optional jitter{}; }; -inline CborError encode_cbor(CborEncoder& e, const ScheduleConfig& v) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 3); - if (err) return err; - err = cbor_encode_text_stringz(&m, "startAtMs"); if (err) return err; - err = encode_cbor(m, v.startAtMs); if (err) return err; - err = cbor_encode_text_stringz(&m, "intervalMs"); if (err) return err; - err = encode_cbor(m, v.intervalMs); if (err) return err; - err = cbor_encode_text_stringz(&m, "jitter"); if (err) return err; - err = encode_cbor(m, v.jitter); if (err) return err; - return cbor_encoder_close_container(&e, &m); +struct ScheduleConfigC { + ::ScheduleConfig c{}; +}; +inline ScheduleConfigC toC(const ScheduleConfig& v) { + ScheduleConfigC h; + h.c.startAtMs = v.startAtMs; + h.c.intervalMs = v.intervalMs; + if (v.jitter.has_value()) { + h.c.jitter_present = 1; + h.c.jitter = (*v.jitter); + } + return h; } -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); +inline ScheduleConfig fromC(const ::ScheduleConfig& c) { + ScheduleConfig v{}; + v.startAtMs = c.startAtMs; + v.intervalMs = c.intervalMs; + if (c.jitter_present) + v.jitter = c.jitter; + return v; } struct ScheduleResult { - std::string jobId; - int64_t willRunCount; - int64_t firstRunAtMs; - int64_t effectiveBackoffMs; + 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 MyTimerCreateCtorReq { - TimerConfig config; +struct ScheduleResultC { + ::ScheduleResult c{}; }; -inline CborError encode_cbor(CborEncoder& e, const MyTimerCreateCtorReq& 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 ScheduleResultC toC(const ScheduleResult& v) { + ScheduleResultC h; + h.c.jobId = v.jobId.c_str(); + h.c.willRunCount = v.willRunCount; + h.c.firstRunAtMs = v.firstRunAtMs; + h.c.effectiveBackoffMs = v.effectiveBackoffMs; + return h; } -inline CborError decode_cbor(CborValue& it, MyTimerCreateCtorReq& 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); +inline ScheduleResult fromC(const ::ScheduleResult& c) { + ScheduleResult v{}; + v.jobId = c.jobId ? std::string(c.jobId) : std::string(); + v.willRunCount = c.willRunCount; + v.firstRunAtMs = c.firstRunAtMs; + v.effectiveBackoffMs = c.effectiveBackoffMs; + return v; } -struct MyTimerEchoReq { - EchoRequest req; +namespace detail { +template struct Capture { + int ret = RET_ERR; + T value{}; + std::string err; + std::promise done; }; -inline CborError encode_cbor(CborEncoder& e, const MyTimerEchoReq& 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, MyTimerEchoReq& 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 MyTimerVersionReq { +struct AckCapture { + int ret = RET_ERR; + std::string err; + std::promise done; }; -inline CborError encode_cbor(CborEncoder& e, const MyTimerVersionReq&) { - CborEncoder m; - CborError err = cbor_encoder_create_map(&e, &m, 0); - if (err) return err; - return cbor_encoder_close_container(&e, &m); +inline std::string rawText(const char* msg, std::size_t len) { + return (msg && len) ? std::string(msg, len) : std::string(); } -inline CborError decode_cbor(CborValue& it, MyTimerVersionReq&) { - if (!cbor_value_is_map(&it)) return CborErrorImproperValue; - return cbor_value_advance(&it); -} - -struct MyTimerComplexReq { - ComplexRequest req; +// Event listener storage: a heap handler kept alive by the node so the +// native callback's userData stays valid until removed. +struct ListenerBase { virtual ~ListenerBase() = default; }; +template struct EventListener : ListenerBase { + std::function handler; + explicit EventListener(std::function h) + : handler(std::move(h)) {} }; -inline CborError encode_cbor(CborEncoder& e, const MyTimerComplexReq& 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, MyTimerComplexReq& 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 MyTimerScheduleReq { - JobSpec job; - RetryPolicy retry; - ScheduleConfig schedule; -}; -inline CborError encode_cbor(CborEncoder& e, const MyTimerScheduleReq& 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, MyTimerScheduleReq& 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 -// ============================================================ +} // namespace detail +struct ListenerHandle { std::uint64_t id = 0; }; extern "C" { -typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data); - -void* my_timer_create_cbor(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); -int my_timer_echo_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int my_timer_version_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int my_timer_complex_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int my_timer_schedule_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int my_timer_destroy(void* ctx); -uint64_t my_timer_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); -int my_timer_remove_event_listener(void* ctx, uint64_t listener_id); +inline void my_timer_native_ack(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast(ud); + c->ret = ret; + if (ret == RET_ERR) c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_str(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = detail::rawText(msg, len); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_my_timer_echo(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg)); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_my_timer_complex(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg)); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_my_timer_schedule(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg)); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_evt_OnEchoFired(int ret, const char* msg, std::size_t, void* ud) { + auto* l = static_cast*>(ud); + if (ret == RET_OK && l->handler) + l->handler(fromC(*reinterpret_cast(msg))); +} } // extern "C" -// ============================================================ -// Synchronous call helper -// ============================================================ -// Guarded so two nim-ffi headers can share a translation unit. -#ifndef NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED -#define NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED - -namespace { - -struct FFICallState_ { - std::mutex mtx; - std::condition_variable cv; - bool done{false}; - bool ok{false}; - std::vector bytes; - std::string err; -}; - -inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { - // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; - // take ownership here so it's freed on every exit path. - std::unique_ptr> handle( - static_cast*>(ud)); - FFICallState_& s = **handle; - - std::lock_guard lock(s.mtx); - s.ok = (ret == 0); - if (msg && len > 0) { - const auto* p = reinterpret_cast(msg); - if (s.ok) s.bytes.assign(p, p + len); - else s.err.assign(msg, len); - } - s.done = true; - s.cv.notify_one(); -} - -inline Result> ffi_call_( - std::function f, - std::chrono::milliseconds timeout) { - using Bytes = std::vector; - auto state = std::make_shared(); - auto* cb_ref = new std::shared_ptr(state); - const int ret = f(ffi_cb_, cb_ref); - if (ret == 2) { - delete cb_ref; - return Result::err("RET_MISSING_CALLBACK (internal error)"); - } - std::unique_lock lock(state->mtx); - const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); - if (!fired) - return Result::err("FFI call timed out after " + - std::to_string(timeout.count()) + "ms"); - if (!state->ok) - return Result::err(state->err); - return Result::ok(std::move(state->bytes)); -} - -} // anonymous namespace - -#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED - -template -inline bool decodeEventPayload(std::span envelope, T& out) { - if (envelope.empty()) return false; - CborParser parser; CborValue it; - if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) != CborNoError) - return false; - if (!cbor_value_is_map(&it)) return false; - CborValue payloadField; - if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) - return false; - return decode_cbor(payloadField, out) == CborNoError; -} - -// ============================================================ -// High-level C++ context class -// ============================================================ - -class MyTimerCtx { -public: - static Result> create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - const auto ffi_req_ = MyTimerCreateCtorReq{config}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result>::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - (void)my_timer_create_cbor(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud); - return 0; - }, timeout); - if (ffi_raw_.isErr()) return Result>::err(ffi_raw_.error()); - auto ffi_addr_ = decodeCborFFI(ffi_raw_.value()); - if (ffi_addr_.isErr()) return Result>::err(ffi_addr_.error()); - const auto& addr_str = ffi_addr_.value(); - std::uint64_t addr = 0; - const char* addr_begin = addr_str.data(); - const char* addr_end = addr_begin + addr_str.size(); - const auto fc_ = std::from_chars(addr_begin, addr_end, addr); - if (fc_.ec != std::errc() || fc_.ptr != addr_end) { - return Result>::err("FFI create returned non-numeric address: " + addr_str); - } - return Result>::ok(std::unique_ptr(new MyTimerCtx(reinterpret_cast(static_cast(addr)), timeout))); +class My_timerNode { + public: + explicit My_timerNode(const TimerConfig& config) { + detail::AckCapture cap; + auto fut = cap.done.get_future(); + auto c_config = toC(config); + ctx_ = my_timer_create(c_config.c, my_timer_native_ack, &cap); + if (!ctx_) throw std::runtime_error("my_timer_create returned null"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); } - static std::future>> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { - return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); + EchoResponse Echo(const EchoRequest& req) { + detail::Capture cap; + auto fut = cap.done.get_future(); + auto c_req = toC(req); + if (my_timer_echo(ctx_, my_timer_native_my_timer_echo, &cap, c_req.c) != RET_OK) + throw std::runtime_error("my_timer_echo dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; } - // Special-member policy: this class owns a my_timer context, which in - // turn owns the library's worker thread(s) and internal state. Moving - // such an object out from under a caller silently tears that state - // down and is easy to misuse (e.g. storing in a container that - // relocates its elements). It also has no clean analogue in the other - // binding languages we generate. So copies and moves are both - // deleted; ownership is transferred via MyTimerCtx::create returning a - // std::unique_ptr. The destructor still releases the - // context. - ~MyTimerCtx() { - if (ptr_) { - my_timer_destroy(ptr_); - ptr_ = nullptr; - } + std::string Version() { + detail::Capture cap; + auto fut = cap.done.get_future(); + if (my_timer_version(ctx_, my_timer_native_str, &cap) != RET_OK) + throw std::runtime_error("my_timer_version dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; } - MyTimerCtx(const MyTimerCtx&) = delete; - MyTimerCtx& operator=(const MyTimerCtx&) = delete; - MyTimerCtx(MyTimerCtx&&) = delete; - MyTimerCtx& operator=(MyTimerCtx&&) = delete; + ComplexResponse Complex(const ComplexRequest& req) { + detail::Capture cap; + auto fut = cap.done.get_future(); + auto c_req = toC(req); + if (my_timer_complex(ctx_, my_timer_native_my_timer_complex, &cap, c_req.c) != RET_OK) + throw std::runtime_error("my_timer_complex dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } - // ── Event listener API ────────────────────────────────── - struct ListenerHandle { std::uint64_t id = 0; }; + ScheduleResult Schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) { + detail::Capture cap; + auto fut = cap.done.get_future(); + auto c_job = toC(job); + auto c_retry = toC(retry); + auto c_schedule = toC(schedule); + if (my_timer_schedule(ctx_, my_timer_native_my_timer_schedule, &cap, c_job.c, c_retry.c, c_schedule.c) != RET_OK) + throw std::runtime_error("my_timer_schedule dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } - ListenerHandle addOnEchoFiredListener(std::function handler) { - auto owned = std::make_unique>(std::move(handler)); - auto* raw = owned.get(); - const auto id = my_timer_add_event_listener_cbor( - ptr_, "on_echo_fired", &MyTimerCtx::typedTrampoline, raw); + ListenerHandle OnEchoFired(std::function handler) { + auto l = std::make_unique>(std::move(handler)); + auto* raw = l.get(); + const auto id = my_timer_add_event_listener(ctx_, "on_echo_fired", &my_timer_evt_OnEchoFired, raw); if (id == 0) return ListenerHandle{0}; - listeners_.emplace(id, std::move(owned)); - return ListenerHandle{id}; - } - - ListenerHandle addEventListener(std::function)> handler) { - auto owned = std::make_unique(std::move(handler)); - auto* raw = owned.get(); - const auto id = my_timer_add_event_listener_cbor( - ptr_, "", &MyTimerCtx::wildcardTrampoline, raw); - if (id == 0) return ListenerHandle{0}; - listeners_.emplace(id, std::move(owned)); + listeners_.emplace(id, std::move(l)); return ListenerHandle{id}; } bool removeEventListener(ListenerHandle handle) { if (handle.id == 0) return false; - const auto rc = my_timer_remove_event_listener(ptr_, handle.id); + const auto rc = my_timer_remove_event_listener(ctx_, handle.id); listeners_.erase(handle.id); return rc == 0; } - Result echo(const EchoRequest& req) const { - const auto ffi_req_ = MyTimerEchoReq{req}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return my_timer_echo_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } + ~My_timerNode() { if (ctx_) my_timer_destroy(ctx_); } + My_timerNode(const My_timerNode&) = delete; + My_timerNode& operator=(const My_timerNode&) = delete; - std::future> echoAsync(const EchoRequest& req) const { - return std::async(std::launch::async, [this, req]() { return this->echo(req); }); - } - - Result version() const { - const auto ffi_req_ = MyTimerVersionReq{}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return my_timer_version_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } - - std::future> versionAsync() const { - return std::async(std::launch::async, [this]() { return this->version(); }); - } - - Result complex(const ComplexRequest& req) const { - const auto ffi_req_ = MyTimerComplexReq{req}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return my_timer_complex_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } - - std::future> complexAsync(const ComplexRequest& req) const { - return std::async(std::launch::async, [this, req]() { return this->complex(req); }); - } - - Result schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { - const auto ffi_req_ = MyTimerScheduleReq{job, retry, schedule}; - auto ffi_enc_ = encodeCborFFI(ffi_req_); - if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); - const auto& ffi_req_bytes_ = ffi_enc_.value(); - auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return my_timer_schedule_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); - }, timeout_); - if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); - return decodeCborFFI(ffi_raw_.value()); - } - - std::future> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { - return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); }); - } - -private: - struct ListenerBase { - virtual ~ListenerBase() = default; - }; - - template - struct TypedListener : ListenerBase { - std::function fn; - explicit TypedListener(std::function f) : fn(std::move(f)) {} - }; - - struct WildcardListener : ListenerBase { - std::function)> fn; - explicit WildcardListener(std::function)> f) : fn(std::move(f)) {} - }; - - template - static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) { - if (!ud || ret != 0 || !msg || len == 0) return; - auto* listener = static_cast*>(ud); - if (!listener->fn) return; - CborParser parser; CborValue it; - if (cbor_parser_init(reinterpret_cast(msg), len, 0, &parser, &it) != CborNoError) return; - if (!cbor_value_is_map(&it)) return; - CborValue payloadField; - if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) return; - T payload{}; - if (decode_cbor(payloadField, payload) != CborNoError) return; - listener->fn(payload); - } - - static void wildcardTrampoline(int ret, const char* msg, std::size_t len, void* ud) { - if (!ud) return; - auto* listener = static_cast(ud); - if (!listener->fn) return; - std::span envelope{}; - if (msg && len > 0) { - envelope = std::span(reinterpret_cast(msg), len); - } - std::string eventId; - if (ret == 0 && !envelope.empty()) { - CborParser parser; CborValue it; - if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) == CborNoError - && cbor_value_is_map(&it)) { - CborValue evtField; - if (cbor_value_map_find_value(&it, "eventType", &evtField) == CborNoError - && cbor_value_is_text_string(&evtField)) { - (void)decode_cbor(evtField, eventId); - } - } - } - listener->fn(ret, eventId, envelope); - } - - void* ptr_; - std::chrono::milliseconds timeout_; - std::unordered_map> listeners_; - explicit MyTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} + private: + void* ctx_ = nullptr; + std::map> listeners_; }; + +} // namespace my_timer + +#endif // NIM_FFI_GEN_MY_TIMER_NATIVE_HPP \ No newline at end of file diff --git a/examples/timer/cpp_bindings/my_timer_cbor.hpp b/examples/timer/cpp_bindings/my_timer_cbor.hpp new file mode 100644 index 0000000..8a0591d --- /dev/null +++ b/examples/timer/cpp_bindings/my_timer_cbor.hpp @@ -0,0 +1,995 @@ +#pragma once +// Generated bindings require C++20 — the event-listener API uses +// std::span for the wildcard callback. +// MSVC keeps __cplusplus at 199711L unless /Zc:__cplusplus is passed, +// so consult _MSVC_LANG when present (it always reflects the active +// /std:c++XX level). +#if defined(_MSVC_LANG) +# if _MSVC_LANG < 202002L +# error "nim-ffi generated headers require C++20 or later (use /std:c++20)" +# endif +#elif !defined(__cplusplus) || __cplusplus < 202002L +# error "nim-ffi generated headers require C++20 or later" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} + +#include +#include +// ============================================================ +// Result — exception-free error channel +// ============================================================ +// The generated bindings never throw: every fallible entry point (create, +// instance methods, and their *Async futures) returns a Result. Callers +// branch on isOk()/isErr() (or the explicit bool conversion) and read +// value()/error(). This mirrors the Nim side's Result[T, string] and keeps +// us off C++23's std::expected. +#ifndef NIM_FFI_RESULT_HPP_INCLUDED +#define NIM_FFI_RESULT_HPP_INCLUDED + +template +class Result { + std::optional value_; + std::string error_; +public: + static Result ok(T value) { + Result r; + r.value_ = std::move(value); + return r; + } + static Result err(std::string message) { + Result r; + r.error_ = std::move(message); + return r; + } + bool isOk() const { return value_.has_value(); } + bool isErr() const { return !value_.has_value(); } + explicit operator bool() const { return isOk(); } + const T& value() const { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + T& value() { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + const T& operator*() const { assert(value_.has_value() && "Result::operator*() called on err Result — check isOk() first"); return *value_; } + const T* operator->() const { assert(value_.has_value() && "Result::operator->() called on err Result — check isOk() first"); return &*value_; } + T&& take() { assert(value_.has_value() && "Result::take() called on err Result — check isOk() first"); return std::move(*value_); } + const std::string& error() const { assert(!value_.has_value() && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +template <> +class Result { + bool ok_ = true; + std::string error_; +public: + static Result ok() { + Result r; + r.ok_ = true; + return r; + } + static Result err(std::string message) { + Result r; + r.ok_ = false; + r.error_ = std::move(message); + return r; + } + Result() = default; + bool isOk() const { return ok_; } + bool isErr() const { return !ok_; } + explicit operator bool() const { return isOk(); } + const std::string& error() const { assert(!ok_ && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +#endif // NIM_FFI_RESULT_HPP_INCLUDED + +// ── 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 they defer into. +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_CBOR_HELPERS_HPP_INCLUDED +#define NIM_FFI_CBOR_HELPERS_HPP_INCLUDED + +inline CborError encode_cbor(CborEncoder& e, bool v) { + return cbor_encode_boolean(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int64_t v) { + return cbor_encode_int(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int32_t v) { + return cbor_encode_int(&e, static_cast(v)); +} +inline CborError encode_cbor(CborEncoder& e, uint64_t v) { + return cbor_encode_uint(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, double v) { + return cbor_encode_double(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, const std::string& v) { + return cbor_encode_text_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(&e, &arr, v.size()); + if (err) return err; + for (const auto& item : v) { + err = encode_cbor(arr, item); + if (err) return err; + } + return cbor_encoder_close_container(&e, &arr); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { + if (!v) return cbor_encode_null(&e); + return encode_cbor(e, *v); +} + +// ── decode_cbor overloads ─────────────────────────────────────────────── + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_boolean(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int64_t& out) { + if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_int64_checked(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, int32_t& out) { + int64_t tmp = 0; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = static_cast(tmp); + return CborNoError; +} +inline CborError decode_cbor(CborValue& it, uint64_t& out) { + if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; + CborError err = cbor_value_get_uint64(&it, &out); + if (err) return err; + return cbor_value_advance(&it); +} +inline CborError decode_cbor(CborValue& it, double& out) { + if (cbor_value_is_double(&it)) { + CborError err = cbor_value_get_double(&it, &out); + if (err) return err; + return cbor_value_advance(&it); + } + if (cbor_value_is_float(&it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(&it, &f); + if (err) return err; + out = static_cast(f); + return cbor_value_advance(&it); + } + return CborErrorImproperValue; +} +inline CborError decode_cbor(CborValue& it, std::string& out) { + if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + +template +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_array(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(&it, &len); + if (err) return err; + out.clear(); + out.resize(len); + CborValue inner; + err = cbor_value_enter_container(&it, &inner); + if (err) return err; + for (size_t i = 0; i < len; ++i) { + err = decode_cbor(inner, out[i]); + if (err) return err; + } + return cbor_value_leave_container(&it, &inner); +} + +template +inline CborError decode_cbor(CborValue& it, std::optional& out) { + if (cbor_value_is_null(&it)) { + out = std::nullopt; + return cbor_value_advance(&it); + } + T tmp{}; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = std::move(tmp); + return CborNoError; +} + +// ── Public entry points ───────────────────────────────────────────────── + +template +inline Result> encodeCborFFI(const T& value) { + // Start with a generous 4 KiB buffer; double on overflow until it fits. + std::vector buf(4096); + while (true) { + CborEncoder enc; + cbor_encoder_init(&enc, buf.data(), buf.size(), 0); + CborError err = encode_cbor(enc, value); + if (err == CborNoError) { + const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); + buf.resize(used); + return Result>::ok(std::move(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; + } + return Result>::err( + std::string("FFI CBOR encode failed: ") + cbor_error_string(err)); + } +} + +template +inline Result decodeCborFFI(const std::vector& bytes) { + CborParser parser; + CborValue it; + CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR parse init failed: ") + + cbor_error_string(err)); + } + T out{}; + err = decode_cbor(it, out); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR decode failed: ") + + cbor_error_string(err)); + } + return Result::ok(std::move(out)); +} + +#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED + +// ============================================================ +// User-declared FFI types +// ============================================================ + +struct TimerConfig { + std::string name; +}; +inline CborError encode_cbor(CborEncoder& e, const TimerConfig& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); if (err) return err; + err = encode_cbor(m, v.name); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, TimerConfig& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "name", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.name); if (err) return err; + return cbor_value_advance(&it); +} + +struct EchoRequest { + std::string message; + int64_t delayMs; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoRequest& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "message"); if (err) return err; + err = encode_cbor(m, v.message); if (err) return err; + err = cbor_encode_text_stringz(&m, "delayMs"); if (err) return err; + err = encode_cbor(m, v.delayMs); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoRequest& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "message", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.message); if (err) return err; + err = cbor_value_map_find_value(&it, "delayMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.delayMs); if (err) return err; + return cbor_value_advance(&it); +} + +struct EchoResponse { + std::string echoed; + std::string timerName; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoResponse& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "echoed"); if (err) return err; + err = encode_cbor(m, v.echoed); if (err) return err; + err = cbor_encode_text_stringz(&m, "timerName"); if (err) return err; + err = encode_cbor(m, v.timerName); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoResponse& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "echoed", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.echoed); if (err) return err; + err = cbor_value_map_find_value(&it, "timerName", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.timerName); if (err) return err; + return cbor_value_advance(&it); +} + +struct ComplexRequest { + std::vector messages; + std::vector tags; + std::optional note; + std::optional retries; +}; +inline CborError encode_cbor(CborEncoder& e, const ComplexRequest& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "messages"); if (err) return err; + err = encode_cbor(m, v.messages); if (err) return err; + err = cbor_encode_text_stringz(&m, "tags"); if (err) return err; + err = encode_cbor(m, v.tags); if (err) return err; + err = cbor_encode_text_stringz(&m, "note"); if (err) return err; + err = encode_cbor(m, v.note); if (err) return err; + err = cbor_encode_text_stringz(&m, "retries"); if (err) return err; + err = encode_cbor(m, v.retries); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ComplexRequest& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "messages", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.messages); if (err) return err; + err = cbor_value_map_find_value(&it, "tags", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.tags); if (err) return err; + err = cbor_value_map_find_value(&it, "note", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.note); if (err) return err; + err = cbor_value_map_find_value(&it, "retries", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.retries); if (err) return err; + return cbor_value_advance(&it); +} + +struct ComplexResponse { + std::string summary; + int64_t itemCount; + bool hasNote; +}; +inline CborError encode_cbor(CborEncoder& e, const ComplexResponse& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "summary"); if (err) return err; + err = encode_cbor(m, v.summary); if (err) return err; + err = cbor_encode_text_stringz(&m, "itemCount"); if (err) return err; + err = encode_cbor(m, v.itemCount); if (err) return err; + err = cbor_encode_text_stringz(&m, "hasNote"); if (err) return err; + err = encode_cbor(m, v.hasNote); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ComplexResponse& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "summary", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.summary); if (err) return err; + err = cbor_value_map_find_value(&it, "itemCount", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.itemCount); if (err) return err; + err = cbor_value_map_find_value(&it, "hasNote", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.hasNote); if (err) return err; + return cbor_value_advance(&it); +} + +struct EchoEvent { + std::string message; + int64_t echoCount; +}; +inline CborError encode_cbor(CborEncoder& e, const EchoEvent& 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, "echoCount"); if (err) return err; + err = encode_cbor(m, v.echoCount); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, EchoEvent& 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, "echoCount", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.echoCount); if (err) return err; + return cbor_value_advance(&it); +} + +struct JobSpec { + std::string name; + std::vector payload; + int64_t priority; +}; +inline CborError encode_cbor(CborEncoder& e, const JobSpec& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); if (err) return err; + err = encode_cbor(m, v.name); if (err) return err; + err = cbor_encode_text_stringz(&m, "payload"); if (err) return err; + err = encode_cbor(m, v.payload); if (err) return err; + err = cbor_encode_text_stringz(&m, "priority"); if (err) return err; + err = encode_cbor(m, v.priority); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, JobSpec& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "name", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.name); if (err) return err; + err = cbor_value_map_find_value(&it, "payload", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.payload); if (err) return err; + err = cbor_value_map_find_value(&it, "priority", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.priority); if (err) return err; + return cbor_value_advance(&it); +} + +struct RetryPolicy { + int64_t maxAttempts; + int64_t backoffMs; + std::vector retryOn; +}; +inline CborError encode_cbor(CborEncoder& e, const RetryPolicy& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "maxAttempts"); if (err) return err; + err = encode_cbor(m, v.maxAttempts); if (err) return err; + err = cbor_encode_text_stringz(&m, "backoffMs"); if (err) return err; + err = encode_cbor(m, v.backoffMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "retryOn"); if (err) return err; + err = encode_cbor(m, v.retryOn); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, RetryPolicy& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "maxAttempts", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.maxAttempts); if (err) return err; + err = cbor_value_map_find_value(&it, "backoffMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.backoffMs); if (err) return err; + err = cbor_value_map_find_value(&it, "retryOn", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.retryOn); if (err) return err; + return cbor_value_advance(&it); +} + +struct ScheduleConfig { + int64_t startAtMs; + int64_t intervalMs; + std::optional jitter; +}; +inline CborError encode_cbor(CborEncoder& e, const ScheduleConfig& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "startAtMs"); if (err) return err; + err = encode_cbor(m, v.startAtMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "intervalMs"); if (err) return err; + err = encode_cbor(m, v.intervalMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "jitter"); if (err) return err; + err = encode_cbor(m, v.jitter); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ScheduleConfig& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "startAtMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.startAtMs); if (err) return err; + err = cbor_value_map_find_value(&it, "intervalMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.intervalMs); if (err) return err; + err = cbor_value_map_find_value(&it, "jitter", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.jitter); if (err) return err; + return cbor_value_advance(&it); +} + +struct ScheduleResult { + std::string jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +}; +inline CborError encode_cbor(CborEncoder& e, const ScheduleResult& v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(&e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "jobId"); if (err) return err; + err = encode_cbor(m, v.jobId); if (err) return err; + err = cbor_encode_text_stringz(&m, "willRunCount"); if (err) return err; + err = encode_cbor(m, v.willRunCount); if (err) return err; + err = cbor_encode_text_stringz(&m, "firstRunAtMs"); if (err) return err; + err = encode_cbor(m, v.firstRunAtMs); if (err) return err; + err = cbor_encode_text_stringz(&m, "effectiveBackoffMs"); if (err) return err; + err = encode_cbor(m, v.effectiveBackoffMs); if (err) return err; + return cbor_encoder_close_container(&e, &m); +} +inline CborError decode_cbor(CborValue& it, ScheduleResult& v) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(&it, "jobId", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.jobId); if (err) return err; + err = cbor_value_map_find_value(&it, "willRunCount", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.willRunCount); if (err) return err; + err = cbor_value_map_find_value(&it, "firstRunAtMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.firstRunAtMs); if (err) return err; + err = cbor_value_map_find_value(&it, "effectiveBackoffMs", &field); if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = decode_cbor(field, v.effectiveBackoffMs); if (err) return err; + return cbor_value_advance(&it); +} + +// ============================================================ +// Per-proc request envelopes (CBOR encoded on the wire) +// ============================================================ + +struct MyTimerCreateCtorReq { + TimerConfig config; +}; +inline CborError encode_cbor(CborEncoder& e, const MyTimerCreateCtorReq& 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, MyTimerCreateCtorReq& 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 MyTimerEchoReq { + EchoRequest req; +}; +inline CborError encode_cbor(CborEncoder& e, const MyTimerEchoReq& 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, MyTimerEchoReq& 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 MyTimerVersionReq { +}; +inline CborError encode_cbor(CborEncoder& e, const MyTimerVersionReq&) { + 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, MyTimerVersionReq&) { + if (!cbor_value_is_map(&it)) return CborErrorImproperValue; + return cbor_value_advance(&it); +} + +struct MyTimerComplexReq { + ComplexRequest req; +}; +inline CborError encode_cbor(CborEncoder& e, const MyTimerComplexReq& 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, MyTimerComplexReq& 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 MyTimerScheduleReq { + JobSpec job; + RetryPolicy retry; + ScheduleConfig schedule; +}; +inline CborError encode_cbor(CborEncoder& e, const MyTimerScheduleReq& 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, MyTimerScheduleReq& 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* my_timer_create_cbor(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); +int my_timer_echo_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_version_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_complex_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_schedule_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_destroy(void* ctx); +uint64_t my_timer_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); +int my_timer_remove_event_listener(void* ctx, uint64_t listener_id); +} // extern "C" + +// ============================================================ +// Synchronous call helper +// ============================================================ +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED +#define NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED + +namespace { + +struct FFICallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::vector bytes; + std::string err; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { + // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; + // take ownership here so it's freed on every exit path. + std::unique_ptr> handle( + static_cast*>(ud)); + FFICallState_& s = **handle; + + std::lock_guard lock(s.mtx); + s.ok = (ret == 0); + if (msg && len > 0) { + const auto* p = reinterpret_cast(msg); + if (s.ok) s.bytes.assign(p, p + len); + else s.err.assign(msg, len); + } + s.done = true; + s.cv.notify_one(); +} + +inline Result> ffi_call_( + std::function f, + std::chrono::milliseconds timeout) { + using Bytes = std::vector; + auto state = std::make_shared(); + auto* cb_ref = new std::shared_ptr(state); + const int ret = f(ffi_cb_, cb_ref); + if (ret == 2) { + delete cb_ref; + return Result::err("RET_MISSING_CALLBACK (internal error)"); + } + std::unique_lock lock(state->mtx); + const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); + if (!fired) + return Result::err("FFI call timed out after " + + std::to_string(timeout.count()) + "ms"); + if (!state->ok) + return Result::err(state->err); + return Result::ok(std::move(state->bytes)); +} + +} // anonymous namespace + +#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED + +template +inline bool decodeEventPayload(std::span envelope, T& out) { + if (envelope.empty()) return false; + CborParser parser; CborValue it; + if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) != CborNoError) + return false; + if (!cbor_value_is_map(&it)) return false; + CborValue payloadField; + if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) + return false; + return decode_cbor(payloadField, out) == CborNoError; +} + +// ============================================================ +// High-level C++ context class +// ============================================================ + +class MyTimerCtx { +public: + static Result> create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + const auto ffi_req_ = MyTimerCreateCtorReq{config}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result>::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + (void)my_timer_create_cbor(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud); + return 0; + }, timeout); + if (ffi_raw_.isErr()) return Result>::err(ffi_raw_.error()); + auto ffi_addr_ = decodeCborFFI(ffi_raw_.value()); + if (ffi_addr_.isErr()) return Result>::err(ffi_addr_.error()); + const auto& addr_str = ffi_addr_.value(); + std::uint64_t addr = 0; + const char* addr_begin = addr_str.data(); + const char* addr_end = addr_begin + addr_str.size(); + const auto fc_ = std::from_chars(addr_begin, addr_end, addr); + if (fc_.ec != std::errc() || fc_.ptr != addr_end) { + return Result>::err("FFI create returned non-numeric address: " + addr_str); + } + return Result>::ok(std::unique_ptr(new MyTimerCtx(reinterpret_cast(static_cast(addr)), timeout))); + } + + static std::future>> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); + } + + // Special-member policy: this class owns a my_timer context, which in + // turn owns the library's worker thread(s) and internal state. Moving + // such an object out from under a caller silently tears that state + // down and is easy to misuse (e.g. storing in a container that + // relocates its elements). It also has no clean analogue in the other + // binding languages we generate. So copies and moves are both + // deleted; ownership is transferred via MyTimerCtx::create returning a + // std::unique_ptr. The destructor still releases the + // context. + ~MyTimerCtx() { + if (ptr_) { + my_timer_destroy(ptr_); + ptr_ = nullptr; + } + } + + MyTimerCtx(const MyTimerCtx&) = delete; + MyTimerCtx& operator=(const MyTimerCtx&) = delete; + MyTimerCtx(MyTimerCtx&&) = delete; + MyTimerCtx& operator=(MyTimerCtx&&) = delete; + + // ── Event listener API ────────────────────────────────── + struct ListenerHandle { std::uint64_t id = 0; }; + + ListenerHandle addOnEchoFiredListener(std::function handler) { + auto owned = std::make_unique>(std::move(handler)); + auto* raw = owned.get(); + const auto id = my_timer_add_event_listener_cbor( + ptr_, "on_echo_fired", &MyTimerCtx::typedTrampoline, raw); + if (id == 0) return ListenerHandle{0}; + listeners_.emplace(id, std::move(owned)); + return ListenerHandle{id}; + } + + ListenerHandle addEventListener(std::function)> handler) { + auto owned = std::make_unique(std::move(handler)); + auto* raw = owned.get(); + const auto id = my_timer_add_event_listener_cbor( + ptr_, "", &MyTimerCtx::wildcardTrampoline, raw); + if (id == 0) return ListenerHandle{0}; + listeners_.emplace(id, std::move(owned)); + return ListenerHandle{id}; + } + + bool removeEventListener(ListenerHandle handle) { + if (handle.id == 0) return false; + const auto rc = my_timer_remove_event_listener(ptr_, handle.id); + listeners_.erase(handle.id); + return rc == 0; + } + + Result echo(const EchoRequest& req) const { + const auto ffi_req_ = MyTimerEchoReq{req}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return my_timer_echo_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> echoAsync(const EchoRequest& req) const { + return std::async(std::launch::async, [this, req]() { return this->echo(req); }); + } + + Result version() const { + const auto ffi_req_ = MyTimerVersionReq{}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return my_timer_version_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> versionAsync() const { + return std::async(std::launch::async, [this]() { return this->version(); }); + } + + Result complex(const ComplexRequest& req) const { + const auto ffi_req_ = MyTimerComplexReq{req}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return my_timer_complex_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> complexAsync(const ComplexRequest& req) const { + return std::async(std::launch::async, [this, req]() { return this->complex(req); }); + } + + Result schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { + const auto ffi_req_ = MyTimerScheduleReq{job, retry, schedule}; + auto ffi_enc_ = encodeCborFFI(ffi_req_); + if (ffi_enc_.isErr()) return Result::err(ffi_enc_.error()); + const auto& ffi_req_bytes_ = ffi_enc_.value(); + auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { + return my_timer_schedule_cbor(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + }, timeout_); + if (ffi_raw_.isErr()) return Result::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + std::future> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { + return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); }); + } + +private: + struct ListenerBase { + virtual ~ListenerBase() = default; + }; + + template + struct TypedListener : ListenerBase { + std::function fn; + explicit TypedListener(std::function f) : fn(std::move(f)) {} + }; + + struct WildcardListener : ListenerBase { + std::function)> fn; + explicit WildcardListener(std::function)> f) : fn(std::move(f)) {} + }; + + template + static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) { + if (!ud || ret != 0 || !msg || len == 0) return; + auto* listener = static_cast*>(ud); + if (!listener->fn) return; + CborParser parser; CborValue it; + if (cbor_parser_init(reinterpret_cast(msg), len, 0, &parser, &it) != CborNoError) return; + if (!cbor_value_is_map(&it)) return; + CborValue payloadField; + if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) return; + T payload{}; + if (decode_cbor(payloadField, payload) != CborNoError) return; + listener->fn(payload); + } + + static void wildcardTrampoline(int ret, const char* msg, std::size_t len, void* ud) { + if (!ud) return; + auto* listener = static_cast(ud); + if (!listener->fn) return; + std::span envelope{}; + if (msg && len > 0) { + envelope = std::span(reinterpret_cast(msg), len); + } + std::string eventId; + if (ret == 0 && !envelope.empty()) { + CborParser parser; CborValue it; + if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) == CborNoError + && cbor_value_is_map(&it)) { + CborValue evtField; + if (cbor_value_map_find_value(&it, "eventType", &evtField) == CborNoError + && cbor_value_is_text_string(&evtField)) { + (void)decode_cbor(evtField, eventId); + } + } + } + listener->fn(ret, eventId, envelope); + } + + void* ptr_; + std::chrono::milliseconds timeout_; + std::unordered_map> listeners_; + explicit MyTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} +}; diff --git a/examples/timer/cpp_native_bindings/Makefile b/examples/timer/cpp_native_bindings/Makefile index f583211..9ce76cb 100644 --- a/examples/timer/cpp_native_bindings/Makefile +++ b/examples/timer/cpp_native_bindings/Makefile @@ -31,7 +31,7 @@ all: example $(LIBNAME): cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC) -example: main.cpp my_timer_native.hpp my_timer.h $(LIBNAME) +example: main.cpp my_timer.hpp my_timer.h $(LIBNAME) $(CXX) $(CXXFLAGS) main.cpp -L. -lmy_timer $(RPATH) -o example run: example diff --git a/examples/timer/cpp_native_bindings/README.md b/examples/timer/cpp_native_bindings/README.md index a0ae586..b1cd1e8 100644 --- a/examples/timer/cpp_native_bindings/README.md +++ b/examples/timer/cpp_native_bindings/README.md @@ -6,7 +6,7 @@ in [`../cpp_bindings`](../cpp_bindings). | File | Description | |------|-------------| -| `my_timer_native.hpp` | Generated wrapper: a C++ struct + `toC`/`fromC` per `{.ffi.}` type, and a `My_timerNode` class whose methods marshal typed args into / read typed struct returns out of the native ABI — no CBOR. | +| `my_timer.hpp` | Generated wrapper: a C++ struct + `toC`/`fromC` per `{.ffi.}` type, and a `My_timerNode` class whose methods marshal typed args into / read typed struct returns out of the native ABI — no CBOR. | | `my_timer.h` | Native C header (structs + entry points) the `.hpp` includes. | | `main.cpp`, `Makefile` | A driver + build. | @@ -35,7 +35,8 @@ echo, complex, schedule all generate and round-trip typed values (ASAN-clean). the C++ argument (valid for the call's duration; the library deep-copies). Native typed events are supported too: `node.On(handler)` registers a -native listener and the typed payload arrives via `fromC` (no CBOR). Still to -come: the -native-bare / `_cbor` filename reconciliation (matching the C headers). Today -this emits `my_timer_native.hpp` so it coexists with the CBOR `my_timer.hpp`. +native listener and the typed payload arrives via `fromC` (no CBOR). + +The native header is the bare `my_timer.hpp` and the CBOR counterpart is +`my_timer_cbor.hpp` — matching the C headers (`my_timer.h` / `my_timer_cbor.h`) +and the `` / `_cbor` symbol naming. diff --git a/examples/timer/cpp_native_bindings/main.cpp b/examples/timer/cpp_native_bindings/main.cpp index 45b1a0a..a996949 100644 --- a/examples/timer/cpp_native_bindings/main.cpp +++ b/examples/timer/cpp_native_bindings/main.cpp @@ -1,5 +1,5 @@ -// Driver for the GENERATED native C++ bindings (my_timer_native.hpp). -#include "my_timer_native.hpp" +// Driver for the GENERATED native C++ bindings (my_timer.hpp). +#include "my_timer.hpp" #include int main() { try { diff --git a/examples/timer/cpp_native_bindings/my_timer_native.hpp b/examples/timer/cpp_native_bindings/my_timer.hpp similarity index 100% rename from examples/timer/cpp_native_bindings/my_timer_native.hpp rename to examples/timer/cpp_native_bindings/my_timer.hpp diff --git a/ffi.nimble b/ffi.nimble index cc6ca57..e136572 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -230,6 +230,7 @@ task check_bindings_cpp, "Verify checked-in C++ bindings match Nim source": exec "nimble genbindings_cpp" exec "git diff --exit-code --" & " examples/timer/cpp_bindings/my_timer.hpp" & + " examples/timer/cpp_bindings/my_timer_cbor.hpp" & " examples/timer/cpp_bindings/CMakeLists.txt" task check_bindings, "Verify all checked-in example bindings match Nim source": diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index a2daa78..e017a26 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -1,7 +1,9 @@ ## C++ binding generator for the nim-ffi framework. -## 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). +## Generates a header-only C++ binding (`_cbor.hpp`) 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). The native (zero-serialization) counterpart is `.hpp`, +## emitted by cpp_native.nim. import std/[os, strutils] import ./meta, ./string_helpers @@ -703,8 +705,11 @@ proc generateCppBindings*( events: seq[FFIEventMeta] = @[], ) = createDir(outputDir) + # CBOR (inter-process) header carries the `_cbor` suffix, matching the C + # generator's `_cbor.h` and the `_cbor` symbol naming; the native + # header is the bare `.hpp`. writeFile( - outputDir / (libName & ".hpp"), + outputDir / (libName & "_cbor.hpp"), generateCppHeader(procs, types, libName, events), ) writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath)) diff --git a/ffi/codegen/cpp_native.nim b/ffi/codegen/cpp_native.nim index 2262702..4160818 100644 --- a/ffi/codegen/cpp_native.nim +++ b/ffi/codegen/cpp_native.nim @@ -433,15 +433,15 @@ proc generateCppNativeBindings*( nimSrcRelPath: string, events: seq[FFIEventMeta] = @[], ) = - # `_native.hpp` for now so it coexists with the CBOR `.hpp`; the - # native-bare / `_cbor` rename (matching C) is a follow-up. Emit the native C - # header too (the structs + entry points the .hpp includes), so the binding is - # self-contained. + # Native header is the bare `.hpp` (matching C's bare `.h` and the + # `` symbol naming); the CBOR counterpart is `_cbor.hpp`. Emit the + # native C header too (the structs + entry points the .hpp includes), so the + # binding is self-contained. writeFile( outputDir / (libName & ".h"), cgen.generateCHeader(procs, types, libName, events), ) writeFile( - outputDir / (libName & "_native.hpp"), + outputDir / (libName & ".hpp"), generateCppNativeHeader(procs, types, libName, events), ) diff --git a/tests/e2e/cpp/README.md b/tests/e2e/cpp/README.md index 4376af8..dfebbf5 100644 --- a/tests/e2e/cpp/README.md +++ b/tests/e2e/cpp/README.md @@ -2,11 +2,11 @@ These tests validate that a Nim FFI library exported with `nim-ffi`'s C++ codegen is usable from a real C++ consumer. They drive the `my_timer` example -through its auto-generated `my_timer.hpp` bindings (constructor, sync method, +through its auto-generated `my_timer_cbor.hpp` bindings (constructor, sync method, async methods, complex types with optional fields, multiple contexts, error propagation, async pipelines, short-lived-thread stress, concurrent hammer) and assert the round-tripped values. The `CrossLibrary` test additionally -loads `examples/echo`'s `echo.hpp` alongside the timer to prove two +loads `examples/echo`'s `echo_cbor.hpp` alongside the timer to prove two independent nim-ffi libraries coexist in one process with no symbol clash and no shared global state. diff --git a/tests/e2e/cpp/test_timer_e2e.cpp b/tests/e2e/cpp/test_timer_e2e.cpp index a181d65..1fcdfce 100644 --- a/tests/e2e/cpp/test_timer_e2e.cpp +++ b/tests/e2e/cpp/test_timer_e2e.cpp @@ -13,8 +13,8 @@ // aborting) when it carries an error, so single-threaded tests read as if // the value came back directly. -#include "my_timer.hpp" -#include "echo.hpp" +#include "my_timer_cbor.hpp" +#include "echo_cbor.hpp" #include #include