./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
30 / 30
100.0%
Branches:
6 / 8
75.0%
Functions:
9 / 9
100.0%

src/internal/diagnostics_population.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP
2 #define DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP
3
4 /**
5 * @file diagnostics_population.hpp
6 * @brief Declares the diagnostics state that other translation units update or retire.
7 */
8
9 #include <array>
10 #include <atomic>
11 #include <cstddef>
12 #include <cstdint>
13
14 namespace DetourModKit::diagnostics
15 {
16 enum class ModulePinReason : std::uint8_t; // Full definition: DetourModKit/diagnostics.hpp.
17 } // namespace DetourModKit::diagnostics
18
19 namespace DetourModKit::detail
20 {
21 namespace hook_population
22 {
23 /// Active occupies the low 32 bits, total the high 32.
24 inline constexpr std::uint64_t ACTIVE_UNIT = 1ULL;
25 inline constexpr std::uint64_t TOTAL_UNIT = 1ULL << 32U;
26 inline constexpr std::uint64_t ACTIVE_MASK = 0xFFFFFFFFULL;
27
28 // Loader-lock teardown requires updates that cannot allocate or enter a lock-backed atomic implementation.
29 static_assert(
30 std::atomic<std::uint64_t>::is_always_lock_free,
31 "the hook population tally must be lock-free to stay callable under the loader lock"
32 );
33
34 // constinit makes any future dynamic initialization a compile-time error.
35 inline constinit std::atomic<std::uint64_t> s_counts{0};
36
37 /**
38 * @brief Records a completed install.
39 * @param active True if creation publishes Active. VMT hooks start Active. Inline and mid hooks start Disabled.
40 */
41 30896 inline void record_created(bool active) noexcept
42 {
43
2/2
✓ Branch 2 → 3 taken 106 times.
✓ Branch 2 → 4 taken 30790 times.
30896 s_counts.fetch_add(active ? TOTAL_UNIT + ACTIVE_UNIT : TOTAL_UNIT, std::memory_order_relaxed);
44 30896 }
45
46 /// Records a completed disabled-to-active transition.
47 29871 inline void record_enabled() noexcept
48 {
49 s_counts.fetch_add(ACTIVE_UNIT, std::memory_order_relaxed);
50 29871 }
51
52 /// Records a completed active-to-disabled transition.
53 18272 inline void record_disabled() noexcept
54 {
55 s_counts.fetch_sub(ACTIVE_UNIT, std::memory_order_relaxed);
56 18272 }
57
58 /**
59 * @brief Records removal from the live population.
60 * @param was_active True if the entry state counted this hook as Active. Teardown can force Disabled before it
61 * emits. Pass the entry state so removal subtracts the Active unit.
62 */
63 30413 inline void record_removed(bool was_active) noexcept
64 {
65
2/2
✓ Branch 2 → 3 taken 11117 times.
✓ Branch 2 → 4 taken 19296 times.
30413 s_counts.fetch_sub(was_active ? TOTAL_UNIT + ACTIVE_UNIT : TOTAL_UNIT, std::memory_order_relaxed);
66 30413 }
67
68 /// Reads all three figures from one load, so they are always mutually consistent.
69 13666 inline void read(std::size_t &total, std::size_t &active, std::size_t &disabled) noexcept
70 {
71 13424 const std::uint64_t packed = s_counts.load(std::memory_order_relaxed);
72 13424 total = static_cast<std::size_t>(packed >> 32U);
73 13424 active = static_cast<std::size_t>(packed & ACTIVE_MASK);
74 13424 disabled = total - active;
75 13424 }
76 } // namespace hook_population
77
78 /**
79 * @brief Provides counters behind @ref DetourModKit::diagnostics::lifecycle_counters.
80 * @details Each relaxed atomic is an independent monotonic event tally with no cross-counter order obligation. The
81 * recorders run on reaper and teardown paths:
82 * - They allocate no memory.
83 * - They take no lock.
84 * - They make no Win32 call.
85 */
86 namespace lifecycle_observability
87 {
88 static_assert(
89 std::atomic<std::size_t>::is_always_lock_free,
90 "lifecycle observability must stay lock-free on teardown paths"
91 );
92
93 inline constinit std::atomic<std::size_t> s_reaper_started{0};
94 inline constinit std::atomic<std::size_t> s_permanent_pins{0};
95 inline constinit std::atomic<std::size_t> s_abandoned_owners{0};
96
97 /// Records the process-lifetime reaper thread launch and its permanent module reference.
98 15 inline void record_reaper_started() noexcept
99 {
100 s_reaper_started.fetch_add(1, std::memory_order_relaxed);
101 s_permanent_pins.fetch_add(1, std::memory_order_relaxed);
102 15 }
103
104 /// Records a failed retirement that the reaper retains permanently.
105 5 inline void record_abandoned_owner() noexcept
106 {
107 s_abandoned_owners.fetch_add(1, std::memory_order_relaxed);
108 5 }
109 } // namespace lifecycle_observability
110
111 /**
112 * @brief Provides the outstanding-count storage behind @ref DetourModKit::diagnostics::module_pin_count.
113 * @details One relaxed atomic per @ref DetourModKit::diagnostics::ModulePinReason holds acquires minus releases.
114 * The recorders run on install, teardown, and loader-lock paths:
115 * - They allocate no memory.
116 * - They take no lock.
117 * - They make no Win32 call.
118 */
119 namespace module_pin_observability
120 {
121 /// Mirrors ModulePinReason::Count. diagnostics.cpp static_asserts that the two values stay equal.
122 inline constexpr std::size_t MODULE_PIN_REASON_COUNT = 11;
123
124 static_assert(
125 std::atomic<std::size_t>::is_always_lock_free,
126 "module pin observability must stay lock-free on teardown paths"
127 );
128
129 inline constinit std::array<std::atomic<std::size_t>, MODULE_PIN_REASON_COUNT> s_outstanding{};
130
131 /// Records one counted module reference taken under @p reason.
132 21957 inline void note_acquired(DetourModKit::diagnostics::ModulePinReason reason) noexcept
133 {
134 21957 const auto index = static_cast<std::size_t>(reason);
135
1/2
✓ Branch 2 → 3 taken 21957 times.
✗ Branch 2 → 7 not taken.
21957 if (index < MODULE_PIN_REASON_COUNT)
136 {
137 21957 s_outstanding[index].fetch_add(1, std::memory_order_relaxed);
138 }
139 21957 }
140
141 /**
142 * @brief Records the release of one counted module reference taken under @p reason.
143 * @details A release must pass the reason its acquire passed.
144 * An unmatched release wraps the unsigned count.
145 * The wrap exposes the imbalance.
146 */
147 21731 inline void note_released(DetourModKit::diagnostics::ModulePinReason reason) noexcept
148 {
149 21731 const auto index = static_cast<std::size_t>(reason);
150
1/2
✓ Branch 2 → 3 taken 21731 times.
✗ Branch 2 → 7 not taken.
21731 if (index < MODULE_PIN_REASON_COUNT)
151 {
152 21731 s_outstanding[index].fetch_sub(1, std::memory_order_relaxed);
153 }
154 21731 }
155 } // namespace module_pin_observability
156
157 /// Names the teardown that returns the emit-chain TLS ownership of the diagnostics dispatchers.
158 enum class DiagnosticsTeardown : std::uint8_t
159 {
160 /// memory::shutdown_cache() without an active Session. A live subscription stays an owner without a record.
161 CacheShutdown,
162 /// Session teardown. A live subscription is a kept index.
163 Session
164 };
165
166 /**
167 * @brief Returns the emit-chain TLS ownership of each idle diagnostics dispatcher.
168 * @param teardown The teardown that calls. It decides whether a live subscription is a kept index.
169 * @param may_block The caller's teardown gate. False never waits, prunes, or logs.
170 * @details @ref DetourModKit::diagnostics::hook_lifecycle owns the contract. A concurrent call returns at once and
171 * leaves the outcome to the call in progress.
172 */
173 void release_diagnostics_emit_owners(DiagnosticsTeardown teardown, bool may_block) noexcept;
174 } // namespace DetourModKit::detail
175
176 #endif // DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP
177