mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-09 00:13:24 +00:00
feat(codegen): native C++ typed event handlers
Adds the ergonomic native event surface to the C++ generator: `node.On<Event>(std::function<void(const <Payload>&)>)` registers a native listener; a per-event extern "C" trampoline reads the typed POD (`fromC(*reinterpret_cast<const ::<Payload>*>(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 <noreply@anthropic.com>
This commit is contained in:
parent
2302e5fb7d
commit
1fd1ad07bb
@ -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<Event>` handlers) and the
|
||||
Native typed events are supported too: `node.On<Event>(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`.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -7,7 +7,10 @@
|
||||
|
||||
#include "my_timer.h"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@ -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 <typename T> struct EventListener : ListenerBase {
|
||||
std::function<void(const T&)> handler;
|
||||
explicit EventListener(std::function<void(const T&)> 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<detail::EventListener<EchoEvent>*>(ud);
|
||||
if (ret == RET_OK && l->handler)
|
||||
l->handler(fromC(*reinterpret_cast<const ::EchoEvent*>(msg)));
|
||||
}
|
||||
} // extern "C"
|
||||
|
||||
class My_timerNode {
|
||||
@ -385,12 +402,29 @@ class My_timerNode {
|
||||
return cap.value;
|
||||
}
|
||||
|
||||
ListenerHandle OnEchoFired(std::function<void(const EchoEvent&)> handler) {
|
||||
auto l = std::make_unique<detail::EventListener<EchoEvent>>(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<std::uint64_t, std::unique_ptr<detail::ListenerBase>> listeners_;
|
||||
};
|
||||
|
||||
} // namespace my_timer
|
||||
|
||||
@ -210,7 +210,10 @@ proc generateCppNativeHeader*(
|
||||
L.add("")
|
||||
L.add("#include \"" & libName & ".h\"")
|
||||
L.add("#include <cstdint>")
|
||||
L.add("#include <functional>")
|
||||
L.add("#include <future>")
|
||||
L.add("#include <map>")
|
||||
L.add("#include <memory>")
|
||||
L.add("#include <optional>")
|
||||
L.add("#include <stdexcept>")
|
||||
L.add("#include <string>")
|
||||
@ -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 <typename T> struct EventListener : ListenerBase {")
|
||||
L.add(" std::function<void(const T&)> handler;")
|
||||
L.add(" explicit EventListener(std::function<void(const T&)> 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<detail::EventListener<" & pt & ">*>(ud);")
|
||||
L.add(" if (ret == RET_OK && l->handler)")
|
||||
L.add(" l->handler(fromC(*reinterpret_cast<const ::" & pt & "*>(msg)));")
|
||||
L.add("}")
|
||||
L.add("} // extern \"C\"")
|
||||
L.add("")
|
||||
|
||||
@ -353,6 +378,37 @@ proc generateCppNativeHeader*(
|
||||
L.add(" }")
|
||||
L.add("")
|
||||
|
||||
# Native typed event handlers: On<Event>(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<void(const " & pt & "&)> handler) {")
|
||||
L.add(" auto l = std::make_unique<detail::EventListener<" & pt &
|
||||
">>(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<std::uint64_t, std::unique_ptr<detail::ListenerBase>> listeners_;")
|
||||
L.add("};")
|
||||
L.add("")
|
||||
L.add("} // namespace " & libName)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user