./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
737 / 959
76.9%
Branches:
397 / 660
60.2%
Functions:
110 / 132
83.3%

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 Installed headers return EventDispatcher<T>&, so this detail/ header declares the type in the DetourModKit
8 * namespace. The directory reflects compile visibility, not privacy.
9 */
10
11 #include "DetourModKit/logger.hpp"
12
13 #include <algorithm>
14 #include <atomic>
15 #include <cstdint>
16 #include <exception>
17 #include <functional>
18 #include <memory>
19 #include <mutex>
20 #include <utility>
21 #include <vector>
22
23 namespace DetourModKit
24 {
25 /** @brief Opaque identifier of one subscription within its dispatcher. */
26 enum class SubscriptionId : std::uint64_t
27 {
28 };
29
30 /** @brief The outcome of a rundown that waits. */
31 enum class Rundown : std::uint8_t
32 {
33 /**
34 * @brief The handler is retired and no invocation of it runs, so its captures can be destroyed.
35 * @note This is not a license to unload the handler's module. See @ref Subscription::tombstone_and_wait.
36 */
37 Drained,
38 /**
39 * @brief The handler is retired, but no wait ran, because the wait cannot be proven to end.
40 * @details The calling thread is inside this dispatcher's emit, an untracked emit runs and can be on the
41 * calling thread, or the writer mutex lock failed. An invocation can still run, so keep its captures
42 * alive.
43 */
44 Unwaitable,
45 /**
46 * @brief This Subscription holds no handler: it is default-constructed, moved-from, or already reset.
47 * @details A Subscription whose handler clear(), EventDispatcher::tombstone_and_wait, or ~EventDispatcher
48 * retired still holds its gate. It reports Drained or Unwaitable, not Inactive.
49 */
50 Inactive
51 };
52 } // namespace DetourModKit
53
54 namespace DetourModKit::detail
55 {
56 /**
57 * @brief The rundown state of one subscription, shared by its Subscription and the published list.
58 * @details The list that an emit loads, not InvocationGuard, keeps the gate alive for the whole emit iteration.
59 */
60 struct EntryGate
61 {
62 /// The rundown tombstone: false means no further invocation of this handler can begin.
63 std::atomic<bool> live{true};
64 /// Invocations that passed the tombstone recheck and did not return yet.
65 std::atomic<std::uint32_t> in_flight{0};
66 };
67
68 /** @brief One frame of the calling thread's emit chain, stored on emit()'s stack. */
69 struct EmitFrame
70 {
71 const void *dispatcher{nullptr};
72 const void *type_tag{nullptr};
73 EmitFrame *prev{nullptr};
74 /// The TLS index this frame was pushed under, so the pop restores that index and no other.
75 std::uint32_t tls_index{0xFFFFFFFFu};
76 };
77
78 /**
79 * @brief Registers one owner of the emit chain's Win32 TLS index. The last owner to deregister returns the index.
80 * @details subscribe() registers the dispatcher before its first handler publishes. Arbitrary host threads use a
81 * Win32 TLS index instead of thread_local ([B-86]). Without an index, every emit is untracked.
82 * Lifecycle.EmitTlsIndexReturnsWithTheLastSubscribedDispatcher proves the return.
83 * @note Setup/control-plane only.
84 */
85 void acquire_emit_frame_owner() noexcept;
86
87 /// Deregisters one owner that @ref acquire_emit_frame_owner registered.
88 void release_emit_frame_owner() noexcept;
89
90 /**
91 * @brief Pushes @p frame onto the calling thread's emit chain.
92 * @return false when the frame was not recorded. The caller must then count itself in @ref untracked_emit_frames.
93 */
94 [[nodiscard]] bool push_emit_frame(EmitFrame &frame) noexcept;
95
96 /// Pops @p frame. If the matching @ref push_emit_frame returned false, do not call it.
97 void pop_emit_frame(const EmitFrame &frame) noexcept;
98
99 /// True when the calling thread is inside @p dispatcher's emit.
100 [[nodiscard]] bool thread_is_emitting_dispatcher(const void *dispatcher) noexcept;
101
102 /// True when the calling thread is inside the emit of any dispatcher that shares @p type_tag.
103 [[nodiscard]] bool thread_is_emitting_type(const void *type_tag) noexcept;
104
105 /**
106 * @brief The count of untracked emits that still run. An untracked emit is one that did not record a frame.
107 * @details The count is process-wide, because no chain walk can see an untracked emit. While it is nonzero,
108 * @ref drain_gate returns Unwaitable and subscribe() returns an inactive Subscription.
109 */
110 [[nodiscard]] std::atomic<std::uint32_t> &untracked_emit_frames() noexcept;
111
112 /**
113 * @brief Waits out the invocations committed before @p gate was tombstoned.
114 * @param gate An already-tombstoned gate. On a live gate, a Drained result proves nothing.
115 * @param dispatcher Compared against this thread's emit chain only. It is never dereferenced.
116 * @return Drained once no invocation remains, or Unwaitable with no wait, as @ref Rundown::Unwaitable documents.
117 */
118 [[nodiscard]] Rundown drain_gate(EntryGate &gate, const void *dispatcher) noexcept;
119
120 /** @brief Test-only accessor to EventDispatcher privates. Only the dispatcher tests define it. */
121 template <typename Event> struct EventDispatcherTestAccess;
122
123 /** @brief Returns the emit-chain TLS ownership of the never-destroyed diagnostics dispatchers at teardown. */
124 struct DiagnosticsEmitOwner;
125 } // namespace DetourModKit::detail
126
127 namespace DetourModKit
128 {
129 /**
130 * @brief RAII guard that retires its handler when destroyed or reset.
131 * @warning The dispatcher must outlive every concurrent Subscription operation. A `~EventDispatcher` that races one
132 * on another thread is a use-after-free. The weak_ptr guard covers only ordered teardown ([B-70]): after a
133 * dispatcher destruction that happens-before the operation, the operation skips compaction.
134 */
135 class Subscription
136 {
137 public:
138 4598 Subscription() noexcept = default;
139
140 10849 ~Subscription() noexcept { reset(); }
141
142 Subscription(const Subscription &) = delete;
143 Subscription &operator=(const Subscription &) = delete;
144
145 27 Subscription(Subscription &&other) noexcept
146 81 : m_alive(std::move(other.m_alive)), m_gate(std::move(other.m_gate)),
147 54 m_dispatcher(std::exchange(other.m_dispatcher, nullptr)), m_compact(std::move(other.m_compact))
148 {
149 27 other.m_compact = nullptr;
150 27 }
151
152 409 Subscription &operator=(Subscription &&other) noexcept
153 {
154
1/2
✓ Branch 2 → 3 taken 409 times.
✗ Branch 2 → 15 not taken.
409 if (this != &other)
155 {
156 409 reset();
157 818 m_alive = std::move(other.m_alive);
158 818 m_gate = std::move(other.m_gate);
159 409 m_dispatcher = std::exchange(other.m_dispatcher, nullptr);
160 818 m_compact = std::move(other.m_compact);
161 409 other.m_compact = nullptr;
162 }
163 409 return *this;
164 }
165
166 /**
167 * @brief Retires the handler. It does not wait and does not reclaim its list slot.
168 * @details After this call, no emit on any thread can begin an invocation it did not already commit to. This
169 * includes an emit nested inside the handler that calls this. The call cannot allocate, block, or
170 * fail.
171 * @note Callback-safe. Idempotent. If an invocation can already run, call @ref tombstone_and_wait before the
172 * handler's captures go away.
173 */
174 15859 void tombstone() noexcept
175 {
176
2/2
✓ Branch 3 → 4 taken 10779 times.
✓ Branch 3 → 6 taken 5080 times.
15859 if (m_gate)
177 {
178 10779 m_gate->live.store(false, std::memory_order_seq_cst);
179 }
180 15859 }
181
182 /**
183 * @brief Retires the handler, then reclaims its list slot best-effort.
184 * @details The @ref tombstone runs first, so the removal is synchronous and cannot fail. The slot reclaim
185 * takes the writer mutex and allocates. If the dispatcher was destroyed first, reset() skips the
186 * reclaim. If the reclaim fails, one retired entry stays in the list, and every later emit rejects it.
187 * @warning This does not wait. An invocation already past the liveness check can still run on another thread.
188 * It is not callback-safe, because the reclaim can block on the writer mutex. For a non-blocking
189 * removal, use @ref tombstone.
190 */
191 15848 void reset() noexcept
192 {
193 15848 tombstone();
194 15848 compact_and_release();
195 15848 }
196
197 /**
198 * @brief Retires the handler and waits until no invocation of it runs.
199 * @return Drained, Unwaitable, or Inactive, as @ref Rundown documents.
200 * @note Setup/control-plane only. On Drained and on Unwaitable, it then reclaims the list slot as @ref reset
201 * does. Afterward, the Subscription holds no handler.
202 * @note Drained is not a license to unload the handler's module. An emit that still iterates on another thread
203 * owns the handler's std::function and later runs its destructor. Module unload needs the loader-grade
204 * quiescence of a teardown host.
205 * @warning The wait has no timeout. A handler that does not return hangs this call.
206 */
207 3 [[nodiscard]] Rundown tombstone_and_wait() noexcept
208 {
209
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
3 if (!m_gate)
210 {
211 ✗ return Rundown::Inactive;
212 }
213 3 tombstone();
214 3 const Rundown result = detail::drain_gate(*m_gate, m_dispatcher);
215 3 compact_and_release();
216 3 return result;
217 }
218
219 /// Returns true if this subscription still holds a live handler.
220 449 [[nodiscard]] bool active() const noexcept
221 {
222
4/4
✓ Branch 3 → 4 taken 433 times.
✓ Branch 3 → 8 taken 16 times.
✓ Branch 6 → 7 taken 426 times.
✓ Branch 6 → 8 taken 7 times.
449 return m_gate != nullptr && m_gate->live.load(std::memory_order_acquire);
223 }
224
225 private:
226 template <typename E> friend class EventDispatcher;
227
228 10779 Subscription(
229 std::weak_ptr<void> alive,
230 std::shared_ptr<detail::EntryGate> gate,
231 const void *dispatcher,
232 std::function<void()> compact
233 ) noexcept
234 32337 : m_alive(std::move(alive)), m_gate(std::move(gate)), m_dispatcher(dispatcher),
235 21558 m_compact(std::move(compact))
236 {
237 10779 }
238
239 15851 void compact_and_release() noexcept
240 {
241
6/6
✓ Branch 3 → 4 taken 10771 times.
✓ Branch 3 → 7 taken 5080 times.
✓ Branch 5 → 6 taken 10770 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 8 → 9 taken 10770 times.
✓ Branch 8 → 10 taken 5081 times.
15851 if (m_compact && !m_alive.expired())
242 {
243 10770 m_compact();
244 }
245 15851 m_compact = nullptr;
246 15851 m_gate.reset();
247 15851 m_dispatcher = nullptr;
248 15851 m_alive.reset();
249 15851 }
250
251 std::weak_ptr<void> m_alive;
252 std::shared_ptr<detail::EntryGate> m_gate;
253 /// Compared against the emit chain to refuse a self-wait. Never dereferenced.
254 const void *m_dispatcher{nullptr};
255 std::function<void()> m_compact;
256 };
257
258 /**
259 * @brief Thread-safe typed event dispatcher with RAII subscription management.
260 *
261 * **Thread safety:**
262 * - `emit()` / `emit_safe()`: no DMK lock and no allocation to dispatch, except as emit_safe() documents. The list
263 * load takes the bounded internal lock of `std::atomic<std::shared_ptr>`. A thread's first emit can make
264 * TlsSetValue allocate a TLS expansion array. If that fails, the emit runs untracked.
265 * - `subscribe()` / `clear()`: copy-on-write under a writer mutex. Consumer callables stay outside it ([B-101]).
266 *
267 * **Reentrancy:** a subscribe() from a handler on any dispatcher of the same Event type returns an inactive
268 * Subscription. A handler on `EventDispatcher<A>` can subscribe to `EventDispatcher<B>`. While an untracked emit
269 * runs, every subscribe() returns an inactive Subscription.
270 *
271 * **Ordering:** subscribe() release-stores the list and the handler count. A thread that observes the returned
272 * Subscription, or synchronizes with such a thread, sees the handler in later emits. Without that happens-before
273 * edge, a concurrent emit can miss a new handler.
274 */
275 template <typename Event> class EventDispatcher
276 {
277 public:
278 /// Handler function signature: receives the event by const reference.
279 using Handler = std::function<void(const Event &)>;
280
281 private:
282 // These types come before the public members that name them.
283 struct Entry
284 {
285 SubscriptionId id;
286 std::shared_ptr<detail::EntryGate> gate;
287 Handler callback;
288 };
289
290 // EntryNode enforces the callable ownership and lock boundary in [B-101] (proof: DispatchCow.*).
291 using EntryNode = std::shared_ptr<const Entry>;
292
293 /**
294 * @brief One published handler list.
295 * @details Every list that one dispatcher publishes shares one epoch, so `epoch.use_count()` counts the live
296 * lists. The epoch member comes first, so a superseded list drops its epoch reference only after it
297 * destroys its entries and their callables.
298 */
299 struct HandlerList
300 {
301 14501 HandlerList(std::shared_ptr<const char> list_epoch, std::vector<EntryNode> list_entries) noexcept
302 43503 : epoch(std::move(list_epoch)), entries(std::move(list_entries))
303 {
304 14501 }
305
306 std::shared_ptr<const char> epoch;
307 std::vector<EntryNode> entries;
308 };
309 using SharedList = std::shared_ptr<const HandlerList>;
310
311 /** @brief The emit-chain key of this Event type: one object per instantiation, identified by its address. */
312 inline static const char s_type_tag{};
313
314 public:
315 3738 EventDispatcher()
316 3738 : m_handlers(
317
12/24
DetourModKit::EventDispatcher<SimpleEvent>::EventDispatcher():
✓ Branch 3 → 4 taken 64 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 64 times.
✗ Branch 4 → 17 not taken.
DetourModKit::EventDispatcher<StringEvent>::EventDispatcher():
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 17 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::EventDispatcher():
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 17 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EventDispatcher():
✓ Branch 3 → 4 taken 143 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 143 times.
✗ Branch 4 → 17 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EventDispatcher():
✓ Branch 3 → 4 taken 1999 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 1999 times.
✗ Branch 4 → 17 not taken.
DetourModKit::EventDispatcher<int>::EventDispatcher():
✓ Branch 3 → 4 taken 1526 times.
✗ Branch 3 → 19 not taken.
✓ Branch 4 → 5 taken 1526 times.
✗ Branch 4 → 17 not taken.
7476 std::make_shared<const HandlerList>(std::make_shared<const char>('\0'), std::vector<EntryNode>{})
318 ),
319
6/12
DetourModKit::EventDispatcher<SimpleEvent>::EventDispatcher():
✓ Branch 13 → 14 taken 64 times.
✗ Branch 13 → 25 not taken.
DetourModKit::EventDispatcher<StringEvent>::EventDispatcher():
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 25 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::EventDispatcher():
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 25 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EventDispatcher():
✓ Branch 13 → 14 taken 143 times.
✗ Branch 13 → 25 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EventDispatcher():
✓ Branch 13 → 14 taken 1999 times.
✗ Branch 13 → 25 not taken.
DetourModKit::EventDispatcher<int>::EventDispatcher():
✓ Branch 13 → 14 taken 1526 times.
✗ Branch 13 → 25 not taken.
3738 m_alive(std::make_shared<char>('\0'))
320 {
321 3738 }
322
323 /**
324 * @brief Retires every handler, so a Subscription that outlives the dispatcher reads its handler as retired.
325 * @details Takes no DMK lock and allocates nothing. A subscribe() that races this is a caller lifetime
326 * violation. A dispatcher that holds an emit-chain TLS ownership returns it here.
327 * @warning This does not wait. Destruction while a handler runs is a caller lifetime violation. If that is
328 * possible, call tombstone_and_wait() first.
329 */
330 79 ~EventDispatcher() noexcept
331 {
332 79 auto current = this->m_handlers.load(std::memory_order_acquire);
333
5/8
DetourModKit::EventDispatcher<SimpleEvent>::~EventDispatcher():
✓ Branch 20 → 6 taken 3 times.
✓ Branch 20 → 21 taken 64 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.
DetourModKit::EventDispatcher<int>::~EventDispatcher():
✗ Branch 20 → 6 not taken.
✓ Branch 20 → 21 taken 9 times.
161 for (const auto &entry : current->entries)
334 {
335 3 entry->gate->live.store(false, std::memory_order_seq_cst);
336 }
337
5/8
DetourModKit::EventDispatcher<SimpleEvent>::~EventDispatcher():
✓ Branch 21 → 22 taken 52 times.
✓ Branch 21 → 23 taken 12 times.
DetourModKit::EventDispatcher<StringEvent>::~EventDispatcher():
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 23 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::~EventDispatcher():
✓ Branch 21 → 22 taken 4 times.
✗ Branch 21 → 23 not taken.
DetourModKit::EventDispatcher<int>::~EventDispatcher():
✓ Branch 21 → 22 taken 9 times.
✗ Branch 21 → 23 not taken.
79 if (this->m_emit_owner)
338 {
339 67 detail::release_emit_frame_owner();
340 }
341 79 }
342
343 EventDispatcher(const EventDispatcher &) = delete;
344 EventDispatcher &operator=(const EventDispatcher &) = delete;
345 EventDispatcher(EventDispatcher &&) = delete;
346 EventDispatcher &operator=(EventDispatcher &&) = delete;
347
348 /**
349 * @brief Subscribes a handler to this event type.
350 * @param handler Invoked on each emit(). It must be safe to call from any thread.
351 * @return The Subscription guard, or an inactive guard (active() == false) with no throw. The inactive cases
352 * are an empty handler, a call from a same-type handler, an untracked emit, and a dispatcher that
353 * tombstone_and_wait closed. If any case is possible, test active().
354 * @throws std::bad_alloc if an allocation fails.
355 * @throws std::system_error if the writer mutex lock fails. After either throw, the call installs nothing.
356 * @note Setup/control-plane only. It allocates the entry node and a new list of N+1 entries. It also
357 * registers this dispatcher as an emit-chain TLS owner, so emit() only reads the index.
358 */
359 10789 [[nodiscard]] Subscription subscribe(Handler handler)
360 {
361
7/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 10297 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 144 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 198 times.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 141 times.
10789 if (!handler)
362 {
363 // Reject here. An empty handler in the list throws bad_function_call from an unrelated emit.
364 1 report_empty_handler_rejection();
365 1 return {};
366 }
367
368
7/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 10 taken 10294 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 144 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 198 times.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 141 times.
10788 if (detail::thread_is_emitting_type(&s_type_tag))
369 {
370 3 report_reentrant_rejection("subscribe");
371 3 return {};
372 }
373
7/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 21 taken 10293 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 144 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 198 times.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 141 times.
21570 if (detail::untracked_emit_frames().load(std::memory_order_seq_cst) != 0)
374 {
375 1 report_untracked_rejection();
376 1 return {};
377 }
378
379 // Register outside the writer mutex. The first publish keeps it, and every other exit returns it.
380 10784 EmitOwnerRegistration registration;
381
382 10784 const auto id = static_cast<SubscriptionId>(this->m_next_id.fetch_add(1, std::memory_order_relaxed));
383
384
6/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 24 → 25 taken 10293 times.
✗ Branch 24 → 110 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 110 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 24 → 25 taken 6 times.
✗ Branch 24 → 110 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 24 → 25 taken 144 times.
✗ Branch 24 → 110 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 24 → 25 taken 198 times.
✗ Branch 24 → 110 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 24 → 25 taken 141 times.
✗ Branch 24 → 110 not taken.
10784 auto gate = std::make_shared<detail::EntryGate>();
385 // Build it before the publish, so a throw cannot leave a live handler that no Subscription can retire.
386 21554 std::function<void()> compact_fn = [this, id]() noexcept { this->compact(id); };
387 // Build the node before the lock, because it can run consumer move or copy code.
388 10784 EntryNode node = std::make_shared<const Entry>(Entry{id, gate, std::move(handler)});
389 // This list outlives the lock, so any callable destruction through it runs after the unlock.
390 10784 SharedList superseded;
391 {
392
6/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 34 → 35 taken 10293 times.
✗ Branch 34 → 101 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 34 → 35 taken 2 times.
✗ Branch 34 → 101 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 34 → 35 taken 6 times.
✗ Branch 34 → 101 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 34 → 35 taken 144 times.
✗ Branch 34 → 101 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 34 → 35 taken 198 times.
✗ Branch 34 → 101 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 34 → 35 taken 141 times.
✗ Branch 34 → 101 not taken.
10784 std::scoped_lock lock{this->m_writer_mutex};
393
8/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 10292 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 144 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 198 times.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 36 → 37 taken 4 times.
✓ Branch 36 → 40 taken 137 times.
10784 if (this->m_closed.load(std::memory_order_seq_cst))
394 {
395 // Test under the mutex, so no handler can publish behind a completed tombstone_and_wait drain.
396 5 report_closed_rejection();
397 5 return {};
398 }
399 10779 superseded = this->m_handlers.load(std::memory_order_acquire);
400
6/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 44 → 45 taken 10292 times.
✗ Branch 44 → 99 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 44 → 45 taken 2 times.
✗ Branch 44 → 99 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 44 → 45 taken 6 times.
✗ Branch 44 → 99 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 44 → 45 taken 144 times.
✗ Branch 44 → 99 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 44 → 45 taken 198 times.
✗ Branch 44 → 99 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 44 → 45 taken 137 times.
✗ Branch 44 → 99 not taken.
10779 auto next = std::make_shared<HandlerList>(*superseded);
401
6/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 48 → 49 taken 10292 times.
✗ Branch 48 → 97 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 97 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 48 → 49 taken 6 times.
✗ Branch 48 → 97 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 48 → 49 taken 144 times.
✗ Branch 48 → 97 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 48 → 49 taken 198 times.
✗ Branch 48 → 97 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 48 → 49 taken 137 times.
✗ Branch 48 → 97 not taken.
21558 next->entries.push_back(std::move(node));
402
10/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 49 → 50 taken 60 times.
✓ Branch 49 → 51 taken 10232 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 49 → 50 taken 2 times.
✗ Branch 49 → 51 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 49 → 50 taken 4 times.
✓ Branch 49 → 51 taken 2 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 49 → 50 taken 144 times.
✗ Branch 49 → 51 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 49 → 50 taken 193 times.
✓ Branch 49 → 51 taken 5 times.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 49 → 50 taken 133 times.
✓ Branch 49 → 51 taken 4 times.
10779 if (!this->m_emit_owner)
403 {
404 536 this->m_emit_owner = true;
405 536 registration.held = false;
406 }
407 // Store the count first, so a reader that reads 0 and skips the load misses no installed handler.
408 10779 this->m_handler_count.store(next->entries.size(), std::memory_order_release);
409 21558 this->m_handlers.store(std::shared_ptr<const HandlerList>(std::move(next)), std::memory_order_release);
410
8/12
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 69 → 70 taken 10292 times.
✓ Branch 69 → 79 taken 1 time.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 69 → 70 taken 2 times.
✗ Branch 69 → 79 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 69 → 70 taken 6 times.
✗ Branch 69 → 79 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 69 → 70 taken 144 times.
✗ Branch 69 → 79 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 69 → 70 taken 198 times.
✗ Branch 69 → 79 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 69 → 70 taken 137 times.
✓ Branch 69 → 79 taken 4 times.
10784 }
411
412 21558 return Subscription(std::weak_ptr<void>(this->m_alive), std::move(gate), this, std::move(compact_fn));
413
12/36
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 30 → 31 taken 10293 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 10293 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::subscribe(std::function<void ((anonymous namespace)::CowEvent const&)>):
✓ Branch 30 → 31 taken 6 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 6 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::subscribe(std::function<void (DetourModKit::diagnostics::ScannerFaultEvent const&)>):
✓ Branch 30 → 31 taken 144 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 144 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::subscribe(std::function<void (DetourModKit::diagnostics::HookLifecycleEvent const&)>):
✓ Branch 30 → 31 taken 198 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 198 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
DetourModKit::EventDispatcher<int>::subscribe(std::function<void (int const&)>):
✓ Branch 30 → 31 taken 141 times.
✗ Branch 30 → 91 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 141 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
43136 }
414
415 /**
416 * @brief Emits an event to each live handler, synchronously and in subscription order.
417 * @note A handler exception propagates to the caller.
418 * @warning A handler exception with no catch frame above the call site terminates the process. In a hook
419 * callback, or any context where an unhandled exception crashes the host, use emit_safe().
420 */
421 44117 void emit(const Event &event) const
422 {
423 // Fast path: with no published slot, skip the list load.
424
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 9 → 10 taken 1012 times.
✓ Branch 9 → 11 taken 42674 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
87805 if (this->m_handler_count.load(std::memory_order_acquire) == 0)
425 {
426 1012 return;
427 }
428
429 42676 SharedList snap = this->m_handlers.load(std::memory_order_acquire);
430 47966 EmitGuard guard{*this};
431
7/8
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 28 → 29 taken 45990 times.
✓ Branch 28 → 31 taken 11 times.
✓ Branch 41 → 16 taken 45370 times.
✓ Branch 41 → 42 taken 44818 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.
180900 for (const auto &entry : snap->entries)
432 {
433 45372 InvocationGuard invocation{*entry->gate};
434
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 22 → 23 taken 143 times.
✓ Branch 22 → 24 taken 46448 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
46587 if (!invocation.admitted())
435 {
436 143 continue;
437 }
438
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 25 → 26 taken 44830 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.
46450 entry->callback(event);
439 }
440 44822 }
441
442 /**
443 * @brief Emits like emit(), but catches and logs each handler exception, then continues with the next handler.
444 * @note Best-effort. The log call for a caught exception can allocate.
445 */
446 2956706 void emit_safe(const Event &event) const noexcept
447 {
448
7/8
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 9 → 10 taken 129762 times.
✓ Branch 9 → 11 taken 2752217 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 9 → 10 taken 269 times.
✓ Branch 9 → 11 taken 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 9 → 10 taken 53501 times.
✓ Branch 9 → 11 taken 2737 times.
DetourModKit::EventDispatcher<int>::emit_safe(int const&) const:
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 134 times.
5900031 if (this->m_handler_count.load(std::memory_order_acquire) == 0)
449 {
450 183532 return;
451 }
452
453 2759793 SharedList snap = this->m_handlers.load(std::memory_order_acquire);
454 3214321 EmitGuard guard{*this};
455
12/16
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 28 → 29 taken 3089163 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 3005940 times.
✓ Branch 41 → 42 taken 2844840 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 28 → 29 taken 4705 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 4705 times.
✓ Branch 41 → 42 taken 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 28 → 29 taken 2747 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 2748 times.
✓ Branch 41 → 42 taken 2739 times.
DetourModKit::EventDispatcher<int>::emit_safe(int const&) const:
✓ Branch 28 → 29 taken 134 times.
✗ Branch 28 → 31 not taken.
✓ Branch 41 → 16 taken 134 times.
✓ Branch 41 → 42 taken 134 times.
11854968 for (const auto &entry : snap->entries)
456 {
457 3013527 InvocationGuard invocation{*entry->gate};
458
5/8
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 22 → 23 taken 3018 times.
✓ Branch 22 → 24 taken 2946941 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2748 times.
DetourModKit::EventDispatcher<int>::emit_safe(int const&) const:
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 134 times.
2968037 if (!invocation.admitted())
459 {
460 3018 continue;
461 }
462 try
463 {
464
5/8
DetourModKit::EventDispatcher<SimpleEvent>::emit_safe(SimpleEvent const&) const:
✓ Branch 25 → 26 taken 2840182 times.
✓ Branch 25 → 46 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::emit_safe(DetourModKit::diagnostics::ScannerFaultEvent const&) const:
✓ Branch 25 → 26 taken 4705 times.
✗ Branch 25 → 46 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::emit_safe(DetourModKit::diagnostics::HookLifecycleEvent const&) const:
✓ Branch 25 → 26 taken 2747 times.
✗ Branch 25 → 46 not taken.
DetourModKit::EventDispatcher<int>::emit_safe(int const&) const:
✓ Branch 25 → 26 taken 134 times.
✗ Branch 25 → 46 not taken.
2954528 entry->callback(event);
465 }
466
2/8
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.
DetourModKit::EventDispatcher<int>::emit_safe(int const&) const:
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 51 not taken.
7 catch (const std::exception &ex)
467 {
468 // Report the swallow with its text. A silent swallow hides a handler bug.
469 3 report_handler_exception(ex.what());
470 }
471 1 catch (...)
472 {
473 // A non-std throw carries no portable message.
474 1 report_handler_exception(nullptr);
475 }
476 }
477 2852418 }
478
479 /** @brief Returns the published slot count. A retired slot counts until compaction removes it. */
480 41 [[nodiscard]] size_t subscriber_count() const noexcept
481 {
482 82 return this->m_handler_count.load(std::memory_order_acquire);
483 }
484
485 /// Returns true if there is no published slot.
486 10 [[nodiscard]] bool empty() const noexcept { return this->m_handler_count.load(std::memory_order_acquire) == 0; }
487
488 /**
489 * @brief Retires every handler before this call returns.
490 * @note Setup/control-plane only. It takes the writer mutex. If the empty-list publish fails, the retired
491 * entries stay until a later mutation reclaims them. This does not wait for a handler that still runs.
492 * To wait, call @ref tombstone_and_wait instead of clear().
493 */
494 13 void clear() noexcept
495 {
496 // This list outlives the lock, so any callable destruction through it runs after the unlock.
497 13 SharedList superseded;
498 try
499 {
500
3/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 72 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 72 not taken.
DetourModKit::EventDispatcher<int>::clear():
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 72 not taken.
13 std::scoped_lock lock{this->m_writer_mutex};
501 // Retire first, so an allocation failure below cannot leave a live handler behind.
502 13 superseded = this->m_handlers.load(std::memory_order_acquire);
503
6/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 23 → 9 taken 9 times.
✓ Branch 23 → 24 taken 6 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 23 → 9 taken 1 time.
✓ Branch 23 → 24 taken 3 times.
DetourModKit::EventDispatcher<int>::clear():
✓ Branch 23 → 9 taken 8 times.
✓ Branch 23 → 24 taken 4 times.
44 for (const auto &entry : superseded->entries)
504 {
505 18 entry->gate->live.store(false, std::memory_order_seq_cst);
506 }
507
508 13 std::shared_ptr<const HandlerList> empty_snap;
509 try
510 {
511
4/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 26 → 27 taken 5 times.
✓ Branch 26 → 59 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 26 → 27 taken 3 times.
✗ Branch 26 → 59 not taken.
DetourModKit::EventDispatcher<int>::clear():
✓ Branch 26 → 27 taken 4 times.
✗ Branch 26 → 59 not taken.
14 empty_snap = std::make_shared<const HandlerList>(superseded->epoch, std::vector<EntryNode>{});
512 }
513
1/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 68 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✗ Branch 66 → 67 not taken.
✗ Branch 66 → 68 not taken.
DetourModKit::EventDispatcher<int>::clear():
✗ Branch 66 → 67 not taken.
✗ Branch 66 → 68 not taken.
2 catch (...)
514 {
515 1 return;
516 }
517 // Zero the counter before the empty-list publish, so an emit that reads 0 cannot see the old list.
518 12 this->m_handler_count.store(0, std::memory_order_release);
519 24 this->m_handlers.store(std::move(empty_snap), std::memory_order_release);
520
8/12
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 45 → 46 taken 5 times.
✓ Branch 45 → 47 taken 1 time.
✓ Branch 49 → 50 taken 5 times.
✓ Branch 49 → 52 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 45 → 46 taken 3 times.
✗ Branch 45 → 47 not taken.
✓ Branch 49 → 50 taken 3 times.
✗ Branch 49 → 52 not taken.
DetourModKit::EventDispatcher<int>::clear():
✓ Branch 45 → 46 taken 4 times.
✗ Branch 45 → 47 not taken.
✓ Branch 49 → 50 taken 4 times.
✗ Branch 49 → 52 not taken.
14 }
521 ✗ catch (...)
522 {
523 // A lock failure must not escape. Retire the visible list. A concurrent unordered subscribe can still
524 // publish after it.
525 ✗ auto current = this->m_handlers.load(std::memory_order_acquire);
526
0/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✗ Branch 93 → 79 not taken.
✗ Branch 93 → 94 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✗ Branch 93 → 79 not taken.
✗ Branch 93 → 94 not taken.
DetourModKit::EventDispatcher<int>::clear():
✗ Branch 93 → 79 not taken.
✗ Branch 93 → 94 not taken.
✗ for (const auto &entry : current->entries)
527 {
528 ✗ entry->gate->live.store(false, std::memory_order_seq_cst);
529 }
530 ✗ return;
531 ✗ }
532
4/6
DetourModKit::EventDispatcher<SimpleEvent>::clear():
✓ Branch 54 → 55 taken 5 times.
✓ Branch 54 → 57 taken 1 time.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::clear():
✓ Branch 54 → 55 taken 3 times.
✗ Branch 54 → 57 not taken.
DetourModKit::EventDispatcher<int>::clear():
✓ Branch 54 → 55 taken 4 times.
✗ Branch 54 → 57 not taken.
13 }
533
534 /**
535 * @brief Retires every handler, then waits until no handler in the published list runs.
536 * @return Drained or Unwaitable, as @ref Rundown documents.
537 * @details This closes the dispatcher permanently: every later subscribe() returns an inactive Subscription.
538 * The list excludes a handler whose slot clear() or its Subscription already reclaimed, so no wait
539 * covers it. After the drain, it runs @ref clear. If the writer mutex lock fails, it retires only the
540 * visible list and returns Unwaitable with no wait and no clear.
541 * @note Setup/control-plane only.
542 * @warning The wait has no timeout, as @ref Subscription::tombstone_and_wait documents.
543 */
544 6 [[nodiscard]] Rundown tombstone_and_wait() noexcept
545 {
546 // Close before the list load. subscribe() tests the flag under the writer mutex, so the list below is
547 // complete. The drain then runs without the mutex, because a handler can call subscribe().
548 6 this->m_closed.store(true, std::memory_order_seq_cst);
549
550 6 SharedList snap;
551 try
552 {
553
2/4
DetourModKit::EventDispatcher<SimpleEvent>::tombstone_and_wait():
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 51 not taken.
DetourModKit::EventDispatcher<int>::tombstone_and_wait():
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 51 not taken.
6 std::scoped_lock lock{this->m_writer_mutex};
554 6 snap = this->m_handlers.load(std::memory_order_acquire);
555
4/4
DetourModKit::EventDispatcher<SimpleEvent>::tombstone_and_wait():
✓ Branch 24 → 10 taken 3 times.
✓ Branch 24 → 25 taken 2 times.
DetourModKit::EventDispatcher<int>::tombstone_and_wait():
✓ Branch 24 → 10 taken 8 times.
✓ Branch 24 → 25 taken 4 times.
23 for (const auto &entry : snap->entries)
556 {
557 11 entry->gate->live.store(false, std::memory_order_seq_cst);
558 }
559 6 }
560 ✗ catch (...)
561 {
562 ✗ snap = this->m_handlers.load(std::memory_order_acquire);
563
0/4
DetourModKit::EventDispatcher<SimpleEvent>::tombstone_and_wait():
✗ Branch 74 → 60 not taken.
✗ Branch 74 → 75 not taken.
DetourModKit::EventDispatcher<int>::tombstone_and_wait():
✗ Branch 74 → 60 not taken.
✗ Branch 74 → 75 not taken.
✗ for (const auto &entry : snap->entries)
564 {
565 ✗ entry->gate->live.store(false, std::memory_order_seq_cst);
566 }
567 ✗ return Rundown::Unwaitable;
568 }
569
570 6 Rundown result = Rundown::Drained;
571
4/4
DetourModKit::EventDispatcher<SimpleEvent>::tombstone_and_wait():
✓ Branch 45 → 29 taken 3 times.
✓ Branch 45 → 46 taken 2 times.
DetourModKit::EventDispatcher<int>::tombstone_and_wait():
✓ Branch 45 → 29 taken 8 times.
✓ Branch 45 → 46 taken 4 times.
23 for (const auto &entry : snap->entries)
572 {
573
2/4
DetourModKit::EventDispatcher<SimpleEvent>::tombstone_and_wait():
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 3 times.
DetourModKit::EventDispatcher<int>::tombstone_and_wait():
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 8 times.
11 if (drain_gate(*entry->gate, this) == Rundown::Unwaitable)
574 {
575 ✗ result = Rundown::Unwaitable;
576 }
577 }
578 6 clear();
579 6 return result;
580 6 }
581
582 private:
583 // Unconditional friends keep this installed definition token-stable under every build macro.
584 friend struct detail::EventDispatcherTestAccess<Event>;
585 friend struct detail::DiagnosticsEmitOwner;
586
587 /// The outcome of @ref release_idle_emit_owner.
588 enum class EmitOwnerRelease : std::uint8_t
589 {
590 /// This dispatcher returned its emit-chain TLS ownership.
591 Released,
592 /// This dispatcher holds no ownership.
593 NotOwner,
594 /// A live entry keeps the ownership.
595 LiveSubscription,
596 /// A writer or an emit holds a list. A later call can succeed.
597 Busy,
598 /// As Busy, but the calling thread can be the holder, so a wait on it cannot end.
599 Unwaitable
600 };
601
602 /**
603 * @brief Returns this dispatcher's emit-chain TLS ownership when no emit can still run a handler.
604 * @param may_prune True to publish an empty list over retired entries before the return. A loader-lock caller
605 * passes false, because the prune allocates and destroys consumer callables.
606 * @details Never waits for the writer mutex. Idle means no live entry, no admitted invocation in the current
607 * list, and no emit that holds any list. A later subscribe that publishes a handler registers again.
608 */
609 10127 [[nodiscard]] EmitOwnerRelease release_idle_emit_owner(bool may_prune) noexcept
610 {
611 // This list outlives the lock, so any callable destruction through it runs after the unlock.
612 10127 SharedList current;
613 10127 std::unique_lock lock{this->m_writer_mutex, std::try_to_lock};
614
4/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 16 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 160 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 9950 times.
10127 if (!lock.owns_lock())
615 {
616 1 return EmitOwnerRelease::Busy;
617 }
618
6/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 14 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 8 taken 141 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 6 → 7 taken 9588 times.
✓ Branch 6 → 8 taken 362 times.
10126 if (!this->m_emit_owner)
619 {
620 9609 return EmitOwnerRelease::NotOwner;
621 }
622 517 current = this->m_handlers.load(std::memory_order_acquire);
623 517 bool admitted = false;
624
6/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 43 → 14 taken 8 times.
✓ Branch 43 → 44 taken 13 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 43 → 14 taken 1 time.
✓ Branch 43 → 44 taken 140 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 43 → 14 taken 10 times.
✓ Branch 43 → 44 taken 354 times.
1043 for (const auto &entry : current->entries)
625 {
626
5/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 7 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 21 taken 2 times.
19 if (entry->gate->live.load(std::memory_order_seq_cst))
627 {
628 10 return EmitOwnerRelease::LiveSubscription;
629 }
630
6/12
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 21 → 22 taken 7 times.
✗ Branch 21 → 32 not taken.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 32 not taken.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 32 not taken.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 1 time.
18 admitted = admitted || entry->gate->in_flight.load(std::memory_order_seq_cst) != 0;
631 }
632 // The in-flight read pairs with InvocationGuard's recheck, as the drain does. Every list copy runs under
633 // the writer mutex, so an epoch count of one proves that no superseded list survives. The atomic and
634 // `current` are then the only expected holders of the current list.
635
20/24
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 44 → 45 taken 11 times.
✓ Branch 44 → 50 taken 2 times.
✓ Branch 46 → 47 taken 11 times.
✗ Branch 46 → 50 not taken.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 49 → 51 taken 8 times.
✓ Branch 52 → 53 taken 5 times.
✓ Branch 52 → 67 taken 8 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 44 → 45 taken 140 times.
✗ Branch 44 → 50 not taken.
✓ Branch 46 → 47 taken 140 times.
✗ Branch 46 → 50 not taken.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 139 times.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 67 taken 139 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 44 → 45 taken 353 times.
✓ Branch 44 → 50 taken 1 time.
✓ Branch 46 → 47 taken 353 times.
✗ Branch 46 → 50 not taken.
✓ Branch 49 → 50 taken 210 times.
✓ Branch 49 → 51 taken 143 times.
✓ Branch 52 → 53 taken 211 times.
✓ Branch 52 → 67 taken 143 times.
507 if (admitted || current.use_count() != 2 || current->epoch.use_count() != 1)
636 {
637 217 return detail::thread_is_emitting_dispatcher(this) ||
638
4/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 65 taken 209 times.
430 detail::untracked_emit_frames().load(std::memory_order_seq_cst) != 0
639
5/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 54 → 55 taken 4 times.
✓ Branch 54 → 64 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 54 → 55 taken 1 time.
✗ Branch 54 → 64 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 54 → 55 taken 210 times.
✓ Branch 54 → 64 taken 1 time.
217 ? EmitOwnerRelease::Unwaitable
640 217 : EmitOwnerRelease::Busy;
641 }
642 // Pairs with the release decrement of the last holder, so its frame pop precedes the TlsFree below.
643 std::atomic_thread_fence(std::memory_order_acquire);
644
13/18
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 68 → 69 taken 7 times.
✓ Branch 68 → 73 taken 1 time.
✓ Branch 71 → 72 taken 3 times.
✓ Branch 71 → 73 taken 4 times.
✓ Branch 74 → 75 taken 3 times.
✓ Branch 74 → 100 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✓ Branch 68 → 69 taken 139 times.
✗ Branch 68 → 73 not taken.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 139 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 100 taken 139 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✓ Branch 68 → 69 taken 142 times.
✓ Branch 68 → 73 taken 1 time.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 142 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 100 taken 143 times.
290 if (may_prune && !current->entries.empty())
645 {
646 3 SharedList empty;
647 try
648 {
649
1/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 77 → 78 taken 3 times.
✗ Branch 77 → 106 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 106 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 106 not taken.
3 empty = std::make_shared<const HandlerList>(current->epoch, std::vector<EntryNode>{});
650 }
651 ✗ catch (...)
652 {
653 ✗ return EmitOwnerRelease::Busy;
654 }
655 3 this->m_handler_count.store(0, std::memory_order_release);
656 6 this->m_handlers.store(std::move(empty), std::memory_order_release);
657
1/6
DetourModKit::EventDispatcher<SimpleEvent>::release_idle_emit_owner(bool):
✓ Branch 96 → 97 taken 3 times.
✗ Branch 96 → 99 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::release_idle_emit_owner(bool):
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 99 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::release_idle_emit_owner(bool):
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 99 not taken.
3 }
658 290 this->m_emit_owner = false;
659 290 detail::release_emit_frame_owner();
660 290 return EmitOwnerRelease::Released;
661 10127 }
662
663 /**
664 * @brief Reclaims the list slot of an already-retired entry.
665 * @details An allocation failure leaves the retired entry in place. A concurrent emit keeps its own list, so
666 * this is safe while an emit iterates.
667 */
668 10770 void compact(SubscriptionId id) noexcept
669 {
670 // This list outlives the lock, so any callable destruction through it runs after the unlock.
671 10770 SharedList superseded;
672 try
673 {
674
6/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 10291 times.
✗ Branch 2 → 84 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 84 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 84 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 143 times.
✗ Branch 2 → 84 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 191 times.
✗ Branch 2 → 84 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 2 → 3 taken 137 times.
✗ Branch 2 → 84 not taken.
10770 std::scoped_lock lock{this->m_writer_mutex};
675 10770 superseded = this->m_handlers.load(std::memory_order_acquire);
676
6/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 10291 times.
✗ Branch 10 → 82 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 82 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 82 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 143 times.
✗ Branch 10 → 82 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 191 times.
✗ Branch 10 → 82 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 10 → 11 taken 137 times.
✗ Branch 10 → 82 not taken.
21540 auto it = std::find_if(
677 10770 superseded->entries.begin(),
678 10770 superseded->entries.end(),
679 10776 [id](const EntryNode &entry) { return entry->id == id; }
680 );
681
9/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 19 → 20 taken 11 times.
✓ Branch 19 → 21 taken 10280 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 143 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 191 times.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 21 taken 129 times.
21540 if (it == superseded->entries.end())
682 {
683 20 return;
684 }
685
686
7/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 10278 times.
✓ Branch 23 → 76 taken 2 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 76 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 5 times.
✗ Branch 23 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 143 times.
✗ Branch 23 → 76 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 191 times.
✗ Branch 23 → 76 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 23 → 24 taken 129 times.
✗ Branch 23 → 76 not taken.
10752 auto next = std::make_shared<HandlerList>(superseded->epoch, std::vector<EntryNode>{});
687
6/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 10278 times.
✗ Branch 28 → 80 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 80 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 5 times.
✗ Branch 28 → 80 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 143 times.
✗ Branch 28 → 80 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 191 times.
✗ Branch 28 → 80 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 129 times.
✗ Branch 28 → 80 not taken.
10748 next->entries.reserve(superseded->entries.size() - 1);
688
12/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 10348 times.
✓ Branch 47 → 48 taken 10278 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 7 times.
✓ Branch 47 → 48 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 143 times.
✓ Branch 47 → 48 taken 143 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 195 times.
✓ Branch 47 → 48 taken 191 times.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 47 → 32 taken 129 times.
✓ Branch 47 → 48 taken 129 times.
32320 for (const auto &entry : superseded->entries)
689 {
690
9/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 35 → 36 taken 70 times.
✓ Branch 35 → 38 taken 10278 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 38 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 38 taken 5 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 38 taken 143 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 38 taken 191 times.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 38 taken 129 times.
10824 if (entry->id != id)
691 {
692
3/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 37 → 38 taken 70 times.
✗ Branch 37 → 79 not taken.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 79 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 79 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 79 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 37 → 38 taken 4 times.
✗ Branch 37 → 79 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 79 not taken.
76 next->entries.push_back(entry);
693 }
694 }
695
696 // Store the list first, then the counter. The gate rejects the removed entry in a stale list.
697 10748 const size_t new_count = next->entries.size();
698 21496 this->m_handlers.store(std::shared_ptr<const HandlerList>(std::move(next)), std::memory_order_release);
699 10748 this->m_handler_count.store(new_count, std::memory_order_release);
700
9/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 10278 times.
✓ Branch 66 → 69 taken 11 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 2 times.
✗ Branch 66 → 69 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 5 times.
✓ Branch 66 → 69 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 143 times.
✗ Branch 66 → 69 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 191 times.
✗ Branch 66 → 69 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 66 → 67 taken 129 times.
✓ Branch 66 → 69 taken 8 times.
10770 }
701 4 catch (...)
702 {
703 2 return;
704 }
705
9/12
DetourModKit::EventDispatcher<SimpleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 10278 times.
✓ Branch 71 → 74 taken 13 times.
DetourModKit::EventDispatcher<StringEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 2 times.
✗ Branch 71 → 74 not taken.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 5 times.
✓ Branch 71 → 74 taken 1 time.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 143 times.
✗ Branch 71 → 74 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 191 times.
✗ Branch 71 → 74 not taken.
DetourModKit::EventDispatcher<int>::compact(DetourModKit::SubscriptionId):
✓ Branch 71 → 72 taken 129 times.
✓ Branch 71 → 74 taken 8 times.
10770 }
706
707 /** @brief Surfaces an otherwise-silent rejection, best-effort. The caller tests the outcome with active(). */
708 3 static void report_reentrant_rejection(const char *op) noexcept
709 {
710 try
711 {
712 3 (void)log().try_log(
713 LogLevel::Debug,
714 "EventDispatcher: {} rejected: the call came from within a handler on a same-type dispatcher "
715 "(per-instantiation reentrancy guard). Defer the mutation until the emit returns.",
716 op
717 );
718 }
719 catch (...)
720 {
721 }
722 3 }
723
724 /// Surfaces an otherwise-silent rejection, best-effort.
725 5 static void report_closed_rejection() noexcept
726 {
727 try
728 {
729 5 (void)log().try_log(
730 LogLevel::Debug,
731 "EventDispatcher: subscribe rejected: tombstone_and_wait closed this "
732 "dispatcher. The returned Subscription is inactive."
733 );
734 }
735 catch (...)
736 {
737 }
738 5 }
739
740 /// Surfaces an otherwise-silent rejection, best-effort.
741 1 static void report_untracked_rejection() noexcept
742 {
743 try
744 {
745 1 (void)log().try_log(
746 LogLevel::Debug,
747 "EventDispatcher: subscribe rejected: an emit did not record its frame, so the dispatcher "
748 "cannot rule out same-type reentrancy. The returned Subscription is inactive."
749 );
750 }
751 catch (...)
752 {
753 }
754 1 }
755
756 /// Surfaces an otherwise-silent rejection, best-effort.
757 1 static void report_empty_handler_rejection() noexcept
758 {
759 try
760 {
761 1 (void)log().try_log(
762 LogLevel::Warning,
763 "EventDispatcher: subscribe rejected an empty handler. The returned Subscription "
764 "is inactive. Pass a callable target."
765 );
766 }
767 catch (...)
768 {
769 }
770 1 }
771
772 /**
773 * @brief Surfaces an exception emit_safe() swallowed, best-effort.
774 * @param what The std::exception::what() text, or nullptr for a non-std throw.
775 */
776 4 static void report_handler_exception(const char *what) noexcept
777 {
778 try
779 {
780 4 (void)log().try_log(
781 LogLevel::Warning,
782 "EventDispatcher: emit_safe swallowed a subscriber handler exception: {}",
783
3/16
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.
DetourModKit::EventDispatcher<int>::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)"
784 );
785 }
786 catch (...)
787 {
788 }
789 4 }
790
791 /**
792 * @brief Admits or refuses one handler invocation, and counts it while it runs.
793 * @details The increment and recheck here, the tombstone store, and the @ref detail::drain_gate load are all
794 * seq_cst, so at least one side observes the other. No invocation can begin after a rundown returns
795 * Drained. The destructor releases the count, so a handler that throws out of emit() still leaves.
796 */
797 struct InvocationGuard
798 {
799 detail::EntryGate &gate;
800 bool entered{false};
801
802 3014995 explicit InvocationGuard(detail::EntryGate &gate_ref) noexcept : gate(gate_ref)
803 {
804 // This pre-check skips a retired entry. It is only an optimization. The recheck below is the guarantee.
805
6/10
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✓ Branch 3 → 4 taken 3110 times.
✓ Branch 3 → 5 taken 3023821 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 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2748 times.
DetourModKit::EventDispatcher<int>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 134 times.
3014995 if (!gate.live.load(std::memory_order_acquire))
806 {
807 3110 return;
808 }
809 3031410 gate.in_flight.fetch_add(1, std::memory_order_seq_cst);
810
6/10
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✓ Branch 8 → 9 taken 57 times.
✓ Branch 8 → 12 taken 3046380 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 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 2748 times.
DetourModKit::EventDispatcher<int>::InvocationGuard::InvocationGuard(DetourModKit::detail::EntryGate&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 134 times.
3031410 if (!gate.live.load(std::memory_order_seq_cst))
811 {
812 57 gate.in_flight.fetch_sub(1, std::memory_order_seq_cst);
813 57 return;
814 }
815 3053969 entered = true;
816 }
817
818 2879778 ~InvocationGuard() noexcept
819 {
820
5/10
DetourModKit::EventDispatcher<SimpleEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 2891507 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 4705 times.
✗ Branch 2 → 6 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 2747 times.
✗ Branch 2 → 6 not taken.
DetourModKit::EventDispatcher<int>::InvocationGuard::~InvocationGuard():
✓ Branch 2 → 3 taken 134 times.
✗ Branch 2 → 6 not taken.
2879778 if (entered)
821 {
822 2899095 gate.in_flight.fetch_sub(1, std::memory_order_seq_cst);
823 }
824 2879778 }
825
826 3003371 [[nodiscard]] bool admitted() const noexcept { return entered; }
827
828 InvocationGuard(const InvocationGuard &) = delete;
829 InvocationGuard &operator=(const InvocationGuard &) = delete;
830 InvocationGuard(InvocationGuard &&) = delete;
831 InvocationGuard &operator=(InvocationGuard &&) = delete;
832 };
833
834 /** @brief Records this dispatcher on the calling thread's emit chain, or counts the emit as untracked. */
835 struct EmitGuard
836 {
837 detail::EmitFrame frame;
838 bool tracked{false};
839
840 3248559 explicit EmitGuard(const EventDispatcher &owner) noexcept
841 3248559 : frame{&owner, &EventDispatcher::s_type_tag, nullptr}
842 {
843 3248559 tracked = detail::push_emit_frame(frame);
844
5/10
DetourModKit::EventDispatcher<SimpleEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<SimpleEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 3159437 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 4705 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 2740 times.
DetourModKit::EventDispatcher<int>::EmitGuard::EmitGuard(DetourModKit::EventDispatcher<int> const&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 134 times.
3167018 if (!tracked)
845 {
846 ✗ detail::untracked_emit_frames().fetch_add(1, std::memory_order_seq_cst);
847 }
848 3167018 }
849
850 2997873 ~EmitGuard() noexcept
851 {
852
5/10
DetourModKit::EventDispatcher<SimpleEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 3015593 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 4705 times.
✗ Branch 2 → 4 not taken.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 2739 times.
✗ Branch 2 → 4 not taken.
DetourModKit::EventDispatcher<int>::EmitGuard::~EmitGuard():
✓ Branch 2 → 3 taken 134 times.
✗ Branch 2 → 4 not taken.
2997873 if (tracked)
853 {
854 3023173 detail::pop_emit_frame(frame);
855 }
856 else
857 {
858 ✗ detail::untracked_emit_frames().fetch_sub(1, std::memory_order_seq_cst);
859 }
860 2973469 }
861
862 EmitGuard(const EmitGuard &) = delete;
863 EmitGuard &operator=(const EmitGuard &) = delete;
864 EmitGuard(EmitGuard &&) = delete;
865 EmitGuard &operator=(EmitGuard &&) = delete;
866 };
867
868 /// One emit-chain TLS ownership that subscribe() returns unless the dispatcher keeps it.
869 struct EmitOwnerRegistration
870 {
871 bool held{true};
872
873 10784 EmitOwnerRegistration() noexcept { detail::acquire_emit_frame_owner(); }
874
875 10784 ~EmitOwnerRegistration() noexcept
876 {
877
10/12
DetourModKit::EventDispatcher<SimpleEvent>::EmitOwnerRegistration::~EmitOwnerRegistration():
✓ Branch 2 → 3 taken 10233 times.
✓ Branch 2 → 4 taken 60 times.
DetourModKit::EventDispatcher<StringEvent>::EmitOwnerRegistration::~EmitOwnerRegistration():
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
DetourModKit::EventDispatcher<(anonymous namespace)::CowEvent>::EmitOwnerRegistration::~EmitOwnerRegistration():
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 4 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>::EmitOwnerRegistration::~EmitOwnerRegistration():
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 144 times.
DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>::EmitOwnerRegistration::~EmitOwnerRegistration():
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 4 taken 193 times.
DetourModKit::EventDispatcher<int>::EmitOwnerRegistration::~EmitOwnerRegistration():
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 133 times.
10784 if (held)
878 {
879 10248 detail::release_emit_frame_owner();
880 }
881 10784 }
882
883 EmitOwnerRegistration(const EmitOwnerRegistration &) = delete;
884 EmitOwnerRegistration &operator=(const EmitOwnerRegistration &) = delete;
885 EmitOwnerRegistration(EmitOwnerRegistration &&) = delete;
886 EmitOwnerRegistration &operator=(EmitOwnerRegistration &&) = delete;
887 };
888
889 // alignas(64) starts the hot atomics on a cache-line boundary. The writer mutex still shares that line.
890 alignas(64) mutable std::atomic<SharedList> m_handlers;
891 mutable std::atomic<size_t> m_handler_count{0};
892 std::atomic<uint64_t> m_next_id{1};
893 /** @brief Set once by tombstone_and_wait and never cleared. subscribe() reads it under m_writer_mutex. */
894 std::atomic<bool> m_closed{false};
895 /// True from the publish that kept an emit-chain TLS ownership until its return. Written under m_writer_mutex.
896 bool m_emit_owner{false};
897 mutable std::mutex m_writer_mutex;
898 // Each Subscription holds a weak_ptr to this. Once it expires, the Subscription skips its compaction.
899 std::shared_ptr<void> m_alive;
900 };
901
902 } // namespace DetourModKit
903
904 #endif // DETOURMODKIT_EVENT_DISPATCHER_HPP
905