GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 74.9% 456 / 0 / 609
Functions: 80.4% 74 / 0 / 92
Branches: 58.5% 241 / 0 / 412

include/DetourModKit/detail/event_dispatcher.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_EVENT_DISPATCHER_HPP
2 #define DETOURMODKIT_EVENT_DISPATCHER_HPP
3
4 /**
5 * @file event_dispatcher.hpp
6 * @brief Typed event dispatcher with RAII subscription management.
7 * @note Sits in detail/ for compile visibility (installed headers return EventDispatcher<T>&) and declares the type
8 * at the module-root DetourModKit namespace. The directory reflects compile visibility, not privacy.
9 * @details Subscribers receive events by const reference. Subscriptions are RAII guards that retire their handler on
10 * destruction. The thread-safety and rundown contracts are on EventDispatcher and Subscription.
11 */
12
13 #include "DetourModKit/logger.hpp"
14
15 #include <algorithm>
16 #include <atomic>
17 #include <cstdint>
18 #include <exception>
19 #include <functional>
20 #include <memory>
21 #include <mutex>
22 #include <utility>
23 #include <vector>
24
25 namespace DetourModKit
26 {
27 /**
28 * @brief Opaque subscription identifier returned by EventDispatcher::subscribe().
29 */
30 enum class SubscriptionId : std::uint64_t
31 {
32 };
33
34 /**
35 * @brief The outcome of a waiting rundown.
36 */
37 enum class Rundown : std::uint8_t
38 {
39 /**
40 * @brief The handler is dead and no invocation of it is running, so its captures may now be destroyed.
41 * @details This does not on its own make it safe to unload the module the handler's own code lives in; see
42 * @ref Subscription::tombstone_and_wait.
43 */
44 Drained,
45 /**
46 * @brief The handler is dead, but waiting cannot be proven to terminate, so nothing was waited on.
47 * @details Either the calling thread is itself inside this dispatcher's emit, or an unrecorded emit cannot be
48 * ruled out as the caller. In both cases the handler is retired and will not be entered again, but an
49 * invocation may still be running, so its captures must be kept alive.
50 */
51 Unwaitable,
52 /**
53 * @brief There was nothing to run down: this Subscription holds no handler at all.
54 * @details Default-constructed, moved-from, or already reset. A subscription whose handler was retired by
55 * someone else (clear(), a dispatcher rundown, ~EventDispatcher) still reports Drained rather than
56 * Inactive, because it still owns the gate and the wait it performed is a real answer about it.
57 */
58 Inactive
59 };
60 } // namespace DetourModKit
61
62 namespace DetourModKit::detail
63 {
64 /**
65 * @brief The rundown state of one subscription, shared by its Subscription and the published snapshot.
66 * @details Non-template so the non-template Subscription can own one and tombstone it without naming the event
67 * type. Holds no callback, so tombstoning costs no allocation and no destruction of user state. Gates
68 * are not recycled: the emit loop's SNAPSHOT (not InvocationGuard) pins the gate alive for the whole
69 * iteration, so anything that shortens the snapshot's lifetime inside emit() invalidates this.
70 */
71 struct EntryGate
72 {
73 /// The rundown tombstone: false means no further invocation of this handler may begin.
74 std::atomic<bool> live{true};
75 /// Invocations that passed the tombstone recheck and have not returned.
76 std::atomic<std::uint32_t> in_flight{0};
77 };
78
79 /**
80 * @brief One frame of the calling thread's dispatcher emit chain.
81 * @details Lives on emit()'s own stack, so maintaining the chain allocates nothing. It exists so a rundown can
82 * answer "is THIS thread inside THIS dispatcher", which is what stops a rundown requested from inside a
83 * handler from waiting on itself forever.
84 */
85 struct EmitFrame
86 {
87 const void *dispatcher{nullptr};
88 const void *type_tag{nullptr};
89 EmitFrame *prev{nullptr};
90 };
91
92 /**
93 * @brief Reserves the emit chain's thread-local storage. Control-plane only; subscribe() calls it.
94 * @return false when the process has no index to give, which leaves every emit untracked and every rundown
95 * Unwaitable rather than wrong.
96 * @details A Win32 TLS index rather than `thread_local`: emit_safe() is reached from hook callbacks on arbitrary
97 * host threads, where the emutls first-touch allocation and uncatchable abort() are unacceptable
98 * ([B-86]).
99 */
100 [[nodiscard]] bool ensure_emit_frame_tls() noexcept;
101
102 /**
103 * @brief Pushes @p frame onto the calling thread's emit chain.
104 * @return false when the frame was not recorded. The caller must then count itself untracked rather than let a
105 * rundown conclude this thread is elsewhere. See @ref untracked_emit_frames.
106 */
107 [[nodiscard]] bool push_emit_frame(EmitFrame &frame) noexcept;
108
109 /// Pops a frame. Only call when the matching @ref push_emit_frame returned true.
110 void pop_emit_frame(const EmitFrame &frame) noexcept;
111
112 /// True when the calling thread is inside @p dispatcher's emit.
113 [[nodiscard]] bool thread_is_emitting_dispatcher(const void *dispatcher) noexcept;
114
115 /// True when the calling thread is inside the emit of any dispatcher sharing @p type_tag.
116 [[nodiscard]] bool thread_is_emitting_type(const void *type_tag) noexcept;
117
118 /**
119 * @brief Emits whose thread was not recorded, so self-entry cannot be disproven for anyone.
120 * @details Process-wide rather than per-dispatcher: a thread that failed to record its frame is invisible to
121 * every chain walk, so no dispatcher may claim it is absent. Counted rather than made sticky so a
122 * rundown recovers once the untracked emit leaves.
123 */
124 [[nodiscard]] std::atomic<std::uint32_t> &untracked_emit_frames() noexcept;
125
126 /**
127 * @brief Waits out the invocations committed before @p gate was tombstoned.
128 * @param gate An already-tombstoned gate. Waiting on a live gate never terminates.
129 * @param dispatcher Identity used only to compare against this thread's emit chain; never dereferenced.
130 * @return Drained once no invocation remains, or Unwaitable when the wait cannot be proven to terminate.
131 * @details Refuses rather than waits when the calling thread is inside @p dispatcher's emit, or when any emit
132 * anywhere did not record its frame. A wrong "this thread is elsewhere" makes the rundown wait on the
133 * very thread running it. A wrong "it can be here" only costs a refusal.
134 */
135 [[nodiscard]] Rundown drain_gate(EntryGate &gate, const void *dispatcher) noexcept;
136
137 /**
138 * @brief Test-only white-box accessor over EventDispatcher privates.
139 * @details Declared here so the dispatcher can befriend it unconditionally. Only the dispatcher test translation
140 * unit defines it, so the installed class definition stays token-stable under every build macro.
141 */
142 template <typename Event> struct EventDispatcherTestAccess;
143 } // namespace DetourModKit::detail
144
145 namespace DetourModKit
146 {
147 /**
148 * @brief RAII subscription guard that unsubscribes on destruction.
149 *
150 * @details Move-only. When the guard is destroyed or reset, the associated handler is retired.
151 *
152 * **Lifetime contract (read before using across threads):** if the dispatcher was destroyed before this
153 * operation with a happens-before edge (ordered teardown), the physical compaction is silently skipped:
154 * the weak_ptr is observed expired and only the tombstone runs. That ordered case is the only lifetime
155 * overlap the weak_ptr guard covers ([B-70]). A `~EventDispatcher` racing a Subscription operation on
156 * another thread is a use-after-free. The caller must ensure the dispatcher outlives every concurrent
157 * Subscription operation.
158 */
159 class Subscription
160 {
161 public:
162 28 Subscription() noexcept = default;
163
164 10387 ~Subscription() noexcept { reset(); }
165
166 Subscription(const Subscription &) = delete;
167 Subscription &operator=(const Subscription &) = delete;
168
169 26 Subscription(Subscription &&other) noexcept
170 78 : m_alive(std::move(other.m_alive)), m_gate(std::move(other.m_gate)),
171 52 m_dispatcher(std::exchange(other.m_dispatcher, nullptr)), m_compact(std::move(other.m_compact))
172 {
173 26 other.m_compact = nullptr;
174 26 }
175
176 23 Subscription &operator=(Subscription &&other) noexcept
177 {
178
1/2
✓ Branch 2 → 3 taken 23 times.
✗ Branch 2 → 15 not taken.
23 if (this != &other)
179 {
180 23 reset();
181 46 m_alive = std::move(other.m_alive);
182 46 m_gate = std::move(other.m_gate);
183 23 m_dispatcher = std::exchange(other.m_dispatcher, nullptr);
184 46 m_compact = std::move(other.m_compact);
185 23 other.m_compact = nullptr;
186 }
187 23 return *this;
188 }
189
190 /**
191 * @brief Retires the handler without waiting or reclaiming its published list slot.
192 * @details The handler is dead the instant the tombstone flips: no emit that has not already committed to
193 * invoking it can begin one, on this thread or any other, including an emit nested inside the very
194 * handler that called this. The operation is a single atomic store, so it does not allocate, block,
195 * or fail and is callback-safe.
196 * @note Safe to call multiple times. Use @ref tombstone_and_wait before destroying the handler's code or
197 * referenced state when an invocation may already be running.
198 */
199 10434 void tombstone() noexcept
200 {
201
2/2
✓ Branch 3 → 4 taken 10334 times.
✓ Branch 3 → 6 taken 100 times.
10434 if (m_gate)
202 {
203 10334 m_gate->live.store(false, std::memory_order_seq_cst);
204 }
205 10434 }
206
207 /**
208 * @brief Retires the handler and best-effort reclaims its published list slot.
209 * @details Calls @ref tombstone first, so logical removal is synchronous and cannot fail.
210 *
211 * Reclaiming the list slot is a separate, best-effort step: it briefly takes the writer mutex, it
212 * allocates, and it is skipped entirely if the dispatcher was destroyed first. Any of that may fail
213 * without consequence. A skipped compaction costs one dead vector entry that every later emit
214 * rejects at its liveness check; it never resurrects the handler.
215 *
216 * @warning This does not wait: an invocation already past the liveness check may still be running on another
217 * thread when this returns. It is not callback-safe because compaction can block on the writer mutex;
218 * use @ref tombstone when only non-blocking logical removal is required.
219 */
220 10430 void reset() noexcept
221 {
222 10430 tombstone();
223 10430 compact_and_release();
224 10430 }
225
226 /**
227 * @brief Retires the handler and waits until no invocation of it is running.
228 * @return Drained when the handler is quiesced and its captures may be destroyed; Unwaitable when the wait
229 * cannot be proven to terminate, so it was not attempted; Inactive when there was nothing to retire.
230 * @details The control-plane half of @ref reset(): use it before destroying what the handler captured. On
231 * Drained, no invocation is running and none can begin, so the objects the handler references may be
232 * destroyed.
233 *
234 * Called from inside a handler on this dispatcher, this returns Unwaitable rather than wait on the
235 * calling thread. The handler is still retired; only the wait is refused. A timeout is not offered,
236 * because a wait that gives up has not drained anything.
237 * @note Drained does NOT by itself make it safe to unload the module the handler's own code lives in. It waits
238 * out running invocations, but an emit still iterating on another thread holds the snapshot that owns the
239 * handler's std::function, and destroying that snapshot later runs the callable's type-erased destructor.
240 * Unloading the code needs the loader-grade quiescence a real teardown host provides, not this call.
241 * @note Not callable while any allocation-free guarantee is required: this blocks.
242 */
243 3 [[nodiscard]] Rundown tombstone_and_wait() noexcept
244 {
245
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
3 if (!m_gate)
246 {
247 return Rundown::Inactive;
248 }
249 3 tombstone();
250 3 const Rundown result = detail::drain_gate(*m_gate, m_dispatcher);
251 3 compact_and_release();
252 3 return result;
253 }
254
255 /// Returns true if this subscription still holds a live handler.
256 30 [[nodiscard]] bool active() const noexcept
257 {
258
4/4
✓ Branch 3 → 4 taken 18 times.
✓ Branch 3 → 8 taken 12 times.
✓ Branch 6 → 7 taken 11 times.
✓ Branch 6 → 8 taken 7 times.
30 return m_gate != nullptr && m_gate->live.load(std::memory_order_acquire);
259 }
260
261 private:
262 template <typename E> friend class EventDispatcher;
263
264 10337 Subscription(
265 std::weak_ptr<void> alive,
266 std::shared_ptr<detail::EntryGate> gate,
267 const void *dispatcher,
268 std::function<void()> compact
269 ) noexcept
270 31011 : m_alive(std::move(alive)), m_gate(std::move(gate)), m_dispatcher(dispatcher),
271 20674 m_compact(std::move(compact))
272 {
273 10337 }
274
275 10433 void compact_and_release() noexcept
276 {
277
6/6
✓ Branch 3 → 4 taken 10333 times.
✓ Branch 3 → 7 taken 100 times.
✓ Branch 5 → 6 taken 10332 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 8 → 9 taken 10332 times.
✓ Branch 8 → 10 taken 101 times.
10433 if (m_compact && !m_alive.expired())
278 {
279 10332 m_compact();
280 }
281 10433 m_compact = nullptr;
282 10433 m_gate.reset();
283 10433 m_dispatcher = nullptr;
284 10433 m_alive.reset();
285 10433 }
286
287 std::weak_ptr<void> m_alive;
288 std::shared_ptr<detail::EntryGate> m_gate;
289 /// Compared against the emit chain to refuse a self-wait. Never dereferenced, so a stale value is harmless.
290 const void *m_dispatcher{nullptr};
291 std::function<void()> m_compact;
292 };
293
294 /**
295 * @brief Thread-safe typed event dispatcher with RAII subscription management.
296 *
297 * @tparam Event The event type. Must be copyable or movable. Handlers receive events by const reference.
298 *
299 * @details Each EventDispatcher manages a single event type. Compose one dispatcher per event type.
300 *
301 * **Thread safety:**
302 * - `emit()` / `emit_safe()`: no lock of ours, and no allocation to dispatch. Per handler, the invocation takes
303 * an enter/recheck/leave pass over that entry's gate. The one exception is emit_safe()'s catch arm, which
304 * reports a throwing handler through the logger and may allocate there.
305 * - `subscribe()` / `clear()`: copy-on-write under a small writer mutex. See EntryNode for the callable boundary.
306 * - `Subscription::tombstone()` is synchronous, non-blocking, allocation-free, and cannot fail. `reset()`
307 * tombstones first, then best-effort compacts under the writer mutex.
308 *
309 * **Reentrancy guard scope:** subscribe() is rejected from within a handler on a dispatcher of the SAME Event
310 * type, including a different instance of it. A handler on `EventDispatcher<A>` may freely subscribe to
311 * `EventDispatcher<B>`. If an emit frame cannot be recorded, subscriptions are conservatively rejected until
312 * that emit leaves.
313 *
314 * **Subscribe/emit ordering invariant:** subscribe() release-stores the snapshot pointer and the handler count.
315 * A thread that observes the returned Subscription (or synchronizes-with the thread that did) sees the
316 * subscription in subsequent emits. Without such a happens-before edge, a concurrent emit may or may not observe
317 * a freshly-published handler.
318 */
319 template <typename Event> class EventDispatcher
320 {
321 public:
322 /// Handler function signature: receives the event by const reference.
323 using Handler = std::function<void(const Event &)>;
324
325 private:
326 // Private type aliases surfaced here so they are visible to the public API's member declarations and
327 // constructor below.
328 struct Entry
329 {
330 SubscriptionId id;
331 std::shared_ptr<detail::EntryGate> gate;
332 Handler callback;
333 };
334
335 // EntryNode enforces the callable ownership and lock boundary in [B-101] (proof: DispatchCow.*).
336 using EntryNode = std::shared_ptr<const Entry>;
337 using HandlerList = std::vector<EntryNode>;
338 using SharedList = std::shared_ptr<const HandlerList>;
339
340 /**
341 * @brief Keys the emit chain by Event type: one object per instantiation, identified by its address.
342 * @details Address-taken so the linker cannot fold two instantiations' tags together and merge two event
343 * types' reentrancy guards.
344 */
345 inline static const char s_type_tag{};
346
347 public:
348
10/20
DetourModKit::EventDispatcher<SimpleEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 54 times.
✗ Branch 2 → 13 not taken.
✓ Branch 9 → 10 taken 54 times.
✗ Branch 9 → 14 not taken.
DetourModKit::EventDispatcher<StringEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 13 not taken.
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 14 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 13 not taken.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 9 → 14 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 13 not taken.
✓ Branch 9 → 10 taken 8 times.
✗ Branch 9 → 14 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 354 times.
✗ Branch 2 → 13 not taken.
✓ Branch 9 → 10 taken 354 times.
✗ Branch 9 → 14 not taken.
422 EventDispatcher() : m_handlers(std::make_shared<const HandlerList>()), m_alive(std::make_shared<char>('\0')) {}
349
350 /**
351 * @brief Retires every handler.
352 * @details A Subscription may outlive its dispatcher, and its gate is the only thing that can tell it the
353 * handler can never run again. Retiring here keeps the gate authoritative instead of making every
354 * reader infer liveness from a second signal.
355 *
356 * Takes no lock of ours and allocates nothing, so the noexcept destructor is total: the gate stores
357 * are atomic and idempotent, and a subscribe() racing this is already a caller lifetime violation.
358 * (The snapshot load still takes the STL's internal lock for atomic<shared_ptr>, as everywhere else.)
359 * @warning Does not wait. Destroying a dispatcher while one of its handlers is running is a caller lifetime
360 * violation; use tombstone_and_wait() first when that is possible.
361 */
362 60 ~EventDispatcher() noexcept
363 {
364 60 auto current = this->m_handlers.load(std::memory_order_acquire);
365
4/6
DetourModKit::EventDispatcher<SimpleEvent>::~EventDispatcher():
✓ Branch 20 → 6 taken 3 times.
✓ Branch 20 → 21 taken 54 times.
DetourModKit::EventDispatcher<StringEvent>::~EventDispatcher():
✗ Branch 20 → 6 not taken.
✓ Branch 20 → 21 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::~EventDispatcher():
✗ Branch 20 → 6 not taken.
✓ Branch 20 → 21 taken 4 times.
123 for (const auto &entry : *current)
366 {
367 3 entry->gate->live.store(false, std::memory_order_seq_cst);
368 }
369 60 }
370
371 EventDispatcher(const EventDispatcher &) = delete;
372 EventDispatcher &operator=(const EventDispatcher &) = delete;
373 EventDispatcher(EventDispatcher &&) = delete;
374 EventDispatcher &operator=(EventDispatcher &&) = delete;
375
376 /**
377 * @brief Subscribes a handler to this event type.
378 * @param handler Callable invoked on each emit(). Must be safe to call from any thread. An empty handler is
379 * rejected (see @return).
380 * @return RAII Subscription guard. The handler is retired when the guard is destroyed or reset(). An EMPTY
381 * handler, a call made from within a same-type handler, ambiguous emit tracking, or a dispatcher
382 * already closed by tombstone_and_wait yields an INACTIVE Subscription (active() == false) rather
383 * than throwing; test active() when any is possible.
384 * @throws std::bad_alloc if the gate or the copy-on-write list cannot be allocated. This is a control-plane
385 * call and is deliberately NOT fail-soft about that: a subscribe that cannot allocate has installed
386 * nothing, which is a truthful failure the caller can act on. Retiring a handler is the operation that
387 * must never depend on an allocation, and it does not.
388 * @note Copy-on-write allocates the entry node and a new handler list of size N+1.
389 * The control path also reserves the process emit-chain TLS index, so emit() only reads that index.
390 * A thread's first TlsSetValue can still allocate an expansion array for an index beyond the TEB inline
391 * slots. This mechanism reports allocation failure, while emutls aborts the process.
392 */
393 10343 [[nodiscard]] Subscription subscribe(Handler handler)
394 {
395
6/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 10283 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 6 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 46 times.
10343 if (!handler)
396 {
397 // Rejected at the point of misuse rather than deferred into an unrelated emit, where it surfaces as
398 // a bad_function_call from someone else's call site.
399 1 report_empty_handler_rejection();
400 1 return {};
401 }
402
403
6/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 10 taken 10280 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 6 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 46 times.
10342 if (detail::thread_is_emitting_type(&s_type_tag))
404 {
405 3 report_reentrant_rejection("subscribe");
406 3 return {};
407 }
408
6/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 21 taken 10279 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 6 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 46 times.
20678 if (detail::untracked_emit_frames().load(std::memory_order_seq_cst) != 0)
409 {
410 1 report_untracked_rejection();
411 1 return {};
412 }
413
414 // Reserved here, on the control plane, so that emit() only ever READS the index. A failure leaves every
415 // rundown Unwaitable rather than wrong, which is why it does not fail the subscribe.
416 10338 (void)detail::ensure_emit_frame_tls();
417
418 10338 const auto id = static_cast<SubscriptionId>(this->m_next_id.fetch_add(1, std::memory_order_relaxed));
419
420
5/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 24 → 25 taken 10279 times.
✗ Branch 24 → 107 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 107 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 24 → 25 taken 6 times.
✗ Branch 24 → 107 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 24 → 25 taken 5 times.
✗ Branch 24 → 107 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 24 → 25 taken 46 times.
✗ Branch 24 → 107 not taken.
10338 auto gate = std::make_shared<detail::EntryGate>();
421 // Build the compaction callback before publishing. If wrapping it in std::function had to allocate (a
422 // future capture overflowing the small-object buffer) and threw, the handler must not already be live in
423 // the list with no Subscription returned to retire it. Constructing it here keeps subscribe's "installs
424 // nothing on allocation failure" contract intact.
425 20670 std::function<void()> compact_fn = [this, id]() noexcept { this->compact(id); };
426 // Node construction occurs before lock acquisition because it can execute consumer move or copy code.
427 10338 EntryNode node = std::make_shared<const Entry>(Entry{id, gate, std::move(handler)});
428 // The outer lifetime follows EntryNode's post-unlock rule.
429 10338 SharedList superseded;
430 {
431
5/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 34 → 35 taken 10279 times.
✗ Branch 34 → 98 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 34 → 35 taken 2 times.
✗ Branch 34 → 98 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 34 → 35 taken 6 times.
✗ Branch 34 → 98 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 34 → 35 taken 5 times.
✗ Branch 34 → 98 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 34 → 35 taken 46 times.
✗ Branch 34 → 98 not taken.
10338 std::scoped_lock lock{this->m_writer_mutex};
432
6/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 10278 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 6 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 46 times.
10338 if (this->m_closed.load(std::memory_order_seq_cst))
433 {
434 // Tested under the mutex, not before it: that is what lets tombstone_and_wait treat the snapshot
435 // it loads as the complete set. A subscribe admitted here has published before that load; one
436 // refused here never publishes at all. Checking outside the lock leaves exactly the window
437 // where a handler is installed live behind a completed drain.
438 1 report_closed_rejection();
439 1 return {};
440 }
441 10337 superseded = this->m_handlers.load(std::memory_order_acquire);
442
5/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 44 → 45 taken 10278 times.
✗ Branch 44 → 96 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 44 → 45 taken 2 times.
✗ Branch 44 → 96 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 44 → 45 taken 6 times.
✗ Branch 44 → 96 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 44 → 45 taken 5 times.
✗ Branch 44 → 96 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 44 → 45 taken 46 times.
✗ Branch 44 → 96 not taken.
10337 auto next = std::make_shared<HandlerList>(*superseded);
443
5/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 48 → 49 taken 10278 times.
✗ Branch 48 → 94 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 94 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 48 → 49 taken 6 times.
✗ Branch 48 → 94 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 48 → 49 taken 5 times.
✗ Branch 48 → 94 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 48 → 49 taken 46 times.
✗ Branch 48 → 94 not taken.
20674 next->push_back(std::move(node));
444 // Publish the new count first so a reader that sees 0 on the counter and skips the snapshot load cannot
445 // miss a handler that has already been installed in the snapshot.
446 10337 this->m_handler_count.store(next->size(), std::memory_order_release);
447 20674 this->m_handlers.store(std::shared_ptr<const HandlerList>(std::move(next)), std::memory_order_release);
448
6/10
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 67 → 68 taken 10278 times.
✓ Branch 67 → 77 taken 1 time.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 67 → 68 taken 2 times.
✗ Branch 67 → 77 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 67 → 68 taken 6 times.
✗ Branch 67 → 77 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 67 → 68 taken 5 times.
✗ Branch 67 → 77 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 67 → 68 taken 46 times.
✗ Branch 67 → 77 not taken.
10338 }
449
450 20674 return Subscription(std::weak_ptr<void>(this->m_alive), std::move(gate), this, std::move(compact_fn));
451
10/30
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 30 → 31 taken 10279 times.
✗ Branch 30 → 88 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 10279 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 88 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 30 → 31 taken 6 times.
✗ Branch 30 → 88 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 6 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 30 → 31 taken 5 times.
✗ Branch 30 → 88 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 5 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 30 → 31 taken 46 times.
✗ Branch 30 → 88 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 46 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
41352 }
452
453 /**
454 * @brief Emits an event to all subscribers.
455 * @param event The event payload, passed by const reference to each handler.
456 * @note Takes no lock of ours and allocates nothing to dispatch. Handlers are invoked synchronously in
457 * subscription order. Exceptions thrown by handlers propagate to the caller.
458 * @warning From a game hook callback, or any context where an unhandled exception crashes the host process,
459 * use emit_safe() instead. emit() lets handler exceptions propagate uncaught, which terminates the
460 * process if no catch frame exists above the call site.
461 */
462 44322 void emit(const Event &event) const
463 {
464 // Fast path: no subscribers means no snapshot load at all.
465
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 9 → 10 taken 1012 times.
✓ Branch 9 → 11 taken 42988 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
88324 if (this->m_handler_count.load(std::memory_order_acquire) == 0)
466 {
467 1012 return;
468 }
469
470 42990 SharedList snap = this->m_handlers.load(std::memory_order_acquire);
471 47767 EmitGuard guard{*this};
472
6/8
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 28 → 29 taken 46330 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 44920 times.
✓ Branch 41 → 42 taken 45982 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 2 times.
✓ Branch 41 → 42 taken 2 times.
180948 for (const auto &entry : *snap)
473 {
474 44922 InvocationGuard invocation{*entry->gate};
475
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 22 → 23 taken 77 times.
✓ Branch 22 → 24 taken 44968 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
46583 if (!invocation.admitted())
476 {
477 77 continue;
478 }
479
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 25 → 26 taken 43731 times.
✓ Branch 25 → 46 taken 1 time.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 46 not taken.
44970 entry->callback(event);
480 }
481 45986 }
482
483 /**
484 * @brief Emits an event, catching and discarding handler exceptions.
485 * @param event The event payload.
486 * @note Same read-path semantics as emit(). Handlers that throw are skipped; remaining handlers still
487 * execute. Prefer this over emit() in hook callbacks and other contexts where an unhandled exception
488 * crashes the host process.
489 */
490 1659754 void emit_safe(const Event &event) const noexcept
491 {
492
6/6
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 9 → 10 taken 165383 times.
✓ Branch 9 → 11 taken 1501210 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 9 → 10 taken 373 times.
✓ Branch 9 → 11 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 9 → 10 taken 8447 times.
✓ Branch 9 → 11 taken 99 times.
3338589 if (this->m_handler_count.load(std::memory_order_acquire) == 0)
493 {
494 174203 return;
495 }
496
497 1504632 SharedList snap = this->m_handlers.load(std::memory_order_acquire);
498 1705164 EmitGuard guard{*this};
499
9/12
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 28 → 29 taken 1640451 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 1624655 times.
✓ Branch 41 → 42 taken 1553393 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 28 → 29 taken 3323 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 3323 times.
✓ Branch 41 → 42 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 28 → 29 taken 107 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 107 times.
✓ Branch 41 → 42 taken 99 times.
6399340 for (const auto &entry : *snap)
500 {
501 1628085 InvocationGuard invocation{*entry->gate};
502
4/6
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 22 → 23 taken 1185 times.
✓ Branch 22 → 24 taken 1573015 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 107 times.
1587635 if (!invocation.admitted())
503 {
504 1185 continue;
505 }
506 try
507 {
508
4/6
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 25 → 26 taken 1518275 times.
✓ Branch 25 → 46 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 25 → 26 taken 3323 times.
✗ Branch 25 → 46 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 25 → 26 taken 107 times.
✗ Branch 25 → 46 not taken.
1576445 entry->callback(event);
509 }
510
2/6
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 51 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 51 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 51 not taken.
7 catch (const std::exception &ex)
511 {
512 // A subscriber threw. emit_safe contains the exception so the remaining handlers still run. A
513 // SILENT swallow hides a real handler bug, so surface it best-effort with the exception text.
514 3 report_handler_exception(ex.what());
515 }
516 1 catch (...)
517 {
518 // A non-std throw carries no portable message; report the swallow without one.
519 1 report_handler_exception(nullptr);
520 }
521 }
522 1556815 }
523
524 /**
525 * @brief Returns the number of published subscriber slots.
526 * @note Counts published entries, including any retired handler whose slot is not compacted yet. It is a
527 * list-occupancy figure, not a count of handlers that run.
528 */
529 34 [[nodiscard]] size_t subscriber_count() const noexcept
530 {
531 68 return this->m_handler_count.load(std::memory_order_acquire);
532 }
533
534 /// Returns true if there are no published subscriber slots.
535 10 [[nodiscard]] bool empty() const noexcept { return this->m_handler_count.load(std::memory_order_acquire) == 0; }
536
537 /**
538 * @brief Retires every subscriber.
539 * @note Every handler is dead the instant this returns; the tombstone pass allocates nothing and cannot fail.
540 * Publishing the empty snapshot afterwards can fail under memory pressure, which leaves the dead
541 * entries occupying the list until a later mutation reclaims them. It never resurrects a handler. Takes
542 * the writer mutex, so this is a control-plane call and does not wait for in-flight handlers; use
543 * @ref tombstone_and_wait when the handlers' code or captures are about to go away.
544 */
545 8 void clear() noexcept
546 {
547 // The outer lifetime follows EntryNode's post-unlock rule.
548 8 SharedList superseded;
549 try
550 {
551
2/4
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 66 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 66 not taken.
8 std::scoped_lock lock{this->m_writer_mutex};
552 // Retire first, so an allocation failure below cannot leave a live handler behind.
553 8 superseded = this->m_handlers.load(std::memory_order_acquire);
554
4/4
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 23 → 9 taken 8 times.
✓ Branch 23 → 24 taken 5 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 23 → 9 taken 1 time.
✓ Branch 23 → 24 taken 3 times.
25 for (const auto &entry : *superseded)
555 {
556 9 entry->gate->live.store(false, std::memory_order_seq_cst);
557 }
558
559 8 std::shared_ptr<const HandlerList> empty_snap;
560 try
561 {
562
3/4
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 56 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 56 not taken.
8 empty_snap = std::make_shared<const HandlerList>();
563 }
564
1/4
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 60 → 61 taken 1 time.
✗ Branch 60 → 62 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✗ Branch 60 → 61 not taken.
✗ Branch 60 → 62 not taken.
2 catch (...)
565 {
566 1 return;
567 }
568 // Counter must go to 0 before publishing the empty snapshot so an emit that reads 0 on the fast-path
569 // counter cannot still see the non-empty old snapshot afterwards.
570 7 this->m_handler_count.store(0, std::memory_order_release);
571 14 this->m_handlers.store(std::move(empty_snap), std::memory_order_release);
572
6/8
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 42 → 43 taken 4 times.
✓ Branch 42 → 44 taken 1 time.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 49 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 42 → 43 taken 3 times.
✗ Branch 42 → 44 not taken.
✓ Branch 46 → 47 taken 3 times.
✗ Branch 46 → 49 not taken.
9 }
573 catch (...)
574 {
575 // A synchronization failure must not escape this no-throw control path. Retire the stable snapshot
576 // visible now; a concurrent unordered subscribe may still publish after it.
577 auto current = this->m_handlers.load(std::memory_order_acquire);
578 for (const auto &entry : *current)
579 {
580 entry->gate->live.store(false, std::memory_order_seq_cst);
581 }
582 return;
583 }
584
3/4
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 51 → 52 taken 4 times.
✓ Branch 51 → 54 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 51 → 52 taken 3 times.
✗ Branch 51 → 54 not taken.
8 }
585
586 /**
587 * @brief Retires every subscriber and waits until no handler of this dispatcher is running.
588 * @return Drained when every handler is quiesced; Unwaitable when the calling thread is inside this
589 * dispatcher's emit, or an emit was not recorded, so no wait was attempted.
590 * @details The rundown form of @ref clear(). On Drained, no handler is running and none can begin, so the
591 * objects every handler references may be destroyed. As with @ref Subscription::tombstone_and_wait,
592 * Drained does not on its own license unloading the module the handlers' code lives in.
593 *
594 * This CLOSES the dispatcher permanently: every later subscribe() is refused and hands back an
595 * inactive Subscription. A rundown is only complete over a set that cannot grow behind it, so the
596 * set is closed before it is read.
597 * @note Holding the writer mutex across the drain is a deadlock: a handler may itself call subscribe().
598 * Closing the set is what makes releasing the mutex safe.
599 */
600 2 [[nodiscard]] Rundown tombstone_and_wait() noexcept
601 {
602 // Close first, and outside the lock. subscribe() tests this flag while HOLDING the writer mutex, so a
603 // subscribe that already published is necessarily in the snapshot loaded below, and one that has not is
604 // necessarily refused. That is what makes the set read below closed rather than merely current.
605 2 this->m_closed.store(true, std::memory_order_seq_cst);
606
607 2 SharedList snap;
608 try
609 {
610
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 51 not taken.
2 std::scoped_lock lock{this->m_writer_mutex};
611 2 snap = this->m_handlers.load(std::memory_order_acquire);
612
2/2
✓ Branch 24 → 10 taken 3 times.
✓ Branch 24 → 25 taken 2 times.
7 for (const auto &entry : *snap)
613 {
614 3 entry->gate->live.store(false, std::memory_order_seq_cst);
615 }
616 2 }
617 catch (...)
618 {
619 snap = this->m_handlers.load(std::memory_order_acquire);
620 for (const auto &entry : *snap)
621 {
622 entry->gate->live.store(false, std::memory_order_seq_cst);
623 }
624 return Rundown::Unwaitable;
625 }
626
627 2 Rundown result = Rundown::Drained;
628
2/2
✓ Branch 45 → 29 taken 3 times.
✓ Branch 45 → 46 taken 2 times.
7 for (const auto &entry : *snap)
629 {
630
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 3 times.
3 if (drain_gate(*entry->gate, this) == Rundown::Unwaitable)
631 {
632 result = Rundown::Unwaitable;
633 }
634 }
635 2 clear();
636 2 return result;
637 2 }
638
639 private:
640 // Unconditional friend: test access lives outside this installed definition, so its tokens never vary with a
641 // build macro.
642 friend struct detail::EventDispatcherTestAccess<Event>;
643
644 /**
645 * @brief Reclaims the list slot of an already-retired entry.
646 * @details Physical compaction only: the handler is dead before this runs, so every outcome here is a
647 * space question, not a correctness one. Allocation failure leaves the dead entry in place.
648 *
649 * Safe to run while an emit of this instance is iterating: that iteration holds its own
650 * copy-on-write snapshot alive, and this only stores a new one for future loads to observe.
651 */
652 10332 void compact(SubscriptionId id) noexcept
653 {
654 // The outer lifetime follows EntryNode's post-unlock rule.
655 10332 SharedList superseded;
656 try
657 {
658
5/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 10277 times.
✗ Branch 2 → 78 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 78 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 78 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 78 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 43 times.
✗ Branch 2 → 78 not taken.
10332 std::scoped_lock lock{this->m_writer_mutex};
659 10332 superseded = this->m_handlers.load(std::memory_order_acquire);
660
5/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 10277 times.
✗ Branch 10 → 76 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 76 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 43 times.
✗ Branch 10 → 76 not taken.
10332 auto it = std::find_if(
661 superseded->begin(),
662 superseded->end(),
663 10351 [id](const EntryNode &entry) { return entry->id == id; }
664 );
665
7/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 19 → 20 taken 6 times.
✓ Branch 19 → 21 taken 10271 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 43 times.
20664 if (it == superseded->end())
666 {
667 7 return;
668 }
669
670
6/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 21 → 22 taken 10269 times.
✓ Branch 21 → 76 taken 2 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 76 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 21 → 22 taken 5 times.
✗ Branch 21 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 21 → 22 taken 4 times.
✗ Branch 21 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 21 → 22 taken 43 times.
✗ Branch 21 → 76 not taken.
10325 auto next = std::make_shared<HandlerList>();
671
5/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 25 → 26 taken 10269 times.
✗ Branch 25 → 74 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 74 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 25 → 26 taken 5 times.
✗ Branch 25 → 74 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 25 → 26 taken 4 times.
✗ Branch 25 → 74 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 25 → 26 taken 43 times.
✗ Branch 25 → 74 not taken.
10323 next->reserve(superseded->size() - 1);
672
10/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 44 → 29 taken 10339 times.
✓ Branch 44 → 45 taken 10269 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 44 → 29 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 44 → 29 taken 7 times.
✓ Branch 44 → 45 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 44 → 29 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 44 → 29 taken 47 times.
✓ Branch 44 → 45 taken 43 times.
31045 for (const auto &entry : *superseded)
673 {
674
8/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 32 → 33 taken 70 times.
✓ Branch 32 → 35 taken 10269 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 35 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 35 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 35 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 32 → 33 taken 4 times.
✓ Branch 32 → 35 taken 43 times.
10399 if (entry->id != id)
675 {
676
3/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 34 → 35 taken 70 times.
✗ Branch 34 → 73 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 73 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 34 → 35 taken 2 times.
✗ Branch 34 → 73 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 73 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 34 → 35 taken 4 times.
✗ Branch 34 → 73 not taken.
76 next->push_back(entry);
677 }
678 }
679
680 // Publish snapshot first, then the counter. An emit that loads a stale snapshot containing the removed
681 // handler is still safe: the entry is tombstoned, so its liveness check rejects it.
682 10323 const size_t new_count = next->size();
683 20646 this->m_handlers.store(std::shared_ptr<const HandlerList>(std::move(next)), std::memory_order_release);
684 10323 this->m_handler_count.store(new_count, std::memory_order_release);
685
7/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 63 → 64 taken 10269 times.
✓ Branch 63 → 66 taken 6 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 63 → 64 taken 2 times.
✗ Branch 63 → 66 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 63 → 64 taken 5 times.
✓ Branch 63 → 66 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 63 → 64 taken 4 times.
✗ Branch 63 → 66 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 63 → 64 taken 43 times.
✗ Branch 63 → 66 not taken.
10332 }
686 4 catch (...)
687 {
688 2 return;
689 }
690
7/10
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 68 → 69 taken 10269 times.
✓ Branch 68 → 71 taken 8 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 68 → 69 taken 2 times.
✗ Branch 68 → 71 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 68 → 69 taken 5 times.
✓ Branch 68 → 71 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 68 → 69 taken 4 times.
✗ Branch 68 → 71 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 68 → 69 taken 43 times.
✗ Branch 68 → 71 not taken.
10332 }
691
692 /**
693 * @brief Surfaces an otherwise-silent rejection, best-effort.
694 * @details Deliberately does not assert: a reentrant subscribe is a defined outcome the caller can test with
695 * active(), not a bug to abort on. The try/catch guards only the logger's first-use construction;
696 * try_log itself never throws.
697 */
698 3 static void report_reentrant_rejection(const char *op) noexcept
699 {
700 try
701 {
702 3 (void)log().try_log(
703 LogLevel::Debug,
704 "EventDispatcher: {} rejected -- called from within a handler on a same-type dispatcher "
705 "(per-instantiation reentrancy guard). Defer the mutation until the emit returns.",
706 op
707 );
708 }
709 catch (...)
710 {
711 }
712 3 }
713
714 /// Surfaces an otherwise-silent rejection, best-effort. Same discipline as report_reentrant_rejection.
715 1 static void report_closed_rejection() noexcept
716 {
717 try
718 {
719 1 (void)log().try_log(
720 LogLevel::Debug,
721 "EventDispatcher: subscribe rejected -- tombstone_and_wait has closed this "
722 "dispatcher. The returned Subscription is inactive."
723 );
724 }
725 catch (...)
726 {
727 }
728 1 }
729
730 /// Surfaces an otherwise-silent rejection, best-effort. Same discipline as report_reentrant_rejection.
731 1 static void report_untracked_rejection() noexcept
732 {
733 try
734 {
735 1 (void)log().try_log(
736 LogLevel::Debug,
737 "EventDispatcher: subscribe rejected -- an emit frame could not be recorded, so "
738 "same-type reentrancy cannot be ruled out. The returned Subscription is inactive."
739 );
740 }
741 catch (...)
742 {
743 }
744 1 }
745
746 /// Surfaces an otherwise-silent rejection, best-effort. Same discipline as report_reentrant_rejection.
747 1 static void report_empty_handler_rejection() noexcept
748 {
749 try
750 {
751 1 (void)log().try_log(
752 LogLevel::Warning,
753 "EventDispatcher: subscribe rejected an empty handler -- the returned Subscription "
754 "is inactive. Pass a callable target."
755 );
756 }
757 catch (...)
758 {
759 }
760 1 }
761
762 /**
763 * @brief Surfaces an exception emit_safe() swallowed, best-effort.
764 * @param what The std::exception::what() text, or nullptr for a non-std throw.
765 */
766 4 static void report_handler_exception(const char *what) noexcept
767 {
768 try
769 {
770 4 (void)log().try_log(
771 LogLevel::Warning,
772 "EventDispatcher: emit_safe swallowed a subscriber handler exception: {}",
773
3/12
DetourModKit::EventDispatcher<SimpleEvent>::report_handler_exception(char const*):
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 6 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::report_handler_exception(char const*):
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 6 not taken.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::report_handler_exception(char const*):
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 6 not taken.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
4 (what != nullptr && what[0] != '\0') ? what : "(non-std exception)"
774 );
775 }
776 catch (...)
777 {
778 }
779 4 }
780
781 /**
782 * @brief Admits or refuses one handler invocation, and counts it for as long as it runs.
783 * @details Enter, recheck, invoke, leave. The counter/recheck pair here and the tombstone/drain pair in
784 * Subscription::tombstone_and_wait() are a Dekker seam: both sides are seq_cst, so at least one of
785 * them observes the other. That is what makes "no invocation begins after a rundown returns
786 * Drained" hold without any lock on the emit path. A bare liveness check before the call does not:
787 * a tombstone landing between that check and the call is missed entirely.
788 *
789 * The count is released by the destructor, so a handler that throws out of emit() still leaves.
790 */
791 struct InvocationGuard
792 {
793 detail::EntryGate &gate;
794 bool entered{false};
795
796 1651382 explicit InvocationGuard(detail::EntryGate &gate_ref) noexcept : gate(gate_ref)
797 {
798 // Cheap pre-check: skips the locked increment for an entry that is already retired and merely
799 // awaiting compaction. It is an optimization, never the guarantee.
800
5/8
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✓ Branch 3 → 4 taken 1235 times.
✓ Branch 3 → 5 taken 1654080 times.
DetourModKit::EventDispatcher<StringEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 107 times.
1651382 if (!gate.live.load(std::memory_order_acquire))
801 {
802 1235 return;
803 }
804 1657512 gate.in_flight.fetch_add(1, std::memory_order_seq_cst);
805
5/8
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✓ Branch 8 → 9 taken 26 times.
✓ Branch 8 → 12 taken 1657876 times.
DetourModKit::EventDispatcher<StringEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 2 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 107 times.
1657512 if (!gate.live.load(std::memory_order_seq_cst))
806 {
807 26 gate.in_flight.fetch_sub(1, std::memory_order_seq_cst);
808 26 return;
809 }
810 1661308 entered = true;
811 }
812
813 1571854 ~InvocationGuard() noexcept
814 {
815
4/8
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 1588635 times.
✗ Branch 2 → 6 not taken.
DetourModKit::EventDispatcher<StringEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 6 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 3323 times.
✗ Branch 2 → 6 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 107 times.
✗ Branch 2 → 6 not taken.
1571854 if (entered)
816 {
817 1592067 gate.in_flight.fetch_sub(1, std::memory_order_seq_cst);
818 }
819 1571854 }
820
821 1623775 [[nodiscard]] bool admitted() const noexcept { return entered; }
822
823 InvocationGuard(const InvocationGuard &) = delete;
824 InvocationGuard &operator=(const InvocationGuard &) = delete;
825 InvocationGuard(InvocationGuard &&) = delete;
826 InvocationGuard &operator=(InvocationGuard &&) = delete;
827 };
828
829 /**
830 * @brief RAII guard that records this dispatcher on the calling thread's emit chain.
831 * @details The chain exists so a rundown can refuse to wait on its own thread, and so subscribe() can reject
832 * reentrancy. An emit whose frame cannot be recorded counts itself untracked instead, because a
833 * rundown that wrongly concludes this thread is elsewhere waits on the very thread running it.
834 */
835 struct EmitGuard
836 {
837 detail::EmitFrame frame;
838 bool tracked{false};
839
840 1749081 explicit EmitGuard(const EventDispatcher &owner) noexcept
841 1749081 : frame{&owner, &EventDispatcher::s_type_tag, nullptr}
842 {
843 1749081 tracked = detail::push_emit_frame(frame);
844
4/8
DetourModKit::EventDispatcher<SimpleEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<SimpleEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1718967 times.
DetourModKit::EventDispatcher<StringEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<StringEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 2 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 3323 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 99 times.
1722391 if (!tracked)
845 {
846 detail::untracked_emit_frames().fetch_add(1, std::memory_order_seq_cst);
847 }
848 1722391 }
849
850 1625390 ~EmitGuard() noexcept
851 {
852
4/8
DetourModKit::EventDispatcher<SimpleEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 1636504 times.
✗ Branch 2 → 4 not taken.
DetourModKit::EventDispatcher<StringEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 3323 times.
✗ Branch 2 → 4 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 99 times.
✗ Branch 2 → 4 not taken.
1625390 if (tracked)
853 {
854 1639928 detail::pop_emit_frame(frame);
855 }
856 else
857 {
858 detail::untracked_emit_frames().fetch_sub(1, std::memory_order_seq_cst);
859 }
860 1563647 }
861
862 EmitGuard(const EmitGuard &) = delete;
863 EmitGuard &operator=(const EmitGuard &) = delete;
864 EmitGuard(EmitGuard &&) = delete;
865 EmitGuard &operator=(EmitGuard &&) = delete;
866 };
867
868 // alignas(64) keeps the hot atomics on their own cache line so the writer mutex and shared_ptr control-block
869 // traffic do not produce false sharing with readers doing the fast-path counter load.
870 alignas(64) mutable std::atomic<SharedList> m_handlers;
871 mutable std::atomic<size_t> m_handler_count{0};
872 std::atomic<uint64_t> m_next_id{1};
873 /**
874 * @brief Set once by tombstone_and_wait, never cleared.
875 * @details Read under m_writer_mutex by subscribe(), which is what closes the set the rundown drains.
876 */
877 std::atomic<bool> m_closed{false};
878 mutable std::mutex m_writer_mutex; // serializes writers
879 // Prevents Subscription::reset() from compacting a destroyed dispatcher.
880 std::shared_ptr<void> m_alive;
881 };
882
883 } // namespace DetourModKit
884
885 #endif // DETOURMODKIT_EVENT_DISPATCHER_HPP
886