mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-26 23:51:08 +00:00
Cpp typed event listeners (#51)
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(my_timer_cpp_bindings CXX C)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
# The generated bindings target C++20. The event-listener API uses
|
||||
# std::span<const std::uint8_t> on its wildcard callback to hand the
|
||||
# CBOR envelope to consumers as a zero-copy view; <span> only became
|
||||
# part of the standard library in C++20.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# MSVC defaults __cplusplus to 199711L regardless of the active /std:c++XX
|
||||
# level — the generated header's C++20 guard would then misfire. /Zc:__cplusplus
|
||||
# makes MSVC report the actual standard. Harmless on every other compiler.
|
||||
if(MSVC)
|
||||
add_compile_options(/Zc:__cplusplus)
|
||||
endif()
|
||||
|
||||
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
|
||||
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(REPO_ROOT "")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "my_timer.hpp"
|
||||
#include <iostream>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
@@ -35,7 +38,6 @@ int main() {
|
||||
<< ", itemCount=" << complex.itemCount
|
||||
<< ", hasNote=" << complex.hasNote << "\n";
|
||||
|
||||
// ── 6. Call with three complex parameters ─────────────────────
|
||||
// Each parameter is its own generated C++ struct. The nim-ffi
|
||||
// macro packs all three into one CBOR envelope on the wire — at
|
||||
// the call site, this is just a typed method invocation.
|
||||
@@ -63,6 +65,57 @@ int main() {
|
||||
<< ", 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";
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
#pragma once
|
||||
// Generated bindings require C++20 — the event-listener API uses
|
||||
// std::span<const std::uint8_t> 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 <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
@@ -16,6 +28,8 @@ extern "C" {
|
||||
#include <tinycbor/cbor.h>
|
||||
}
|
||||
|
||||
#include <unordered_map>
|
||||
#include <span>
|
||||
// ── 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.
|
||||
@@ -693,6 +707,19 @@ inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)
|
||||
|
||||
#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED
|
||||
|
||||
template <class T>
|
||||
inline bool decodeEventPayload(std::span<const std::uint8_t> 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
|
||||
// ============================================================
|
||||
@@ -740,20 +767,34 @@ public:
|
||||
MyTimerCtx(MyTimerCtx&&) = delete;
|
||||
MyTimerCtx& operator=(MyTimerCtx&&) = delete;
|
||||
|
||||
// ── Typed event handlers ────────────────────────────────
|
||||
struct Events {
|
||||
std::function<void(const std::string&)> on_error;
|
||||
std::function<void(const EchoEvent&)> onEchoFired;
|
||||
};
|
||||
// ── Event listener API ──────────────────────────────────
|
||||
struct ListenerHandle { std::uint64_t id = 0; };
|
||||
|
||||
void setEventHandlers(Events handlers) {
|
||||
if (event_listener_id_ != 0) {
|
||||
my_timer_remove_event_listener(ptr_, event_listener_id_);
|
||||
event_listener_id_ = 0;
|
||||
}
|
||||
events_ = std::make_unique<Events>(std::move(handlers));
|
||||
event_listener_id_ = my_timer_add_event_listener(
|
||||
ptr_, "", &MyTimerCtx::eventTrampoline, events_.get());
|
||||
ListenerHandle addOnEchoFiredListener(std::function<void(const EchoEvent&)> handler) {
|
||||
auto owned = std::make_unique<TypedListener<EchoEvent>>(std::move(handler));
|
||||
auto* raw = owned.get();
|
||||
const auto id = my_timer_add_event_listener(
|
||||
ptr_, "on_echo_fired", &MyTimerCtx::typedTrampoline<EchoEvent>, raw);
|
||||
if (id == 0) return ListenerHandle{0};
|
||||
listeners_.emplace(id, std::move(owned));
|
||||
return ListenerHandle{id};
|
||||
}
|
||||
|
||||
ListenerHandle addEventListener(std::function<void(int, const std::string&, std::span<const std::uint8_t>)> handler) {
|
||||
auto owned = std::make_unique<WildcardListener>(std::move(handler));
|
||||
auto* raw = owned.get();
|
||||
const auto id = my_timer_add_event_listener(
|
||||
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;
|
||||
}
|
||||
|
||||
EchoResponse echo(const EchoRequest& req) const {
|
||||
@@ -809,38 +850,61 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void* ptr_;
|
||||
std::chrono::milliseconds timeout_;
|
||||
std::unique_ptr<Events> events_;
|
||||
uint64_t event_listener_id_ = 0;
|
||||
explicit MyTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}
|
||||
static void eventTrampoline(int ret, const char* msg, std::size_t len, void* ud) {
|
||||
if (!ud) return;
|
||||
auto* events = static_cast<Events*>(ud);
|
||||
if (ret != 0) {
|
||||
if (events->on_error) {
|
||||
std::string err(msg ? msg : "", len);
|
||||
events->on_error(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!msg || len == 0) return;
|
||||
std::vector<std::uint8_t> bytes(reinterpret_cast<const std::uint8_t*>(msg),
|
||||
reinterpret_cast<const std::uint8_t*>(msg) + len);
|
||||
struct ListenerBase {
|
||||
virtual ~ListenerBase() = default;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct TypedListener : ListenerBase {
|
||||
std::function<void(const T&)> fn;
|
||||
explicit TypedListener(std::function<void(const T&)> f) : fn(std::move(f)) {}
|
||||
};
|
||||
|
||||
struct WildcardListener : ListenerBase {
|
||||
std::function<void(int, const std::string&, std::span<const std::uint8_t>)> fn;
|
||||
explicit WildcardListener(std::function<void(int, const std::string&, std::span<const std::uint8_t>)> f) : fn(std::move(f)) {}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
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<TypedListener<T>*>(ud);
|
||||
if (!listener->fn) return;
|
||||
CborParser parser; CborValue it;
|
||||
if (cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it) != CborNoError) return;
|
||||
if (cbor_parser_init(reinterpret_cast<const std::uint8_t*>(msg), len, 0, &parser, &it) != CborNoError) return;
|
||||
if (!cbor_value_is_map(&it)) return;
|
||||
CborValue evtField;
|
||||
if (cbor_value_map_find_value(&it, "eventType", &evtField) != CborNoError) return;
|
||||
if (!cbor_value_is_text_string(&evtField)) return;
|
||||
std::string evtName; if (decode_cbor(evtField, evtName) != CborNoError) return;
|
||||
CborValue payloadField;
|
||||
if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) return;
|
||||
if (evtName == "on_echo_fired") {
|
||||
if (events->onEchoFired) {
|
||||
EchoEvent payload{}; if (decode_cbor(payloadField, payload) == CborNoError) events->onEchoFired(payload);
|
||||
}
|
||||
}
|
||||
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<WildcardListener*>(ud);
|
||||
if (!listener->fn) return;
|
||||
std::span<const std::uint8_t> envelope{};
|
||||
if (msg && len > 0) {
|
||||
envelope = std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t*>(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<std::uint64_t, std::unique_ptr<ListenerBase>> listeners_;
|
||||
explicit MyTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user