mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-24 02:09:32 +00:00
chore: avoid throwing exceptions in C++ bindings (#46)
This commit is contained in:
parent
e394166c46
commit
7ccf34591d
@ -14,7 +14,7 @@
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <charconv>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
@ -24,10 +24,73 @@
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
extern "C" {
|
||||
#include <tinycbor/cbor.h>
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Result<T> — exception-free error channel
|
||||
// ============================================================
|
||||
// The generated bindings never throw: every fallible entry point (create,
|
||||
// instance methods, and their *Async futures) returns a Result<T>. 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 <typename T>
|
||||
class Result {
|
||||
std::optional<T> value_;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<T> ok(T value) {
|
||||
Result<T> r;
|
||||
r.value_ = std::move(value);
|
||||
return r;
|
||||
}
|
||||
static Result<T> err(std::string message) {
|
||||
Result<T> 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<void> {
|
||||
bool ok_ = true;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<void> ok() {
|
||||
Result<void> r;
|
||||
r.ok_ = true;
|
||||
return r;
|
||||
}
|
||||
static Result<void> err(std::string message) {
|
||||
Result<void> 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<void>::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.
|
||||
@ -159,7 +222,7 @@ inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
|
||||
// ── Public entry points ─────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
inline Result<std::vector<std::uint8_t>> encodeCborFFI(const T& value) {
|
||||
// Start with a generous 4 KiB buffer; double on overflow until it fits.
|
||||
std::vector<std::uint8_t> buf(4096);
|
||||
while (true) {
|
||||
@ -169,34 +232,34 @@ inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
if (err == CborNoError) {
|
||||
const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data());
|
||||
buf.resize(used);
|
||||
return buf;
|
||||
return Result<std::vector<std::uint8_t>>::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;
|
||||
}
|
||||
throw std::runtime_error(std::string("FFI CBOR encode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<std::vector<std::uint8_t>>::err(
|
||||
std::string("FFI CBOR encode failed: ") + cbor_error_string(err));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline T decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
inline Result<T> decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
T out{};
|
||||
err = decode_cbor(it, out);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
return out;
|
||||
return Result<T>::ok(std::move(out));
|
||||
}
|
||||
|
||||
#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED
|
||||
@ -384,22 +447,25 @@ inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) {
|
||||
s.cv.notify_one();
|
||||
}
|
||||
|
||||
inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
auto state = std::make_shared<FFICallState_>();
|
||||
auto* cb_ref = new std::shared_ptr<FFICallState_>(state);
|
||||
const int ret = f(ffi_cb_, cb_ref);
|
||||
if (ret == 2) {
|
||||
delete cb_ref;
|
||||
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
|
||||
return Result<Bytes>::err("RET_MISSING_CALLBACK (internal error)");
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state->mtx);
|
||||
const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });
|
||||
if (!fired)
|
||||
throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms");
|
||||
return Result<Bytes>::err("FFI call timed out after " +
|
||||
std::to_string(timeout.count()) + "ms");
|
||||
if (!state->ok)
|
||||
throw std::runtime_error(state->err);
|
||||
return state->bytes;
|
||||
return Result<Bytes>::err(state->err);
|
||||
return Result<Bytes>::ok(std::move(state->bytes));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@ -412,23 +478,30 @@ inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)
|
||||
|
||||
class EchoCtx {
|
||||
public:
|
||||
static std::unique_ptr<EchoCtx> create(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
static Result<std::unique_ptr<EchoCtx>> create(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
const auto ffi_req_ = EchoCreateCtorReq{config};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<std::unique_ptr<EchoCtx>>::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);
|
||||
const auto addr_str = decodeCborFFI<std::string>(ffi_raw_);
|
||||
try {
|
||||
const auto addr = std::stoull(addr_str);
|
||||
return std::unique_ptr<EchoCtx>(new EchoCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout));
|
||||
} catch (const std::exception&) {
|
||||
throw std::runtime_error("FFI create returned non-numeric address: " + addr_str);
|
||||
if (ffi_raw_.isErr()) return Result<std::unique_ptr<EchoCtx>>::err(ffi_raw_.error());
|
||||
auto ffi_addr_ = decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
if (ffi_addr_.isErr()) return Result<std::unique_ptr<EchoCtx>>::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<std::unique_ptr<EchoCtx>>::err("FFI create returned non-numeric address: " + addr_str);
|
||||
}
|
||||
return Result<std::unique_ptr<EchoCtx>>::ok(std::unique_ptr<EchoCtx>(new EchoCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout)));
|
||||
}
|
||||
|
||||
static std::future<std::unique_ptr<EchoCtx>> createAsync(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
static std::future<Result<std::unique_ptr<EchoCtx>>> createAsync(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
|
||||
}
|
||||
|
||||
@ -453,29 +526,35 @@ public:
|
||||
EchoCtx(EchoCtx&&) = delete;
|
||||
EchoCtx& operator=(EchoCtx&&) = delete;
|
||||
|
||||
ShoutResponse shout(const ShoutRequest& req) const {
|
||||
Result<ShoutResponse> shout(const ShoutRequest& req) const {
|
||||
const auto ffi_req_ = EchoShoutReq{req};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<ShoutResponse>::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_);
|
||||
return decodeCborFFI<ShoutResponse>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<ShoutResponse>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<ShoutResponse>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<ShoutResponse> shoutAsync(const ShoutRequest& req) const {
|
||||
std::future<Result<ShoutResponse>> shoutAsync(const ShoutRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return this->shout(req); });
|
||||
}
|
||||
|
||||
std::string version() const {
|
||||
Result<std::string> version() const {
|
||||
const auto ffi_req_ = EchoVersionReq{};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<std::string>::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_);
|
||||
return decodeCborFFI<std::string>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<std::string>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<std::string> versionAsync() const {
|
||||
std::future<Result<std::string>> versionAsync() const {
|
||||
return std::async(std::launch::async, [this]() { return this->version(); });
|
||||
}
|
||||
|
||||
|
||||
@ -5,121 +5,142 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
// The generated bindings never throw: every call returns a Result<T>. We
|
||||
// branch on isErr() and read value()/error() instead of using try/catch.
|
||||
int main() {
|
||||
try {
|
||||
auto ctx = MyTimerCtx::create(TimerConfig{"cpp-demo"});
|
||||
std::cout << "[1] Context created\n";
|
||||
|
||||
auto versionFuture = ctx->versionAsync();
|
||||
auto echo1Future = ctx->echoAsync(EchoRequest{"hello from C++", 200});
|
||||
auto echo2Future = ctx->echoAsync(EchoRequest{"second C++ request", 50});
|
||||
|
||||
auto version = versionFuture.get();
|
||||
std::cout << "[2] Version: " << version << "\n";
|
||||
|
||||
auto echo = echo1Future.get();
|
||||
std::cout << "[3] Echo 1: echoed=" << echo.echoed
|
||||
<< ", timerName=" << echo.timerName << "\n";
|
||||
|
||||
auto echo2 = echo2Future.get();
|
||||
std::cout << "[4] Echo 2: echoed=" << echo2.echoed
|
||||
<< ", timerName=" << echo2.timerName << "\n";
|
||||
|
||||
auto complexReq = ComplexRequest{
|
||||
std::vector<EchoRequest>{EchoRequest{"one", 10}, EchoRequest{"two", 20}},
|
||||
std::vector<std::string>{"fast", "async"},
|
||||
std::optional<std::string>("extra note"),
|
||||
std::optional<int64_t>(3)
|
||||
};
|
||||
|
||||
auto complexFuture = ctx->complexAsync(complexReq);
|
||||
auto complex = complexFuture.get();
|
||||
std::cout << "[5] Complex: summary=" << complex.summary
|
||||
<< ", itemCount=" << complex.itemCount
|
||||
<< ", hasNote=" << complex.hasNote << "\n";
|
||||
|
||||
// Each parameter is its own generated C++ struct. The nim-ffi
|
||||
// macro packs all three into one CBOR envelope on the wire — at
|
||||
// the call site, this is just a typed method invocation.
|
||||
auto job = JobSpec{
|
||||
/*name*/ "nightly-rollup",
|
||||
/*payload*/ std::vector<std::string>{"rollup", "v2"},
|
||||
/*priority*/ 10,
|
||||
};
|
||||
auto retry = RetryPolicy{
|
||||
/*maxAttempts*/ 3,
|
||||
/*backoffMs*/ 500,
|
||||
/*retryOn*/ std::vector<std::string>{"timeout", "5xx"},
|
||||
};
|
||||
auto schedule = ScheduleConfig{
|
||||
/*startAtMs*/ 1000,
|
||||
/*intervalMs*/ 15000,
|
||||
/*jitter*/ std::optional<int64_t>(250),
|
||||
};
|
||||
|
||||
auto scheduleFuture = ctx->scheduleAsync(job, retry, schedule);
|
||||
auto scheduleRes = scheduleFuture.get();
|
||||
std::cout << "[6] Schedule (3 complex params): jobId=" << scheduleRes.jobId
|
||||
<< ", willRunCount=" << scheduleRes.willRunCount
|
||||
<< ", firstRunAtMs=" << scheduleRes.firstRunAtMs
|
||||
<< ", effectiveBackoffMs=" << scheduleRes.effectiveBackoffMs
|
||||
<< "\n";
|
||||
|
||||
// Each `{.ffiEvent.}` declared on the Nim side gets a typed
|
||||
// registration method — `addOnEchoFiredListener(handler)` here.
|
||||
// A second `addEventListener` overload registers a catch-all
|
||||
// wildcard listener that receives every event as raw envelope
|
||||
// bytes plus the FFI return code. Both fire from the lib's
|
||||
// dispatch thread, so synchronise via std::promise / atomics.
|
||||
std::promise<EchoEvent> echoEvtPromise;
|
||||
auto echoEvtFuture = echoEvtPromise.get_future();
|
||||
const auto typedHandle = ctx->addOnEchoFiredListener(
|
||||
[&](const EchoEvent& evt) { echoEvtPromise.set_value(evt); });
|
||||
|
||||
std::atomic<int> wildcardHits{0};
|
||||
// Wildcard listener receives every event with the wire `eventId`
|
||||
// pre-extracted plus a span view over the raw CBOR envelope
|
||||
// bytes (zero-copy; valid only for the duration of this call).
|
||||
// Dispatch on `eventId` and use `decodeEventPayload<T>` to lift
|
||||
// the payload into a typed value without hand-parsing CBOR.
|
||||
const auto wildcardHandle = ctx->addEventListener(
|
||||
[&](int retCode, const std::string& eventId,
|
||||
std::span<const std::uint8_t> envelope) {
|
||||
wildcardHits.fetch_add(1);
|
||||
std::cout << "[7] wildcard event: retCode=" << retCode
|
||||
<< ", eventId=" << eventId
|
||||
<< ", envelope bytes=" << envelope.size() << "\n";
|
||||
if (retCode != 0) return;
|
||||
if (eventId == "on_echo_fired") {
|
||||
EchoEvent decoded{};
|
||||
if (decodeEventPayload(envelope, decoded)) {
|
||||
std::cout << " decoded EchoEvent: message="
|
||||
<< decoded.message
|
||||
<< ", echoCount=" << decoded.echoCount << "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ctx->echo(EchoRequest{"event-demo", 1});
|
||||
const auto evt = echoEvtFuture.get();
|
||||
std::cout << "[7] typed event onEchoFired: message=" << evt.message
|
||||
<< ", echoCount=" << evt.echoCount
|
||||
<< ", wildcardHits=" << wildcardHits.load() << "\n";
|
||||
|
||||
// Drop the typed listener — only the wildcard fires for the
|
||||
// follow-up echo. Sleep briefly to give the lib thread time to
|
||||
// deliver before we tear the ctx down.
|
||||
ctx->removeEventListener(typedHandle);
|
||||
ctx->echo(EchoRequest{"event-demo-after-remove", 1});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
std::cout << "[7] after removeEventListener: wildcardHits="
|
||||
<< wildcardHits.load() << "\n";
|
||||
ctx->removeEventListener(wildcardHandle);
|
||||
|
||||
std::cout << "\nDone.\n";
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << "Error: " << ex.what() << "\n";
|
||||
auto ctxRes = MyTimerCtx::create(TimerConfig{"cpp-demo"});
|
||||
if (ctxRes.isErr()) {
|
||||
std::cerr << "Error: " << ctxRes.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
auto ctx = std::move(ctxRes.value());
|
||||
std::cout << "[1] Context created\n";
|
||||
|
||||
auto versionFuture = ctx->versionAsync();
|
||||
auto echo1Future = ctx->echoAsync(EchoRequest{"hello from C++", 200});
|
||||
auto echo2Future = ctx->echoAsync(EchoRequest{"second C++ request", 50});
|
||||
|
||||
auto version = versionFuture.get();
|
||||
if (version.isErr()) {
|
||||
std::cerr << "Error: " << version.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[2] Version: " << version.value() << "\n";
|
||||
|
||||
auto echo = echo1Future.get();
|
||||
if (echo.isErr()) {
|
||||
std::cerr << "Error: " << echo.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[3] Echo 1: echoed=" << echo->echoed
|
||||
<< ", timerName=" << echo->timerName << "\n";
|
||||
|
||||
auto echo2 = echo2Future.get();
|
||||
if (echo2.isErr()) {
|
||||
std::cerr << "Error: " << echo2.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[4] Echo 2: echoed=" << echo2->echoed
|
||||
<< ", timerName=" << echo2->timerName << "\n";
|
||||
|
||||
auto complexReq = ComplexRequest{
|
||||
std::vector<EchoRequest>{EchoRequest{"one", 10}, EchoRequest{"two", 20}},
|
||||
std::vector<std::string>{"fast", "async"},
|
||||
std::optional<std::string>("extra note"),
|
||||
std::optional<int64_t>(3)
|
||||
};
|
||||
|
||||
auto complex = ctx->complexAsync(complexReq).get();
|
||||
if (complex.isErr()) {
|
||||
std::cerr << "Error: " << complex.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[5] Complex: summary=" << complex->summary
|
||||
<< ", itemCount=" << complex->itemCount
|
||||
<< ", hasNote=" << complex->hasNote << "\n";
|
||||
|
||||
// ── 6. Call with three complex parameters ─────────────────────
|
||||
// Each parameter is its own generated C++ struct. The nim-ffi
|
||||
// macro packs all three into one CBOR envelope on the wire — at
|
||||
// the call site, this is just a typed method invocation.
|
||||
auto job = JobSpec{
|
||||
/*name*/ "nightly-rollup",
|
||||
/*payload*/ std::vector<std::string>{"rollup", "v2"},
|
||||
/*priority*/ 10,
|
||||
};
|
||||
auto retry = RetryPolicy{
|
||||
/*maxAttempts*/ 3,
|
||||
/*backoffMs*/ 500,
|
||||
/*retryOn*/ std::vector<std::string>{"timeout", "5xx"},
|
||||
};
|
||||
auto schedule = ScheduleConfig{
|
||||
/*startAtMs*/ 1000,
|
||||
/*intervalMs*/ 15000,
|
||||
/*jitter*/ std::optional<int64_t>(250),
|
||||
};
|
||||
|
||||
auto scheduleRes = ctx->scheduleAsync(job, retry, schedule).get();
|
||||
if (scheduleRes.isErr()) {
|
||||
std::cerr << "Error: " << scheduleRes.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[6] Schedule (3 complex params): jobId=" << scheduleRes->jobId
|
||||
<< ", willRunCount=" << scheduleRes->willRunCount
|
||||
<< ", firstRunAtMs=" << scheduleRes->firstRunAtMs
|
||||
<< ", effectiveBackoffMs=" << scheduleRes->effectiveBackoffMs
|
||||
<< "\n";
|
||||
|
||||
// Each `{.ffiEvent.}` declared on the Nim side gets a typed
|
||||
// registration method — `addOnEchoFiredListener(handler)` here.
|
||||
// A second `addEventListener` overload registers a catch-all
|
||||
// wildcard listener that receives every event as raw envelope
|
||||
// bytes plus the FFI return code. Both fire from the lib's
|
||||
// dispatch thread, so synchronise via std::promise / atomics.
|
||||
std::promise<EchoEvent> echoEvtPromise;
|
||||
auto echoEvtFuture = echoEvtPromise.get_future();
|
||||
const auto typedHandle = ctx->addOnEchoFiredListener(
|
||||
[&](const EchoEvent& evt) { echoEvtPromise.set_value(evt); });
|
||||
|
||||
std::atomic<int> wildcardHits{0};
|
||||
// Wildcard listener receives every event with the wire `eventId`
|
||||
// pre-extracted plus a span view over the raw CBOR envelope
|
||||
// bytes (zero-copy; valid only for the duration of this call).
|
||||
// Dispatch on `eventId` and use `decodeEventPayload<T>` to lift
|
||||
// the payload into a typed value without hand-parsing CBOR.
|
||||
const auto wildcardHandle = ctx->addEventListener(
|
||||
[&](int retCode, const std::string& eventId,
|
||||
std::span<const std::uint8_t> envelope) {
|
||||
wildcardHits.fetch_add(1);
|
||||
std::cout << "[7] wildcard event: retCode=" << retCode
|
||||
<< ", eventId=" << eventId
|
||||
<< ", envelope bytes=" << envelope.size() << "\n";
|
||||
if (retCode != 0) return;
|
||||
if (eventId == "on_echo_fired") {
|
||||
EchoEvent decoded{};
|
||||
if (decodeEventPayload(envelope, decoded)) {
|
||||
std::cout << " decoded EchoEvent: message="
|
||||
<< decoded.message
|
||||
<< ", echoCount=" << decoded.echoCount << "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ctx->echo(EchoRequest{"event-demo", 1});
|
||||
const auto evt = echoEvtFuture.get();
|
||||
std::cout << "[7] typed event onEchoFired: message=" << evt.message
|
||||
<< ", echoCount=" << evt.echoCount
|
||||
<< ", wildcardHits=" << wildcardHits.load() << "\n";
|
||||
|
||||
// Drop the typed listener — only the wildcard fires for the
|
||||
// follow-up echo. Sleep briefly to give the lib thread time to
|
||||
// deliver before we tear the ctx down.
|
||||
ctx->removeEventListener(typedHandle);
|
||||
ctx->echo(EchoRequest{"event-demo-after-remove", 1});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
std::cout << "[7] after removeEventListener: wildcardHits="
|
||||
<< wildcardHits.load() << "\n";
|
||||
ctx->removeEventListener(wildcardHandle);
|
||||
|
||||
std::cout << "\nDone.\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <charconv>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
@ -24,12 +24,75 @@
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
extern "C" {
|
||||
#include <tinycbor/cbor.h>
|
||||
}
|
||||
|
||||
#include <unordered_map>
|
||||
#include <span>
|
||||
// ============================================================
|
||||
// Result<T> — exception-free error channel
|
||||
// ============================================================
|
||||
// The generated bindings never throw: every fallible entry point (create,
|
||||
// instance methods, and their *Async futures) returns a Result<T>. 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 <typename T>
|
||||
class Result {
|
||||
std::optional<T> value_;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<T> ok(T value) {
|
||||
Result<T> r;
|
||||
r.value_ = std::move(value);
|
||||
return r;
|
||||
}
|
||||
static Result<T> err(std::string message) {
|
||||
Result<T> 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<void> {
|
||||
bool ok_ = true;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<void> ok() {
|
||||
Result<void> r;
|
||||
r.ok_ = true;
|
||||
return r;
|
||||
}
|
||||
static Result<void> err(std::string message) {
|
||||
Result<void> 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<void>::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.
|
||||
@ -161,7 +224,7 @@ inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
|
||||
// ── Public entry points ─────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
inline Result<std::vector<std::uint8_t>> encodeCborFFI(const T& value) {
|
||||
// Start with a generous 4 KiB buffer; double on overflow until it fits.
|
||||
std::vector<std::uint8_t> buf(4096);
|
||||
while (true) {
|
||||
@ -171,34 +234,34 @@ inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
if (err == CborNoError) {
|
||||
const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data());
|
||||
buf.resize(used);
|
||||
return buf;
|
||||
return Result<std::vector<std::uint8_t>>::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;
|
||||
}
|
||||
throw std::runtime_error(std::string("FFI CBOR encode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<std::vector<std::uint8_t>>::err(
|
||||
std::string("FFI CBOR encode failed: ") + cbor_error_string(err));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline T decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
inline Result<T> decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
T out{};
|
||||
err = decode_cbor(it, out);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
return out;
|
||||
return Result<T>::ok(std::move(out));
|
||||
}
|
||||
|
||||
#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED
|
||||
@ -685,22 +748,25 @@ inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) {
|
||||
s.cv.notify_one();
|
||||
}
|
||||
|
||||
inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
auto state = std::make_shared<FFICallState_>();
|
||||
auto* cb_ref = new std::shared_ptr<FFICallState_>(state);
|
||||
const int ret = f(ffi_cb_, cb_ref);
|
||||
if (ret == 2) {
|
||||
delete cb_ref;
|
||||
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
|
||||
return Result<Bytes>::err("RET_MISSING_CALLBACK (internal error)");
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state->mtx);
|
||||
const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });
|
||||
if (!fired)
|
||||
throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms");
|
||||
return Result<Bytes>::err("FFI call timed out after " +
|
||||
std::to_string(timeout.count()) + "ms");
|
||||
if (!state->ok)
|
||||
throw std::runtime_error(state->err);
|
||||
return state->bytes;
|
||||
return Result<Bytes>::err(state->err);
|
||||
return Result<Bytes>::ok(std::move(state->bytes));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@ -726,23 +792,30 @@ inline bool decodeEventPayload(std::span<const std::uint8_t> envelope, T& out) {
|
||||
|
||||
class MyTimerCtx {
|
||||
public:
|
||||
static std::unique_ptr<MyTimerCtx> create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
static Result<std::unique_ptr<MyTimerCtx>> create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
const auto ffi_req_ = MyTimerCreateCtorReq{config};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<std::unique_ptr<MyTimerCtx>>::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(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);
|
||||
return 0;
|
||||
}, timeout);
|
||||
const auto addr_str = decodeCborFFI<std::string>(ffi_raw_);
|
||||
try {
|
||||
const auto addr = std::stoull(addr_str);
|
||||
return std::unique_ptr<MyTimerCtx>(new MyTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout));
|
||||
} catch (const std::exception&) {
|
||||
throw std::runtime_error("FFI create returned non-numeric address: " + addr_str);
|
||||
if (ffi_raw_.isErr()) return Result<std::unique_ptr<MyTimerCtx>>::err(ffi_raw_.error());
|
||||
auto ffi_addr_ = decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
if (ffi_addr_.isErr()) return Result<std::unique_ptr<MyTimerCtx>>::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<std::unique_ptr<MyTimerCtx>>::err("FFI create returned non-numeric address: " + addr_str);
|
||||
}
|
||||
return Result<std::unique_ptr<MyTimerCtx>>::ok(std::unique_ptr<MyTimerCtx>(new MyTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout)));
|
||||
}
|
||||
|
||||
static std::future<std::unique_ptr<MyTimerCtx>> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
static std::future<Result<std::unique_ptr<MyTimerCtx>>> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
|
||||
}
|
||||
|
||||
@ -797,55 +870,67 @@ public:
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
EchoResponse echo(const EchoRequest& req) const {
|
||||
Result<EchoResponse> echo(const EchoRequest& req) const {
|
||||
const auto ffi_req_ = MyTimerEchoReq{req};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<EchoResponse>::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(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
|
||||
}, timeout_);
|
||||
return decodeCborFFI<EchoResponse>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<EchoResponse>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<EchoResponse>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<EchoResponse> echoAsync(const EchoRequest& req) const {
|
||||
std::future<Result<EchoResponse>> echoAsync(const EchoRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return this->echo(req); });
|
||||
}
|
||||
|
||||
std::string version() const {
|
||||
Result<std::string> version() const {
|
||||
const auto ffi_req_ = MyTimerVersionReq{};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<std::string>::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(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
|
||||
}, timeout_);
|
||||
return decodeCborFFI<std::string>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<std::string>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<std::string> versionAsync() const {
|
||||
std::future<Result<std::string>> versionAsync() const {
|
||||
return std::async(std::launch::async, [this]() { return this->version(); });
|
||||
}
|
||||
|
||||
ComplexResponse complex(const ComplexRequest& req) const {
|
||||
Result<ComplexResponse> complex(const ComplexRequest& req) const {
|
||||
const auto ffi_req_ = MyTimerComplexReq{req};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<ComplexResponse>::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(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
|
||||
}, timeout_);
|
||||
return decodeCborFFI<ComplexResponse>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<ComplexResponse>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<ComplexResponse>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<ComplexResponse> complexAsync(const ComplexRequest& req) const {
|
||||
std::future<Result<ComplexResponse>> complexAsync(const ComplexRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return this->complex(req); });
|
||||
}
|
||||
|
||||
ScheduleResult schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
Result<ScheduleResult> schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
const auto ffi_req_ = MyTimerScheduleReq{job, retry, schedule};
|
||||
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
|
||||
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
if (ffi_enc_.isErr()) return Result<ScheduleResult>::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(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
|
||||
}, timeout_);
|
||||
return decodeCborFFI<ScheduleResult>(ffi_raw_);
|
||||
if (ffi_raw_.isErr()) return Result<ScheduleResult>::err(ffi_raw_.error());
|
||||
return decodeCborFFI<ScheduleResult>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
std::future<ScheduleResult> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
std::future<Result<ScheduleResult>> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); });
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ const CppPtrType* = "uint64_t"
|
||||
## reflected in the generated bindings without touching this codegen.
|
||||
const
|
||||
HeaderPreludeTpl = staticRead("templates/cpp/header_prelude.hpp.tpl")
|
||||
ResultTpl = staticRead("templates/cpp/result.hpp.tpl")
|
||||
CborHelpersTpl = staticRead("templates/cpp/cbor_helpers.hpp.tpl")
|
||||
SyncCallHelperTpl = staticRead("templates/cpp/sync_call_helper.hpp.tpl")
|
||||
ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl")
|
||||
@ -339,6 +340,11 @@ proc generateCppHeader*(
|
||||
lines.add("#include <unordered_map>")
|
||||
lines.add("#include <span>")
|
||||
|
||||
# Result<T> is the exception-free return channel used by every generated
|
||||
# entry point. It must precede the CBOR helpers and sync-call helper below,
|
||||
# which now hand their failures back as Result rather than throwing.
|
||||
lines.add(ResultTpl)
|
||||
|
||||
# CBOR primitive / container helpers must precede the per-struct codecs
|
||||
# below, because each emitted `encode_cbor`/`decode_cbor(T)` calls the
|
||||
# generic overloads for the struct's fields (std::string, std::vector,
|
||||
@ -519,32 +525,43 @@ proc generateCppHeader*(
|
||||
# context owns library threads, so we forbid copy/move on the class
|
||||
# itself (see ContextRuleOf5Tpl) and hand out ownership through a
|
||||
# smart pointer that callers can move, store in containers, etc.
|
||||
let createRet = "Result<std::unique_ptr<$1>>" % [ctxTypeName]
|
||||
lines.add(
|
||||
" static std::unique_ptr<$1> create($2) {" %
|
||||
[ctxTypeName, ctorParamsWithTimeout]
|
||||
" static $1 create($2) {" % [createRet, ctorParamsWithTimeout]
|
||||
)
|
||||
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
|
||||
lines.add(" const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);")
|
||||
lines.add(" const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
|
||||
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
|
||||
lines.add(" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [createRet])
|
||||
lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();")
|
||||
lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
|
||||
lines.add(
|
||||
" (void)$1(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);" %
|
||||
[ctor.procName]
|
||||
)
|
||||
lines.add(" return 0;")
|
||||
lines.add(" }, timeout);")
|
||||
lines.add(" const auto addr_str = decodeCborFFI<std::string>(ffi_raw_);")
|
||||
lines.add(" try {")
|
||||
lines.add(" const auto addr = std::stoull(addr_str);")
|
||||
# Use `new` directly (not std::make_unique) so the ctor can stay private.
|
||||
lines.add(" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [createRet])
|
||||
lines.add(" auto ffi_addr_ = decodeCborFFI<std::string>(ffi_raw_.value());")
|
||||
lines.add(" if (ffi_addr_.isErr()) return $1::err(ffi_addr_.error());" % [createRet])
|
||||
lines.add(" const auto& addr_str = ffi_addr_.value();")
|
||||
# Parse the ctx address without exceptions: std::stoull would throw on a
|
||||
# non-numeric payload, so use std::from_chars and surface the failure as
|
||||
# an err() Result instead.
|
||||
lines.add(" std::uint64_t addr = 0;")
|
||||
lines.add(" const char* addr_begin = addr_str.data();")
|
||||
lines.add(" const char* addr_end = addr_begin + addr_str.size();")
|
||||
lines.add(" const auto fc_ = std::from_chars(addr_begin, addr_end, addr);")
|
||||
lines.add(" if (fc_.ec != std::errc() || fc_.ptr != addr_end) {")
|
||||
lines.add(
|
||||
" return std::unique_ptr<$1>(new $1(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout));" %
|
||||
[ctxTypeName]
|
||||
)
|
||||
lines.add(" } catch (const std::exception&) {")
|
||||
lines.add(
|
||||
" throw std::runtime_error(\"FFI create returned non-numeric address: \" + addr_str);"
|
||||
" return $1::err(\"FFI create returned non-numeric address: \" + addr_str);" %
|
||||
[createRet]
|
||||
)
|
||||
lines.add(" }")
|
||||
# Use `new` directly (not std::make_unique) so the ctor can stay private.
|
||||
lines.add(
|
||||
" return $1::ok(std::unique_ptr<$2>(new $2(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout)));" %
|
||||
[createRet, ctxTypeName]
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
@ -559,7 +576,7 @@ proc generateCppHeader*(
|
||||
else:
|
||||
"timeout"
|
||||
lines.add(
|
||||
" static std::future<std::unique_ptr<$1>> createAsync($2) {" %
|
||||
" static std::future<Result<std::unique_ptr<$1>>> createAsync($2) {" %
|
||||
[ctxTypeName, ctorParamsWithTimeout]
|
||||
)
|
||||
lines.add(
|
||||
@ -602,18 +619,22 @@ proc generateCppHeader*(
|
||||
|
||||
let reqInit = cppBracedInit(reqName, methParamNames)
|
||||
|
||||
let methRet = "Result<$1>" % [retCppType]
|
||||
# Use a single-underscore-suffixed local for the Req envelope so it can't
|
||||
# shadow a method parameter whose name happens to be `req` (or similar).
|
||||
lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr])
|
||||
lines.add(" $1 $2($3) const {" % [methRet, methodName, methParamsStr])
|
||||
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
|
||||
lines.add(" const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);")
|
||||
lines.add(" const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
|
||||
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
|
||||
lines.add(" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [methRet])
|
||||
lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();")
|
||||
lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
|
||||
lines.add(
|
||||
" return $1(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" %
|
||||
[m.procName]
|
||||
)
|
||||
lines.add(" }, timeout_);")
|
||||
lines.add(" return decodeCborFFI<$1>(ffi_raw_);" % [retCppType])
|
||||
lines.add(" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [methRet])
|
||||
lines.add(" return decodeCborFFI<$1>(ffi_raw_.value());" % [retCppType])
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
# The async wrapper calls the sync method via `this->methodName(...)` so
|
||||
@ -623,7 +644,7 @@ proc generateCppHeader*(
|
||||
if methParamsStr.len > 0:
|
||||
lines.add(
|
||||
" std::future<$1> $2Async($3) const {" %
|
||||
[retCppType, methodName, methParamsStr]
|
||||
[methRet, methodName, methParamsStr]
|
||||
)
|
||||
lines.add(
|
||||
" return std::async(std::launch::async, [this, $1]() { return this->$2($3); });" %
|
||||
@ -631,7 +652,7 @@ proc generateCppHeader*(
|
||||
)
|
||||
lines.add(" }")
|
||||
else:
|
||||
lines.add(" std::future<$1> $2Async() const {" % [retCppType, methodName])
|
||||
lines.add(" std::future<$1> $2Async() const {" % [methRet, methodName])
|
||||
lines.add(
|
||||
" return std::async(std::launch::async, [this]() { return this->$1(); });" %
|
||||
[methodName]
|
||||
|
||||
@ -129,7 +129,7 @@ inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
|
||||
// ── Public entry points ─────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
inline Result<std::vector<std::uint8_t>> encodeCborFFI(const T& value) {
|
||||
// Start with a generous 4 KiB buffer; double on overflow until it fits.
|
||||
std::vector<std::uint8_t> buf(4096);
|
||||
while (true) {
|
||||
@ -139,34 +139,34 @@ inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
|
||||
if (err == CborNoError) {
|
||||
const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data());
|
||||
buf.resize(used);
|
||||
return buf;
|
||||
return Result<std::vector<std::uint8_t>>::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;
|
||||
}
|
||||
throw std::runtime_error(std::string("FFI CBOR encode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<std::vector<std::uint8_t>>::err(
|
||||
std::string("FFI CBOR encode failed: ") + cbor_error_string(err));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline T decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
inline Result<T> decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR parse init failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
T out{};
|
||||
err = decode_cbor(it, out);
|
||||
if (err != CborNoError) {
|
||||
throw std::runtime_error(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
return Result<T>::err(std::string("FFI CBOR decode failed: ") +
|
||||
cbor_error_string(err));
|
||||
}
|
||||
return out;
|
||||
return Result<T>::ok(std::move(out));
|
||||
}
|
||||
|
||||
#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <charconv>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
@ -24,6 +24,7 @@
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
extern "C" {
|
||||
#include <tinycbor/cbor.h>
|
||||
}
|
||||
|
||||
61
ffi/codegen/templates/cpp/result.hpp.tpl
Normal file
61
ffi/codegen/templates/cpp/result.hpp.tpl
Normal file
@ -0,0 +1,61 @@
|
||||
// ============================================================
|
||||
// Result<T> — exception-free error channel
|
||||
// ============================================================
|
||||
// The generated bindings never throw: every fallible entry point (create,
|
||||
// instance methods, and their *Async futures) returns a Result<T>. 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 <typename T>
|
||||
class Result {
|
||||
std::optional<T> value_;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<T> ok(T value) {
|
||||
Result<T> r;
|
||||
r.value_ = std::move(value);
|
||||
return r;
|
||||
}
|
||||
static Result<T> err(std::string message) {
|
||||
Result<T> 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<void> {
|
||||
bool ok_ = true;
|
||||
std::string error_;
|
||||
public:
|
||||
static Result<void> ok() {
|
||||
Result<void> r;
|
||||
r.ok_ = true;
|
||||
return r;
|
||||
}
|
||||
static Result<void> err(std::string message) {
|
||||
Result<void> 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<void>::error() called on ok Result — check isErr() first"); return error_; }
|
||||
};
|
||||
|
||||
#endif // NIM_FFI_RESULT_HPP_INCLUDED
|
||||
@ -34,22 +34,25 @@ inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) {
|
||||
s.cv.notify_one();
|
||||
}
|
||||
|
||||
inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
std::function<int(FFICallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
auto state = std::make_shared<FFICallState_>();
|
||||
auto* cb_ref = new std::shared_ptr<FFICallState_>(state);
|
||||
const int ret = f(ffi_cb_, cb_ref);
|
||||
if (ret == 2) {
|
||||
delete cb_ref;
|
||||
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
|
||||
return Result<Bytes>::err("RET_MISSING_CALLBACK (internal error)");
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state->mtx);
|
||||
const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });
|
||||
if (!fired)
|
||||
throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms");
|
||||
return Result<Bytes>::err("FFI call timed out after " +
|
||||
std::to_string(timeout.count()) + "ms");
|
||||
if (!state->ok)
|
||||
throw std::runtime_error(state->err);
|
||||
return state->bytes;
|
||||
return Result<Bytes>::err(state->err);
|
||||
return Result<Bytes>::ok(std::move(state->bytes));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
// genbindings_cpp` is callable end-to-end from C++.
|
||||
// The CrossLibrary test also loads `examples/echo/cpp_bindings` to prove
|
||||
// two nim-ffi libraries can coexist in one process.
|
||||
//
|
||||
// The generated bindings never throw: every call returns a Result<T>. The
|
||||
// `mustOk` helper below unwraps a Result and fails the test (without
|
||||
// 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"
|
||||
@ -23,8 +28,20 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// Unwrap a Result<T> in a single-threaded test context. On error it records a
|
||||
// non-fatal gtest failure and returns a default-constructed T so the caller
|
||||
// can keep going (subsequent expectations will fail loudly).
|
||||
template <typename T>
|
||||
T mustOk(Result<T> r) {
|
||||
if (r.isErr()) {
|
||||
ADD_FAILURE() << "unexpected FFI error: " << r.error() << " line: " << __LINE__;
|
||||
return T{};
|
||||
}
|
||||
return r.take();
|
||||
}
|
||||
|
||||
std::unique_ptr<MyTimerCtx> makeCtx(const std::string& name = "e2e") {
|
||||
return MyTimerCtx::create(TimerConfig{name});
|
||||
return mustOk(MyTimerCtx::create(TimerConfig{name}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@ -38,19 +55,19 @@ TEST(TimerE2E, CreateAndDestroy) {
|
||||
|
||||
TEST(TimerE2E, VersionSync) {
|
||||
auto ctx = makeCtx("version-sync");
|
||||
const auto v = ctx->version();
|
||||
const auto v = mustOk(ctx->version());
|
||||
EXPECT_EQ(v, "nim-timer v0.1.0");
|
||||
}
|
||||
|
||||
TEST(TimerE2E, VersionAsync) {
|
||||
auto ctx = makeCtx("version-async");
|
||||
auto fut = ctx->versionAsync();
|
||||
EXPECT_EQ(fut.get(), "nim-timer v0.1.0");
|
||||
EXPECT_EQ(mustOk(fut.get()), "nim-timer v0.1.0");
|
||||
}
|
||||
|
||||
TEST(TimerE2E, EchoRoundTripsMessageAndTimerName) {
|
||||
auto ctx = makeCtx("echo-ctx");
|
||||
const auto resp = ctx->echo(EchoRequest{"hello", 10});
|
||||
const auto resp = mustOk(ctx->echo(EchoRequest{"hello", 10}));
|
||||
EXPECT_EQ(resp.echoed, "hello");
|
||||
EXPECT_EQ(resp.timerName, "echo-ctx");
|
||||
}
|
||||
@ -60,7 +77,7 @@ TEST(TimerE2E, EchoHonoursDelay) {
|
||||
constexpr int delayMs = 150;
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto resp = ctx->echo(EchoRequest{"waited", delayMs});
|
||||
const auto resp = mustOk(ctx->echo(EchoRequest{"waited", delayMs}));
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
|
||||
@ -76,9 +93,9 @@ TEST(TimerE2E, ConcurrentAsyncCallsAreIndependent) {
|
||||
auto f2 = ctx->echoAsync(EchoRequest{"two", 40});
|
||||
auto f3 = ctx->echoAsync(EchoRequest{"three", 20});
|
||||
|
||||
const auto r3 = f3.get();
|
||||
const auto r2 = f2.get();
|
||||
const auto r1 = f1.get();
|
||||
const auto r3 = mustOk(f3.get());
|
||||
const auto r2 = mustOk(f2.get());
|
||||
const auto r1 = mustOk(f1.get());
|
||||
|
||||
EXPECT_EQ(r1.echoed, "one");
|
||||
EXPECT_EQ(r2.echoed, "two");
|
||||
@ -97,7 +114,7 @@ TEST(TimerE2E, ComplexWithOptionalNotePresent) {
|
||||
std::optional<int64_t>(2),
|
||||
};
|
||||
|
||||
const auto resp = ctx->complex(req);
|
||||
const auto resp = mustOk(ctx->complex(req));
|
||||
EXPECT_EQ(resp.itemCount, 2);
|
||||
EXPECT_TRUE(resp.hasNote);
|
||||
EXPECT_NE(resp.summary.find("note=a note"), std::string::npos)
|
||||
@ -115,7 +132,7 @@ TEST(TimerE2E, ComplexWithOptionalNoteAbsent) {
|
||||
std::nullopt,
|
||||
};
|
||||
|
||||
const auto resp = ctx->complex(req);
|
||||
const auto resp = mustOk(ctx->complex(req));
|
||||
EXPECT_EQ(resp.itemCount, 0);
|
||||
EXPECT_FALSE(resp.hasNote);
|
||||
EXPECT_NE(resp.summary.find("note=<none>"), std::string::npos)
|
||||
@ -128,8 +145,8 @@ TEST(TimerE2E, IndependentContextsKeepTheirOwnState) {
|
||||
auto ctxA = makeCtx("alpha");
|
||||
auto ctxB = makeCtx("beta");
|
||||
|
||||
const auto rA = ctxA->echo(EchoRequest{"x", 5});
|
||||
const auto rB = ctxB->echo(EchoRequest{"x", 5});
|
||||
const auto rA = mustOk(ctxA->echo(EchoRequest{"x", 5}));
|
||||
const auto rB = mustOk(ctxB->echo(EchoRequest{"x", 5}));
|
||||
|
||||
EXPECT_EQ(rA.timerName, "alpha");
|
||||
EXPECT_EQ(rB.timerName, "beta");
|
||||
@ -137,8 +154,8 @@ TEST(TimerE2E, IndependentContextsKeepTheirOwnState) {
|
||||
|
||||
// N contexts keep independent state; an error on one must not poison siblings.
|
||||
// Empty JobSpec.name is the chosen error trigger: schedule() returns
|
||||
// err("job name must not be empty"), which the bindings rethrow as
|
||||
// std::runtime_error carrying the exact string.
|
||||
// err("job name must not be empty"), which the bindings surface as an
|
||||
// err() Result carrying the exact string.
|
||||
TEST(TimerE2E, MultiContextIsolation) {
|
||||
constexpr int kCtxCount = 5;
|
||||
std::vector<std::unique_ptr<MyTimerCtx>> ctxs;
|
||||
@ -148,7 +165,7 @@ TEST(TimerE2E, MultiContextIsolation) {
|
||||
}
|
||||
|
||||
for (int i = 0; i < kCtxCount; ++i) {
|
||||
const auto resp = ctxs[i]->echo(EchoRequest{"ping", 0});
|
||||
const auto resp = mustOk(ctxs[i]->echo(EchoRequest{"ping", 0}));
|
||||
EXPECT_EQ(resp.echoed, "ping");
|
||||
EXPECT_EQ(resp.timerName, "iso-" + std::to_string(i));
|
||||
}
|
||||
@ -156,20 +173,17 @@ TEST(TimerE2E, MultiContextIsolation) {
|
||||
const auto bad = JobSpec{/*name*/ "", /*payload*/ {}, /*priority*/ 0};
|
||||
const auto retry = RetryPolicy{1, 10, {}};
|
||||
const auto sched = ScheduleConfig{0, 0, std::nullopt};
|
||||
try {
|
||||
(void)ctxs[2]->schedule(bad, retry, sched);
|
||||
FAIL() << "expected schedule() to throw on empty job name";
|
||||
} catch (const std::runtime_error& ex) {
|
||||
EXPECT_STREQ(ex.what(), "job name must not be empty");
|
||||
}
|
||||
const auto scheduleRes = ctxs[2]->schedule(bad, retry, sched);
|
||||
ASSERT_TRUE(scheduleRes.isErr()) << "expected schedule() to fail on empty job name";
|
||||
EXPECT_EQ(scheduleRes.error(), "job name must not be empty");
|
||||
|
||||
const auto recovered = ctxs[2]->echo(EchoRequest{"after-err", 0});
|
||||
const auto recovered = mustOk(ctxs[2]->echo(EchoRequest{"after-err", 0}));
|
||||
EXPECT_EQ(recovered.echoed, "after-err");
|
||||
EXPECT_EQ(recovered.timerName, "iso-2");
|
||||
|
||||
for (int i = 0; i < kCtxCount; ++i) {
|
||||
if (i == 2) continue;
|
||||
const auto resp = ctxs[i]->echo(EchoRequest{"still-here", 0});
|
||||
const auto resp = mustOk(ctxs[i]->echo(EchoRequest{"still-here", 0}));
|
||||
EXPECT_EQ(resp.echoed, "still-here");
|
||||
EXPECT_EQ(resp.timerName, "iso-" + std::to_string(i));
|
||||
}
|
||||
@ -177,31 +191,31 @@ TEST(TimerE2E, MultiContextIsolation) {
|
||||
|
||||
// Two nim-ffi libraries in one process must not share state or symbols.
|
||||
TEST(TimerE2E, CrossLibrary) {
|
||||
auto timerCtx = MyTimerCtx::create(TimerConfig{"x-timer"});
|
||||
auto echoCtx = EchoCtx::create(EchoConfig{"X-ECHO"});
|
||||
auto timerCtx = mustOk(MyTimerCtx::create(TimerConfig{"x-timer"}));
|
||||
auto echoCtx = mustOk(EchoCtx::create(EchoConfig{"X-ECHO"}));
|
||||
|
||||
EXPECT_EQ(timerCtx->version(), "nim-timer v0.1.0");
|
||||
EXPECT_EQ(echoCtx->version(), "nim-echo v0.1.0");
|
||||
EXPECT_EQ(mustOk(timerCtx->version()), "nim-timer v0.1.0");
|
||||
EXPECT_EQ(mustOk(echoCtx->version()), "nim-echo v0.1.0");
|
||||
|
||||
const auto timerResp = timerCtx->echo(EchoRequest{"hello", 0});
|
||||
const auto timerResp = mustOk(timerCtx->echo(EchoRequest{"hello", 0}));
|
||||
EXPECT_EQ(timerResp.echoed, "hello");
|
||||
EXPECT_EQ(timerResp.timerName, "x-timer");
|
||||
|
||||
const auto echoResp = echoCtx->shout(ShoutRequest{"hello"});
|
||||
const auto echoResp = mustOk(echoCtx->shout(ShoutRequest{"hello"}));
|
||||
EXPECT_EQ(echoResp.shouted, "X-ECHO: HELLO");
|
||||
EXPECT_EQ(echoResp.prefix, "X-ECHO");
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const auto t = timerCtx->echo(EchoRequest{"t" + std::to_string(i), 0});
|
||||
const auto e = echoCtx->shout(ShoutRequest{"e" + std::to_string(i)});
|
||||
const auto t = mustOk(timerCtx->echo(EchoRequest{"t" + std::to_string(i), 0}));
|
||||
const auto e = mustOk(echoCtx->shout(ShoutRequest{"e" + std::to_string(i)}));
|
||||
EXPECT_EQ(t.timerName, "x-timer");
|
||||
EXPECT_EQ(e.prefix, "X-ECHO");
|
||||
}
|
||||
|
||||
auto tFut = timerCtx->echoAsync(EchoRequest{"async-t", 30});
|
||||
auto eFut = echoCtx->shoutAsync(ShoutRequest{"async-e"});
|
||||
const auto t = tFut.get();
|
||||
const auto e = eFut.get();
|
||||
const auto t = mustOk(tFut.get());
|
||||
const auto e = mustOk(eFut.get());
|
||||
EXPECT_EQ(t.echoed, "async-t");
|
||||
EXPECT_EQ(t.timerName, "x-timer");
|
||||
EXPECT_EQ(e.shouted, "X-ECHO: ASYNC-E");
|
||||
@ -212,9 +226,9 @@ TEST(TimerE2E, TriplePipeline) {
|
||||
auto ctx = makeCtx("pipeline");
|
||||
|
||||
auto pipeline = std::async(std::launch::async, [&ctx]() {
|
||||
auto a = ctx->echoAsync(EchoRequest{"A", 20}).get();
|
||||
auto b = ctx->echoAsync(EchoRequest{a.echoed + "->B", 10}).get();
|
||||
auto c = ctx->echoAsync(EchoRequest{b.echoed + "->C", 5}).get();
|
||||
auto a = mustOk(ctx->echoAsync(EchoRequest{"A", 20}).get());
|
||||
auto b = mustOk(ctx->echoAsync(EchoRequest{a.echoed + "->B", 10}).get());
|
||||
auto c = mustOk(ctx->echoAsync(EchoRequest{b.echoed + "->C", 5}).get());
|
||||
return c;
|
||||
});
|
||||
|
||||
@ -224,6 +238,8 @@ TEST(TimerE2E, TriplePipeline) {
|
||||
}
|
||||
|
||||
// Per-thread context create -> one call -> destroy churns the FFI context pool.
|
||||
// Worker threads avoid gtest assertion macros (not thread-safe) and report via
|
||||
// the atomic `errors` counter instead.
|
||||
TEST(TimerE2E, StressShortLivedPerThreadContext) {
|
||||
constexpr int kThreads = 16;
|
||||
|
||||
@ -233,14 +249,13 @@ TEST(TimerE2E, StressShortLivedPerThreadContext) {
|
||||
|
||||
for (int t = 0; t < kThreads; ++t) {
|
||||
workers.emplace_back([&, t] {
|
||||
try {
|
||||
auto ctx = makeCtx("short-" + std::to_string(t));
|
||||
const auto resp = ctx->echo(EchoRequest{"hi", 0});
|
||||
if (resp.echoed != "hi") ++errors;
|
||||
if (resp.timerName != "short-" + std::to_string(t)) ++errors;
|
||||
} catch (const std::exception&) {
|
||||
++errors;
|
||||
}
|
||||
auto ctxRes = MyTimerCtx::create(TimerConfig{"short-" + std::to_string(t)});
|
||||
if (ctxRes.isErr()) { ++errors; return; }
|
||||
auto ctx = std::move(ctxRes.value());
|
||||
const auto resp = ctx->echo(EchoRequest{"hi", 0});
|
||||
if (resp.isErr()) { ++errors; return; }
|
||||
if (resp->echoed != "hi") ++errors;
|
||||
if (resp->timerName != "short-" + std::to_string(t)) ++errors;
|
||||
});
|
||||
}
|
||||
for (auto& w : workers) w.join();
|
||||
@ -259,13 +274,10 @@ TEST(TimerE2E, StressShortLivedSharedContext) {
|
||||
|
||||
for (int t = 0; t < kThreads; ++t) {
|
||||
workers.emplace_back([&, t] {
|
||||
try {
|
||||
const auto resp = shared->echo(EchoRequest{"x" + std::to_string(t), 0});
|
||||
if (resp.echoed != "x" + std::to_string(t)) ++errors;
|
||||
if (resp.timerName != "shared-short") ++errors;
|
||||
} catch (const std::exception&) {
|
||||
++errors;
|
||||
}
|
||||
const auto resp = shared->echo(EchoRequest{"x" + std::to_string(t), 0});
|
||||
if (resp.isErr()) { ++errors; return; }
|
||||
if (resp->echoed != "x" + std::to_string(t)) ++errors;
|
||||
if (resp->timerName != "shared-short") ++errors;
|
||||
});
|
||||
}
|
||||
for (auto& w : workers) w.join();
|
||||
@ -289,14 +301,17 @@ TEST(TimerE2E, ThreadedHammer) {
|
||||
|
||||
for (int t = 0; t < kThreads; ++t) {
|
||||
workers.emplace_back([&, t] {
|
||||
auto own = makeCtx("hammer-t" + std::to_string(t));
|
||||
auto ownRes = MyTimerCtx::create(TimerConfig{"hammer-t" + std::to_string(t)});
|
||||
if (ownRes.isErr()) { ++errors; return; }
|
||||
auto own = std::move(ownRes.value());
|
||||
for (int i = 0; i < kIters; ++i) {
|
||||
if ((i & 1) == 0) {
|
||||
const auto r = shared->echo(EchoRequest{"s", 0});
|
||||
if (r.echoed != "s") ++errors;
|
||||
if (r.isErr() || r->echoed != "s") ++errors;
|
||||
} else {
|
||||
auto f = own->echoAsync(EchoRequest{"a", 1});
|
||||
if (f.get().echoed != "a") ++errors;
|
||||
const auto r = f.get();
|
||||
if (r.isErr() || r->echoed != "a") ++errors;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -322,7 +337,7 @@ TEST(TimerE2E, TypedEventFiresAfterEcho) {
|
||||
[&](const EchoEvent& evt) { evtPromise.set_value(evt); });
|
||||
ASSERT_NE(handle.id, 0u) << "addOnEchoFiredListener returned zero id";
|
||||
|
||||
const auto resp = ctx->echo(EchoRequest{"event-msg", 1});
|
||||
const auto resp = mustOk(ctx->echo(EchoRequest{"event-msg", 1}));
|
||||
EXPECT_EQ(resp.echoed, "event-msg");
|
||||
|
||||
const auto status = evtFuture.wait_for(std::chrono::seconds(2));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user