From 1fd1ad07bb5eedb534a758742921821351750d23 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 18:23:02 +0200 Subject: [PATCH] feat(codegen): native C++ typed event handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the ergonomic native event surface to the C++ generator: `node.On(std::function&)>)` registers a native listener; a per-event extern "C" trampoline reads the typed POD (`fromC(*reinterpret_cast*>(msg))`) and invokes the handler — no CBOR. The handler is owned by the node (a `std::map` of `ListenerBase`) so its address stays valid until `removeEventListener`. The example registers `OnEchoFired` and receives a typed `EchoEvent` when Echo fires it. Verified end-to-end and ASAN-clean. With this the native C++ generator covers the full surface: requests (scalar/string/bool/seq/Option/nested), typed struct returns, and typed events. Co-Authored-By: Claude Opus 4.8 --- examples/timer/cpp_native_bindings/README.md | 4 +- examples/timer/cpp_native_bindings/main.cpp | 5 ++ .../cpp_native_bindings/my_timer_native.hpp | 34 +++++++++++ ffi/codegen/cpp_native.nim | 58 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/examples/timer/cpp_native_bindings/README.md b/examples/timer/cpp_native_bindings/README.md index bb7bbb5..a0ae586 100644 --- a/examples/timer/cpp_native_bindings/README.md +++ b/examples/timer/cpp_native_bindings/README.md @@ -34,6 +34,8 @@ echo, complex, schedule all generate and round-trip typed values (ASAN-clean). `toC` uses a holder that owns the C-array backing while string pointers borrow the C++ argument (valid for the call's duration; the library deep-copies). -Still to come: **native typed events** (`On` handlers) and the +Native typed events are supported too: `node.On(handler)` registers a +native listener and the typed payload arrives via `fromC` (no CBOR). Still to +come: the native-bare / `_cbor` filename reconciliation (matching the C headers). Today this emits `my_timer_native.hpp` so it coexists with the CBOR `my_timer.hpp`. diff --git a/examples/timer/cpp_native_bindings/main.cpp b/examples/timer/cpp_native_bindings/main.cpp index ef3d72e..45b1a0a 100644 --- a/examples/timer/cpp_native_bindings/main.cpp +++ b/examples/timer/cpp_native_bindings/main.cpp @@ -6,8 +6,13 @@ int main() { my_timer::My_timerNode node(my_timer::TimerConfig{"cpp-native-gen"}); std::cout << "version: " << node.Version() << "\n"; + my_timer::EchoEvent gotEvt; + bool got = false; + node.OnEchoFired([&](const my_timer::EchoEvent& e){ gotEvt = e; got = true; }); + auto r = node.Echo(my_timer::EchoRequest{"hello from generated C++", 5}); std::cout << "echo: echoed=" << r.echoed << " timerName=" << r.timerName << "\n"; + if (got) std::cout << "event OnEchoFired: message=\"" << gotEvt.message << "\" echoCount=" << gotEvt.echoCount << "\n"; // seq + Option params (ComplexRequest), typed ComplexResponse return. my_timer::ComplexRequest creq; diff --git a/examples/timer/cpp_native_bindings/my_timer_native.hpp b/examples/timer/cpp_native_bindings/my_timer_native.hpp index 0cc42ef..29e5d1b 100644 --- a/examples/timer/cpp_native_bindings/my_timer_native.hpp +++ b/examples/timer/cpp_native_bindings/my_timer_native.hpp @@ -7,7 +7,10 @@ #include "my_timer.h" #include +#include #include +#include +#include #include #include #include @@ -289,7 +292,16 @@ struct AckCapture { inline std::string rawText(const char* msg, std::size_t len) { return (msg && len) ? std::string(msg, len) : std::string(); } +// Event listener storage: a heap handler kept alive by the node so the +// native callback's userData stays valid until removed. +struct ListenerBase { virtual ~ListenerBase() = default; }; +template struct EventListener : ListenerBase { + std::function handler; + explicit EventListener(std::function h) + : handler(std::move(h)) {} +}; } // namespace detail +struct ListenerHandle { std::uint64_t id = 0; }; extern "C" { inline void my_timer_native_ack(int ret, const char* msg, std::size_t len, void* ud) { @@ -326,6 +338,11 @@ inline void my_timer_native_my_timer_schedule(int ret, const char* msg, std::siz else c->err = detail::rawText(msg, len); c->done.set_value(); } +inline void my_timer_evt_OnEchoFired(int ret, const char* msg, std::size_t, void* ud) { + auto* l = static_cast*>(ud); + if (ret == RET_OK && l->handler) + l->handler(fromC(*reinterpret_cast(msg))); +} } // extern "C" class My_timerNode { @@ -385,12 +402,29 @@ class My_timerNode { return cap.value; } + ListenerHandle OnEchoFired(std::function handler) { + auto l = std::make_unique>(std::move(handler)); + auto* raw = l.get(); + const auto id = my_timer_add_event_listener(ctx_, "on_echo_fired", &my_timer_evt_OnEchoFired, raw); + if (id == 0) return ListenerHandle{0}; + listeners_.emplace(id, std::move(l)); + return ListenerHandle{id}; + } + + bool removeEventListener(ListenerHandle handle) { + if (handle.id == 0) return false; + const auto rc = my_timer_remove_event_listener(ctx_, handle.id); + listeners_.erase(handle.id); + return rc == 0; + } + ~My_timerNode() { if (ctx_) my_timer_destroy(ctx_); } My_timerNode(const My_timerNode&) = delete; My_timerNode& operator=(const My_timerNode&) = delete; private: void* ctx_ = nullptr; + std::map> listeners_; }; } // namespace my_timer diff --git a/ffi/codegen/cpp_native.nim b/ffi/codegen/cpp_native.nim index 6ecb58d..2262702 100644 --- a/ffi/codegen/cpp_native.nim +++ b/ffi/codegen/cpp_native.nim @@ -210,7 +210,10 @@ proc generateCppNativeHeader*( L.add("") L.add("#include \"" & libName & ".h\"") L.add("#include ") + L.add("#include ") L.add("#include ") + L.add("#include ") + L.add("#include ") L.add("#include ") L.add("#include ") L.add("#include ") @@ -238,7 +241,18 @@ proc generateCppNativeHeader*( L.add("inline std::string rawText(const char* msg, std::size_t len) {") L.add(" return (msg && len) ? std::string(msg, len) : std::string();") L.add("}") + if events.len > 0: + L.add("// Event listener storage: a heap handler kept alive by the node so the") + L.add("// native callback's userData stays valid until removed.") + L.add("struct ListenerBase { virtual ~ListenerBase() = default; };") + L.add("template struct EventListener : ListenerBase {") + L.add(" std::function handler;") + L.add(" explicit EventListener(std::function h)") + L.add(" : handler(std::move(h)) {}") + L.add("};") L.add("} // namespace detail") + if events.len > 0: + L.add("struct ListenerHandle { std::uint64_t id = 0; };") L.add("") # Find ctor / dtor. @@ -282,6 +296,17 @@ proc generateCppNativeHeader*( L.add(" else c->err = detail::rawText(msg, len);") L.add(" c->done.set_value();") L.add("}") + # One native event trampoline per event: read the typed POD, call the handler. + for e in events: + if not isStructT(e.payloadTypeName, types): + continue + let pt = e.payloadTypeName + L.add("inline void " & libName & "_evt_" & snakeToPascalCase(e.wireName) & + "(int ret, const char* msg, std::size_t, void* ud) {") + L.add(" auto* l = static_cast*>(ud);") + L.add(" if (ret == RET_OK && l->handler)") + L.add(" l->handler(fromC(*reinterpret_cast(msg)));") + L.add("}") L.add("} // extern \"C\"") L.add("") @@ -353,6 +378,37 @@ proc generateCppNativeHeader*( L.add(" }") L.add("") + # Native typed event handlers: On(handler) registers a native listener + # that delivers the typed payload (read via fromC). The handler is owned by the + # node so its address stays valid until removeEventListener. + for e in events: + if not isStructT(e.payloadTypeName, types): + continue + let pascal = snakeToPascalCase(e.wireName) + let pt = e.payloadTypeName + L.add(" ListenerHandle " & pascal & + "(std::function handler) {") + L.add(" auto l = std::make_unique>(std::move(handler));") + L.add(" auto* raw = l.get();") + L.add(" const auto id = " & libName & + "_add_event_listener(ctx_, \"" & e.wireName & "\", &" & libName & "_evt_" & + pascal & ", raw);") + L.add(" if (id == 0) return ListenerHandle{0};") + L.add(" listeners_.emplace(id, std::move(l));") + L.add(" return ListenerHandle{id};") + L.add(" }") + L.add("") + if events.len > 0: + L.add(" bool removeEventListener(ListenerHandle handle) {") + L.add(" if (handle.id == 0) return false;") + L.add(" const auto rc = " & libName & + "_remove_event_listener(ctx_, handle.id);") + L.add(" listeners_.erase(handle.id);") + L.add(" return rc == 0;") + L.add(" }") + L.add("") + if haveDtor: L.add(" ~" & nodeT & "() { if (ctx_) " & dtor.procName & "(ctx_); }") L.add(" " & nodeT & "(const " & nodeT & "&) = delete;") @@ -360,6 +416,8 @@ proc generateCppNativeHeader*( L.add("") L.add(" private:") L.add(" void* ctx_ = nullptr;") + if events.len > 0: + L.add(" std::map> listeners_;") L.add("};") L.add("") L.add("} // namespace " & libName)