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 | 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 | 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 | 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 | 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 |