GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 75.9% 500 / 0 / 659
Functions: 98.7% 78 / 0 / 79
Branches: 35.3% 301 / 0 / 853

include/DetourModKit/hook_manager.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_HOOK_MANAGER_HPP
2 #define DETOURMODKIT_HOOK_MANAGER_HPP
3
4 #include "DetourModKit/format.hpp"
5 #include "DetourModKit/logger.hpp"
6 #include "DetourModKit/scanner.hpp"
7 #include "DetourModKit/srw_shared_mutex.hpp"
8
9 #include "safetyhook.hpp"
10
11 #include <atomic>
12 #include <concepts>
13 #include <cstdint>
14 #include <expected>
15 #include <format>
16 #include <functional>
17 #include <memory>
18 #include <mutex>
19 #include <optional>
20 #include <shared_mutex>
21 #include <span>
22 #include <string>
23 #include <string_view>
24 #include <type_traits>
25 #include <unordered_map>
26 #include <utility>
27 #include <vector>
28
29 namespace DetourModKit
30 {
31 namespace detail
32 {
33 /**
34 * @brief Transparent hash functor for heterogeneous lookup in string-keyed maps.
35 * @details Allows std::string_view lookups without constructing a temporary std::string.
36 */
37 struct TransparentStringHash
38 {
39 using is_transparent = void;
40 30554 size_t operator()(std::string_view sv) const noexcept { return std::hash<std::string_view>{}(sv); }
41 };
42 } // namespace detail
43
44 /**
45 * @enum HookType
46 * @brief Enumeration of supported hook types, corresponding to SafetyHook capabilities.
47 */
48 enum class HookType
49 {
50 Inline,
51 Mid,
52 Vmt
53 };
54
55 /**
56 * @enum HookStatus
57 * @brief Represents the current operational status of a managed hook.
58 */
59 enum class HookStatus
60 {
61 Active,
62 Disabled,
63 Enabling,
64 Disabling
65 };
66
67 /**
68 * @enum InlineProloguePolicy
69 * @brief Escalation policy for the inline/mid hook prologue pre-flight.
70 * @details Controls what happens when the target's first opcode is a leading E8 (call rel32) or a breakpoint byte
71 * (0xCC int3 / 0xCD int n) at create time. A leading call means the inline hook's 5-byte E9 patch would
72 * steal a relative call whose displacement was computed from the original site, so the relocated
73 * trampoline copy can dispatch the call to the wrong absolute target; a leading int3 means the slot is
74 * already a breakpoint -- a foreign hook's stub, a patched byte, or alignment padding -- not a real
75 * function body. @ref Warn logs and installs anyway (the default, preserving historical behaviour); @ref
76 * Fail refuses the create with @ref HookError::TargetPrologueUnsafe.
77 */
78 enum class InlineProloguePolicy
79 {
80 Warn,
81 Fail
82 };
83
84 /**
85 * @enum HookError
86 * @brief Error codes for hook creation/operation failures.
87 */
88 enum class HookError
89 {
90 AllocatorNotAvailable,
91 InvalidTargetAddress,
92 InvalidDetourFunction,
93 InvalidTrampolinePointer,
94 HookAlreadyExists,
95 HookNotFound,
96 ShutdownInProgress,
97 SafetyHookError,
98 EnableFailed,
99 DisableFailed,
100 InvalidHookState,
101 InvalidObject,
102 VmtHookNotFound,
103 MethodAlreadyHooked,
104 MethodNotFound,
105 TargetAlreadyHookedInProcess,
106 ReentrantCallRejected,
107 TargetPrologueUnsafe,
108 UnknownError
109 };
110
111 /**
112 * @struct HookConfig
113 * @brief Configuration options used during the creation of a new hook.
114 */
115 struct HookConfig
116 {
117 bool auto_enable = true;
118
119 /**
120 * @brief Refuses hooks when the target already appears hooked.
121 * @details Applies to inline and mid hooks. The pre-flight first checks this HookManager's registry for an
122 * exact same-address hook, then falls back to a foreign JMP prologue heuristic. With the default
123 * (false), a warning is logged and bulk teardown unwinds managed layers newest-first.
124 */
125 bool fail_if_already_hooked = false;
126
127 /**
128 * @brief Escalation policy when the target prologue leads with a call (E8) or a breakpoint (0xCC/0xCD) byte.
129 * @details A leading 0xE8 call rel32 or a 0xCC/0xCD breakpoint at the hook site is the risk this surfaces:
130 * hooking a prologue that is itself a relative call steals a displacement that was relative to the
131 * original address (the relocated trampoline copy can then call the wrong absolute target), and
132 * hooking an int3 byte means the entry is already a breakpoint stub (a foreign hook or a patched /
133 * padding byte), not a real function body. The pre-flight decodes the first byte under a fault guard
134 * and, on a match, either logs a warning and installs anyway (@ref InlineProloguePolicy::Warn, the
135 * default, preserving current behaviour) or refuses with @ref HookError::TargetPrologueUnsafe (@ref
136 * InlineProloguePolicy::Fail). Applies to inline and mid hooks; both patch the same prologue. A rare
137 * legitimate function whose true first instruction is a call thunk will warn (harmless) or, under
138 * Fail, be refused -- opt into Fail only for targets known to be ordinary function bodies.
139 */
140 InlineProloguePolicy prologue_policy = InlineProloguePolicy::Warn;
141 };
142
143 /**
144 * @struct VmtHookConfig
145 * @brief Configuration options used during VMT hook creation and apply, symmetric with @ref HookConfig.
146 * @details Mirrors @ref HookConfig for VMT hooks so the inline and VMT code paths expose the same operational
147 * surface. The defaults are chosen to preserve the historical single-argument API's behavior exactly
148 * (no pre-flight checks; pre-existing failures such as a null object, a duplicate name, shutdown in
149 * progress, or a SafetyHook error still apply), so existing call sites that build a default config
150 * compile and run unchanged.
151 */
152 struct VmtHookConfig
153 {
154 /**
155 * @brief When true, refuse to clone an object whose vptr already points at a VMT cloned by this HookManager.
156 * @details SafetyHook::VmtHook::create replaces the object's vptr with a pointer into a freshly-allocated
157 * cloned vtable. If a second call later clones the same object, the second create reads the first
158 * clone as if it were the original vtable: the first mod's hooked methods are now baked into the
159 * second mod's "original" and a third call layered on top of the second sees a third level of
160 * redirection. This is the silent "double hook" failure mode the inline hook's
161 * @ref HookConfig::fail_if_already_hooked guards against; VMT cloning is the same shape of risk and
162 * gets the same knob. Default false preserves the legacy permissive behavior.
163 */
164 bool fail_if_already_hooked = false;
165
166 /**
167 * @brief When true, pre-flight-decode the first byte of the original vtable slot and refuse to clone when
168 * the byte is an int3/int padding breakpoint or a same-module jump stub.
169 * @details A VMT slot whose first byte is 0xCC is an alignment pad or permanent breakpoint, not a real
170 * function. Replacing it and dispatching through it yields __debugbreak under the consumer's
171 * debugger and an instant crash in shipping builds. A VMT slot whose first instruction is `jmp
172 * rel8/rel32` to a target inside the same module is a jump stub (e.g. an incremental-link ILT
173 * entry), not a function body; MSVC adjustor thunks for multiple-inheritance vtables start with the
174 * this-adjust instruction, so they pass. The check is intentionally conservative: real functions
175 * (any other first byte, or a tail-call `mov reg,reg; jmp <out-of-module>`) pass. Known false
176 * positive: consumer binaries built with /INCREMENTAL route every function through an ILT jump
177 * stub, which this check rejects. The default is false to preserve the historical no-pre-flight
178 * behavior; opt in for the safety net on mods that exclusively target well-formed C++ vtables.
179 */
180 bool fail_on_non_function_pointer = false;
181 };
182
183 /**
184 * @class Hook
185 * @brief Abstract base class for managed hooks.
186 * @details Defines a common interface for interacting with different types of hooks managed by the HookManager.
187 * Implements the Template Method pattern for enable/disable state management.
188 */
189 class Hook
190 {
191 public:
192 120 virtual ~Hook() noexcept = default;
193
194 31 [[nodiscard]] const std::string &get_name() const noexcept { return m_name; }
195 869 [[nodiscard]] HookType get_type() const noexcept { return m_type; }
196 39 [[nodiscard]] uintptr_t get_target_address() const noexcept { return m_target_address; }
197 30568 [[nodiscard]] HookStatus get_status() const noexcept { return m_status.load(std::memory_order_acquire); }
198
199 /**
200 * @brief Enables the hook.
201 * @return Success if the hook was enabled or already active. On failure, the HookError indicates the reason
202 * (SafetyHookError, EnableFailed, InvalidHookState).
203 * @note Uses atomic CAS for lock-free status transitions. Thread-safe without requiring external
204 * synchronization. Uses an intermediate Enabling state to prevent other threads from observing a
205 * speculative terminal state while the SafetyHook enable call is in progress.
206 */
207 417 [[nodiscard]] std::expected<void, HookError> enable()
208 {
209
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 417 times.
417 if (!is_impl_valid())
210 return std::unexpected(HookError::SafetyHookError);
211
212 417 HookStatus expected = HookStatus::Disabled;
213
2/2
✓ Branch 9 → 10 taken 355 times.
✓ Branch 9 → 18 taken 62 times.
417 if (!m_status.compare_exchange_strong(expected, HookStatus::Enabling, std::memory_order_acq_rel))
214 {
215
2/2
✓ Branch 10 → 11 taken 18 times.
✓ Branch 10 → 14 taken 337 times.
355 if (expected == HookStatus::Active)
216 18 return {};
217 337 return std::unexpected(HookError::InvalidHookState);
218 }
219
220
2/4
✓ Branch 18 → 19 taken 62 times.
✗ Branch 18 → 31 not taken.
✓ Branch 19 → 20 taken 62 times.
✗ Branch 19 → 24 not taken.
62 if (do_enable())
221 {
222 62 m_status.store(HookStatus::Active, std::memory_order_release);
223 62 return {};
224 }
225
226 m_status.store(HookStatus::Disabled, std::memory_order_release);
227 return std::unexpected(HookError::EnableFailed);
228 }
229
230 /**
231 * @brief Disables the hook.
232 * @return Success if the hook was disabled or already disabled. On failure, the HookError indicates the reason
233 * (SafetyHookError, DisableFailed, InvalidHookState).
234 * @note Uses atomic CAS for lock-free status transitions. Thread-safe without requiring external
235 * synchronization. Uses an intermediate Disabling state to prevent other threads from observing a
236 * speculative terminal state while the SafetyHook disable call is in progress.
237 */
238 543 [[nodiscard]] std::expected<void, HookError> disable()
239 {
240
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 543 times.
543 if (!is_impl_valid())
241 return std::unexpected(HookError::SafetyHookError);
242
243 543 HookStatus expected = HookStatus::Active;
244
2/2
✓ Branch 9 → 10 taken 367 times.
✓ Branch 9 → 18 taken 176 times.
543 if (!m_status.compare_exchange_strong(expected, HookStatus::Disabling, std::memory_order_acq_rel))
245 {
246
2/2
✓ Branch 10 → 11 taken 27 times.
✓ Branch 10 → 14 taken 340 times.
367 if (expected == HookStatus::Disabled)
247 27 return {};
248 340 return std::unexpected(HookError::InvalidHookState);
249 }
250
251
2/4
✓ Branch 18 → 19 taken 176 times.
✗ Branch 18 → 31 not taken.
✓ Branch 19 → 20 taken 176 times.
✗ Branch 19 → 24 not taken.
176 if (do_disable())
252 {
253 176 m_status.store(HookStatus::Disabled, std::memory_order_release);
254 176 return {};
255 }
256
257 m_status.store(HookStatus::Active, std::memory_order_release);
258 return std::unexpected(HookError::DisableFailed);
259 }
260
261 [[nodiscard]] bool is_enabled() const noexcept
262 {
263 return m_status.load(std::memory_order_acquire) == HookStatus::Active;
264 }
265
266 680 [[nodiscard]] static constexpr std::string_view status_to_string(HookStatus status) noexcept
267 {
268
5/5
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 347 times.
✓ Branch 2 → 6 taken 330 times.
✓ Branch 2 → 7 taken 1 time.
680 switch (status)
269 {
270 1 case HookStatus::Active:
271 1 return "Active";
272 1 case HookStatus::Disabled:
273 1 return "Disabled";
274 347 case HookStatus::Enabling:
275 347 return "Enabling";
276 330 case HookStatus::Disabling:
277 330 return "Disabling";
278 1 default:
279 1 return "Unknown";
280 }
281 }
282
283 33 [[nodiscard]] static constexpr std::string_view error_to_string(HookError error) noexcept
284 {
285
16/20
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 2 → 7 taken 2 times.
✓ Branch 2 → 8 taken 4 times.
✓ Branch 2 → 9 taken 4 times.
✓ Branch 2 → 10 taken 2 times.
✓ Branch 2 → 11 taken 1 time.
✓ Branch 2 → 12 taken 1 time.
✓ Branch 2 → 13 taken 1 time.
✓ Branch 2 → 14 taken 2 times.
✓ Branch 2 → 15 taken 2 times.
✓ Branch 2 → 16 taken 2 times.
✓ Branch 2 → 17 taken 1 time.
✗ Branch 2 → 18 not taken.
✗ Branch 2 → 19 not taken.
✗ Branch 2 → 20 not taken.
✓ Branch 2 → 21 taken 2 times.
✗ Branch 2 → 22 not taken.
33 switch (error)
286 {
287 3 case HookError::AllocatorNotAvailable:
288 3 return "Allocator not available";
289 2 case HookError::InvalidTargetAddress:
290 2 return "Invalid target address";
291 2 case HookError::InvalidDetourFunction:
292 2 return "Invalid detour function";
293 2 case HookError::InvalidTrampolinePointer:
294 2 return "Invalid trampoline pointer";
295 2 case HookError::HookAlreadyExists:
296 2 return "Hook already exists";
297 4 case HookError::HookNotFound:
298 4 return "Hook not found";
299 4 case HookError::ShutdownInProgress:
300 4 return "Shutdown in progress";
301 2 case HookError::SafetyHookError:
302 2 return "SafetyHook error";
303 1 case HookError::EnableFailed:
304 1 return "Hook enable failed";
305 1 case HookError::DisableFailed:
306 1 return "Hook disable failed";
307 1 case HookError::InvalidHookState:
308 1 return "Hook is in a transitional state";
309 2 case HookError::InvalidObject:
310 2 return "Invalid object pointer";
311 2 case HookError::VmtHookNotFound:
312 2 return "VMT hook not found";
313 2 case HookError::MethodAlreadyHooked:
314 2 return "VMT method already hooked";
315 1 case HookError::MethodNotFound:
316 1 return "VMT method hook not found";
317 case HookError::TargetAlreadyHookedInProcess:
318 return "Target address is already hooked in this process";
319 case HookError::ReentrantCallRejected:
320 return "Mutator called reentrantly from within a with_* callback";
321 case HookError::TargetPrologueUnsafe:
322 return "Target prologue leads with a call (E8) or breakpoint (0xCC/0xCD) byte";
323 2 case HookError::UnknownError:
324 2 return "Unknown error";
325 default:
326 return "Invalid error code";
327 }
328 }
329
330 protected:
331 std::string m_name;
332 HookType m_type;
333 uintptr_t m_target_address;
334 std::atomic<HookStatus> m_status;
335
336 120 Hook(std::string name, HookType type, uintptr_t target_address, HookStatus initial_status)
337 240 : m_name(std::move(name)), m_type(type), m_target_address(target_address), m_status(initial_status)
338 {
339 120 }
340
341 virtual bool is_impl_valid() const noexcept = 0;
342 virtual bool do_enable() = 0;
343 virtual bool do_disable() = 0;
344
345 Hook(const Hook &) = delete;
346 Hook &operator=(const Hook &) = delete;
347 Hook(Hook &&) = delete;
348 Hook &operator=(Hook &&) = delete;
349 };
350
351 namespace detail
352 {
353 /// Satisfied only by a pointer-to-function type; the valid cast target for InlineHook::get_original.
354 template <typename T>
355 concept FunctionPointer = std::is_pointer_v<T> && std::is_function_v<std::remove_pointer_t<T>>;
356 } // namespace detail
357
358 /**
359 * @class InlineHook
360 * @brief Represents a managed inline hook, wrapping a SafetyHook::InlineHook object.
361 */
362 class InlineHook : public Hook
363 {
364 public:
365 95 InlineHook(std::string name, uintptr_t target_address, safetyhook::InlineHook hook_obj,
366 HookStatus initial_status)
367 190 : Hook(std::move(name), HookType::Inline, target_address, initial_status),
368 285 m_safetyhook_impl(std::move(hook_obj))
369 {
370 95 }
371
372 /**
373 * @brief Retrieves the trampoline to call the original function.
374 * @tparam T The function pointer type of the original function.
375 * @return A function pointer of type T to the original function's trampoline.
376 */
377 2 template <detail::FunctionPointer T> [[nodiscard]] T get_original() const noexcept
378 {
379
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
2 return m_safetyhook_impl ? m_safetyhook_impl.original<T>() : nullptr;
380 }
381
382 protected:
383 927 bool is_impl_valid() const noexcept override { return static_cast<bool>(m_safetyhook_impl); }
384 59 bool do_enable() override
385 {
386
1/2
✓ Branch 2 → 3 taken 59 times.
✗ Branch 2 → 6 not taken.
59 auto result = m_safetyhook_impl.enable();
387 118 return result.has_value();
388 }
389 150 bool do_disable() override
390 {
391
1/2
✓ Branch 2 → 3 taken 150 times.
✗ Branch 2 → 6 not taken.
150 auto result = m_safetyhook_impl.disable();
392 300 return result.has_value();
393 }
394
395 private:
396 safetyhook::InlineHook m_safetyhook_impl;
397 };
398
399 /**
400 * @class MidHook
401 * @brief Represents a managed mid-function hook, wrapping a SafetyHook::MidHook object.
402 */
403 class MidHook : public Hook
404 {
405 public:
406 25 MidHook(std::string name, uintptr_t target_address, safetyhook::MidHook hook_obj, HookStatus initial_status)
407 50 : Hook(std::move(name), HookType::Mid, target_address, initial_status),
408 75 m_safetyhook_impl(std::move(hook_obj))
409 {
410 25 }
411
412 /**
413 * @brief Gets the destination function of this mid-hook.
414 * @return safetyhook::MidHookFn The function pointer to the detour.
415 */
416 1 [[nodiscard]] safetyhook::MidHookFn get_destination() const noexcept
417 {
418
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 return m_safetyhook_impl ? m_safetyhook_impl.destination() : nullptr;
419 }
420
421 protected:
422 32 bool is_impl_valid() const noexcept override { return static_cast<bool>(m_safetyhook_impl); }
423 3 bool do_enable() override
424 {
425
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 6 not taken.
3 auto result = m_safetyhook_impl.enable();
426 6 return result.has_value();
427 }
428 26 bool do_disable() override
429 {
430
1/2
✓ Branch 2 → 3 taken 26 times.
✗ Branch 2 → 6 not taken.
26 auto result = m_safetyhook_impl.disable();
431 52 return result.has_value();
432 }
433
434 private:
435 safetyhook::MidHook m_safetyhook_impl;
436 };
437
438 namespace detail
439 {
440 /**
441 * @class VmtHookEntry
442 * @brief Manages a VMT hook for a single object class, wrapping SafetyHook's VmtHook.
443 * @details Owns the cloned vtable and tracks individual method hooks by vtable index. VMT hooks operate at the
444 * object level by replacing the vptr with a cloned vtable. Individual methods are hooked by index
445 * within the cloned table. Does not support enable/disable toggling (SafetyHook VmHook limitation).
446 * Internal: held only inside @ref VmtHookMap and not part of the public API.
447 */
448 class VmtHookEntry
449 {
450 public:
451 /**
452 * @brief Constructs an entry that takes ownership of a SafetyHook VMT hook.
453 * @param name The registered hook name.
454 * @param vmt_hook The VMT hook to own.
455 * @param new_vptr_base The vptr value SafetyHook installed on the seeded object, i.e. `&m_new_vmt[1]`.
456 * Stored so subsequent apply_vmt_hook calls can detect "object is already on this
457 * clone" without touching the private SafetyHook layout. Zero is reserved for "not
458 * recorded" and never matches a real vptr.
459 */
460 33 VmtHookEntry(std::string name, safetyhook::VmtHook vmt_hook, std::uintptr_t new_vptr_base)
461 99 : m_name(std::move(name)), m_vmt_hook(std::move(vmt_hook)), m_cloned_vptr_base(new_vptr_base)
462 {
463 33 }
464
465 /// Returns the registered hook name.
466 4 [[nodiscard]] const std::string &get_name() const noexcept { return m_name; }
467
468 /// Returns the underlying SafetyHook VMT hook.
469 10 [[nodiscard]] safetyhook::VmtHook &vmt_hook() noexcept { return m_vmt_hook; }
470
471 /// Returns the vptr this entry installed on its seed object, or 0 if unknown.
472 8 [[nodiscard]] std::uintptr_t cloned_vptr_base() const noexcept { return m_cloned_vptr_base; }
473
474 /// Returns true if a method at the given vtable index is hooked.
475 8 [[nodiscard]] bool has_method_hook(size_t index) const noexcept
476 {
477 8 return m_method_hooks.find(index) != m_method_hooks.end();
478 }
479
480 /**
481 * @brief Returns the method hook installed at a vtable index.
482 * @param index The vtable index to look up.
483 * @return Pointer to the method hook, or nullptr if none is installed.
484 */
485 7 [[nodiscard]] safetyhook::VmHook *get_method_hook(size_t index)
486 {
487
1/2
✓ Branch 2 → 3 taken 7 times.
✗ Branch 2 → 12 not taken.
7 auto it = m_method_hooks.find(index);
488
2/2
✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 8 taken 1 time.
7 return it != m_method_hooks.end() ? &it->second : nullptr;
489 }
490
491 /**
492 * @brief Installs a method hook at a vtable index.
493 * @param index The vtable index being hooked.
494 * @param hook The method hook to own.
495 */
496 7 void add_method_hook(size_t index, safetyhook::VmHook hook)
497 {
498
1/2
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 6 not taken.
14 m_method_hooks.emplace(index, std::move(hook));
499 7 }
500
501 /**
502 * @brief Removes the method hook at a vtable index.
503 * @param index The vtable index to clear.
504 * @return true if a hook was removed, false if none was installed.
505 */
506 2 [[nodiscard]] bool remove_method_hook(size_t index) { return m_method_hooks.erase(index) > 0; }
507
508 VmtHookEntry(const VmtHookEntry &) = delete;
509 VmtHookEntry &operator=(const VmtHookEntry &) = delete;
510 VmtHookEntry(VmtHookEntry &&) = default;
511 VmtHookEntry &operator=(VmtHookEntry &&) = default;
512
513 private:
514 std::string m_name;
515 safetyhook::VmtHook m_vmt_hook;
516 std::uintptr_t m_cloned_vptr_base{0};
517 std::unordered_map<size_t, safetyhook::VmHook> m_method_hooks;
518 };
519
520 /**
521 * @brief Container type for the inline / mid hook registry, keyed by hook name.
522 * @details Centralized once so every site that references this exact instantiation sees identical template
523 * arguments.
524 */
525 using HookMap = std::unordered_map<std::string, std::unique_ptr<Hook>, TransparentStringHash, std::equal_to<>>;
526
527 /**
528 * @brief Container type for the VMT hook registry, keyed by hook name.
529 * @details Centralized once so every site that references this exact instantiation sees identical template
530 * arguments.
531 */
532 using VmtHookMap = std::unordered_map<std::string, VmtHookEntry, TransparentStringHash, std::equal_to<>>;
533 } // namespace detail
534
535 /**
536 * @class HookManager
537 * @brief Manages the lifecycle of all hooks (Inline, Mid, and VMT) using SafetyHook.
538 * @details Provides a centralized API for creating, removing, enabling, and disabling hooks. Thread-safe for all
539 * public methods. Uses std::expected for explicit error handling.
540 * @note Lock ordering: 1. m_mutator_gate (shared or exclusive) then 2. m_hooks_mutex (shared or exclusive).
541 * Mutators (create_*_hook, enable, disable, remove) acquire shared m_mutator_gate first, then shared or
542 * exclusive m_hooks_mutex. Shutdown and remove_all_hooks acquire exclusive m_mutator_gate first to block
543 * new mutators, then proceed with two-phase cleanup.
544 */
545 class HookManager
546 {
547 public:
548 /**
549 * @brief Provides access to the singleton instance of the HookManager.
550 * @return HookManager& Reference to the global HookManager instance.
551 */
552 static HookManager &get_instance();
553
554 ~HookManager() noexcept;
555
556 /**
557 * @brief Explicitly shuts down the HookManager, removing all hooks without logging.
558 * @details This method is safe to call during shutdown when Logger may be destroyed. It removes all hooks
559 * without attempting to log, preventing use-after-free. The shutdown flag is reset after hooks are
560 * cleared, allowing subsequent hook creation for hot-reload scenarios. The destructor becomes a no-op
561 * only while the flag is set during the shutdown operation itself.
562 * @note Two-phase teardown / quiesce contract: acquires the mutator gate exclusively to block new mutators,
563 * disables all hooks first (shared registry lock), then clears the maps under the exclusive lock, both
564 * phases newest-first so layered hooks unwind onto still-valid bytes. SafetyHook relocates a thread
565 * caught in the patched prologue but cannot drain a thread already inside the detour or trampoline body,
566 * so the caller must quiesce the hooked functions before shutdown to close that residual window. Do not
567 * call this (or any mutator) from within a with_* / try_with_* callback; defer the teardown until the
568 * callback returns (the reentrancy guard fails such calls closed).
569 */
570 void shutdown() noexcept;
571
572 // Non-copyable, non-movable (mutex member)
573 HookManager(const HookManager &) = delete;
574 HookManager &operator=(const HookManager &) = delete;
575 HookManager(HookManager &&) = delete;
576 HookManager &operator=(HookManager &&) = delete;
577
578 /**
579 * @brief Creates an inline hook at a specific target memory address.
580 * @param name A unique, descriptive name for the hook.
581 * @param target_address The memory address of the function to hook.
582 * @param detour_function Pointer to the detour function.
583 * @param original_trampoline Output pointer to receive trampoline address.
584 * @param config Optional configuration settings for the hook.
585 * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise.
586 */
587 [[nodiscard]] std::expected<std::string, HookError>
588 create_inline_hook(std::string_view name, uintptr_t target_address, void *detour_function,
589 void **original_trampoline, const HookConfig &config = HookConfig());
590
591 /**
592 * @brief Creates an inline hook by finding target address via AOB scan.
593 * @param name A unique, descriptive name for the hook.
594 * @param module_base Base address of the memory module to scan.
595 * @param module_size Size of the memory module to scan.
596 * @param aob_pattern_str The AOB pattern string.
597 * @param aob_offset Offset to add to the found pattern's address.
598 * @param detour_function Pointer to the detour function.
599 * @param original_trampoline Output pointer to store trampoline address.
600 * @param config Optional configuration settings for the hook.
601 * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise.
602 * @note The AOB scan over [module_base, module_base + module_size) is page-filtered: it walks VirtualQuery
603 * and skips guard, no-access, and non-readable pages, so passing a full SizeOfImage span is safe even
604 * when the image contains a guard or no-access section -- unlike the raw Scanner::find_pattern
605 * overloads, which read the span unconditionally and fault the host on an unreadable byte. A signature
606 * straddling a protection split inside the image is still found, and @p aob_offset is applied to the
607 * located address. For code that lives outside any mapped module (packed payloads unpacked into
608 * anonymous pages), resolve the address with the whole-process Scanner sweeps and call
609 * create_inline_hook with the result instead.
610 */
611 [[nodiscard]] std::expected<std::string, HookError>
612 create_inline_hook_aob(std::string_view name, uintptr_t module_base, size_t module_size,
613 std::string_view aob_pattern_str, ptrdiff_t aob_offset, void *detour_function,
614 void **original_trampoline, const HookConfig &config = HookConfig());
615
616 /**
617 * @brief Creates a mid-function hook at a specific target memory address.
618 * @param name A unique, descriptive name for the hook.
619 * @param target_address The memory address within a function to hook.
620 * @param detour_function The function to be called when the mid-hook is executed.
621 * @param config Optional configuration settings for the hook.
622 * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise.
623 */
624 [[nodiscard]] std::expected<std::string, HookError> create_mid_hook(std::string_view name,
625 uintptr_t target_address,
626 safetyhook::MidHookFn detour_function,
627 const HookConfig &config = HookConfig());
628
629 /**
630 * @brief Creates a mid-function hook by finding target address via AOB scan.
631 * @param name A unique, descriptive name for the hook.
632 * @param module_base Base address of the memory module to scan.
633 * @param module_size Size of the memory module to scan.
634 * @param aob_pattern_str The AOB pattern string.
635 * @param aob_offset Offset to add to the found pattern's address.
636 * @param detour_function The mid-hook detour function.
637 * @param config Optional configuration settings for the hook.
638 * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise.
639 * @note The AOB scan over [module_base, module_base + module_size) is page-filtered: it walks VirtualQuery
640 * and skips guard, no-access, and non-readable pages, so passing a full SizeOfImage span is safe even
641 * when the image contains a guard or no-access section -- unlike the raw Scanner::find_pattern
642 * overloads, which read the span unconditionally and fault the host on an unreadable byte. A signature
643 * straddling a protection split inside the image is still found, and @p aob_offset is applied to the
644 * located address. For code that lives outside any mapped module (packed payloads unpacked into
645 * anonymous pages), resolve the address with the whole-process Scanner sweeps and call
646 * create_mid_hook with the result instead.
647 */
648 [[nodiscard]] std::expected<std::string, HookError>
649 create_mid_hook_aob(std::string_view name, uintptr_t module_base, size_t module_size,
650 std::string_view aob_pattern_str, ptrdiff_t aob_offset,
651 safetyhook::MidHookFn detour_function, const HookConfig &config = HookConfig());
652
653 /**
654 * @brief Creates a VMT hook for the given object, cloning its vtable.
655 * @param name A unique, descriptive name for the VMT hook.
656 * @param object Pointer to the polymorphic object whose vptr will be replaced.
657 * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise.
658 * @note Setup/control-plane only: clones a vtable, allocates, and takes the HookManager exclusive lock. Call
659 * from init/shutdown or a worker thread, never from a hook or input callback.
660 * @warning VMT hooks have no enable/disable: creation swaps the object's vptr to the cloned table and removal
661 * restores it. Removal is a bare vptr write with no thread protection at all -- weaker than inline/mid
662 * teardown, which at least relocates a thread that faults on the patched page via SafetyHook's
663 * vectored exception handler. A thread already dispatching through the cloned slot can call into the
664 * freed clone, so the caller must guarantee no thread is calling a hooked method on @p object across
665 * create/remove, and that @p object outlives the hook. The vptr must also stay stable for the hook's
666 * lifetime; if the game reconstructs the object in place (rewriting its vptr) the hook is silently
667 * lost.
668 */
669 [[nodiscard]] std::expected<std::string, HookError> create_vmt_hook(std::string_view name, void *object);
670
671 /**
672 * @brief Configurable VMT hook creation, symmetric with the inline hook's @ref create_inline_hook overload.
673 * @details Single source of truth for VMT hook policy. The single-argument overload above is a thin delegating
674 * wrapper around this one with a default-constructed @ref VmtHookConfig, so call sites that only need
675 * a name and an object compile and behave exactly as before.
676 * @param name A unique, descriptive name for the VMT hook.
677 * @param object Pointer to the polymorphic object whose vptr will be replaced.
678 * @param cfg VMT policy (fail-if-already-hooked, pre-flight slot decoding).
679 * @return std::expected<std::string, HookError> The hook name if successful. @ref HookError::HookAlreadyExists
680 * is returned when @p cfg.fail_if_already_hooked is set and the object's vptr already points at a
681 * vtable cloned by this HookManager. @ref HookError::InvalidObject is returned when
682 * @p cfg.fail_on_non_function_pointer is set and the pre-flight decoder rejects the first byte of
683 * the vtable, and also when either flag is set and the object's vptr or vtable is unreadable.
684 */
685 [[nodiscard]] std::expected<std::string, HookError> create_vmt_hook(std::string_view name, void *object,
686 const VmtHookConfig &cfg);
687
688 /**
689 * @brief Hooks a specific virtual method by index in a named VMT hook.
690 * @tparam T The type of the destination function (function pointer or member function pointer).
691 * @param vmt_name The name of the VMT hook (from create_vmt_hook).
692 * @param method_index The zero-based vtable index of the method to hook.
693 * @param destination The replacement function.
694 * @return std::expected<size_t, HookError> The method index if successful, error code otherwise.
695 */
696 template <typename T>
697 11 [[nodiscard]] std::expected<size_t, HookError> hook_vmt_method(std::string_view vmt_name, size_t method_index,
698 T destination)
699 {
700
2/4
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
11 if (m_shutdown_called.load(std::memory_order_acquire))
701 {
702 m_logger.error("HookManager: Shutdown in progress. Cannot hook VMT method on '{}'.", vmt_name);
703 return std::unexpected(HookError::ShutdownInProgress);
704 }
705
2/4
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 14 not taken.
11 if (get_reentrancy_guard() > 0)
706 {
707
1/4
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 42 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 42 not taken.
1 m_logger.error("HookManager: Reentrant hook_vmt_method('{}'/{}) from within a with_*/try_with_* "
708 "callback rejected; defer hook mutation until the callback returns.",
709 vmt_name, method_index);
710 1 return std::unexpected(HookError::ReentrantCallRejected);
711 }
712
713 10 auto [result,
714
6/136
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 14 → 15 taken 10 times.
✗ Branch 14 → 43 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 43 not taken.
DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int))::{lambda()#1}::operator()() const:
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 110 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 130 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 2 times.
✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 150 not taken.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 1 time.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 170 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 91 → 92 not taken.
✓ Branch 91 → 93 taken 7 times.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 115 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 135 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 155 not taken.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 175 not taken.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
✗ Branch 190 → 191 not taken.
✗ Branch 190 → 192 not taken.
✗ Branch 194 → 195 not taken.
✗ Branch 194 → 198 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 198 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 269 not taken.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 252 not taken.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 252 not taken.
✗ Branch 266 → 267 not taken.
✗ Branch 266 → 268 not taken.
✗ Branch 270 → 271 not taken.
✗ Branch 270 → 274 not taken.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 274 not taken.
DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int))::{lambda()#1}::operator()() const:
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 110 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 130 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 150 not taken.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 170 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 115 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 135 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 155 not taken.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 175 not taken.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
✗ Branch 190 → 191 not taken.
✗ Branch 190 → 192 not taken.
✗ Branch 194 → 195 not taken.
✗ Branch 194 → 198 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 198 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 269 not taken.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 252 not taken.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 252 not taken.
✗ Branch 266 → 267 not taken.
✗ Branch 266 → 268 not taken.
✗ Branch 270 → 271 not taken.
✗ Branch 270 → 274 not taken.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 274 not taken.
33 deferred_logs] = [&]() -> std::pair<std::expected<size_t, HookError>, std::vector<DeferredLogEntry>>
715 {
716 10 std::shared_lock<detail::SrwSharedMutex> mutator_gate(m_mutator_gate);
717
1/2
✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 283 not taken.
10 std::unique_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
718
719
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 20 taken 10 times.
10 if (m_shutdown_called.load(std::memory_order_acquire))
720 {
721 return {
722 std::unexpected(HookError::ShutdownInProgress),
723 {{std::format("HookManager: Shutdown in progress. Cannot hook VMT method on '{}'.", vmt_name),
724 LogLevel::Error}}};
725 }
726
727
1/2
✓ Branch 20 → 21 taken 10 times.
✗ Branch 20 → 281 not taken.
10 auto vmt_it = m_vmt_hooks.find(vmt_name);
728
2/2
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 38 taken 8 times.
10 if (vmt_it == m_vmt_hooks.end())
729 {
730 2 return {std::unexpected(HookError::VmtHookNotFound),
731 {{std::format("HookManager: VMT hook '{}' not found for method hook at index {}.", vmt_name,
732 method_index),
733
3/6
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 120 not taken.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 2 times.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
10 LogLevel::Error}}};
734 }
735
736
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 55 taken 7 times.
8 if (vmt_it->second.has_method_hook(method_index))
737 {
738 1 return {std::unexpected(HookError::MethodAlreadyHooked),
739 {{std::format("HookManager: VMT '{}' method index {} is already hooked.", vmt_name,
740 method_index),
741
3/6
✓ Branch 45 → 46 taken 1 time.
✗ Branch 45 → 140 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 1 time.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
5 LogLevel::Error}}};
742 }
743
744 try
745 {
746
1/2
✓ Branch 57 → 58 taken 7 times.
✗ Branch 57 → 203 not taken.
7 auto hook_result = vmt_it->second.vmt_hook().hook_method(method_index, destination);
747
748
1/2
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 74 taken 7 times.
7 if (!hook_result)
749 {
750 return {std::unexpected(HookError::SafetyHookError),
751 {{std::format("HookManager: Failed to hook VMT '{}' method index {}.", vmt_name,
752 method_index),
753 LogLevel::Error}}};
754 }
755
756
2/4
✓ Branch 75 → 76 taken 7 times.
✗ Branch 75 → 182 not taken.
✓ Branch 79 → 80 taken 7 times.
✗ Branch 79 → 180 not taken.
14 vmt_it->second.add_method_hook(method_index, std::move(hook_result.value()));
757
758 return {method_index,
759 {{std::format("HookManager: Successfully hooked VMT '{}' method index {}.", vmt_name,
760 method_index),
761
4/8
✓ Branch 81 → 82 taken 7 times.
✗ Branch 81 → 193 not taken.
✓ Branch 84 → 85 taken 7 times.
✗ Branch 84 → 183 not taken.
✓ Branch 89 → 90 taken 7 times.
✓ Branch 89 → 91 taken 7 times.
✗ Branch 187 → 188 not taken.
✗ Branch 187 → 189 not taken.
35 LogLevel::Info}}};
762 7 }
763 catch (const std::exception &e)
764 {
765 return {std::unexpected(HookError::UnknownError),
766 {{std::format("HookManager: Exception hooking VMT '{}' method index {}: {}", vmt_name,
767 method_index, e.what()),
768 LogLevel::Error}}};
769 }
770 catch (...)
771 {
772 return {std::unexpected(HookError::UnknownError),
773 {{std::format("HookManager: Unknown exception hooking VMT '{}' method index {}.", vmt_name,
774 method_index),
775 LogLevel::Error}}};
776 }
777 10 }();
778
779
2/4
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 32 → 19 taken 10 times.
✓ Branch 32 → 33 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 32 → 19 not taken.
✗ Branch 32 → 33 not taken.
30 for (const auto &entry : deferred_logs)
780 {
781
1/4
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 22 → 23 taken 10 times.
✗ Branch 22 → 44 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 44 not taken.
10 m_logger.log(entry.level, entry.msg);
782 }
783 10 return result;
784
1/8
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 10 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
20 }
785
786 /**
787 * @brief Removes an entire VMT hook, restoring the original vtable on all applied objects.
788 * @details Bulk teardown (remove_all_vmt_hooks, remove_all_hooks, shutdown, destructor) destroys VMT hooks
789 * in reverse creation order so clones layered on the same object unwind safely. Explicit removal
790 * does not reorder for the caller: removing an inner layer while a clone created later on top of it
791 * is still installed frees the clone that the newer hook recorded as its "original", so remove
792 * layered hooks newest-first.
793 * @warning Restoring a VMT hook writes the saved original vptr back into the object. If the game reconstructed
794 * the object in place or layered its own vptr on top after the clone was installed, this write
795 * restores a vtable the game has since overwritten, silently clobbering the game's pointer (or
796 * restoring a stale one). VMT removal is a bare vptr write with no thread protection, so the caller
797 * must guarantee no thread is dispatching through the cloned slot across removal and that the object's
798 * vptr was not re-layered since create.
799 * @note Two-phase teardown / quiesce contract: the registry entry is mutated under the exclusive lock; do not
800 * call this from within a with_* / try_with_* callback (defer until the callback returns -- the
801 * reentrancy guard fails such calls closed).
802 * @param vmt_name The name of the VMT hook to remove.
803 * @return Success if removed, or HookError::VmtHookNotFound.
804 */
805 [[nodiscard]] std::expected<void, HookError> remove_vmt_hook(std::string_view vmt_name);
806
807 /**
808 * @brief Removes a single method hook from a VMT, restoring the original method.
809 * @warning Restoring a method hook rewrites the cloned vtable slot back to the original function pointer. If
810 * the game overwrote that slot or relaid the object's vptr after the hook was installed, the restore
811 * writes over a pointer the game has since changed. Removal carries no thread protection, so the
812 * caller must guarantee no thread is dispatching through the slot across removal.
813 * @note Two-phase teardown / quiesce contract: mutates the registry entry under the exclusive lock; do not call
814 * this from within a with_* / try_with_* callback (defer until the callback returns -- the reentrancy
815 * guard fails such calls closed).
816 * @param vmt_name The name of the VMT hook.
817 * @param method_index The vtable index of the method to unhook.
818 * @return Success if removed, or a HookError describing the failure.
819 */
820 [[nodiscard]] std::expected<void, HookError> remove_vmt_method(std::string_view vmt_name, size_t method_index);
821
822 /**
823 * @brief Applies the cloned (hooked) vtable to an additional object.
824 * @param vmt_name The name of the VMT hook.
825 * @param object The object to apply the hooked vtable to.
826 * @return true if the VMT hook was found and applied, false otherwise.
827 */
828 [[nodiscard]] bool apply_vmt_hook(std::string_view vmt_name, void *object);
829
830 /**
831 * @brief Configurable form of @ref apply_vmt_hook, symmetric with @ref create_vmt_hook.
832 * @details The two-argument overload above is a thin delegating wrapper that uses a default-constructed
833 * @ref VmtHookConfig, preserving the historical permissive apply semantics (apply still returns
834 * false on shutdown, a null object, an unknown name, or an apply exception).
835 * @param vmt_name The name of the VMT hook whose cloned vtable should be installed.
836 * @param object The object to apply the cloned vtable to.
837 * @param cfg Apply policy. @p cfg.fail_if_already_hooked lets a mod refuse to install its vtable on an
838 * object that is already on a clone from this HookManager (a re-apply of the same clone is a
839 * no-op for SafetyHook; the guard exists for symmetry and for callers that want a single
840 * create/apply that is a no-op on a re-invocation). @p cfg.fail_on_non_function_pointer re-runs
841 * the pre-flight decoder against the vtable currently installed on the object (the one about to
842 * be replaced).
843 * @return true if the VMT hook was found and applied, false otherwise.
844 */
845 [[nodiscard]] bool apply_vmt_hook(std::string_view vmt_name, void *object, const VmtHookConfig &cfg);
846
847 /**
848 * @brief Removes the hooked vtable from a specific object, restoring its original vptr.
849 * @warning This writes the saved original vptr back into @p object. If the game reconstructed @p object in
850 * place
851 * or layered its own vptr on top after the clone was installed, the restore overwrites the vptr the
852 * game has since set, silently clobbering it (or installing a stale vtable). The write has no thread
853 * protection, so the caller must guarantee no thread is dispatching through the cloned slot across the
854 * restore and that the object's vptr was not re-layered since the clone went on.
855 * @note Two-phase teardown / quiesce contract: mutates the registry entry under the exclusive lock; do not call
856 * this from within a with_* / try_with_* callback (defer until the callback returns -- the reentrancy
857 * guard fails such calls closed).
858 * @param vmt_name The name of the VMT hook.
859 * @param object The object to restore.
860 * @return true if the VMT hook was found and the object was restored, false otherwise.
861 */
862 [[nodiscard]] bool remove_vmt_from_object(std::string_view vmt_name, void *object);
863
864 /**
865 * @brief Removes all VMT hooks, restoring original vtables on all applied objects.
866 * @details Destroys VMT hooks newest-first so clones layered on the same object unwind safely.
867 * @warning Each restore writes a saved original vptr back into its applied objects. If the game reconstructed
868 * an
869 * object in place or layered its own vptr on top after the clone was installed, the restore overwrites
870 * the vptr the game has since set, silently clobbering it (or installing a stale vtable). VMT removal
871 * is a bare vptr write with no thread protection, so the caller must quiesce all hooked objects -- no
872 * thread dispatching through any cloned slot -- across this teardown.
873 * @note Two-phase teardown / quiesce contract: mutates the registry under the exclusive lock; do not call this
874 * (or any mutator) from within a with_* / try_with_* callback (defer until the callback returns -- the
875 * reentrancy guard fails such calls closed).
876 */
877 void remove_all_vmt_hooks();
878
879 /**
880 * @brief Returns the names of all active VMT hooks.
881 * @return std::vector<std::string> Vector containing the names of the VMT hooks.
882 */
883 [[nodiscard]] std::vector<std::string> get_vmt_hook_names() const;
884
885 /**
886 * @brief Safely accesses a VmHook (method hook) within a named VMT hook.
887 * @details The callback is invoked while the hook registry is held under a reader lock.
888 * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor
889 * from the callback (each checks the reentrancy guard and fails closed). Queue mutations and apply
890 * them after the callback returns.
891 * @tparam F Callable type accepting (safetyhook::VmHook&) and returning a value.
892 * @param vmt_name The name of the VMT hook.
893 * @param method_index The vtable index of the method hook.
894 * @param fn The callback to invoke with the VmHook reference.
895 * @return std::optional<R> The callback's return value, or std::nullopt if not found.
896 */
897 template <typename F>
898 requires std::invocable<F, safetyhook::VmHook &> &&
899 (!std::is_void_v<std::invoke_result_t<F, safetyhook::VmHook &>>) &&
900 (!std::is_reference_v<std::invoke_result_t<F, safetyhook::VmHook &>>)
901 3 [[nodiscard]] auto with_vmt_method(std::string_view vmt_name, size_t method_index, F &&fn)
902 -> std::optional<std::invoke_result_t<F, safetyhook::VmHook &>>
903 {
904
3/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
3 if (get_reentrancy_guard() > 0)
905 {
906 m_logger.error("HookManager: Reentrant callback detected in with_vmt_method('{}'/{})!", vmt_name,
907 method_index);
908 return std::nullopt;
909 }
910 3 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
911 3 ReentrancyGuard guard(get_reentrancy_guard());
912
3/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
3 auto vmt_it = m_vmt_hooks.find(vmt_name);
913
3/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 24 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 24 not taken.
3 if (vmt_it != m_vmt_hooks.end())
914 {
915
2/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 34 not taken.
2 auto *vm_hook = vmt_it->second.get_method_hook(method_index);
916
2/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 24 taken 1 time.
2 if (vm_hook)
917 {
918
1/6
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 32 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 32 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 32 not taken.
1 return std::invoke(std::forward<F>(fn), *vm_hook);
919 }
920 }
921 2 return std::nullopt;
922 3 }
923
924 /**
925 * @brief Safely accesses a VmHook for a void-returning callback.
926 * @details Same locking and reentrancy semantics as the value-returning overload.
927 * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor
928 * from the callback (each checks the reentrancy guard and fails closed). Queue mutations and apply
929 * them after the callback returns.
930 * @param vmt_name The name of the VMT hook.
931 * @param method_index The vtable index of the method hook.
932 * @param fn The void-returning callback to invoke with the VmHook reference.
933 * @return true if the method hook was found and the callback was invoked, false otherwise.
934 */
935 template <typename F>
936 requires std::invocable<F, safetyhook::VmHook &> &&
937 std::is_void_v<std::invoke_result_t<F, safetyhook::VmHook &>>
938 5 [[nodiscard]] bool with_vmt_method(std::string_view vmt_name, size_t method_index, F &&fn)
939 {
940
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
5 if (get_reentrancy_guard() > 0)
941 {
942 m_logger.error("HookManager: Reentrant callback detected in with_vmt_method('{}'/{})!", vmt_name,
943 method_index);
944 return false;
945 }
946 5 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
947 5 ReentrancyGuard guard(get_reentrancy_guard());
948
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
5 auto vmt_it = m_vmt_hooks.find(vmt_name);
949
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
5 if (vmt_it != m_vmt_hooks.end())
950 {
951
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
5 auto *vm_hook = vmt_it->second.get_method_hook(method_index);
952
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
5 if (vm_hook)
953 {
954
5/10
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
5 std::invoke(std::forward<F>(fn), *vm_hook);
955 5 return true;
956 }
957 }
958 return false;
959 5 }
960
961 /**
962 * @brief Reports whether this manager patches @p target_address.
963 * @details Inline and mid hooks both patch the target prologue, so both are reported here. The query walks only
964 * this HookManager's registry; hooks installed by other statically-linked DMK consumers in the same
965 * process are not visible.
966 *
967 * Use this to short-circuit redundant create_inline_hook or create_mid_hook calls. To detect hooks
968 * installed outside this manager, pass HookConfig::fail_if_already_hooked during creation.
969 * @param target_address Function address to query.
970 * @return true if a managed inline or mid hook already targets this address.
971 */
972 [[nodiscard]] bool is_target_already_hooked(uintptr_t target_address) const noexcept;
973
974 /**
975 * @brief Removes a hook identified by its name.
976 * @details Bulk teardown (remove_all_hooks, shutdown, destructor) disables and destroys hooks in reverse
977 * creation order so hooks layered on one target address unwind safely. Explicit single removal does
978 * not reorder for the caller: removing an older hook while a newer hook layered on the same address is
979 * still installed restores the prologue to the original bytes underneath the newer hook, leaving its
980 * entry jump pointing into a trampoline that is about to be freed. Remove layered hooks newest-first.
981 * @note Two-phase teardown / quiesce contract: disables the hook under the shared registry lock, then erases
982 * its
983 * entry under the exclusive lock. SafetyHook relocates a thread caught in the patched prologue but cannot
984 * drain a thread already inside the detour or trampoline body, so the caller must quiesce the hooked
985 * function before removal to close that residual window. Do not call this from within a with_* /
986 * try_with_* callback; defer the removal until the callback returns (the reentrancy guard fails such
987 * calls closed).
988 * @param hook_id The name of the hook to remove.
989 * @return Success if removed, or HookError::HookNotFound.
990 */
991 [[nodiscard]] std::expected<void, HookError> remove_hook(std::string_view hook_id);
992
993 /**
994 * @brief Removes all hooks currently managed by this HookManager instance.
995 * @details Uses two-phase removal: disables all hooks under a shared lock first, then clears the maps under an
996 * exclusive lock. Both phases walk the hooks in reverse creation order so hooks layered on one target
997 * address unwind newest-first and each prologue restore writes onto still-valid bytes rather than into
998 * a freed trampoline. The shared phase lets DetourModKit's own with_* readers finish before Hook
999 * storage is destroyed. SafetyHook can relocate threads caught in the patched prologue, but it cannot
1000 * drain threads already running the detour or trampoline body; callers must quiesce the hooked
1001 * function during planned teardown to close that residual window. Do not call this (or any mutator)
1002 * from within a with_* / try_with_* callback; defer the teardown until the callback returns (the
1003 * reentrancy guard fails such calls closed).
1004 *
1005 * After clearing, resets the internal shutdown flag to false, allowing subsequent create_*_hook()
1006 * calls to succeed for hot-reload workflows.
1007 */
1008 void remove_all_hooks() noexcept;
1009
1010 /**
1011 * @brief Enables a previously disabled hook.
1012 * @details Idempotent: enabling an already-active hook returns success. Returns HookError::InvalidHookState
1013 * only when the hook is in a transitional state (Enabling or Disabling). Other HookError values
1014 * indicate lookup or SafetyHook failures.
1015 * @param hook_id The name of the hook to enable.
1016 * @return Success if the hook is now active (or was already active), or a HookError describing the failure.
1017 */
1018 [[nodiscard]] std::expected<void, HookError> enable_hook(std::string_view hook_id);
1019
1020 /**
1021 * @brief Disables an active hook temporarily without removing it.
1022 * @details Idempotent: disabling an already-disabled hook returns success. Returns HookError::InvalidHookState
1023 * only when the hook is in a transitional state (Enabling or Disabling). Other HookError values
1024 * indicate lookup or SafetyHook failures.
1025 * @param hook_id The name of the hook to disable.
1026 * @return Success if the hook is now disabled (or was already disabled), or a HookError describing the failure.
1027 */
1028 [[nodiscard]] std::expected<void, HookError> disable_hook(std::string_view hook_id);
1029
1030 /**
1031 * @brief Enables several hooks by name in a single locked pass.
1032 * @details Convenience for startup and hot-reload phases that toggle many hooks at once: takes the manager's
1033 * locks once for the whole batch instead of once per hook. An unknown id is warned and skipped, not
1034 * fatal, and an already-active hook counts as a success (enable is idempotent). This is an ergonomic
1035 * wrapper, not a performance optimization over repeated enable_hook calls: the SafetyHook backend
1036 * installs via a vectored exception handler and does not suspend threads, so there is no process-wide
1037 * suspension to amortize.
1038 * @param hook_ids The names of the hooks to enable.
1039 * @return The number of hooks now active.
1040 */
1041 [[nodiscard]] std::size_t enable_hooks(std::span<const std::string_view> hook_ids);
1042
1043 /**
1044 * @brief Disables several hooks by name in a single locked pass.
1045 * @details The disable counterpart to @ref enable_hooks: locks once, warns and skips unknown ids, and counts an
1046 * already-disabled hook as a success (disable is idempotent). Ergonomics only (see @ref enable_hooks).
1047 * @param hook_ids The names of the hooks to disable.
1048 * @return The number of hooks now disabled.
1049 */
1050 [[nodiscard]] std::size_t disable_hooks(std::span<const std::string_view> hook_ids);
1051
1052 /**
1053 * @brief Enables every hook currently managed by this instance in one pass.
1054 * @return The number of hooks now active.
1055 */
1056 [[nodiscard]] std::size_t enable_all_hooks();
1057
1058 /**
1059 * @brief Disables every hook currently managed by this instance in one pass.
1060 * @return The number of hooks now disabled.
1061 */
1062 [[nodiscard]] std::size_t disable_all_hooks();
1063
1064 /**
1065 * @brief Retrieves the current status of a hook.
1066 * @param hook_id The name of the hook.
1067 * @return std::optional<HookStatus> The current status, or std::nullopt if not found.
1068 */
1069 [[nodiscard]] std::optional<HookStatus> get_hook_status(std::string_view hook_id) const;
1070
1071 /**
1072 * @brief Gets a summary of hook counts categorized by their status.
1073 * @return std::unordered_map<HookStatus, size_t> Map of statuses to counts.
1074 */
1075 [[nodiscard]] std::unordered_map<HookStatus, size_t> get_hook_counts() const;
1076
1077 /**
1078 * @brief Retrieves a list of hook names.
1079 * @param status_filter Optional status filter for returned hooks.
1080 * @return std::vector<std::string> Vector containing the names of the hooks.
1081 */
1082 [[nodiscard]] std::vector<std::string>
1083 56 get_hook_ids(std::optional<HookStatus> status_filter = std::nullopt) const;
1084
1085 // clang-format off
1086 /**
1087 * @brief Safely accesses an InlineHook by its ID while holding the internal lock.
1088 * @details The callback receives an InlineHook reference while the hook registry is held under a reader lock.
1089 * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor
1090 * from the callback. The callback holds m_hooks_mutex shared: create/remove/teardown paths acquire it
1091 * exclusively and toggle paths re-acquire it shared (UB on a non-recursive lock), while nested
1092 * accessors check the reentrancy guard and fail closed. Queue mutations and apply them after the
1093 * callback returns.
1094 * @tparam F Callable type accepting (InlineHook&) and returning a value.
1095 * @param hook_id The name of the inline hook.
1096 * @param fn The callback to invoke with the hook reference.
1097 * @return The callback's return value, or std::nullopt if the hook was not found.
1098 */
1099 // clang-format on
1100 template <typename F>
1101 requires std::invocable<F, InlineHook &> && (!std::is_void_v<std::invoke_result_t<F, InlineHook &>>) &&
1102 (!std::is_reference_v<std::invoke_result_t<F, InlineHook &>>)
1103 10 [[nodiscard]] auto with_inline_hook(std::string_view hook_id, F &&fn)
1104 -> std::optional<std::invoke_result_t<F, InlineHook &>>
1105 {
1106
10/20
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
10 if (get_reentrancy_guard() > 0)
1107 {
1108 m_logger.error("HookManager: Reentrant callback detected in with_inline_hook('{}')! "
1109 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1110 "Perform mutations outside the callback or use an asynchronous operation.",
1111 hook_id);
1112 return std::nullopt;
1113 }
1114 10 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
1115 10 ReentrancyGuard guard(get_reentrancy_guard());
1116
10/20
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 35 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
10 auto it = m_hooks.find(hook_id);
1117
28/60
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 28 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
10 if (it != m_hooks.end() && it->second->get_type() == HookType::Inline)
1118 {
1119
7/20
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
14 return std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second));
1120 }
1121 3 return std::nullopt;
1122 10 }
1123
1124 /**
1125 * @brief Safely accesses an InlineHook by its ID for a void-returning callback.
1126 * @details Same locking and reentrancy semantics as the value-returning overload.
1127 * @param hook_id The name of the inline hook.
1128 * @param fn The void-returning callback to invoke with the hook reference.
1129 * @return true if the hook was found and the callback was invoked, false otherwise.
1130 */
1131 template <typename F>
1132 requires std::invocable<F, InlineHook &> && std::is_void_v<std::invoke_result_t<F, InlineHook &>>
1133 3 [[nodiscard]] bool with_inline_hook(std::string_view hook_id, F &&fn)
1134 {
1135
3/6
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
3 if (get_reentrancy_guard() > 0)
1136 {
1137 m_logger.error("HookManager: Reentrant callback detected in with_inline_hook('{}')! "
1138 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1139 "Perform mutations outside the callback or use an asynchronous operation.",
1140 hook_id);
1141 return false;
1142 }
1143 3 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
1144 3 ReentrancyGuard guard(get_reentrancy_guard());
1145
3/6
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
3 auto it = m_hooks.find(hook_id);
1146
8/18
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 1 time.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 26 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
3 if (it != m_hooks.end() && it->second->get_type() == HookType::Inline)
1147 {
1148
2/6
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
4 std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second));
1149 2 return true;
1150 }
1151 1 return false;
1152 3 }
1153
1154 /**
1155 * @brief Try-safe access to an InlineHook by its ID using a non-blocking lock.
1156 * @details Provides a non-blocking alternative to with_inline_hook(). The callback is invoked only if the lock
1157 * is immediately acquired via std::try_to_lock. Note: try_to_lock only avoids blocking on initial
1158 * acquisition - it does NOT make callbacks safe to re-enter HookManager mutators or teardown methods
1159 * that also acquire the same non-recursive mutex. If a callback needs to call those methods, it must
1160 * release the lock first or perform those calls asynchronously to avoid deadlock. See with_inline_hook
1161 * for the blocking analogue.
1162 * @param hook_id The name of the inline hook.
1163 * @param fn The callback to invoke with the hook reference.
1164 * @return std::optional<R> The callback's return value. Returns std::nullopt if either the lock could not be
1165 * acquired or the hook was not found.
1166 */
1167 template <typename F>
1168 requires std::invocable<F, InlineHook &> && (!std::is_void_v<std::invoke_result_t<F, InlineHook &>>) &&
1169 (!std::is_reference_v<std::invoke_result_t<F, InlineHook &>>)
1170 3 [[nodiscard]] auto try_with_inline_hook(std::string_view hook_id, F &&fn)
1171 -> std::optional<std::invoke_result_t<F, InlineHook &>>
1172 {
1173
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
3 if (get_reentrancy_guard() > 0)
1174 {
1175 m_logger.error("HookManager: Reentrant callback detected in try_with_inline_hook('{}')! "
1176 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1177 "Perform mutations outside the callback or use an asynchronous operation.",
1178 hook_id);
1179 return std::nullopt;
1180 }
1181 3 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex, std::try_to_lock);
1182
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
3 if (!lock.owns_lock())
1183 {
1184 return std::nullopt;
1185 }
1186 3 ReentrancyGuard guard(get_reentrancy_guard());
1187
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
3 auto it = m_hooks.find(hook_id);
1188
8/18
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 31 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 25 taken 1 time.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 35 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 35 not taken.
3 if (it != m_hooks.end() && it->second->get_type() == HookType::Inline)
1189 {
1190
2/6
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 37 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 43 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 43 not taken.
4 return std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second));
1191 }
1192 1 return std::nullopt;
1193 3 }
1194
1195 // clang-format off
1196 /**
1197 * @brief Safely accesses a MidHook by its ID while holding the internal lock.
1198 * @details The callback receives a MidHook reference while the hook registry is held under a reader lock.
1199 * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor
1200 * from the callback. The callback holds m_hooks_mutex shared: create/remove/teardown paths acquire it
1201 * exclusively and toggle paths re-acquire it shared (UB on a non-recursive lock), while nested
1202 * accessors check the reentrancy guard and fail closed. Queue mutations and apply them after the
1203 * callback returns.
1204 * @tparam F Callable type accepting (MidHook&) and returning a value.
1205 * @param hook_id The name of the mid hook.
1206 * @param fn The callback to invoke with the hook reference.
1207 * @return The callback's return value, or std::nullopt if the hook was not found.
1208 */
1209 // clang-format on
1210 template <typename F>
1211 requires std::invocable<F, MidHook &> && (!std::is_void_v<std::invoke_result_t<F, MidHook &>>) &&
1212 (!std::is_reference_v<std::invoke_result_t<F, MidHook &>>)
1213 7 [[nodiscard]] auto with_mid_hook(std::string_view hook_id, F &&fn)
1214 -> std::optional<std::invoke_result_t<F, MidHook &>>
1215 {
1216
7/14
std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
7 if (get_reentrancy_guard() > 0)
1217 {
1218 m_logger.error("HookManager: Reentrant callback detected in with_mid_hook('{}')! "
1219 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1220 "Perform mutations outside the callback or use an asynchronous operation.",
1221 hook_id);
1222 return std::nullopt;
1223 }
1224 7 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
1225 7 ReentrancyGuard guard(get_reentrancy_guard());
1226
7/14
std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 35 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
7 auto it = m_hooks.find(hook_id);
1227
20/42
std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 28 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
7 if (it != m_hooks.end() && it->second->get_type() == HookType::Mid)
1228 {
1229
5/14
std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
10 return std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second));
1230 }
1231 2 return std::nullopt;
1232 7 }
1233
1234 /**
1235 * @brief Safely accesses a MidHook by its ID for a void-returning callback.
1236 * @details Same locking and reentrancy semantics as the value-returning overload.
1237 * @param hook_id The name of the mid hook.
1238 * @param fn The void-returning callback to invoke with the hook reference.
1239 * @return true if the hook was found and the callback was invoked, false otherwise.
1240 */
1241 template <typename F>
1242 requires std::invocable<F, MidHook &> && std::is_void_v<std::invoke_result_t<F, MidHook &>>
1243 2 [[nodiscard]] bool with_mid_hook(std::string_view hook_id, F &&fn)
1244 {
1245
2/4
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
2 if (get_reentrancy_guard() > 0)
1246 {
1247 m_logger.error("HookManager: Reentrant callback detected in with_mid_hook('{}')! "
1248 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1249 "Perform mutations outside the callback or use an asynchronous operation.",
1250 hook_id);
1251 return false;
1252 }
1253 2 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex);
1254 2 ReentrancyGuard guard(get_reentrancy_guard());
1255
2/4
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
2 auto it = m_hooks.find(hook_id);
1256
5/12
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 1 time.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 26 taken 1 time.
2 if (it != m_hooks.end() && it->second->get_type() == HookType::Mid)
1257 {
1258
1/4
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 32 not taken.
2 std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second));
1259 1 return true;
1260 }
1261 1 return false;
1262 2 }
1263
1264 /**
1265 * @brief Try-safe access to a MidHook by its ID using a non-blocking lock.
1266 * @details Provides a non-blocking alternative to with_mid_hook(). The callback is invoked only if the lock is
1267 * immediately acquired via std::try_to_lock. Note: try_to_lock only avoids blocking on initial
1268 * acquisition - it does NOT make callbacks safe to re-enter HookManager mutators or teardown methods
1269 * that also acquire the same non-recursive mutex. If a callback needs to call those methods, it must
1270 * release the lock first or perform those calls asynchronously to avoid deadlock. See with_mid_hook
1271 * for the blocking analogue.
1272 * @param hook_id The name of the mid hook.
1273 * @param fn The callback to invoke with the hook reference.
1274 * @return std::optional<R> The callback's return value. Returns std::nullopt if either the lock could not be
1275 * acquired or the hook was not found.
1276 */
1277 template <typename F>
1278 requires std::invocable<F, MidHook &> && (!std::is_void_v<std::invoke_result_t<F, MidHook &>>) &&
1279 (!std::is_reference_v<std::invoke_result_t<F, MidHook &>>)
1280 3 [[nodiscard]] auto try_with_mid_hook(std::string_view hook_id, F &&fn)
1281 -> std::optional<std::invoke_result_t<F, MidHook &>>
1282 {
1283
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
3 if (get_reentrancy_guard() > 0)
1284 {
1285 m_logger.error("HookManager: Reentrant callback detected in try_with_mid_hook('{}')! "
1286 "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. "
1287 "Perform mutations outside the callback or use an asynchronous operation.",
1288 hook_id);
1289 return std::nullopt;
1290 }
1291 3 std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex, std::try_to_lock);
1292
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
3 if (!lock.owns_lock())
1293 {
1294 return std::nullopt;
1295 }
1296 3 ReentrancyGuard guard(get_reentrancy_guard());
1297
3/6
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
3 auto it = m_hooks.find(hook_id);
1298
8/18
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 31 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 25 taken 1 time.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 35 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 35 not taken.
3 if (it != m_hooks.end() && it->second->get_type() == HookType::Mid)
1299 {
1300
2/6
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 37 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 43 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 43 not taken.
4 return std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second));
1301 }
1302 1 return std::nullopt;
1303 3 }
1304
1305 private:
1306 /** @brief Internal log entry used to defer logging outside held locks. */
1307 struct DeferredLogEntry
1308 {
1309 std::string msg;
1310 LogLevel level;
1311 };
1312 explicit HookManager(Logger &logger = Logger::get_instance());
1313
1314 mutable detail::SrwSharedMutex m_hooks_mutex;
1315 detail::HookMap m_hooks;
1316 detail::VmtHookMap m_vmt_hooks;
1317
1318 /**
1319 * @brief VMT hook names in creation order, maintained under m_hooks_mutex alongside m_vmt_hooks.
1320 * @details Bulk teardown walks this vector in reverse via clear_vmt_hooks_locked() so clones layered on the
1321 * same object are destroyed newest-first instead of in unordered_map bucket order.
1322 */
1323 std::vector<std::string> m_vmt_creation_order;
1324
1325 /**
1326 * @brief Inline and mid hook names in creation order, maintained under m_hooks_mutex alongside m_hooks.
1327 * @details Teardown disables and destroys hooks by walking this vector in reverse (see
1328 * disable_hooks_reverse_order_locked() and clear_hooks_locked()) so hooks layered on one target
1329 * address unwind newest-first. SafetyHook saves the live prologue at create time, so a second hook on
1330 * a target stores "jmp -> first detour" as its own original bytes; restoring oldest-first would
1331 * rewrite the entry to jump into the older hook's about-to-be-freed trampoline. A single global
1332 * reverse walk yields the required per-address LIFO because the hooks on any one address are a
1333 * subsequence of the global creation order.
1334 */
1335 std::vector<std::string> m_hook_creation_order;
1336
1337 Logger &m_logger;
1338 std::shared_ptr<safetyhook::Allocator> m_allocator;
1339 std::atomic<bool> m_shutdown_called{false};
1340
1341 /**
1342 * @brief Gate that mutators (create_*_hook, enable_hook, disable_hook, remove_hook) acquire shared on entry,
1343 * allowing shutdown/remove_all_hooks to acquire exclusive to block new work. Teardown serialization uses
1344 * compare_exchange_strong on m_shutdown_called rather than a separate mutex.
1345 */
1346 mutable detail::SrwSharedMutex m_mutator_gate;
1347
1348 /**
1349 * @brief Returns the thread-local reentrancy depth counter.
1350 * @details Declared const so the const read-only query accessors can consult the guard. The counter is
1351 * thread-local and independent of object state, so const is honest here.
1352 */
1353 31067 [[nodiscard]] int &get_reentrancy_guard() const noexcept
1354 {
1355 thread_local int reentrancy_counter{0};
1356 31067 return reentrancy_counter;
1357 }
1358
1359 struct ReentrancyGuard
1360 {
1361 int &counter;
1362 36 explicit ReentrancyGuard(int &cnt) noexcept : counter(cnt) { ++counter; }
1363 36 ~ReentrancyGuard() noexcept { --counter; }
1364 ReentrancyGuard(const ReentrancyGuard &) = delete;
1365 ReentrancyGuard &operator=(const ReentrancyGuard &) = delete;
1366 ReentrancyGuard(ReentrancyGuard &&) = delete;
1367 ReentrancyGuard &operator=(ReentrancyGuard &&) = delete;
1368 };
1369
1370 // clang-format off
1371 /**
1372 * @brief Acquires m_hooks_mutex shared, or returns a disengaged lock when reentrant.
1373 * @details A with_* or try_with_* callback already holds m_hooks_mutex shared on this thread and has bumped the
1374 * reentrancy guard. Re-locking the non-recursive reader/writer mutex from the same thread is undefined
1375 * behavior and can deadlock if a writer is queued between the two acquisitions. A const query accessor
1376 * invoked from inside such a callback must read under the lock the callback already holds instead of
1377 * taking a second shared lock. When not reentrant, the returned lock owns m_hooks_mutex for the
1378 * caller's scope exactly as a direct shared_lock would.
1379 * @return An engaged shared_lock when guard == 0, otherwise a disengaged one.
1380 */
1381 // clang-format on
1382 29369 [[nodiscard]] std::shared_lock<detail::SrwSharedMutex> lock_hooks_shared_reentrant() const
1383 {
1384
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 29372 times.
29369 if (get_reentrancy_guard() > 0)
1385 {
1386 5 return std::shared_lock<detail::SrwSharedMutex>{};
1387 }
1388 29372 return std::shared_lock<detail::SrwSharedMutex>(m_hooks_mutex);
1389 }
1390
1391 std::string error_to_string(const safetyhook::InlineHook::Error &err) const;
1392 std::string error_to_string(const safetyhook::MidHook::Error &err) const;
1393
1394 /**
1395 * @brief Enables or disables one already-located hook under the held locks.
1396 * @details Shared body of the batch toggle methods. The caller must hold m_mutator_gate and m_hooks_mutex (both
1397 * shared) and must have confirmed the manager is not shutting down. Logs the outcome exactly like the
1398 * single-hook enable_hook / disable_hook path.
1399 * @param hook_id The hook's name, used only for logging.
1400 * @param hook The hook to toggle.
1401 * @param enable true to enable, false to disable.
1402 * @param logs Sink for the outcome message; emitted after the locks release so no logger sink I/O happens
1403 * inside the critical section.
1404 * @return true if the hook is now in the requested state.
1405 */
1406 [[nodiscard]] bool toggle_hook_locked(std::string_view hook_id, Hook &hook, bool enable,
1407 std::vector<DeferredLogEntry> &logs);
1408
1409 131 [[nodiscard]] bool hook_id_exists_locked(std::string_view hook_id) const
1410 {
1411
1/2
✓ Branch 3 → 4 taken 131 times.
✗ Branch 3 → 8 not taken.
131 return m_hooks.find(hook_id) != m_hooks.end();
1412 }
1413
1414 37 [[nodiscard]] bool vmt_hook_exists_locked(std::string_view name) const
1415 {
1416
1/2
✓ Branch 3 → 4 taken 37 times.
✗ Branch 3 → 8 not taken.
37 return m_vmt_hooks.find(name) != m_vmt_hooks.end();
1417 }
1418
1419 /**
1420 * @brief Destroys every VMT hook entry in reverse creation order and empties the registry.
1421 * @details When two hooks layer on the same object, the newer hook records the older hook's clone as its
1422 * "original" vtable. SafetyHook::VmtHook::destroy frees a hook's clone allocation unconditionally
1423 * but restores an object's vptr only when it still points at that hook's own clone, so destroying
1424 * the older hook first would leave the newer hook to restore the object's vptr to freed memory.
1425 * Newest-first destruction unwinds the layers so every restore writes a vptr that is still alive.
1426 * @note Must be called with m_hooks_mutex held exclusively.
1427 */
1428 void clear_vmt_hooks_locked() noexcept;
1429
1430 /**
1431 * @brief Disables every inline and mid hook in reverse creation order.
1432 * @details Phase 1 of teardown. SafetyHook::InlineHook::disable() copies each hook's saved prologue bytes back
1433 * over the target; for hooks layered on one address the newer hook saved "jmp -> older detour" as its
1434 * original, so it must restore first (returning the prologue to that jump) before the older hook
1435 * restores the true original bytes. Disabling oldest-first would instead leave the live prologue
1436 * jumping into the older hook's trampoline, which clear_hooks_locked() then frees -- a use-after-free.
1437 * Hooks on distinct addresses are independent, so one global reverse-creation-order walk yields the
1438 * correct per-address LIFO for all of them at once.
1439 * @note Must be called with m_hooks_mutex held (shared is sufficient: this mutates Hook state, not the map).
1440 */
1441 void disable_hooks_reverse_order_locked() noexcept;
1442
1443 /**
1444 * @brief Destroys every inline and mid hook in reverse creation order and empties the registry.
1445 * @details Phase 2 of teardown. By the time this runs disable_hooks_reverse_order_locked() has already restored
1446 * every prologue, so destruction only frees trampolines; freeing them newest-first keeps the order
1447 * uniform with the disable phase and with clear_vmt_hooks_locked() rather than depending on
1448 * unordered_map bucket order.
1449 * @note Must be called with m_hooks_mutex held exclusively.
1450 */
1451 void clear_hooks_locked() noexcept;
1452
1453 /**
1454 * @brief Returns the name of the VMT hook whose cloned vptr matches @p vptr, or nullptr if none.
1455 * @details Walks the live VMT registry under the held m_hooks_mutex shared or exclusive lock and compares the
1456 * recorded cloned-vptr base. The check is O(N) over the (small) registry and is the only reliable way
1457 * to detect "object is already on a clone installed by this HookManager" without poking at
1458 * SafetyHook's private VmtHook layout. The caller-supplied @p vptr must already be plausibly a
1459 * userspace address; the comparison is a single qword, so no extra bounds work is needed here.
1460 * @note Must be called with m_hooks_mutex held (shared or exclusive). The read-only accessors that need the
1461 * reentrancy-aware lock use the same primitive.
1462 */
1463 [[nodiscard]] const std::string *find_vmt_owner_of_vptr_locked(std::uintptr_t vptr) const noexcept;
1464
1465 /**
1466 * @brief Returns the managed hook installed at @p target_address, or nullptr if none.
1467 * @details Inline and mid hooks at the same address patch the same prologue bytes and share the teardown-order
1468 * hazard. The registry check is exact, unlike the prologue-byte heuristic used for foreign hooks.
1469 * @note Must be called with m_hooks_mutex held (shared or exclusive).
1470 */
1471 [[nodiscard]] const std::string *find_hook_owner_of_target_locked(uintptr_t target_address) const noexcept;
1472
1473 /**
1474 * @brief Decodes the first few bytes of @p slot_value to decide if it looks like a callable function body.
1475 * @details Mirrors the inline pre-flight's detail::decode_* blacklist but is applied to a VMT slot value
1476 * directly so the VMT path does not need to round-trip through the scanner module. The first byte
1477 * is read with a single SEH-guarded byte load (no further decoding); when the first byte is one of
1478 * 0xCC/0xCD/0xC2/0xC3/0x00 the slot is rejected, when it is 0xEB/0xE9 the next 1-4 bytes are
1479 * inspected to see if the relative jump target is inside the same module per GetModuleHandleEx
1480 * HMODULE identity (heuristic for a jump stub). Everything else passes. Allocation-free, noexcept,
1481 * no logger dependency.
1482 * @param slot_value The first pointer-sized value read from the VMT slot.
1483 * @return true when the slot is a real function pointer (or a tail-call to outside the same module),
1484 * false when it is a breakpoint, bare RET, or same-module jump stub.
1485 */
1486 [[nodiscard]] static bool looks_like_function_vmt_slot(std::uintptr_t slot_value) noexcept;
1487 };
1488
1489 /**
1490 * @brief Convenience wrapper that installs an inline hook by direct address.
1491 * @details Forwards every argument to HookManager::create_inline_hook. Returns the registered hook name on success,
1492 * std::nullopt on failure. Diagnostic logging on failure is delegated to the underlying create_inline_hook
1493 * call, which already formats a richly-detailed Error line for every failure code; this wrapper does not
1494 * emit a duplicate.
1495 */
1496 3 [[nodiscard]] inline std::optional<std::string> try_install_inline(std::string_view name, uintptr_t target_address,
1497 void *detour_function,
1498 void **original_trampoline,
1499 const HookConfig &config = HookConfig())
1500 {
1501
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 15 not taken.
3 auto result = HookManager::get_instance().create_inline_hook(name, target_address, detour_function,
1502
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 15 not taken.
3 original_trampoline, config);
1503
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 2 times.
3 if (result)
1504 {
1505
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 13 not taken.
1 return *result;
1506 }
1507 2 return std::nullopt;
1508 3 }
1509
1510 /**
1511 * @brief Convenience wrapper that installs an inline hook by AOB scan.
1512 * @details Diagnostic logging on failure is delegated to the underlying create_inline_hook_aob call
1513 * (pattern-resolution failures and create_inline_hook failures both emit their own Error line), so this
1514 * wrapper does not emit a duplicate. The AOB scan is page-filtered exactly as create_inline_hook_aob
1515 * documents: a full SizeOfImage span containing a guard or no-access section is safe to pass.
1516 */
1517 [[nodiscard]] inline std::optional<std::string>
1518 1 try_install_inline_aob(std::string_view name, uintptr_t module_base, size_t module_size,
1519 std::string_view aob_pattern, std::ptrdiff_t aob_offset, void *detour_function,
1520 void **original_trampoline, const HookConfig &config = HookConfig())
1521 {
1522
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
1 auto result = HookManager::get_instance().create_inline_hook_aob(
1523
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
1 name, module_base, module_size, aob_pattern, aob_offset, detour_function, original_trampoline, config);
1524
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
1 if (result)
1525 {
1526 return *result;
1527 }
1528 1 return std::nullopt;
1529 1 }
1530
1531 /**
1532 * @brief Convenience wrapper that installs a mid-function hook by direct address.
1533 * @details Diagnostic logging on failure is delegated to the underlying create_mid_hook call.
1534 */
1535 1 [[nodiscard]] inline std::optional<std::string> try_install_mid(std::string_view name, uintptr_t target_address,
1536 safetyhook::MidHookFn detour_function,
1537 const HookConfig &config = HookConfig())
1538 {
1539
2/4
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
1 auto result = HookManager::get_instance().create_mid_hook(name, target_address, detour_function, config);
1540
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
1 if (result)
1541 {
1542 return *result;
1543 }
1544 1 return std::nullopt;
1545 1 }
1546
1547 /**
1548 * @brief Convenience wrapper that installs a mid-function hook by AOB scan.
1549 * @details Diagnostic logging on failure is delegated to the underlying create_mid_hook_aob call. The AOB scan is
1550 * page-filtered exactly as create_mid_hook_aob documents: a full SizeOfImage span containing a guard or
1551 * no-access section is safe to pass.
1552 */
1553 [[nodiscard]] inline std::optional<std::string>
1554 1 try_install_mid_aob(std::string_view name, uintptr_t module_base, size_t module_size, std::string_view aob_pattern,
1555 std::ptrdiff_t aob_offset, safetyhook::MidHookFn detour_function,
1556 const HookConfig &config = HookConfig())
1557 {
1558
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
1 auto result = HookManager::get_instance().create_mid_hook_aob(name, module_base, module_size, aob_pattern,
1559
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
1 aob_offset, detour_function, config);
1560
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
1 if (result)
1561 {
1562 return *result;
1563 }
1564 1 return std::nullopt;
1565 1 }
1566 } // namespace DetourModKit
1567
1568 #endif // DETOURMODKIT_HOOK_MANAGER_HPP
1569