mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-21 16:59:30 +00:00
remove the wildcard event listener
This commit is contained in:
parent
c43563f82f
commit
8a3a438837
@ -1,10 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(echo_cpp_bindings CXX C)
|
||||
|
||||
# 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.
|
||||
# The generated bindings target C++20: designated initializers and other
|
||||
# C++20 constructs are used throughout the emitted code.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// Generated bindings require C++20 — the event-listener API uses
|
||||
// std::span<const std::uint8_t> for the wildcard callback.
|
||||
// Generated bindings require C++20 (designated initializers and other
|
||||
// C++20 constructs are used throughout the emitted code).
|
||||
// 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).
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(my_timer_cpp_bindings CXX C)
|
||||
|
||||
# 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.
|
||||
# The generated bindings target C++20: designated initializers and other
|
||||
# C++20 constructs are used throughout the emitted code.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
|
||||
@ -92,54 +92,25 @@ int main() {
|
||||
|
||||
// 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
|
||||
// Subscribe to each event separately; handlers 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";
|
||||
<< ", echoCount=" << evt.echoCount << "\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.
|
||||
// Drop the typed listener — no handler fires for the follow-up echo.
|
||||
// Sleep briefly to give the lib thread time to settle 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 << "[7] after removeEventListener: typed listener removed\n";
|
||||
|
||||
std::cout << "\nDone.\n";
|
||||
return 0;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// Generated bindings require C++20 — the event-listener API uses
|
||||
// std::span<const std::uint8_t> for the wildcard callback.
|
||||
// Generated bindings require C++20 (designated initializers and other
|
||||
// C++20 constructs are used throughout the emitted code).
|
||||
// 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).
|
||||
@ -30,7 +30,6 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include <unordered_map>
|
||||
#include <span>
|
||||
// ============================================================
|
||||
// Result<T> — exception-free error channel
|
||||
// ============================================================
|
||||
@ -773,19 +772,6 @@ inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
|
||||
#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
|
||||
// ============================================================
|
||||
@ -853,16 +839,6 @@ public:
|
||||
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);
|
||||
@ -945,11 +921,6 @@ private:
|
||||
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;
|
||||
@ -965,29 +936,6 @@ private:
|
||||
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_;
|
||||
|
||||
@ -117,48 +117,9 @@ unsafe extern "C" fn on_echo_fired_trampoline(
|
||||
}
|
||||
}
|
||||
|
||||
struct WildcardHandler {
|
||||
f: Box<dyn Fn(c_int, &str, &[u8]) + Send + Sync>,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn my_timer_wildcard_trampoline(
|
||||
ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,
|
||||
) {
|
||||
if ud.is_null() { return; }
|
||||
let h = &*(ud as *const WildcardHandler);
|
||||
let bytes = if !msg.is_null() && len > 0 {
|
||||
slice::from_raw_parts(msg as *const u8, len)
|
||||
} else { &[] };
|
||||
let event_id = if ret == 0 && !bytes.is_empty() {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EnvelopeMeta {
|
||||
#[serde(rename = "eventType")]
|
||||
event_type: String,
|
||||
}
|
||||
ciborium::de::from_reader::<EnvelopeMeta, _>(bytes)
|
||||
.map(|m| m.event_type).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
(h.f)(ret, event_id.as_str(), bytes);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ListenerHandle { pub id: u64 }
|
||||
|
||||
/// Decode the `payload` field of a CBOR `EventEnvelope` as `T`.
|
||||
/// Returns `Err` if the envelope is empty / malformed / the payload
|
||||
/// cannot be deserialised as `T`.
|
||||
pub fn decode_event_payload<T: serde::de::DeserializeOwned>(
|
||||
envelope: &[u8],
|
||||
) -> Result<T, String> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Envelope<T> { payload: T }
|
||||
let env: Envelope<T> = ciborium::de::from_reader(envelope)
|
||||
.map_err(|e| format!("decode event payload: {e}"))?;
|
||||
Ok(env.payload)
|
||||
}
|
||||
|
||||
/// High-level context for `MyTimer`.
|
||||
pub struct MyTimerCtx {
|
||||
ptr: *mut c_void,
|
||||
@ -237,19 +198,6 @@ impl MyTimerCtx {
|
||||
self.add_listener_inner(b"on_echo_fired\0".as_ptr() as *const c_char, on_echo_fired_trampoline, raw, owned)
|
||||
}
|
||||
|
||||
/// Register a catch-all listener that receives every event.
|
||||
/// The handler arguments are (return_code, event_id, envelope_bytes):
|
||||
/// `event_id` is the wire `eventType` string extracted from the
|
||||
/// envelope (empty on error or malformed envelope); `envelope_bytes`
|
||||
/// is the full CBOR envelope, suitable for `decode_event_payload::<T>`.
|
||||
pub fn add_event_listener<F>(&self, handler: F) -> ListenerHandle
|
||||
where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static,
|
||||
{
|
||||
let owned: Box<WildcardHandler> = Box::new(WildcardHandler { f: Box::new(handler) });
|
||||
let raw = &*owned as *const WildcardHandler as *mut c_void;
|
||||
self.add_listener_inner(b"\0".as_ptr() as *const c_char, my_timer_wildcard_trampoline, raw, owned)
|
||||
}
|
||||
|
||||
/// Remove a previously-registered listener by handle. Returns true
|
||||
/// if the listener existed and was removed; false otherwise.
|
||||
pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {
|
||||
|
||||
@ -145,10 +145,7 @@ proc emitEventDispatcher(
|
||||
## per declared `{.ffiEvent.}`. Internally registers under the wire
|
||||
## event name; the per-listener trampoline decodes the CBOR
|
||||
## envelope's `payload` field as `T` and invokes the user handler.
|
||||
## - `addEventListener(std::function<void(int, const std::string&)>) ->
|
||||
## ListenerHandle` registers a catch-all wildcard listener
|
||||
## (event_name == "") that receives every event as raw envelope
|
||||
## bytes plus the FFI return code.
|
||||
## Callers subscribe to each event separately.
|
||||
## - `removeEventListener(ListenerHandle) -> bool` drops a listener by
|
||||
## handle. After it returns true, no further callbacks for that id
|
||||
## are in flight on the FFI side (the Nim-side registry lock plus
|
||||
@ -189,30 +186,6 @@ proc emitEventDispatcher(
|
||||
lines.add(" return ListenerHandle{id};")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
# Generic wildcard registration.
|
||||
#
|
||||
# The handler receives the FFI return code, the wire `eventType` string
|
||||
# extracted from the CBOR envelope (empty if the envelope is malformed
|
||||
# or `ret != 0`), and a `std::span` view over the raw envelope bytes —
|
||||
# the buffer is owned by the dylib and stays valid for the duration of
|
||||
# the synchronous callback only. Pair with `decodeEventPayload<T>` to
|
||||
# lift the payload into a typed value without hand-rolling CBOR parsing.
|
||||
lines.add(
|
||||
" ListenerHandle addEventListener(std::function<void(int, const std::string&, std::span<const std::uint8_t>)> handler) {"
|
||||
)
|
||||
lines.add(
|
||||
" auto owned = std::make_unique<WildcardListener>(std::move(handler));"
|
||||
)
|
||||
lines.add(" auto* raw = owned.get();")
|
||||
lines.add(" const auto id = $1_add_event_listener(" % [libName])
|
||||
lines.add(
|
||||
" ptr_, \"\", &$1::wildcardTrampoline, raw);" % [ctxTypeName]
|
||||
)
|
||||
lines.add(" if (id == 0) return ListenerHandle{0};")
|
||||
lines.add(" listeners_.emplace(id, std::move(owned));")
|
||||
lines.add(" return ListenerHandle{id};")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
# Remove by handle.
|
||||
lines.add(" bool removeEventListener(ListenerHandle handle) {")
|
||||
lines.add(" if (handle.id == 0) return false;")
|
||||
@ -231,16 +204,10 @@ proc emitEventTrampoline(
|
||||
## `emitEventDispatcher`:
|
||||
##
|
||||
## - `ListenerBase` is a polymorphic base so the context's
|
||||
## `listeners_` map can own typed and wildcard listeners under a
|
||||
## single value type.
|
||||
## `listeners_` map can own typed listeners under a single value type.
|
||||
## - `TypedListener<T>` holds the user's `std::function<void(const T&)>`
|
||||
## and is the target of `typedTrampoline<T>`, which CBOR-decodes the
|
||||
## envelope's `payload` field as `T` and invokes the handler.
|
||||
## - `WildcardListener` holds a `std::function<void(int, const
|
||||
## std::string&, std::span<const std::uint8_t>)>` and is the target
|
||||
## of `wildcardTrampoline`, which forwards the FFI return code plus
|
||||
## a span view over the raw payload bytes (no copy — the dylib owns
|
||||
## the buffer for the duration of the synchronous callback).
|
||||
if events.len == 0:
|
||||
return
|
||||
lines.add(" struct ListenerBase {")
|
||||
@ -253,15 +220,6 @@ proc emitEventTrampoline(
|
||||
lines.add(" explicit TypedListener(std::function<void(const T&)> f) : fn(std::move(f)) {}")
|
||||
lines.add(" };")
|
||||
lines.add("")
|
||||
lines.add(" struct WildcardListener : ListenerBase {")
|
||||
lines.add(
|
||||
" std::function<void(int, const std::string&, std::span<const std::uint8_t>)> fn;"
|
||||
)
|
||||
lines.add(
|
||||
" explicit WildcardListener(std::function<void(int, const std::string&, std::span<const std::uint8_t>)> f) : fn(std::move(f)) {}"
|
||||
)
|
||||
lines.add(" };")
|
||||
lines.add("")
|
||||
# Typed trampoline — one instantiation per payload type, all sharing a body.
|
||||
lines.add(" template <class T>")
|
||||
lines.add(" static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) {")
|
||||
@ -284,45 +242,6 @@ proc emitEventTrampoline(
|
||||
lines.add(" listener->fn(payload);")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
# Wildcard trampoline — extracts `eventType` from the CBOR envelope so
|
||||
# the user can match on the event name without hand-parsing. Falls back
|
||||
# to an empty `eventId` if the envelope is missing, malformed, or the
|
||||
# FFI return code signals an error (in which case the bytes are an
|
||||
# error string, not a CBOR envelope).
|
||||
lines.add(
|
||||
" static void wildcardTrampoline(int ret, const char* msg, std::size_t len, void* ud) {"
|
||||
)
|
||||
lines.add(" if (!ud) return;")
|
||||
lines.add(" auto* listener = static_cast<WildcardListener*>(ud);")
|
||||
lines.add(" if (!listener->fn) return;")
|
||||
# The buffer pointed to by `msg` is owned by the dylib and stays valid
|
||||
# for the duration of this synchronous call — safe to hand to the user
|
||||
# as a span view rather than copying.
|
||||
lines.add(" std::span<const std::uint8_t> envelope{};")
|
||||
lines.add(" if (msg && len > 0) {")
|
||||
lines.add(
|
||||
" envelope = std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t*>(msg), len);"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" std::string eventId;")
|
||||
lines.add(" if (ret == 0 && !envelope.empty()) {")
|
||||
lines.add(" CborParser parser; CborValue it;")
|
||||
lines.add(
|
||||
" if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) == CborNoError"
|
||||
)
|
||||
lines.add(" && cbor_value_is_map(&it)) {")
|
||||
lines.add(" CborValue evtField;")
|
||||
lines.add(
|
||||
" if (cbor_value_map_find_value(&it, \"eventType\", &evtField) == CborNoError"
|
||||
)
|
||||
lines.add(" && cbor_value_is_text_string(&evtField)) {")
|
||||
lines.add(" (void)decode_cbor(evtField, eventId);")
|
||||
lines.add(" }")
|
||||
lines.add(" }")
|
||||
lines.add(" }")
|
||||
lines.add(" listener->fn(ret, eventId, envelope);")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
proc generateCppHeader*(
|
||||
procs: seq[FFIProcMeta],
|
||||
@ -335,10 +254,8 @@ proc generateCppHeader*(
|
||||
lines.add(HeaderPreludeTpl)
|
||||
if events.len > 0:
|
||||
# Only pulled in when the library declares `{.ffiEvent.}` procs —
|
||||
# `<unordered_map>` backs the `listeners_` map, `<span>` is the
|
||||
# zero-copy view type handed to wildcard callbacks.
|
||||
# `<unordered_map>` backs the `listeners_` map.
|
||||
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,
|
||||
@ -437,33 +354,6 @@ proc generateCppHeader*(
|
||||
|
||||
lines.add(SyncCallHelperTpl)
|
||||
|
||||
# ── Event-payload decoder helper ──────────────────────────────────────────
|
||||
# Lets wildcard-listener bodies lift the `payload` field out of a CBOR
|
||||
# envelope into any registered event type with a single call, e.g.
|
||||
# EchoEvent evt;
|
||||
# if (decodeEventPayload(envelope, evt)) { ... }
|
||||
# Relies on the per-struct `decode_cbor` codec emitted above.
|
||||
if events.len > 0:
|
||||
lines.add("template <class T>")
|
||||
lines.add(
|
||||
"inline bool decodeEventPayload(std::span<const std::uint8_t> envelope, T& out) {"
|
||||
)
|
||||
lines.add(" if (envelope.empty()) return false;")
|
||||
lines.add(" CborParser parser; CborValue it;")
|
||||
lines.add(
|
||||
" if (cbor_parser_init(envelope.data(), envelope.size(), 0, &parser, &it) != CborNoError)"
|
||||
)
|
||||
lines.add(" return false;")
|
||||
lines.add(" if (!cbor_value_is_map(&it)) return false;")
|
||||
lines.add(" CborValue payloadField;")
|
||||
lines.add(
|
||||
" if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError)"
|
||||
)
|
||||
lines.add(" return false;")
|
||||
lines.add(" return decode_cbor(payloadField, out) == CborNoError;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── High-level C++ context class ──────────────────────────────────────────
|
||||
var ctors: seq[FFIProcMeta] = @[]
|
||||
var methods: seq[FFIProcMeta] = @[]
|
||||
@ -661,13 +551,13 @@ proc generateCppHeader*(
|
||||
lines.add("")
|
||||
|
||||
lines.add("private:")
|
||||
# Listener machinery (`ListenerBase`, `TypedListener<T>`,
|
||||
# `WildcardListener`, plus the static trampolines) must appear before
|
||||
# the `listeners_` data member declaration — C++ requires the value
|
||||
# type of a member to be complete at point of declaration. The public
|
||||
# add*/remove methods above also reference these types, but member
|
||||
# function bodies see the full class scope regardless of declaration
|
||||
# order, so emitting here is sufficient for both.
|
||||
# Listener machinery (`ListenerBase`, `TypedListener<T>`, plus the
|
||||
# static trampolines) must appear before the `listeners_` data member
|
||||
# declaration — C++ requires the value type of a member to be complete
|
||||
# at point of declaration. The public add*/remove methods above also
|
||||
# reference these types, but member function bodies see the full class
|
||||
# scope regardless of declaration order, so emitting here is sufficient
|
||||
# for both.
|
||||
emitEventTrampoline(lines, events)
|
||||
lines.add(" void* ptr_;")
|
||||
lines.add(" std::chrono::milliseconds timeout_;")
|
||||
|
||||
@ -449,8 +449,8 @@ proc generateApiRs*(
|
||||
# ── Per-listener handler boxes + extern "C" trampolines ─────────────────
|
||||
# Each registered listener owns a `Box<…Handler>` that is kept alive in
|
||||
# `$1::listeners` (keyed by listener id). The raw pointer to the inner
|
||||
# handler is handed to the dylib as `user_data` for the per-event or
|
||||
# wildcard trampoline below.
|
||||
# handler is handed to the dylib as `user_data` for the per-event
|
||||
# trampoline below.
|
||||
if events.len > 0:
|
||||
for ev in events:
|
||||
let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler"
|
||||
@ -481,73 +481,11 @@ proc generateApiRs*(
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# Wildcard handler — receives every event as raw envelope bytes,
|
||||
# the FFI return code, and the `eventType` string pre-extracted
|
||||
# from the CBOR envelope. `event_id` is empty when `ret != 0` or
|
||||
# the envelope is malformed (the bytes are an error string, not a
|
||||
# CBOR envelope, in that case).
|
||||
lines.add("struct WildcardHandler {")
|
||||
lines.add(" f: Box<dyn Fn(c_int, &str, &[u8]) + Send + Sync>,")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("unsafe extern \"C\" fn $1_wildcard_trampoline(" % [libName])
|
||||
lines.add(" ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,")
|
||||
lines.add(") {")
|
||||
lines.add(" if ud.is_null() { return; }")
|
||||
lines.add(" let h = &*(ud as *const WildcardHandler);")
|
||||
lines.add(" let bytes = if !msg.is_null() && len > 0 {")
|
||||
lines.add(" slice::from_raw_parts(msg as *const u8, len)")
|
||||
lines.add(" } else { &[] };")
|
||||
lines.add(" let event_id = if ret == 0 && !bytes.is_empty() {")
|
||||
lines.add(" #[derive(serde::Deserialize)]")
|
||||
lines.add(" struct EnvelopeMeta {")
|
||||
lines.add(" #[serde(rename = \"eventType\")]")
|
||||
lines.add(" event_type: String,")
|
||||
lines.add(" }")
|
||||
lines.add(
|
||||
" ciborium::de::from_reader::<EnvelopeMeta, _>(bytes)"
|
||||
)
|
||||
lines.add(" .map(|m| m.event_type).unwrap_or_default()")
|
||||
lines.add(" } else {")
|
||||
lines.add(" String::new()")
|
||||
lines.add(" };")
|
||||
lines.add(" (h.f)(ret, event_id.as_str(), bytes);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# Public handle returned by every add_…_listener call.
|
||||
lines.add("#[derive(Debug, Clone, Copy)]")
|
||||
lines.add("pub struct ListenerHandle { pub id: u64 }")
|
||||
lines.add("")
|
||||
|
||||
# Helper: decode an event envelope's `payload` field into any typed
|
||||
# `T` that the generated `types.rs` already derives `Deserialize` on.
|
||||
# Pair with `add_event_listener` to lift raw envelope bytes into a
|
||||
# typed payload without hand-rolling ciborium calls in each branch.
|
||||
lines.add(
|
||||
"/// Decode the `payload` field of a CBOR `EventEnvelope` as `T`."
|
||||
)
|
||||
lines.add(
|
||||
"/// Returns `Err` if the envelope is empty / malformed / the payload"
|
||||
)
|
||||
lines.add("/// cannot be deserialised as `T`.")
|
||||
lines.add(
|
||||
"pub fn decode_event_payload<T: serde::de::DeserializeOwned>("
|
||||
)
|
||||
lines.add(" envelope: &[u8],")
|
||||
lines.add(") -> Result<T, String> {")
|
||||
lines.add(" #[derive(serde::Deserialize)]")
|
||||
lines.add(" struct Envelope<T> { payload: T }")
|
||||
lines.add(
|
||||
" let env: Envelope<T> = ciborium::de::from_reader(envelope)"
|
||||
)
|
||||
lines.add(
|
||||
" .map_err(|e| format!(\"decode event payload: {e}\"))?;"
|
||||
)
|
||||
lines.add(" Ok(env.payload)")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── Context struct ─────────────────────────────────────────────────────────
|
||||
lines.add("/// High-level context for `$1`." % [libTypeName])
|
||||
lines.add("pub struct $1 {" % [ctxTypeName])
|
||||
@ -692,11 +630,11 @@ proc generateApiRs*(
|
||||
# ── Listener-registration API ─────────────────────────────────────────
|
||||
if events.len > 0:
|
||||
# Private helper shared by every public `add_*_listener`: the
|
||||
# FFI call + map insertion is identical across the typed and
|
||||
# wildcard variants, so it lives in one place. The caller owns
|
||||
# the box (typed as the concrete handler struct so the raw
|
||||
# pointer matches the trampoline's expected type) and only
|
||||
# erases it to `dyn Any + Send` when handing ownership over.
|
||||
# FFI call + map insertion is identical across the typed event
|
||||
# variants, so it lives in one place. The caller owns the box
|
||||
# (typed as the concrete handler struct so the raw pointer matches
|
||||
# the trampoline's expected type) and only erases it to
|
||||
# `dyn Any + Send` when handing ownership over.
|
||||
lines.add(" fn add_listener_inner(")
|
||||
lines.add(" &self,")
|
||||
lines.add(" event_name: *const c_char,")
|
||||
@ -746,43 +684,6 @@ proc generateApiRs*(
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# Generic wildcard listener — receives every event with the wire
|
||||
# `eventType` string pre-extracted plus the raw envelope bytes. Pair
|
||||
# with `decode_event_payload::<T>` to lift the payload into a typed
|
||||
# value.
|
||||
lines.add(
|
||||
" /// Register a catch-all listener that receives every event."
|
||||
)
|
||||
lines.add(
|
||||
" /// The handler arguments are (return_code, event_id, envelope_bytes):"
|
||||
)
|
||||
lines.add(
|
||||
" /// `event_id` is the wire `eventType` string extracted from the"
|
||||
)
|
||||
lines.add(
|
||||
" /// envelope (empty on error or malformed envelope); `envelope_bytes`"
|
||||
)
|
||||
lines.add(
|
||||
" /// is the full CBOR envelope, suitable for `decode_event_payload::<T>`."
|
||||
)
|
||||
lines.add(
|
||||
" pub fn add_event_listener<F>(&self, handler: F) -> ListenerHandle"
|
||||
)
|
||||
lines.add(" where F: Fn(c_int, &str, &[u8]) + Send + Sync + 'static,")
|
||||
lines.add(" {")
|
||||
lines.add(
|
||||
" let owned: Box<WildcardHandler> = Box::new(WildcardHandler { f: Box::new(handler) });"
|
||||
)
|
||||
lines.add(
|
||||
" let raw = &*owned as *const WildcardHandler as *mut c_void;"
|
||||
)
|
||||
lines.add(
|
||||
" self.add_listener_inner(b\"\\0\".as_ptr() as *const c_char, $1_wildcard_trampoline, raw, owned)" %
|
||||
[libName]
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# Remove by handle. Drops the Box (and the user's closure) after the
|
||||
# C ABI confirms the listener has been unregistered.
|
||||
lines.add(
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project({{LIB}}_cpp_bindings CXX C)
|
||||
|
||||
# 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.
|
||||
# The generated bindings target C++20: designated initializers and other
|
||||
# C++20 constructs are used throughout the emitted code.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// Generated bindings require C++20 — the event-listener API uses
|
||||
// std::span<const std::uint8_t> for the wildcard callback.
|
||||
// Generated bindings require C++20 (designated initializers and other
|
||||
// C++20 constructs are used throughout the emitted code).
|
||||
// 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).
|
||||
|
||||
@ -107,8 +107,7 @@ proc onNotResponding*(ctx: ptr FFIContext) =
|
||||
## Mirrors the dispatch templates' lock-during-invocation contract
|
||||
## (see `ffi_events.nim`).
|
||||
withLock ctx[].eventRegistry.lock:
|
||||
let snap = ctx[].eventRegistry.byEvent.getOrDefault("onNotResponding") &
|
||||
ctx[].eventRegistry.wildcard
|
||||
let snap = ctx[].eventRegistry.byEvent.getOrDefault("onNotResponding")
|
||||
if snap.len == 0:
|
||||
chronicles.debug "onNotResponding - no listener registered"
|
||||
return
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
## This module owns two concerns so they can evolve together without dragging
|
||||
## in the rest of `FFIContext`:
|
||||
##
|
||||
## 1. A multi-listener registry. Each event name maps to a `seq` of listeners;
|
||||
## the empty event name `""` is the wildcard channel and receives every
|
||||
## dispatched event in addition to its own per-name subscribers.
|
||||
## 1. A multi-listener registry. Each event name maps to a `seq` of
|
||||
## listeners; a dispatched event reaches exactly the listeners
|
||||
## subscribed to its name. Callers subscribe to each event separately.
|
||||
## 2. The dispatch templates (`dispatchFFIEvent`, `dispatchFFIEventCbor`) used
|
||||
## by `{.ffiEvent.}`-generated procs. They snapshot the registry under its
|
||||
## lock, then invoke each listener *outside* the lock so re-entrant
|
||||
@ -49,10 +49,6 @@ type
|
||||
lock*: Lock
|
||||
nextId*: uint64 ## Monotonic id source. 0 is reserved as "invalid"; ids start at 1.
|
||||
byEvent*: Table[string, seq[FFIEventListener]]
|
||||
wildcard*: seq[FFIEventListener]
|
||||
|
||||
const WildcardEventName* = ""
|
||||
## Empty string registers a wildcard listener that receives every event.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry lifecycle and mutation
|
||||
@ -66,7 +62,6 @@ proc initEventRegistry*(reg: var FFIEventRegistry) =
|
||||
reg.lock.initLock()
|
||||
reg.nextId = 0'u64
|
||||
reg.byEvent = initTable[string, seq[FFIEventListener]]()
|
||||
reg.wildcard.setLen(0)
|
||||
|
||||
proc deinitEventRegistry*(reg: var FFIEventRegistry) =
|
||||
## Mirror of `initEventRegistry`: must be called exactly once, by the
|
||||
@ -80,7 +75,6 @@ proc deinitEventRegistry*(reg: var FFIEventRegistry) =
|
||||
## assignment destructor against this thread's heap allocations.
|
||||
reg.lock.deinitLock()
|
||||
reg.byEvent = default(Table[string, seq[FFIEventListener]])
|
||||
reg.wildcard = @[]
|
||||
reg.nextId = 0'u64
|
||||
|
||||
proc addEventListener*(
|
||||
@ -90,9 +84,10 @@ proc addEventListener*(
|
||||
userData: pointer,
|
||||
): uint64 {.raises: [].} =
|
||||
## Registers `callback` for `eventName` and returns the listener's stable
|
||||
## id (always non-zero on success). `eventName == ""` registers a wildcard
|
||||
## listener that receives every dispatched event. Returns 0 if `callback`
|
||||
## is nil — the only documented failure mode.
|
||||
## id (always non-zero on success). A listener only receives events
|
||||
## dispatched under its own `eventName` — subscribe to each event
|
||||
## separately. Returns 0 if `callback` is nil — the only documented
|
||||
## failure mode.
|
||||
if callback.isNil():
|
||||
return 0
|
||||
|
||||
@ -103,10 +98,7 @@ proc addEventListener*(
|
||||
assigned = reg.nextId
|
||||
let listener =
|
||||
FFIEventListener(id: assigned, callback: callback, userData: userData)
|
||||
if eventName.len == 0:
|
||||
reg.wildcard.add(listener)
|
||||
else:
|
||||
reg.byEvent.mgetOrPut(eventName, @[]).add(listener)
|
||||
reg.byEvent.mgetOrPut(eventName, @[]).add(listener)
|
||||
return assigned
|
||||
|
||||
proc removeEventListener*(reg: var FFIEventRegistry, id: uint64): bool {.raises: [].} =
|
||||
@ -120,54 +112,44 @@ proc removeEventListener*(reg: var FFIEventRegistry, id: uint64): bool {.raises:
|
||||
var removed = false
|
||||
|
||||
withLock reg.lock:
|
||||
for i in 0 ..< reg.wildcard.len:
|
||||
if reg.wildcard[i].id == id:
|
||||
reg.wildcard.delete(i)
|
||||
removed = true
|
||||
break
|
||||
if not removed:
|
||||
var emptyKey = ""
|
||||
var prune = false
|
||||
for key, listeners in reg.byEvent.mpairs:
|
||||
var idx = -1
|
||||
for i in 0 ..< listeners.len:
|
||||
if listeners[i].id == id:
|
||||
idx = i
|
||||
break
|
||||
if idx >= 0:
|
||||
listeners.delete(idx)
|
||||
removed = true
|
||||
if listeners.len == 0:
|
||||
emptyKey = key
|
||||
prune = true
|
||||
var emptyKey = ""
|
||||
var prune = false
|
||||
for key, listeners in reg.byEvent.mpairs:
|
||||
var idx = -1
|
||||
for i in 0 ..< listeners.len:
|
||||
if listeners[i].id == id:
|
||||
idx = i
|
||||
break
|
||||
if prune:
|
||||
reg.byEvent.del(emptyKey)
|
||||
if idx >= 0:
|
||||
listeners.delete(idx)
|
||||
removed = true
|
||||
if listeners.len == 0:
|
||||
emptyKey = key
|
||||
prune = true
|
||||
break
|
||||
if prune:
|
||||
reg.byEvent.del(emptyKey)
|
||||
return removed
|
||||
|
||||
proc removeAllEventListeners*(reg: var FFIEventRegistry) {.raises: [].} =
|
||||
## Drops every registered listener (per-event and wildcard). Does not
|
||||
## reset the listener-id counter — subsequent `addEventListener` calls
|
||||
## still return strictly increasing ids.
|
||||
## Drops every registered listener. Does not reset the listener-id
|
||||
## counter — subsequent `addEventListener` calls still return strictly
|
||||
## increasing ids.
|
||||
withLock reg.lock:
|
||||
reg.wildcard.setLen(0)
|
||||
reg.byEvent.clear()
|
||||
|
||||
proc snapshotListeners*(
|
||||
reg: var FFIEventRegistry, eventName: string
|
||||
): seq[FFIEventListener] {.raises: [].} =
|
||||
## Returns a copy of the listener slice for `eventName`, plus every
|
||||
## wildcard listener. The copy is what makes re-entrant add/remove from
|
||||
## inside a handler deadlock-free: dispatch holds the lock only for the
|
||||
## duration of the copy, then iterates the copy outside the lock.
|
||||
## Returns a copy of the listener slice for `eventName`. The copy is what
|
||||
## makes re-entrant add/remove from inside a handler deadlock-free:
|
||||
## dispatch holds the lock only for the duration of the copy, then
|
||||
## iterates the copy outside the lock.
|
||||
var snap: seq[FFIEventListener] = @[]
|
||||
withLock reg.lock:
|
||||
if eventName.len > 0:
|
||||
# `getOrDefault` returns an empty seq when the key is absent —
|
||||
# avoids the raising `[]` operator path.
|
||||
for l in reg.byEvent.getOrDefault(eventName):
|
||||
snap.add(l)
|
||||
for l in reg.wildcard:
|
||||
# `getOrDefault` returns an empty seq when the key is absent —
|
||||
# avoids the raising `[]` operator path.
|
||||
for l in reg.byEvent.getOrDefault(eventName):
|
||||
snap.add(l)
|
||||
return snap
|
||||
|
||||
@ -192,8 +174,7 @@ template withFFIEventDispatch(
|
||||
return
|
||||
|
||||
withLock regPtr[].lock:
|
||||
let listeners =
|
||||
regPtr[].byEvent.getOrDefault(eventName) & regPtr[].wildcard
|
||||
let listeners = regPtr[].byEvent.getOrDefault(eventName)
|
||||
if listeners.len == 0:
|
||||
chronicles.debug eventName & " - no listener registered"
|
||||
else:
|
||||
@ -212,8 +193,8 @@ template withFFIEventDispatch(
|
||||
)
|
||||
|
||||
template dispatchFFIEvent*(eventName: string, body: untyped) =
|
||||
## Dispatches an FFI event to every listener for `eventName` plus every
|
||||
## wildcard listener. `body` must yield a `string` or `seq[byte]`.
|
||||
## Dispatches an FFI event to every listener subscribed to `eventName`.
|
||||
## `body` must yield a `string` or `seq[byte]`.
|
||||
##
|
||||
## Valid only on the FFI thread (where `ffiCurrentEventRegistry` is
|
||||
## set). Holds `reg.lock` for the entire snapshot + invocation so a
|
||||
|
||||
@ -112,8 +112,9 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
|
||||
## ABI on its `FFIContext`:
|
||||
##
|
||||
## - `{libraryName}_add_event_listener(ctx, event_name, cb, ud) -> uint64`
|
||||
## — registers `cb` for `event_name` and returns its stable id. An
|
||||
## empty `event_name` subscribes `cb` to *every* event (catch-all).
|
||||
## — registers `cb` for `event_name` and returns its stable id. `cb`
|
||||
## only receives events dispatched under `event_name`; subscribe to
|
||||
## each event separately.
|
||||
## - `{libraryName}_remove_event_listener(ctx, id) -> cint` — returns 0 on
|
||||
## success, non-zero if no listener with that id exists.
|
||||
##
|
||||
|
||||
@ -404,54 +404,3 @@ TEST(TimerE2E, RemoveEventListenerStopsDelivery) {
|
||||
EXPECT_EQ(keptHits.load(), 2);
|
||||
EXPECT_EQ(removedHits.load(), 1) << "removed listener fired after removeEventListener";
|
||||
}
|
||||
|
||||
// The wildcard `addEventListener` overload receives every event with the
|
||||
// wire `eventId` pre-extracted plus a `std::span` view over the raw
|
||||
// envelope bytes. The helper `decodeEventPayload<T>` lifts the payload
|
||||
// into a typed value.
|
||||
TEST(TimerE2E, WildcardListenerReceivesEventIdAndDecodesPayload) {
|
||||
auto ctx = makeCtx("wildcard");
|
||||
|
||||
struct Capture {
|
||||
int retCode;
|
||||
std::string eventId;
|
||||
std::size_t envelopeBytes;
|
||||
std::optional<EchoEvent> decoded;
|
||||
};
|
||||
|
||||
std::mutex mu;
|
||||
std::vector<Capture> captured;
|
||||
auto handle = ctx->addEventListener(
|
||||
[&](int retCode, const std::string& eventId,
|
||||
std::span<const std::uint8_t> envelope) {
|
||||
Capture c{retCode, eventId, envelope.size(), std::nullopt};
|
||||
if (retCode == 0 && eventId == "on_echo_fired") {
|
||||
EchoEvent evt{};
|
||||
if (decodeEventPayload(envelope, evt)) {
|
||||
c.decoded = evt;
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
captured.push_back(std::move(c));
|
||||
});
|
||||
ASSERT_NE(handle.id, 0u);
|
||||
|
||||
ctx->echo(EchoRequest{"hello", 1});
|
||||
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (!captured.empty()) break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
ASSERT_GE(captured.size(), 1u);
|
||||
EXPECT_EQ(captured.front().retCode, 0);
|
||||
EXPECT_EQ(captured.front().eventId, "on_echo_fired");
|
||||
EXPECT_GT(captured.front().envelopeBytes, 0u);
|
||||
ASSERT_TRUE(captured.front().decoded.has_value());
|
||||
EXPECT_EQ(captured.front().decoded->message, "hello");
|
||||
EXPECT_EQ(captured.front().decoded->echoCount, 1);
|
||||
}
|
||||
|
||||
@ -86,8 +86,8 @@ registerReqFFI(EmitRawBytesEventRequest, lib: ptr TestEvtLib):
|
||||
return ok("emitted")
|
||||
|
||||
## Setter-thread worker for the registry race regression test. Each
|
||||
## iteration adds then immediately removes a wildcard listener so a
|
||||
## TSan-instrumented build can confirm `FFIEventRegistry.lock`
|
||||
## iteration adds then immediately removes a listener for the dispatched
|
||||
## event so a TSan-instrumented build can confirm `FFIEventRegistry.lock`
|
||||
## serialises the cross-thread mutation against dispatch-time
|
||||
## `snapshotListeners` reads from the FFI thread.
|
||||
type SetterArgs = tuple
|
||||
@ -98,7 +98,7 @@ type SetterArgs = tuple
|
||||
proc setterThreadBody(args: SetterArgs) {.thread.} =
|
||||
while not args.stop[].load():
|
||||
let id = addEventListener(
|
||||
args.ctx[].eventRegistry, WildcardEventName, captureCb, args.target
|
||||
args.ctx[].eventRegistry, "message_sent", captureCb, args.target
|
||||
)
|
||||
discard removeEventListener(args.ctx[].eventRegistry, id)
|
||||
|
||||
@ -116,10 +116,9 @@ suite "dispatchFFIEventCbor":
|
||||
defer:
|
||||
deinitCallbackData(evt)
|
||||
|
||||
# Register the event callback via the same locked helper that the
|
||||
# codegen-emitted `{libname}_set_event_callback` uses.
|
||||
# Subscribe to the specific event the request below dispatches.
|
||||
discard addEventListener(
|
||||
ctx[].eventRegistry, WildcardEventName, captureCb, addr evt
|
||||
ctx[].eventRegistry, "message_sent", captureCb, addr evt
|
||||
)
|
||||
|
||||
# Trigger the dispatch from the FFI thread; the response callback is
|
||||
@ -159,7 +158,7 @@ suite "dispatchFFIEvent with seq[byte]":
|
||||
deinitCallbackData(evt)
|
||||
|
||||
discard addEventListener(
|
||||
ctx[].eventRegistry, WildcardEventName, captureCb, addr evt
|
||||
ctx[].eventRegistry, "raw_bytes", captureCb, addr evt
|
||||
)
|
||||
|
||||
var rsp: CallbackData
|
||||
@ -178,8 +177,8 @@ suite "dispatchFFIEvent with seq[byte]":
|
||||
check callbackBytes(evt) == @[byte 0x01, 0x02, 0x03]
|
||||
|
||||
when not defined(gcRefc):
|
||||
## Skipped under `--mm:refc`: each setter thread grows / shrinks
|
||||
## `reg.wildcard` (a `seq[FFIEventListener]`) via `addEventListener`,
|
||||
## Skipped under `--mm:refc`: each setter thread grows / shrinks the
|
||||
## per-event listener `seq[FFIEventListener]` via `addEventListener`,
|
||||
## and refc's per-thread GC heap ownership makes cross-thread seq
|
||||
## buffer reallocation unsafe even when the surrounding lock is held.
|
||||
## ORC + the FFI thread + tsan (the combo this test was written for)
|
||||
@ -206,7 +205,7 @@ when not defined(gcRefc):
|
||||
# (callback, userData) pair — what matters is the cross-thread write
|
||||
# racing the FFI thread's read, not which pair "wins".
|
||||
discard addEventListener(
|
||||
ctx[].eventRegistry, WildcardEventName, captureCb, addr evt
|
||||
ctx[].eventRegistry, "message_sent", captureCb, addr evt
|
||||
)
|
||||
|
||||
const NumSetterThreads = 4
|
||||
|
||||
@ -68,7 +68,7 @@ suite "FFIEventRegistry mutation":
|
||||
|
||||
let id1 = addEventListener(reg, "evt", tagCb, addr t)
|
||||
let id2 = addEventListener(reg, "evt", tagCb, addr t)
|
||||
let id3 = addEventListener(reg, "", tagCb, addr t)
|
||||
let id3 = addEventListener(reg, "other", tagCb, addr t)
|
||||
check id1 == 1'u64
|
||||
check id2 == 2'u64
|
||||
check id3 == 3'u64
|
||||
@ -89,7 +89,7 @@ suite "FFIEventRegistry mutation":
|
||||
check not removeEventListener(reg, 0'u64)
|
||||
check not removeEventListener(reg, 99'u64)
|
||||
|
||||
test "removeEventListener removes from per-event seq and wildcard":
|
||||
test "removeEventListener removes listeners across distinct events":
|
||||
var reg: FFIEventRegistry
|
||||
initEventRegistry(reg)
|
||||
defer:
|
||||
@ -101,18 +101,18 @@ suite "FFIEventRegistry mutation":
|
||||
var t = Tag(name: "a", rec: addr rec)
|
||||
|
||||
let id1 = addEventListener(reg, "evt", tagCb, addr t)
|
||||
let id2 = addEventListener(reg, "", tagCb, addr t)
|
||||
let id2 = addEventListener(reg, "other", tagCb, addr t)
|
||||
|
||||
check removeEventListener(reg, id1)
|
||||
check removeEventListener(reg, id2)
|
||||
# Second remove of the same id is a no-op.
|
||||
check not removeEventListener(reg, id1)
|
||||
|
||||
let snap = snapshotListeners(reg, "evt")
|
||||
check snap.len == 0
|
||||
check snapshotListeners(reg, "evt").len == 0
|
||||
check snapshotListeners(reg, "other").len == 0
|
||||
|
||||
suite "FFIEventRegistry snapshot semantics":
|
||||
test "snapshot includes both per-event listeners and wildcards":
|
||||
test "snapshot returns only the listeners for the requested event":
|
||||
var reg: FFIEventRegistry
|
||||
initEventRegistry(reg)
|
||||
defer:
|
||||
@ -126,17 +126,17 @@ suite "FFIEventRegistry snapshot semantics":
|
||||
var c = Tag(name: "c", rec: addr rec)
|
||||
|
||||
discard addEventListener(reg, "evt", tagCb, addr a)
|
||||
discard addEventListener(reg, "other", tagCb, addr b)
|
||||
discard addEventListener(reg, "", tagCb, addr c)
|
||||
discard addEventListener(reg, "evt", tagCb, addr b)
|
||||
discard addEventListener(reg, "other", tagCb, addr c)
|
||||
|
||||
let snapEvt = snapshotListeners(reg, "evt")
|
||||
check snapEvt.len == 2 # listener for "evt" + wildcard
|
||||
check snapEvt.len == 2 # both listeners for "evt"
|
||||
|
||||
let snapOther = snapshotListeners(reg, "other")
|
||||
check snapOther.len == 2 # listener for "other" + wildcard
|
||||
check snapOther.len == 1 # only the listener for "other"
|
||||
|
||||
let snapUnknown = snapshotListeners(reg, "no-subscriber")
|
||||
check snapUnknown.len == 1 # only the wildcard
|
||||
check snapUnknown.len == 0 # no listener for this event
|
||||
|
||||
test "snapshot is a copy: post-snapshot mutation does not affect it":
|
||||
var reg: FFIEventRegistry
|
||||
@ -161,7 +161,7 @@ suite "FFIEventRegistry snapshot semantics":
|
||||
check snap[0].id == id1
|
||||
|
||||
suite "removeAllEventListeners":
|
||||
test "drops every listener (per-event and wildcard)":
|
||||
test "drops every registered listener":
|
||||
var reg: FFIEventRegistry
|
||||
initEventRegistry(reg)
|
||||
defer:
|
||||
@ -174,8 +174,8 @@ suite "removeAllEventListeners":
|
||||
var b = Tag(name: "b", rec: addr rec)
|
||||
|
||||
discard addEventListener(reg, "evt", tagCb, addr a)
|
||||
discard addEventListener(reg, WildcardEventName, tagCb, addr b)
|
||||
discard addEventListener(reg, "other", tagCb, addr b)
|
||||
removeAllEventListeners(reg)
|
||||
|
||||
check snapshotListeners(reg, "evt").len == 0
|
||||
check snapshotListeners(reg, WildcardEventName).len == 0
|
||||
check snapshotListeners(reg, "other").len == 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user