GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 89.8% 44 / 0 / 49
Functions: 100.0% 7 / 0 / 7
Branches: 80.0% 24 / 0 / 30

src/event_dispatcher.cpp
Line Branch Exec Source
1 /**
2 * @file event_dispatcher.cpp
3 * @brief The dispatcher's per-thread emit chain and rundown drain.
4 *
5 * EventDispatcher is otherwise a header-only template. What lives here is the part that cannot: the emit chain is
6 * backed by a Win32 TLS index, and the installed detail/ header must stay Win32-free.
7 */
8
9 #include "DetourModKit/detail/event_dispatcher.hpp"
10
11 #include "internal/drain_backoff.hpp"
12 #include "platform.hpp"
13
14 #include <atomic>
15 #include <thread>
16
17 namespace DetourModKit::detail
18 {
19 namespace
20 {
21 /**
22 * @brief The TLS index holding this thread's EmitFrame chain head.
23 * @details Reserved by subscribe() before any emit of a subscribed dispatcher can run, so a dispatcher with
24 * subscribers always has a valid index. TLS_OUT_OF_INDEXES means the process had none to give, which
25 * leaves every emit untracked and every rundown Unwaitable rather than wrong.
26 */
27 std::atomic<DWORD> s_emit_tls_index{TLS_OUT_OF_INDEXES};
28
29 std::atomic<std::uint32_t> s_untracked_emit_frames{0};
30 } // namespace
31
32 10338 bool ensure_emit_frame_tls() noexcept
33 {
34
2/2
✓ Branch 9 → 10 taken 10241 times.
✓ Branch 9 → 11 taken 97 times.
10338 if (s_emit_tls_index.load(std::memory_order_acquire) != TLS_OUT_OF_INDEXES)
35 {
36 10241 return true;
37 }
38
39 // Reserve first and publish with a CAS rather than serializing on a lock. A mutex here would be a static with a
40 // destructor on a path a late subscriber can still reach after this TU's statics are torn down; racing losers
41 // simply return their surplus index instead.
42 97 const DWORD index = ::TlsAlloc();
43
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 97 times.
97 if (index == TLS_OUT_OF_INDEXES)
44 {
45 return false;
46 }
47 97 DWORD unreserved = TLS_OUT_OF_INDEXES;
48 97 if (!s_emit_tls_index
49
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 97 times.
97 .compare_exchange_strong(unreserved, index, std::memory_order_release, std::memory_order_relaxed))
50 {
51 ::TlsFree(index);
52 }
53 97 return true;
54 }
55
56 1741645 bool push_emit_frame(EmitFrame &frame) noexcept
57 {
58 1736589 const DWORD index = s_emit_tls_index.load(std::memory_order_acquire);
59
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1736589 times.
1736589 if (index == TLS_OUT_OF_INDEXES)
60 {
61 return false;
62 }
63 1736589 frame.prev = static_cast<EmitFrame *>(::TlsGetValue(index));
64 // A store can still fail: an index past the TEB's inline slots is backed by a lazily heap-allocated
65 // expansion array. Report it rather than leave the chain claiming this thread is elsewhere.
66 1734109 return ::TlsSetValue(index, &frame) != FALSE;
67 }
68
69 1606524 void pop_emit_frame(const EmitFrame &frame) noexcept
70 {
71 1594949 const DWORD index = s_emit_tls_index.load(std::memory_order_acquire);
72
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1594949 times.
1594949 if (index == TLS_OUT_OF_INDEXES)
73 {
74 return;
75 }
76 // The matching push succeeded, so the expansion array for this index already exists on this thread and this
77 // store cannot fail for want of one.
78 1594949 (void)::TlsSetValue(index, frame.prev);
79 }
80
81 7 bool thread_is_emitting_dispatcher(const void *dispatcher) noexcept
82 {
83 7 const DWORD index = s_emit_tls_index.load(std::memory_order_acquire);
84
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 6 times.
7 if (index == TLS_OUT_OF_INDEXES)
85 {
86 1 return false;
87 }
88
2/2
✓ Branch 16 → 13 taken 2 times.
✓ Branch 16 → 17 taken 5 times.
7 for (const auto *node = static_cast<const EmitFrame *>(::TlsGetValue(index)); node != nullptr;
89 1 node = node->prev)
90 {
91
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 1 time.
2 if (node->dispatcher == dispatcher)
92 {
93 1 return true;
94 }
95 }
96 5 return false;
97 }
98
99 10342 bool thread_is_emitting_type(const void *type_tag) noexcept
100 {
101 10342 const DWORD index = s_emit_tls_index.load(std::memory_order_acquire);
102
2/2
✓ Branch 9 → 10 taken 98 times.
✓ Branch 9 → 11 taken 10244 times.
10342 if (index == TLS_OUT_OF_INDEXES)
103 {
104 98 return false;
105 }
106
2/2
✓ Branch 16 → 13 taken 3 times.
✓ Branch 16 → 17 taken 10241 times.
10244 for (const auto *node = static_cast<const EmitFrame *>(::TlsGetValue(index)); node != nullptr;
107 node = node->prev)
108 {
109
1/2
✓ Branch 13 → 14 taken 3 times.
✗ Branch 13 → 15 not taken.
3 if (node->type_tag == type_tag)
110 {
111 3 return true;
112 }
113 }
114 10241 return false;
115 }
116
117 10341 std::atomic<std::uint32_t> &untracked_emit_frames() noexcept
118 {
119 10341 return s_untracked_emit_frames;
120 }
121
122 7 Rundown drain_gate(EntryGate &gate, const void *dispatcher) noexcept
123 {
124
5/6
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 12 taken 1 time.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 6 times.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 6 times.
13 if (thread_is_emitting_dispatcher(dispatcher) || s_untracked_emit_frames.load(std::memory_order_seq_cst) != 0)
125 {
126 1 return Rundown::Unwaitable;
127 }
128
129 // The tombstone is already published (seq_cst) by the caller, and every invocation increments in_flight
130 // seq_cst before rechecking it. Those two orders are the Dekker seam: an entrant that misses the tombstone
131 // is guaranteed visible here, so reaching zero means no invocation remains and none can start.
132 //
133 // This wait has no timeout. The tombstone closes the entrant set, so it cannot grow. A tracked handler can
134 // still remain parked indefinitely. Self-entry deadlocks, and an untracked entry makes completion
135 // unprovable, so both cases are refused above. Backoff limits CPU use while a tracked handler finishes.
136 6 DrainBackoff backoff;
137
2/2
✓ Branch 25 → 17 taken 132 times.
✓ Branch 25 → 26 taken 6 times.
276 while (gate.in_flight.load(std::memory_order_seq_cst) != 0)
138 {
139 132 backoff.pause();
140 }
141 6 return Rundown::Drained;
142 }
143 } // namespace DetourModKit::detail
144