Added more functionality to memoryMap/Unmap. (#384)

This commit is contained in:
Branimir Karadžić
2026-04-23 21:05:11 -07:00
committed by GitHub
parent 3ea49f98d6
commit 8d98acc6f7
3 changed files with 449 additions and 34 deletions

View File

@@ -21,54 +21,168 @@ BX_ERROR_RESULT(kErrorMemoryUnmapFailed, BX_MAKEFOURCC('b', 'x', '8', '1') );
namespace bx
{
/// Virtual memory flags used by `memoryMap`, `memoryUnmap`, and `memoryPageSize`.
/// Flags are grouped into mutually exclusive fields (State, Protection, Page, Advise).
/// Zero in a field means "no change" to that aspect of an existing region.
struct Memory
{
enum Enum : uint32_t
{
// State (bits 0..3) - zero = no change to existing region.
Reserve = 0x00000001, //!< Reserve address range (no backing).
Commit = 0x00000003, //!< Reserve + back with physical pages.
Decommit = 0x00000004, //!< Drop physical pages (keep reservation).
StateMask = 0x0000000f,
// Protection (bits 4..7) - zero = no change to existing region. Mutually exclusive values.
ProtectNone = 0x00000010, //!< No access.
ProtectRead = 0x00000020, //!< Read-only.
ProtectReadWrite = 0x00000030, //!< Read/Write.
ProtectReadExec = 0x00000040, //!< Read/Execute.
ProtectMask = 0x000000f0,
// Page size (bits 8..11) - zero = default system page size. Applied only on fresh reservation.
PageLarge = 0x00000100, //!< Large/huge pages (~2 MiB).
PageHuge = 0x00000200, //!< Gigantic pages (~1 GiB) where supported.
PageMask = 0x00000f00,
// Access advice (bits 12..15) - zero = no change. Mutually exclusive values.
Normal = 0x00001000, //!< Reset access pattern to default.
SequentialAccess = 0x00002000, //!< Hint: memory is accessed sequentially.
RandomAccess = 0x00003000, //!< Hint: memory is accessed randomly.
Prefetch = 0x00004000, //!< Fault-in / prefault pages.
DontNeed = 0x00005000, //!< Can drop pages opportunistically.
AdviseMask = 0x0000f000,
ReadWrite = Commit | ProtectReadWrite, //!< Reserve + commit + read/write.
};
};
/// Suspend calling thread for at least `_ms` milliseconds.
///
/// @param[in] _ms Minimum sleep duration, in milliseconds.
///
void sleep(uint32_t _ms);
///
/// Yield remainder of the current thread's time slice to another runnable thread.
void yield();
/// Get the OS-level identifier of the calling thread.
///
/// @returns Platform-specific thread id.
///
uint32_t getTid();
/// Get resident memory used by the current process, in bytes.
///
/// @returns Process resident memory footprint in bytes, or 0 if unavailable.
///
size_t getProcessMemoryUsed();
/// Open a dynamic library.
///
/// @param[in] _filePath Path to the library (extension may be platform-specific, see `BX_DL_EXT`).
/// @returns Handle to the loaded library, or NULL on failure.
///
void* dlopen(const FilePath& _filePath);
/// Close a dynamic library previously opened with `dlopen`.
///
/// @param[in] _handle Handle returned by `dlopen`.
///
void dlclose(void* _handle);
/// Look up a symbol in a dynamic library.
///
/// @param[in] _handle Handle returned by `dlopen`.
/// @param[in] _symbol Symbol name to resolve.
/// @returns Address of the symbol, or NULL if not found.
///
void* dlsym(void* _handle, const StringView& _symbol);
/// Look up a typed symbol in a dynamic library.
///
/// @param[in] _handle Handle returned by `dlopen`.
/// @param[in] _symbol Symbol name to resolve.
/// @returns Address of the symbol cast to `ProtoT`, or NULL if not found.
///
template<typename ProtoT>
ProtoT dlsym(void* _handle, const StringView& _symbol);
/// Read an environment variable.
///
/// @param[out] _out Buffer that receives the value (may be NULL to query required size).
/// @param[in,out] _inOutSize On input the capacity of `_out`, on output the required size including terminator.
/// @param[in] _name Name of the environment variable.
/// @returns true if the variable exists and its value fits in `_out`.
///
bool getEnv(char* _out, uint32_t* _inOutSize, const StringView& _name);
/// Set an environment variable.
///
/// @param[in] _name Name of the environment variable.
/// @param[in] _value Value to assign.
///
void setEnv(const StringView& _name, const StringView& _value);
/// Change current working directory of the process.
///
/// @param[in] _path Target directory path.
/// @returns 0 on success, non-zero on failure.
///
int chdir(const char* _path);
/// Execute a program in a child process.
///
/// @param[in] _argv NULL-terminated argument vector; `_argv[0]` is the executable path.
/// @returns Platform-specific process handle, or NULL on failure.
///
void* exec(const char* const* _argv);
/// Terminate the current process.
///
/// @param[in] _exitCode Exit code returned to the OS.
/// @param[in] _cleanup When true, run cleanup (atexit handlers, etc.); when false, exit immediately.
///
[[noreturn]] void exit(int32_t _exitCode, bool _cleanup = true);
/// Map, reconfigure, or reshape a virtual memory region.
///
void* memoryMap(void* _address, size_t _size, Error* _err);
/// - If `_address` is NULL: reserves a new region of `_size`, aligned to `_alignment`
/// (0 = page size), and applies the requested state/protect/advise flags. Flags
/// must include `Memory::Reserve`.
/// - If `_address` is non-NULL: reconfigures the existing region according to the
/// flags present. Each flag group (State / Protection / Advise) is only touched
/// when its bits are non-zero, allowing targeted changes.
///
/// Page-size bits select which page size to use for a fresh reservation; they
/// are ignored for in-place reconfiguration of an existing region.
///
/// @param[in] _address Existing base, or NULL to allocate a new region.
/// @param[in] _size Size in bytes, rounded up to the selected page size.
/// @param[in] _alignment Base alignment in bytes, 0 = page size.
/// @param[in] _flags OR-combination of `Memory::Enum` values.
/// @param[out] _err Error state.
/// @returns Base address of the region (same as `_address` for reconfiguration), or NULL on failure.
///
void* memoryMap(void* _address, size_t _size, size_t _alignment, uint32_t _flags, Error* _err);
/// Release a region previously returned by `memoryMap`. Pages are decommitted and
/// the reservation is returned to the OS.
///
/// @param[in] _address Base address returned by `memoryMap`.
/// @param[in] _size Size in bytes of the region to release.
/// @param[out] _err Error state.
///
void memoryUnmap(void* _address, size_t _size, Error* _err);
/// Query virtual memory page size.
///
size_t memoryPageSize();
/// @param[in] _flags Pass `Memory::PageLarge` / `Memory::PageHuge` to query large/huge
/// page sizes; 0 returns the default system page size.
/// @returns Page size in bytes, or 0 when the requested page kind is unsupported.
///
size_t memoryPageSize(uint32_t _flags = 0);
} // namespace bx

View File

@@ -375,37 +375,228 @@ namespace bx
#endif // BX_PLATFORM_*
}
void* memoryMap(void* _address, size_t _size, Error* _err)
namespace
{
#if BX_PLATFORM_LINUX || BX_PLATFORM_OSX
static int32_t toPosixProt(uint32_t _protect)
{
switch (_protect)
{
case Memory::ProtectRead: return PROT_READ;
case Memory::ProtectReadWrite: return PROT_READ | PROT_WRITE;
case Memory::ProtectReadExec: return PROT_READ | PROT_EXEC;
case Memory::ProtectNone: // fall through
default: return PROT_NONE;
}
}
static int32_t toPosixAdvice(uint32_t _advise)
{
switch (_advise)
{
case Memory::Normal: return MADV_NORMAL;
case Memory::SequentialAccess: return MADV_SEQUENTIAL;
case Memory::RandomAccess: return MADV_RANDOM;
case Memory::Prefetch: return MADV_WILLNEED;
case Memory::DontNeed: return MADV_DONTNEED;
default: return -1;
}
}
#elif BX_PLATFORM_WINDOWS
static DWORD toWinProtect(uint32_t _protect)
{
switch (_protect)
{
case Memory::ProtectRead: return PAGE_READONLY;
case Memory::ProtectReadWrite: return PAGE_READWRITE;
case Memory::ProtectReadExec: return PAGE_EXECUTE_READ;
case Memory::ProtectNone: // fall through
default: return PAGE_NOACCESS;
}
}
#endif // BX_PLATFORM_*
} // namespace
void* memoryMap(void* _address, size_t _size, size_t _alignment, uint32_t _flags, Error* _err)
{
BX_ERROR_SCOPE(_err);
const uint32_t state = _flags & Memory::StateMask;
const uint32_t protect = _flags & Memory::ProtectMask;
const uint32_t advise = _flags & Memory::AdviseMask;
const uint32_t page = _flags & Memory::PageMask;
const size_t pageSize = memoryPageSize();
BX_ASSERT(_alignment <= pageSize, "Alignments greater than the page size are not implemented (requested %zu, page %zu).", _alignment, pageSize);
BX_UNUSED(_alignment, pageSize);
#if BX_PLATFORM_LINUX || BX_PLATFORM_OSX
constexpr int32_t flags = 0
| MAP_ANON
| MAP_PRIVATE
;
void* result = mmap(_address, _size, PROT_READ | PROT_WRITE, flags, -1 /*fd*/, 0 /*offset*/);
if (MAP_FAILED == result)
if (NULL == _address)
{
BX_ERROR_SET(
_err
, kErrorMemoryMapFailed
, "kErrorMemoryMapFailed"
);
if (0 == (state & Memory::Reserve) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Memory::Reserve required for fresh allocation.");
return NULL;
}
return result;
#elif BX_PLATFORM_WINDOWS
void* result = VirtualAlloc(_address, _size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
uint32_t effectiveProtect = protect;
if (0 == effectiveProtect)
{
effectiveProtect = (Memory::Commit == (state & Memory::Commit) ) ? Memory::ProtectReadWrite : Memory::ProtectNone;
}
int32_t mmapFlags = MAP_ANON | MAP_PRIVATE;
# if BX_PLATFORM_LINUX
if (0 != (page & Memory::PageHuge) )
{
mmapFlags |= MAP_HUGETLB;
# ifdef MAP_HUGE_1GB
mmapFlags |= MAP_HUGE_1GB;
# endif // MAP_HUGE_1GB
}
else if (0 != (page & Memory::PageLarge) )
{
mmapFlags |= MAP_HUGETLB;
# ifdef MAP_HUGE_2MB
mmapFlags |= MAP_HUGE_2MB;
# endif // MAP_HUGE_2MB
}
# else
BX_UNUSED(page);
# endif // BX_PLATFORM_LINUX
void* result = mmap(_address, _size, toPosixProt(effectiveProtect), mmapFlags, -1 /*fd*/, 0 /*offset*/);
if (MAP_FAILED == result)
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mmap failed.");
return NULL;
}
if (0 != advise)
{
const int32_t adv = toPosixAdvice(advise);
if (-1 != adv)
{
madvise(result, _size, adv);
}
}
return result;
}
// Reconfigure existing region.
if (0 != (state & Memory::Decommit) )
{
// Drop physical pages, then revoke access so use-after-free faults.
madvise(_address, _size, MADV_DONTNEED);
const int32_t prot = (0 != protect) ? toPosixProt(protect) : PROT_NONE;
if (0 != mprotect(_address, _size, prot) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect (decommit) failed.");
return NULL;
}
}
else if (Memory::Commit == (state & Memory::Commit) )
{
const uint32_t effectiveProtect = (0 != protect) ? protect : Memory::ProtectReadWrite;
if (0 != mprotect(_address, _size, toPosixProt(effectiveProtect) ) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect (commit) failed.");
return NULL;
}
}
else if (0 != protect)
{
if (0 != mprotect(_address, _size, toPosixProt(protect) ) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect failed.");
return NULL;
}
}
if (0 != advise)
{
const int32_t adv = toPosixAdvice(advise);
if (-1 != adv)
{
madvise(_address, _size, adv);
}
}
return _address;
#elif BX_PLATFORM_WINDOWS
if (NULL == _address)
{
if (0 == (state & Memory::Reserve) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Memory::Reserve required for fresh allocation.");
return NULL;
}
DWORD allocType = MEM_RESERVE;
DWORD winProtect = PAGE_NOACCESS;
if (Memory::Commit == (state & Memory::Commit) )
{
allocType |= MEM_COMMIT;
winProtect = toWinProtect( (0 != protect) ? protect : Memory::ProtectReadWrite);
}
else if (0 != protect)
{
winProtect = toWinProtect(protect);
}
if (0 != (page & (Memory::PageLarge | Memory::PageHuge) ) )
{
allocType |= MEM_LARGE_PAGES;
}
void* result = VirtualAlloc(_address, _size, allocType, winProtect);
if (NULL == result)
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualAlloc failed.");
return NULL;
}
BX_UNUSED(advise);
return result;
}
// Reconfigure existing region.
if (0 != (state & Memory::Decommit) )
{
if (!VirtualFree(_address, _size, MEM_DECOMMIT) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualFree(MEM_DECOMMIT) failed.");
return NULL;
}
}
else if (Memory::Commit == (state & Memory::Commit) )
{
const DWORD winProtect = toWinProtect( (0 != protect) ? protect : Memory::ProtectReadWrite);
if (NULL == VirtualAlloc(_address, _size, MEM_COMMIT, winProtect) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualAlloc(MEM_COMMIT) failed.");
return NULL;
}
}
else if (0 != protect)
{
DWORD oldProtect = 0;
if (!VirtualProtect(_address, _size, toWinProtect(protect), &oldProtect) )
{
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualProtect failed.");
return NULL;
}
}
BX_UNUSED(advise);
return _address;
#else
BX_UNUSED(_address, _size);
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "Not implemented!");
BX_UNUSED(_address, _size, _alignment, _flags, state, protect, advise, page, pageSize);
BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Not implemented!");
return NULL;
#endif // BX_PLATFORM_*
}
@@ -426,7 +617,8 @@ namespace bx
);
}
#elif BX_PLATFORM_WINDOWS
if (!VirtualFree(_address, _size, MEM_RELEASE) )
BX_UNUSED(_size);
if (!VirtualFree(_address, 0, MEM_RELEASE) )
{
BX_ERROR_SET(
_err
@@ -440,21 +632,33 @@ namespace bx
#endif // BX_PLATFORM_*
}
size_t memoryPageSize()
size_t memoryPageSize(uint32_t _flags)
{
size_t pageSize;
const uint32_t page = _flags & Memory::PageMask;
#if BX_PLATFORM_LINUX || BX_PLATFORM_OSX
pageSize = sysconf(_SC_PAGESIZE);
if (0 != (page & Memory::PageHuge) )
{
return 1ull << 30; // 1 GiB nominal
}
if (0 != (page & Memory::PageLarge) )
{
return 2ull << 20; // 2 MiB nominal
}
return sysconf(_SC_PAGESIZE);
#elif BX_PLATFORM_WINDOWS
if (0 != (page & (Memory::PageLarge | Memory::PageHuge) ) )
{
return ::GetLargePageMinimum();
}
SYSTEM_INFO si;
memSet(&si, 0, sizeof(si) );
::GetSystemInfo(&si);
pageSize = si.dwAllocationGranularity;
return si.dwAllocationGranularity;
#else
pageSize = 16<<10;
BX_UNUSED(page);
return 16<<10;
#endif // BX_PLATFORM_WINDOWS
return pageSize;
}
} // namespace bx

View File

@@ -8,7 +8,7 @@
#include <bx/semaphore.h>
#include <bx/timer.h>
TEST_CASE("getProcessMemoryUsed", "")
TEST_CASE("getProcessMemoryUsed", "[os]")
{
if (BX_ENABLED(BX_PLATFORM_EMSCRIPTEN) )
{
@@ -20,7 +20,7 @@ TEST_CASE("getProcessMemoryUsed", "")
#if BX_CONFIG_SUPPORTS_THREADING
TEST_CASE("semaphore_timeout", "")
TEST_CASE("semaphore_timeout", "[os]")
{
bx::Semaphore sem;
@@ -34,3 +34,100 @@ TEST_CASE("semaphore_timeout", "")
}
#endif // BX_CONFIG_SUPPORTS_THREADING
TEST_CASE("memoryPageSize", "[os]")
{
const size_t pageSize = bx::memoryPageSize();
REQUIRE(0 != pageSize);
// Page sizes are always powers of two on supported platforms.
REQUIRE(0 == (pageSize & (pageSize - 1) ) );
// Large/huge page queries must not return a value smaller than the base page size.
const size_t large = bx::memoryPageSize(bx::Memory::PageLarge);
const size_t huge = bx::memoryPageSize(bx::Memory::PageHuge);
REQUIRE( (0 == large || large >= pageSize) );
REQUIRE( (0 == huge || huge >= pageSize) );
}
TEST_CASE("memoryMap-readWrite-roundtrip", "[os]")
{
const size_t size = 4 * bx::memoryPageSize();
bx::Error err;
void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::ReadWrite, &err);
REQUIRE(err.isOk() );
REQUIRE(nullptr != addr);
uint8_t* bytes = (uint8_t*)addr;
for (size_t ii = 0; ii < size; ++ii)
{
bytes[ii] = uint8_t(ii & 0xff);
}
for (size_t ii = 0; ii < size; ++ii)
{
REQUIRE(uint8_t(ii & 0xff) == bytes[ii]);
}
bx::memoryUnmap(addr, size, &err);
REQUIRE(err.isOk() );
}
TEST_CASE("memoryMap-reserve-then-commit", "[os]")
{
const size_t pageSize = bx::memoryPageSize();
const size_t size = 8 * pageSize;
bx::Error err;
void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::Reserve | bx::Memory::ProtectNone, &err);
REQUIRE(err.isOk() );
REQUIRE(nullptr != addr);
// Commit one page inside the reservation and write to it.
uint8_t* bytes = (uint8_t*)addr;
bx::memoryMap(bytes + pageSize, pageSize, 0, bx::Memory::Commit | bx::Memory::ProtectReadWrite, &err);
REQUIRE(err.isOk() );
for (size_t ii = 0; ii < pageSize; ++ii)
{
bytes[pageSize + ii] = uint8_t( (ii * 7) & 0xff);
}
for (size_t ii = 0; ii < pageSize; ++ii)
{
REQUIRE(uint8_t( (ii * 7) & 0xff) == bytes[pageSize + ii]);
}
// Decommit the page; reservation stays, but physical backing is dropped.
bx::memoryMap(bytes + pageSize, pageSize, 0, bx::Memory::Decommit | bx::Memory::ProtectNone, &err);
REQUIRE(err.isOk() );
// Release the whole reservation.
bx::memoryUnmap(addr, size, &err);
REQUIRE(err.isOk() );
}
TEST_CASE("memoryMap-protection-change", "[os]")
{
const size_t size = bx::memoryPageSize();
bx::Error err;
void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::ReadWrite, &err);
REQUIRE(err.isOk() );
REQUIRE(nullptr != addr);
uint8_t* bytes = (uint8_t*)addr;
bytes[0] = 0xab;
REQUIRE(0xab == bytes[0]);
// Flip to read-only, then back to read/write. Neither call should fail.
bx::memoryMap(addr, size, 0, bx::Memory::ProtectRead, &err);
REQUIRE(err.isOk() );
REQUIRE(0xab == bytes[0]);
bx::memoryMap(addr, size, 0, bx::Memory::ProtectReadWrite, &err);
REQUIRE(err.isOk() );
bytes[0] = 0xcd;
REQUIRE(0xcd == bytes[0]);
bx::memoryUnmap(addr, size, &err);
REQUIRE(err.isOk() );
}