Cpp typed event listeners (#51)

This commit is contained in:
Ivan FB
2026-05-28 22:40:33 +02:00
committed by GitHub
parent 3f19411684
commit e394166c46
9 changed files with 550 additions and 125 deletions
+12 -1
View File
@@ -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 "")
+55 -2
View File
@@ -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";
+105 -41
View File
@@ -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) {}
};