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/20DetourModKit::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/6DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/30DetourModKit::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/4DetourModKit::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/8DetourModKit::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/4DetourModKit::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/4DetourModKit::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/6DetourModKit::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/12DetourModKit::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/6DetourModKit::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/6DetourModKit::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/6DetourModKit::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/4DetourModKit::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/4DetourModKit::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/4DetourModKit::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/4DetourModKit::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/8DetourModKit::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/4DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/10DetourModKit::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/12DetourModKit::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/8DetourModKit::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/8DetourModKit::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/8DetourModKit::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/8DetourModKit::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/8DetourModKit::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 |