#pragma once // Generated bindings require C++20 (designated initializers and other // C++20 constructs are used throughout the emitted code). // 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(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); int echo_version(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(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(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(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(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) {} };