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 |