GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 21 / 0 / 21
Functions: 100.0% 2 / 0 / 2
Branches: 100.0% 10 / 0 / 10

include/DetourModKit/memory.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_MEMORY_HPP
2 #define DETOURMODKIT_MEMORY_HPP
3
4 #include <cstddef>
5 #include <cstdint>
6 #include <cstring>
7 #include <expected>
8 #include <string>
9 #include <string_view>
10
11 namespace DetourModKit
12 {
13 /**
14 * @enum MemoryError
15 * @brief Error codes for memory operation failures.
16 */
17 enum class MemoryError
18 {
19 NullTargetAddress,
20 NullSourceBytes,
21 SizeTooLarge,
22 ProtectionChangeFailed,
23 ProtectionRestoreFailed
24 };
25
26 /**
27 * @brief Converts a MemoryError to a human-readable string.
28 * @param error The error code.
29 * @return A string view describing the error.
30 */
31 6 constexpr std::string_view memory_error_to_string(MemoryError error) noexcept
32 {
33
6/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 2 → 7 taken 1 time.
✓ Branch 2 → 8 taken 1 time.
6 switch (error)
34 {
35 1 case MemoryError::NullTargetAddress:
36 1 return "Target address is null";
37 1 case MemoryError::NullSourceBytes:
38 1 return "Source bytes pointer is null";
39 1 case MemoryError::SizeTooLarge:
40 1 return "Write size exceeds maximum allowed";
41 1 case MemoryError::ProtectionChangeFailed:
42 1 return "Failed to change memory protection";
43 1 case MemoryError::ProtectionRestoreFailed:
44 1 return "Failed to restore original memory protection";
45 1 default:
46 1 return "Unknown memory error";
47 }
48 }
49
50 // Maximum write size for write_bytes (64 MiB)
51 inline constexpr size_t MAX_WRITE_SIZE = 64 * 1024 * 1024;
52
53 // Memory cache configuration defaults
54 inline constexpr size_t DEFAULT_CACHE_SIZE = 256;
55 inline constexpr unsigned int DEFAULT_CACHE_EXPIRY_MS = 50;
56 inline constexpr size_t MIN_CACHE_SIZE = 1;
57 inline constexpr size_t DEFAULT_CACHE_SHARD_COUNT = 16;
58 inline constexpr size_t DEFAULT_MAX_CACHE_SIZE_MULTIPLIER = 2;
59
60 namespace Memory
61 {
62 /**
63 * @brief Initializes the memory region cache with specified parameters.
64 * @details Sets up an internal cache to store information about memory regions,
65 * reducing overhead of frequent VirtualQuery system calls.
66 * @param cache_size The desired number of entries in the cache. Defaults to 256.
67 * @param expiry_ms Cache entry expiry time in milliseconds. Defaults to 50ms.
68 * @param shard_count Number of cache shards for concurrent access. Defaults to 16.
69 * @return true if the cache is ready for use (newly or previously initialized), false on failure.
70 * @note Only the first call configures the cache; subsequent calls return true without reconfiguring.
71 * To reconfigure, call shutdown_cache() first.
72 * @note Starts a background cleanup thread that runs periodically.
73 */
74 [[nodiscard]] bool init_cache(size_t cache_size = DEFAULT_CACHE_SIZE,
75 unsigned int expiry_ms = DEFAULT_CACHE_EXPIRY_MS,
76 size_t shard_count = DEFAULT_CACHE_SHARD_COUNT);
77
78 /**
79 * @brief Clears all entries from the memory region cache.
80 * @details Invalidates all currently cached memory region information.
81 * The background cleanup thread continues running.
82 */
83 void clear_cache();
84
85 /**
86 * @brief Shuts down the memory cache and stops the background cleanup thread.
87 * @details Call this before program exit to cleanly terminate the cleanup thread.
88 * After calling shutdown, the cache cannot be reused without re-initialization.
89 */
90 void shutdown_cache();
91
92 /**
93 * @brief Retrieves statistics about the memory cache usage.
94 * @details Returns cache hits, misses, and hit rate information.
95 * Statistics are only available in debug builds.
96 * @return std::string A human-readable string of cache statistics.
97 */
98 std::string get_cache_stats();
99
100 /**
101 * @brief Invalidates cache entries that overlap with the specified address range.
102 * @details Used to force re-query of memory region info after external changes
103 * such as VirtualProtect calls by other code.
104 * @param address Starting address of the range to invalidate.
105 * @param size Size of the range to invalidate.
106 */
107 void invalidate_range(const void *address, size_t size);
108
109 /**
110 * @brief Checks if a specified memory region is readable.
111 * @details Verifies if the memory range has read permissions and is committed.
112 * @param address Starting address of the memory region.
113 * @param size Number of bytes in the memory region to check.
114 * @return true if the entire region is readable, false otherwise.
115 */
116 bool is_readable(const void *address, size_t size);
117
118 /**
119 * @enum ReadableStatus
120 * @brief Tri-state result for non-blocking readability checks.
121 */
122 enum class ReadableStatus
123 {
124 Readable,
125 NotReadable,
126 Unknown
127 };
128
129 /**
130 * @brief Non-blocking readability check that avoids contention on shared locks.
131 * @details Attempts a try-lock on the cache shard. Returns Unknown if the lock
132 * cannot be acquired, allowing callers on latency-sensitive threads to
133 * fall back to SEH instead of stalling.
134 * @param address Starting address of the memory region.
135 * @param size Number of bytes in the memory region to check.
136 * @return ReadableStatus indicating readable, not readable, or unknown (lock busy).
137 */
138 ReadableStatus is_readable_nonblocking(const void *address, size_t size);
139
140 /**
141 * @brief Reads a pointer-sized value at (base + offset), returning 0 on fault.
142 * @details On MSVC, uses SEH (__try/__except) to catch access violations with
143 * zero overhead on the success path. On MinGW, falls back to a single
144 * VirtualQuery guard before dereferencing (no cache interaction).
145 * Suitable for hot paths that already manage their own error recovery.
146 * @note On MinGW the implementation additionally probes the cache via a
147 * non-blocking try_lock_shared before falling back to VirtualQuery,
148 * so cache hits avoid a syscall. This is invisible to callers; the
149 * function still exposes no caller-observable cache state.
150 * @param base The base address to read from.
151 * @param offset Byte offset added to base before dereferencing.
152 * @return The pointer-sized value at the address, or 0 if the read faults.
153 */
154 uintptr_t read_ptr_unsafe(uintptr_t base, ptrdiff_t offset) noexcept;
155
156 /**
157 * @brief Fastest pointer dereference with low-address validity guards only.
158 * @details Validates the source address (base + offset) before dereferencing,
159 * then rejects result values at or below min_valid. Addresses below
160 * 0x10000 are never valid usermode pointers on Windows (null page +
161 * guard pages), so both checks terminate stale/dangling pointer chain
162 * traversals early without requiring an SEH frame.
163 *
164 * This function does NOT provide fault protection against unmapped or
165 * freed memory above min_valid. If the pointer chain may reference
166 * deallocated heap memory or unmapped regions, use read_ptr_unsafe()
167 * instead (SEH-protected on MSVC, VirtualQuery-guarded on MinGW).
168 *
169 * The "unchecked" in the name refers to the absence of OS-level
170 * memory validation (no SEH, no VirtualQuery, no cache lookup).
171 * Only low-address guards are applied. Intended for hot paths
172 * where the caller can guarantee structural pointer validity
173 * (e.g. game objects known to be alive this frame).
174 * @param base The base address to read from.
175 * @param offset Byte offset added to base before dereferencing.
176 * @param min_valid Minimum address value to accept (default 0x10000).
177 * @return The pointer-sized value at the address, or 0 if either the source
178 * address or the dereferenced value is at or below min_valid.
179 */
180 13 inline uintptr_t read_ptr_unchecked(uintptr_t base, ptrdiff_t offset,
181 uintptr_t min_valid = 0x10000) noexcept
182 {
183 13 const auto src = base + static_cast<uintptr_t>(offset);
184
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 11 times.
13 if (src <= min_valid)
185 2 return 0;
186 11 uintptr_t addr{0};
187 11 std::memcpy(&addr, reinterpret_cast<const void *>(src), sizeof(addr));
188
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 6 taken 5 times.
11 return (addr > min_valid) ? addr : 0;
189 }
190
191 /**
192 * @brief Checks if a specified memory region is writable.
193 * @details Verifies if the memory range has write permissions and is committed.
194 * @param address Starting address of the memory region.
195 * @param size Number of bytes in the memory region to check.
196 * @return true if the entire region is writable, false otherwise.
197 */
198 bool is_writable(void *address, size_t size);
199
200 /**
201 * @brief Writes a sequence of bytes to a target memory address.
202 * @details Handles changing memory protection, performs the write operation,
203 * and restores original protection. Also flushes instruction cache.
204 * Automatically invalidates the affected cache range.
205 * If numBytes exceeds MAX_WRITE_SIZE the function performs no write
206 * and returns MemoryError::SizeTooLarge.
207 * @param targetAddress Destination memory address.
208 * @param sourceBytes Pointer to the source buffer containing data to write.
209 * @param numBytes Number of bytes to write. Must not exceed MAX_WRITE_SIZE.
210 * @return std::expected<void, MemoryError> on success, or the specific error on failure.
211 */
212 [[nodiscard]] std::expected<void, MemoryError> write_bytes(std::byte *targetAddress, const std::byte *sourceBytes, size_t numBytes);
213 } // namespace Memory
214 } // namespace DetourModKit
215
216 #endif // DETOURMODKIT_MEMORY_HPP
217