Add project files.

This commit is contained in:
alejo committed 2026-09-12 00:08:03 -03:00
commit 3087fcea8a
71 files changed
+8325

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
#include "Architecture.hpp"
#include <atomic>
#include <cstdint>
#include <cstring>
/* WHY THERE CAN BE A "LANDING PAD" IN FRONT OF A FUNCTION
* -----------------------------------------------------------
* Modern toolchains can emit a marker instruction as the very first
* instruction of a function, required by a CPU-level indirect-branch
* protection feature: x86 CET ("Control-flow Enforcement Technology",
* ENDBR64/ENDBR32) and ARM64 BTI ("Branch Target Identification").
* When the CPU enforces either, an indirect call/jump (which is
* exactly how a call through a function pointer, or a call across a
* shared-library boundary via the PLT/GOT, actually reaches a
* function) is only allowed to land on one of these marker
* instructions -- landing anywhere else raises a fault.
*
* This matters here because it is genuinely on by default in
* mainstream environments, not a rare opt-in: this exact toolchain
* (Ubuntu 24.04's system GCC 13, x86-64) emits ENDBR64 at every
* function's entry with no extra flags beyond -fpatchable-function-entry,
* confirmed empirically while building this library --
* `gcc -O2 -fpatchable-function-entry=16,0` alone was enough to produce
* an `endbr64` as the very first instruction, ahead of the requested
* NOP padding. Overwriting it unconditionally would silently work on
* a CPU/kernel combination that does not enforce IBT and silently
* crash on one that does -- exactly the kind of environment-dependent
* failure this library exists to avoid.
*
* So every backend below first checks the live bytes at the function's
* entry for its architecture's landing-pad encoding and, if present,
* treats it as a fixed, immovable prefix: the jump is written
* immediately after it rather than over it. Since a landing pad is
* defined to otherwise behave as a NOP, falling through it into our
* jump is always correct whether or not the CPU actually enforces the
* protection it marks.
*
* WRITE ORDERING (AND WHAT IT DOES / DOES NOT PROTECT AGAINST)
* -----------------------------------------------------------
* Every backend below writes the target-address bytes first and the
* leading opcode/instruction word that "arms" the jump last, with a
* release fence in between. This protects the realistic case this
* library is designed for: hot_reload_reload() runs at a quiescent
* point (e.g. your main thread between frames) where nothing is
* actively calling INTO the function while it is being patched, and
* this ordering guarantees that any call arriving right at that
* boundary sees either the fully-old or the fully-new jump, never a
* target address that is half-old-half-new. It does NOT make the
* write safe against a thread that is concurrently, actively
* executing through the exact bytes being patched -- doing that in
* general on a variable-length ISA needs a signal-handler-based retry
* protocol (a much bigger mechanism, well beyond a dev-loop tool's
* requirements). See the README for this precondition stated plainly.
*/
namespace hot_reload::detail
{
namespace
{
#if defined(__x86_64__) || defined(_M_X64)
constexpr std::size_t kLandingPadBytes = 4;
constexpr unsigned char kEndbr64[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFA};
// jmp qword ptr [rip+0]; <8-byte absolute address> -- reaches
// any 64-bit address regardless of how far apart the old and
// new modules were loaded, unlike a 5-byte relative jump
// (E9 rel32) which only reaches +/-2GB. Verified byte-for-byte
// against `as`/objdump output while building this library.
constexpr std::size_t kJumpBytes = 14;
bool hasLandingPad(const unsigned char* entry)
{
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
}
void writeJumpAt(unsigned char* site, const void* target)
{
std::uint64_t targetValue = 0;
std::memcpy(&targetValue, &target, sizeof(target));
// Payload first: the absolute address (site+6) and the
// constant zero rip-displacement (site+2) are only ever
// consumed as DATA once the opcode below is in place, so
// writing them early cannot be misinterpreted as a
// different, fresh instruction stream by anything that
// might fall through this region in its old, all-NOP state.
std::memcpy(site + 6, &targetValue, sizeof(targetValue));
std::memcpy(site + 2, "\x00\x00\x00\x00", 4);
std::atomic_thread_fence(std::memory_order_release);
// Arm last: until these 2 bytes land, site+0 still reads as
// whatever it was (a NOP, on first patch; a previous FF 25
// on a later reload -- either way harmless/inert).
unsigned char opcode[2] = {0xFF, 0x25};
std::memcpy(site, opcode, sizeof(opcode));
std::atomic_thread_fence(std::memory_order_release);
}
#elif defined(__i386__) || defined(_M_IX86)
constexpr std::size_t kLandingPadBytes = 4;
constexpr unsigned char kEndbr32[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFB};
// e9 rel32 -- a plain relative near jump. On 32-bit x86 the
// entire address space fits in the signed 32-bit displacement
// (mod 2^32 wraparound gives the right answer regardless of
// "distance"), so unlike x86-64 there is no need for an
// absolute/indirect form here.
constexpr std::size_t kJumpBytes = 5;
bool hasLandingPad(const unsigned char* entry)
{
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
}
void writeJumpAt(unsigned char* site, const void* target)
{
std::int32_t rel32 = static_cast<std::int32_t>(
reinterpret_cast<std::intptr_t>(target) - reinterpret_cast<std::intptr_t>(site) - 5);
// Payload (the displacement) first, opcode byte last --
// same reasoning as the x86-64 backend above.
std::memcpy(site + 1, &rel32, sizeof(rel32));
std::atomic_thread_fence(std::memory_order_release);
site[0] = 0xE9;
std::atomic_thread_fence(std::memory_order_release);
}
#elif defined(__aarch64__) || defined(_M_ARM64)
constexpr std::size_t kLandingPadBytes = 4;
// Matches any BTI variant (plain/C/J/JC): bits [31:6] are fixed
// at 0b010101000000001001001, differing only in bits [6:5]
// (the qualifier) -- equivalently, the low byte is one of
// 0x1F/0x5F/0x9F/0xDF and the next 3 bytes are always
// 24 03 D5. Verified with `as`/objdump for all 4 variants
// while building this library.
bool isBti(const unsigned char* entry)
{
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
}
bool hasLandingPad(const unsigned char* entry)
{
return isBti(entry);
}
// ldr x17, #8 ; br x17 ; <8-byte absolute address>. x16/x17
// are AArch64's designated "intra-procedure-call" scratch
// registers (IP0/IP1) -- the AAPCS64 explicitly reserves them
// for exactly this kind of linker/veneer-style long branch, so
// clobbering them at a function's entry point, before any of
// its real prologue has executed, is always safe. Byte values
// verified with `as`/objdump while building this library.
constexpr std::size_t kJumpBytes = 16;
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
void writeJumpAt(unsigned char* site, const void* target)
{
std::uint64_t targetValue = 0;
std::memcpy(&targetValue, &target, sizeof(target));
// Payload (the literal address) first. ARM64 instructions
// are fixed-width and naturally aligned, so -- unlike the
// variable-length x86 case -- each 4-byte slot here is
// unambiguous: it is either the old NOP, the old
// instruction, or the new one, never a byte-misaligned
// mix. Writing high-to-low keeps the two real instructions
// (which is what any in-flight fall-through NOP execution
// could actually reach) inert for as long as possible.
std::memcpy(site + 8, &targetValue, sizeof(targetValue));
std::atomic_thread_fence(std::memory_order_release);
std::memcpy(site + 4, kBrX17, sizeof(kBrX17));
std::atomic_thread_fence(std::memory_order_release);
// Arm last: until this word lands, site+0 still reads as a
// NOP (or a previous ldr, on a later reload) and execution
// never reaches the br at site+4 via fall-through.
std::memcpy(site, kLdrX17Pc8, sizeof(kLdrX17Pc8));
std::atomic_thread_fence(std::memory_order_release);
}
#endif
}
bool isArchitectureSupported()
{
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
defined(_M_ARM64)
return true;
#else
return false;
#endif
}
PatchLayout planPatch(const void* entryAddress)
{
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
defined(_M_ARM64)
const unsigned char* entry = static_cast<const unsigned char*>(entryAddress);
PatchLayout layout;
layout.patchOffset = hasLandingPad(entry) ? kLandingPadBytes : 0;
layout.totalBytes = layout.patchOffset + kJumpBytes;
return layout;
#else
(void) entryAddress;
return PatchLayout{};
#endif
}
bool writeJump(void* entryAddress, const void* targetAddress)
{
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
defined(_M_ARM64)
PatchLayout layout = planPatch(entryAddress);
unsigned char* site = static_cast<unsigned char*>(entryAddress) + layout.patchOffset;
writeJumpAt(site, targetAddress);
return true;
#else
(void) entryAddress;
(void) targetAddress;
return false;
#endif
}
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstddef>
namespace hot_reload::detail
{
/// What planPatch() found at one specific function's entry point --
/// how many bytes from the entry address must be safe to overwrite,
/// and how many of those (if any) are a landing pad that must be
/// preserved rather than overwritten. See Architecture.cpp's file
/// comment for why a landing pad can be present at all.
struct PatchLayout
{
/// Total bytes from the function's entry address that this
/// patch touches, landing pad included -- compare this against
/// the symbol's compiled size before writing anything.
std::size_t totalBytes = 0;
/// How many leading bytes are a landing pad and must be left
/// untouched; the jump itself is written starting at
/// entryAddress + patchOffset.
std::size_t patchOffset = 0;
};
/// True if this build has a jump-patch backend for the CPU
/// architecture it was compiled for (decided at compile time --
/// see the top of Architecture.cpp). x86, x86-64, and ARM64 are
/// supported; anything else (including 32-bit ARM, whose ARM/Thumb
/// interworking this library does not attempt -- see the README)
/// returns false here, and HotReloadContext surfaces that as
/// HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
bool isArchitectureSupported();
/// Inspects the bytes already sitting at entryAddress to determine
/// the real patch layout for THIS function, accounting for a
/// possible leading landing pad. Safe to call even when
/// isArchitectureSupported() is false (returns a zeroed PatchLayout).
PatchLayout planPatch(const void* entryAddress);
/// Overwrites entryAddress so that control transfers unconditionally
/// to targetAddress, following the layout planPatch() would return
/// for this same entryAddress. The caller must already have made
/// [entryAddress, entryAddress + planPatch(entryAddress).totalBytes)
/// writable (see MemoryProtection.hpp) and must flush the
/// instruction cache over that same range afterwards -- this
/// function does neither.
///
/// Only meaningful precondition: the caller has already checked the
/// target symbol's compiled size against planPatch(entryAddress)
/// .totalBytes; this function does not re-check it and will happily
/// overwrite whatever comes next in memory if asked to.
///
/// Returns false only if isArchitectureSupported() is false.
bool writeJump(void* entryAddress, const void* targetAddress);
}
+274
View File
@@ -0,0 +1,274 @@
#include "HotReloadContext.hpp"
#include "Architecture.hpp"
#include "MemoryProtection.hpp"
#include <cstdio>
#include <fstream>
#include <unordered_set>
namespace hot_reload
{
namespace
{
bool copyFile(const std::string& from, const std::string& to)
{
std::ifstream in(from, std::ios::binary);
if (!in)
{
return false;
}
std::ofstream out(to, std::ios::binary | std::ios::trunc);
if (!out)
{
return false;
}
out << in.rdbuf();
return static_cast<bool>(out);
}
}
HotReloadContext::HotReloadContext(const HotReloadOptions* options)
: m_onSymbolEvent(nullptr)
, m_onDeletedSymbol(nullptr)
, m_userData(nullptr)
{
if (options != nullptr)
{
m_onSymbolEvent = options->on_symbol_event;
m_onDeletedSymbol = options->on_deleted_symbol;
m_userData = options->user_data;
}
}
void HotReloadContext::emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress)
{
if (m_onSymbolEvent == nullptr)
{
return;
}
HotReloadSymbolEventInfo info;
info.name = name.c_str();
info.kind = kind;
info.old_address = oldAddress;
info.new_address = newAddress;
m_onSymbolEvent(&info, m_userData);
}
std::string HotReloadContext::stagePathForLoad(
LoadedModule& module, const std::string& moduleName, const std::string& sourcePath)
{
// Every load (first and subsequent) goes through a freshly,
// uniquely-named copy rather than dlopen()/LoadLibrary()'ing
// sourcePath directly. Without this, a build that overwrites
// the SAME path in place for every rebuild (rather than the
// more common write-temp-then-rename pattern) can hit
// dlopen's device+inode-based caching and hand back the
// already-mapped OLD content instead of genuinely reloading --
// a well-known gotcha for this style of tool. Copying to a
// name guaranteed never reused sidesteps it regardless of how
// the build produced sourcePath.
++module.reloadCounter;
std::string staged = sourcePath + ".hotreload." + moduleName + "." + std::to_string(module.reloadCounter);
if (!copyFile(sourcePath, staged))
{
return {};
}
return staged;
}
HotReloadStatus HotReloadContext::load(const std::string& moduleName, const std::string& path)
{
m_lastError.clear();
if (m_modules.find(moduleName) != m_modules.end())
{
// Already loaded -- call reload() to load a new build of
// an existing name, rather than silently discarding
// whatever was already patched into the first one.
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
}
LoadedModule module;
std::string staged = stagePathForLoad(module, moduleName, path);
if (staged.empty())
{
m_lastError = "could not stage '" + path + "' for loading (source unreadable, or destination path unwritable)";
return HOT_RELOAD_ERROR_LOAD_FAILED;
}
detail::ModuleHandle handle = detail::ModuleHandle::load(staged);
if (!handle.isValid())
{
m_lastError = handle.errorMessage();
std::remove(staged.c_str());
return HOT_RELOAD_ERROR_LOAD_FAILED;
}
module.currentSymbols = detail::readExportedSymbols(staged, handle.loadedBase());
std::remove(staged.c_str()); // best-effort tidy-up; the mapping survives regardless (see stagePathForLoad)
module.versions.push_back(std::move(handle));
m_modules.emplace(moduleName, std::move(module));
return HOT_RELOAD_OK;
}
HotReloadStatus HotReloadContext::reload(const std::string& moduleName, const std::string& newPath)
{
m_lastError.clear();
auto it = m_modules.find(moduleName);
if (it == m_modules.end())
{
return HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION;
}
if (!detail::isArchitectureSupported())
{
return HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE;
}
LoadedModule& module = it->second;
std::string staged = stagePathForLoad(module, moduleName, newPath);
if (staged.empty())
{
m_lastError =
"could not stage '" + newPath + "' for loading (source unreadable, or destination path unwritable)";
return HOT_RELOAD_ERROR_LOAD_FAILED;
}
detail::ModuleHandle newHandle = detail::ModuleHandle::load(staged);
if (!newHandle.isValid())
{
m_lastError = newHandle.errorMessage();
std::remove(staged.c_str());
return HOT_RELOAD_ERROR_LOAD_FAILED; // nothing touched -- the previous version keeps running
}
std::vector<detail::ExportedSymbol> newSymbols = detail::readExportedSymbols(staged, newHandle.loadedBase());
std::remove(staged.c_str());
std::unordered_map<std::string, const detail::ExportedSymbol*> newByName;
newByName.reserve(newSymbols.size());
for (const detail::ExportedSymbol& symbol : newSymbols)
{
newByName[symbol.name] = &symbol;
}
std::unordered_set<std::string> matchedNewNames;
for (const detail::ExportedSymbol& oldSymbol : module.currentSymbols)
{
auto found = newByName.find(oldSymbol.name);
if (found == newByName.end())
{
handleRemoved(oldSymbol);
continue;
}
const detail::ExportedSymbol& newSymbol = *found->second;
matchedNewNames.insert(oldSymbol.name);
if (!oldSymbol.isFunction || !newSymbol.isFunction)
{
// Present on both sides, but classified as data (or
// something this library does not recognize as code)
// on at least one side -- the mechanical half of the
// "only executable code changes" guarantee. Never
// touched, in either direction.
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE, oldSymbol.name, oldSymbol.address, newSymbol.address);
continue;
}
patchOne(oldSymbol, newSymbol);
}
for (const detail::ExportedSymbol& newSymbol : newSymbols)
{
if (matchedNewNames.find(newSymbol.name) == matchedNewNames.end())
{
emitEvent(HOT_RELOAD_SYMBOL_ADDED, newSymbol.name, nullptr, newSymbol.address);
}
}
module.currentSymbols = std::move(newSymbols);
module.versions.push_back(std::move(newHandle));
return HOT_RELOAD_OK;
}
void* HotReloadContext::getSymbol(const std::string& moduleName, const std::string& symbolName) const
{
auto it = m_modules.find(moduleName);
if (it == m_modules.end())
{
return nullptr;
}
for (const detail::ExportedSymbol& symbol : it->second.currentSymbols)
{
if (symbol.name == symbolName)
{
return symbol.address;
}
}
return nullptr;
}
void HotReloadContext::patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol)
{
detail::PatchLayout layout = detail::planPatch(oldSymbol.address);
if (oldSymbol.size != 0 && oldSymbol.size < layout.totalBytes)
{
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL, oldSymbol.name, oldSymbol.address, newSymbol.address);
return;
}
detail::ScopedCodeWriteAccess writable(oldSymbol.address, layout.totalBytes);
if (!writable.isValid())
{
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE, oldSymbol.name, oldSymbol.address, newSymbol.address);
return;
}
detail::writeJump(oldSymbol.address, newSymbol.address);
writable.finishWrite();
emitEvent(HOT_RELOAD_SYMBOL_PATCHED, oldSymbol.name, oldSymbol.address, newSymbol.address);
}
void HotReloadContext::handleRemoved(const detail::ExportedSymbol& oldSymbol)
{
// Ask for a stub at most once, and only for an old symbol that
// was itself real code -- there is no sensible "redirect" for
// a removed data symbol. A returned NULL (including because no
// callback was supplied at all) means the default: leave the
// old implementation running untouched.
void* stub = nullptr;
if (oldSymbol.isFunction && m_onDeletedSymbol != nullptr)
{
stub = m_onDeletedSymbol(oldSymbol.name.c_str(), m_userData);
}
if (stub != nullptr)
{
detail::PatchLayout layout = detail::planPatch(oldSymbol.address);
bool sizeOk = oldSymbol.size == 0 || oldSymbol.size >= layout.totalBytes;
if (sizeOk)
{
detail::ScopedCodeWriteAccess writable(oldSymbol.address, layout.totalBytes);
if (writable.isValid())
{
detail::writeJump(oldSymbol.address, stub);
writable.finishWrite();
}
// If the OS refused write access, or the function was
// too small to safely hold the stub redirect, this
// silently falls back to leaving the old implementation
// running -- the same safe default as not supplying a
// stub at all, rather than adding a second kind of
// event on top of HOT_RELOAD_SYMBOL_REMOVED below for
// what is already a rare, best-effort extra.
}
}
emitEvent(HOT_RELOAD_SYMBOL_REMOVED, oldSymbol.name, oldSymbol.address, nullptr);
}
}
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include "ModuleHandle.hpp"
#include "SymbolTable.hpp"
#include "hot_reload/hot_reload.h"
#include <string>
#include <unordered_map>
#include <vector>
namespace hot_reload
{
/// The real type behind the opaque ::HotReloadContext the public C
/// API hands out -- see HotReloadApi.cpp for the extern "C" shim
/// that casts between the two, following the same opaque-pointer
/// PIMPL pattern as cpp-style-guide.md's own NetManager example.
class HotReloadContext
{
public:
explicit HotReloadContext(const HotReloadOptions* options);
HotReloadContext(const HotReloadContext&) = delete;
HotReloadContext& operator=(const HotReloadContext&) = delete;
/// See hot_reload_load()'s doc comment in hot_reload.h.
HotReloadStatus load(const std::string& moduleName, const std::string& path);
/// See hot_reload_reload()'s doc comment in hot_reload.h.
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
/// See hot_reload_get_symbol()'s doc comment in hot_reload.h.
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
/// See hot_reload_last_error()'s doc comment in hot_reload.h.
const std::string& lastError() const
{
return m_lastError;
}
private:
/// Everything tracked for one logical module name. Has no
/// meaning outside HotReloadContext, so it lives here rather
/// than in its own header -- see cpp-style-guide.md's carve-out
/// for small types with no use outside their owning type.
struct LoadedModule
{
// Every version ever loaded for this name, kept alive for
// the rest of the process's lifetime: once a function is
// patched, anything that already holds its old address
// must stay valid memory forever, even though it now holds
// nothing but a jump. versions.back() is the active one.
std::vector<detail::ModuleHandle> versions;
std::vector<detail::ExportedSymbol> currentSymbols;
int reloadCounter = 0;
};
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
std::unordered_map<std::string, LoadedModule> m_modules;
HotReloadSymbolEventFn m_onSymbolEvent;
HotReloadDeletedSymbolFn m_onDeletedSymbol;
void* m_userData;
std::string m_lastError;
};
}
+92
View File
@@ -0,0 +1,92 @@
#include "MemoryProtection.hpp"
#include <cstdint>
#if defined(_WIN32)
#include <windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace hot_reload::detail
{
namespace
{
std::size_t systemPageSize()
{
#if defined(_WIN32)
SYSTEM_INFO info;
GetSystemInfo(&info);
return static_cast<std::size_t>(info.dwPageSize);
#else
long size = sysconf(_SC_PAGESIZE);
return size > 0 ? static_cast<std::size_t>(size) : 4096;
#endif
}
}
ScopedCodeWriteAccess::ScopedCodeWriteAccess(void* address, std::size_t length)
: m_pageAddress(nullptr)
, m_pageLength(0)
, m_rangeAddress(address)
, m_rangeLength(length)
, m_previousProtection(0)
, m_valid(false)
{
const std::size_t page = systemPageSize();
const std::uintptr_t start = reinterpret_cast<std::uintptr_t>(address);
const std::uintptr_t alignedStart = start - (start % page);
const std::size_t frontSlack = static_cast<std::size_t>(start - alignedStart);
const std::size_t alignedLength = ((frontSlack + length + page - 1) / page) * page;
m_pageAddress = reinterpret_cast<void*>(alignedStart);
m_pageLength = alignedLength;
#if defined(_WIN32)
DWORD oldProtect = 0;
if (VirtualProtect(m_pageAddress, m_pageLength, PAGE_EXECUTE_READWRITE, &oldProtect))
{
m_previousProtection = oldProtect;
m_valid = true;
}
#else
if (mprotect(m_pageAddress, m_pageLength, PROT_READ | PROT_WRITE | PROT_EXEC) == 0)
{
m_previousProtection = PROT_READ | PROT_EXEC;
m_valid = true;
}
#endif
}
ScopedCodeWriteAccess::~ScopedCodeWriteAccess()
{
if (!m_valid)
{
return;
}
#if defined(_WIN32)
DWORD ignored = 0;
VirtualProtect(m_pageAddress, m_pageLength, static_cast<DWORD>(m_previousProtection), &ignored);
#else
mprotect(m_pageAddress, m_pageLength, static_cast<int>(m_previousProtection));
#endif
}
void ScopedCodeWriteAccess::finishWrite()
{
if (!m_valid)
{
return;
}
#if defined(_WIN32)
FlushInstructionCache(GetCurrentProcess(), m_rangeAddress, m_rangeLength);
#else
char* begin = static_cast<char*>(m_rangeAddress);
__builtin___clear_cache(begin, begin + m_rangeLength);
#endif
}
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
namespace hot_reload::detail
{
/// RAII guard: makes [address, address + length) writable for its
/// lifetime (the OS only grants permissions in whole pages, so the
/// guard rounds out to the enclosing page range internally) and
/// restores the original protection when it goes out of scope.
///
/// Assumes the range started out READ+EXECUTE, as every platform's
/// loader maps a .text/code section -- restoring to exactly that,
/// rather than querying and restoring whatever the previous
/// protection actually was, since POSIX mprotect() has no portable
/// way to ask for the current protection of a range and code pages
/// are never anything else in practice.
class ScopedCodeWriteAccess
{
public:
ScopedCodeWriteAccess(void* address, std::size_t length);
~ScopedCodeWriteAccess();
ScopedCodeWriteAccess(const ScopedCodeWriteAccess&) = delete;
ScopedCodeWriteAccess& operator=(const ScopedCodeWriteAccess&) = delete;
/// False if the platform call to relax protection failed --
/// when false, nothing was changed and the range must not be
/// written to (the destructor is still safe to run either way).
bool isValid() const
{
return m_valid;
}
/// Call exactly once, after writing the new bytes and before
/// this guard is destroyed: flushes the instruction cache over
/// the original [address, address + length) range so every
/// core sees the new instructions rather than a stale fetch
/// (required on ARM64, where instruction and data caches are
/// not automatically coherent; effectively a formality on
/// x86/x86-64, called anyway for correctness on that
/// architecture too).
void finishWrite();
private:
void* m_pageAddress;
std::size_t m_pageLength;
void* m_rangeAddress;
std::size_t m_rangeLength;
unsigned long m_previousProtection;
bool m_valid;
};
}
+155
View File
@@ -0,0 +1,155 @@
#include "ModuleHandle.hpp"
#include <string>
#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#include <link.h>
#endif
namespace hot_reload::detail
{
#if defined(_WIN32)
namespace
{
/// GetLastError() alone is just a number; this renders it the
/// way a person would want to read it, the same information
/// dlerror() gives directly on POSIX. Written against
/// documented Win32 behavior; not exercised on real Windows --
/// see source/HotReload/README.md's verification notes.
std::string formatLastWindowsError()
{
DWORD errorCode = GetLastError();
LPSTR buffer = nullptr;
DWORD length = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<LPSTR>(&buffer), 0, nullptr);
std::string message;
if (length > 0 && buffer != nullptr)
{
message.assign(buffer, length);
while (!message.empty() && (message.back() == '\n' || message.back() == '\r'))
{
message.pop_back();
}
}
if (buffer != nullptr)
{
LocalFree(buffer);
}
if (message.empty())
{
message = "LoadLibrary failed (error code " + std::to_string(errorCode) + ")";
}
return message;
}
}
#endif
ModuleHandle::~ModuleHandle()
{
if (m_native != nullptr)
{
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(m_native));
#else
dlclose(m_native);
#endif
}
}
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
: m_native(other.m_native)
, m_loadedBase(other.m_loadedBase)
{
other.m_native = nullptr;
other.m_loadedBase = nullptr;
}
ModuleHandle& ModuleHandle::operator=(ModuleHandle&& other) noexcept
{
if (this != &other)
{
if (m_native != nullptr)
{
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(m_native));
#else
dlclose(m_native);
#endif
}
m_native = other.m_native;
m_loadedBase = other.m_loadedBase;
other.m_native = nullptr;
other.m_loadedBase = nullptr;
}
return *this;
}
ModuleHandle ModuleHandle::load(const std::string& path)
{
ModuleHandle handle;
#if defined(_WIN32)
HMODULE module = LoadLibraryA(path.c_str());
if (module != nullptr)
{
handle.m_native = module;
handle.m_loadedBase = module; // the HMODULE already IS the mapped base on Windows
}
else
{
handle.m_errorMessage = formatLastWindowsError();
}
#else
// RTLD_LOCAL: keep each version's symbols out of the global
// symbol namespace. Hot reload deliberately resolves symbols
// itself (see SymbolTable.cpp) rather than through the dynamic
// linker's own global name lookup, and RTLD_GLOBAL would let
// an old and a new build of the same plugin -- which export
// the very same names on purpose -- collide there.
//
// RTLD_NOW (eager symbol resolution), not RTLD_LAZY: this
// means a plugin with a genuinely undefined symbol (e.g. a
// non-inline static class member declared but never defined,
// only ODR-used once some previously-dead code path becomes
// reachable -- a real case found while building this feature)
// fails right here, cleanly and reportably, INSTEAD of
// crashing the whole host process the moment that code path
// first runs under RTLD_LAZY's deferred binding. For a
// long-running host, a clean, catchable load failure is far
// preferable to a delayed, uncatchable one.
void* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (module == nullptr)
{
const char* error = dlerror();
handle.m_errorMessage = error != nullptr ? error : "dlopen failed with no further detail";
}
else
{
struct link_map* map = nullptr;
if (dlinfo(module, RTLD_DI_LINKMAP, &map) == 0 && map != nullptr)
{
handle.m_native = module;
handle.m_loadedBase = reinterpret_cast<void*>(map->l_addr);
}
else
{
// Captured before dlclose() below, which could
// otherwise overwrite dlerror()'s state first.
const char* error = dlerror();
handle.m_errorMessage =
error != nullptr ? error : "dlinfo(RTLD_DI_LINKMAP) failed with no further detail";
dlclose(module);
}
}
#endif
return handle;
}
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <string>
namespace hot_reload::detail
{
/// RAII wrapper around dlopen()/LoadLibrary(). Move-only, closes
/// the underlying module on destruction (or on being replaced by a
/// move-assignment).
class ModuleHandle
{
public:
ModuleHandle() = default;
~ModuleHandle();
ModuleHandle(const ModuleHandle&) = delete;
ModuleHandle& operator=(const ModuleHandle&) = delete;
ModuleHandle(ModuleHandle&& other) noexcept;
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
/// Loads path. On failure the returned handle's isValid() is
/// false and loadedBase() is null.
static ModuleHandle load(const std::string& path);
bool isValid() const
{
return m_native != nullptr;
}
/// The address to pass as SymbolTable's loadedBase: on Windows
/// this is simply the HMODULE; on POSIX it is the runtime load
/// bias (struct link_map::l_addr) that turns an ELF symbol's
/// file-relative st_value into a real address.
void* loadedBase() const
{
return m_loadedBase;
}
/// Only meaningful when isValid() is false: the underlying
/// platform's own description of why loading failed --
/// dlerror() on POSIX, the formatted message for
/// GetLastError() on Windows. Captured immediately inside
/// load()'s failing branch, before any other dl*/Win32 call
/// that could otherwise overwrite it first. Empty if load()
/// was never attempted, or if the platform gave no message.
const std::string& errorMessage() const
{
return m_errorMessage;
}
private:
void* m_native = nullptr;
void* m_loadedBase = nullptr;
std::string m_errorMessage;
};
}
+365
View File
@@ -0,0 +1,365 @@
#include "SymbolTable.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <map>
#include <vector>
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
* ---------------------------------------------------------
* hot_reload.h promises that data -- globals, class instances, vtables,
* anything that isn't executable code -- is never patched. That
* guarantee is enforced in exactly one place: here. Every symbol this
* file reports carries a real classification pulled from the loader's
* own metadata (ELF's symbol type + section flags, or a PE export's
* containing section's characteristics), never inferred from the
* symbol's name or any convention the hot-reloadable module is
* expected to follow. HotReloadContext (the only caller) simply
* refuses to patch anything this file marks isFunction = false, on
* either side of a reload -- so nothing downstream has to be trusted
* to double-check that part.
*/
namespace hot_reload::detail
{
#if defined(_WIN32)
// ---------------------------------------------------------------
// PE / Windows: read straight out of the image already mapped at
// loadedBase. Unlike ELF (below), the OS loader has already fully
// relocated this image and handed us its exact base address, so
// there is no separate "load bias" to compute -- every RVA in the
// headers just gets added directly to loadedBase.
// ---------------------------------------------------------------
#include <windows.h>
namespace
{
struct SectionInfo
{
std::uint32_t rvaBegin;
std::uint32_t rvaEnd;
bool executable;
};
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
{
return reinterpret_cast<const char*>(base + rva);
}
}
std::vector<ExportedSymbol> readExportedSymbols(const std::string& /*imagePath*/, void* loadedBase)
{
std::vector<ExportedSymbol> result;
if (loadedBase == nullptr)
{
return result;
}
std::uint8_t* base = static_cast<std::uint8_t*>(loadedBase);
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE)
{
return result;
}
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE)
{
return result;
}
// Section table for the executable-section check, following
// the same "code lives in a section marked executable" rule
// as the ELF path's SHF_EXECINSTR check below.
std::vector<SectionInfo> sections;
const auto* sectionHeaders = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i)
{
const IMAGE_SECTION_HEADER& section = sectionHeaders[i];
SectionInfo info;
info.rvaBegin = section.VirtualAddress;
info.rvaEnd = section.VirtualAddress + std::max(section.Misc.VirtualSize, section.SizeOfRawData);
info.executable = (section.Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
sections.push_back(info);
}
auto sectionFor = [&sections](std::uint32_t rva) -> const SectionInfo*
{
for (const SectionInfo& section : sections)
{
if (rva >= section.rvaBegin && rva < section.rvaEnd)
{
return &section;
}
}
return nullptr;
};
if (nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT)
{
return result;
}
const IMAGE_DATA_DIRECTORY& exportDirEntry = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDirEntry.VirtualAddress == 0 || exportDirEntry.Size == 0)
{
return result; // no exports at all
}
const auto* exportDir = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(base + exportDirEntry.VirtualAddress);
const auto* nameRvas = reinterpret_cast<const DWORD*>(base + exportDir->AddressOfNames);
const auto* nameOrdinals = reinterpret_cast<const WORD*>(base + exportDir->AddressOfNameOrdinals);
const auto* functionRvas = reinterpret_cast<const DWORD*>(base + exportDir->AddressOfFunctions);
const std::uint32_t exportDirBegin = exportDirEntry.VirtualAddress;
const std::uint32_t exportDirEnd = exportDirEntry.VirtualAddress + exportDirEntry.Size;
struct Candidate
{
std::string name;
std::uint32_t rva;
bool isFunction;
};
std::vector<Candidate> candidates;
for (DWORD i = 0; i < exportDir->NumberOfNames; ++i)
{
const char* name = rvaToPointer(base, nameRvas[i]);
WORD ordinal = nameOrdinals[i];
if (ordinal >= exportDir->NumberOfFunctions)
{
continue;
}
std::uint32_t functionRva = functionRvas[ordinal];
if (functionRva == 0)
{
continue;
}
// A forwarder export's "RVA" actually points at a string
// like "OtherDll.OtherFunction" living inside the export
// directory itself, not at real code -- never classified
// as a patchable function.
bool isForwarder = functionRva >= exportDirBegin && functionRva < exportDirEnd;
const SectionInfo* section = sectionFor(functionRva);
bool isFunction = !isForwarder && section != nullptr && section->executable;
candidates.push_back(Candidate{name, functionRva, isFunction});
}
// Best-effort size estimate: distance to the next function
// export in the same section. This can OVER-estimate (there
// may be non-exported code, or alignment padding, between two
// exported functions, so a large gap does not guarantee a
// large real function) -- it only ever gives a hard "not
// enough room" answer when the gap itself is already small.
// PE has no per-export size field the way ELF does (see
// below), so /hotpatch (applied automatically by
// cmake/HotReload.cmake) is the primary safety mechanism on
// Windows; this is a secondary backstop, not a substitute.
std::vector<std::uint32_t> functionRvasSorted;
for (const Candidate& candidate : candidates)
{
if (candidate.isFunction)
{
functionRvasSorted.push_back(candidate.rva);
}
}
std::sort(functionRvasSorted.begin(), functionRvasSorted.end());
for (const Candidate& candidate : candidates)
{
ExportedSymbol symbol;
symbol.name = candidate.name;
symbol.address = base + candidate.rva;
symbol.isFunction = candidate.isFunction;
symbol.size = 0;
if (candidate.isFunction)
{
const SectionInfo* section = sectionFor(candidate.rva);
std::uint32_t upperBound = section != nullptr ? section->rvaEnd : candidate.rva;
auto it = std::upper_bound(functionRvasSorted.begin(), functionRvasSorted.end(), candidate.rva);
if (it != functionRvasSorted.end())
{
upperBound = std::min(upperBound, *it);
}
symbol.size = upperBound > candidate.rva ? (upperBound - candidate.rva) : 0;
}
result.push_back(std::move(symbol));
}
return result;
}
#else
// ---------------------------------------------------------------
// ELF / POSIX: read imagePath on disk rather than the mapped
// image. Reading from disk means walking the ordinary section
// header table (.dynsym / .dynstr, plus every section's sh_flags
// for the executable check) instead of re-deriving the same
// information from the .dynamic section's DT_* tags the way the
// dynamic linker itself does internally -- simpler to get right,
// at the cost of one known gap: a shared object with its section
// header table deliberately stripped (rare -- an explicit, extra
// step beyond an ordinary `strip`, essentially never done to a
// library you are actively iterating on) will read back as having
// no exported functions at all, handled the same as any other
// unparseable image: an empty result, never a crash.
//
// st_value in the file is relative to the module's own link-time
// base, so it is loadedBase (the actual runtime load address --
// the "load bias" ModuleHandle reads via dlinfo) that turns it
// into a real, callable address.
// ---------------------------------------------------------------
#include <elf.h>
namespace
{
std::vector<char> readWholeFile(const std::string& path)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file)
{
return {};
}
std::streamsize length = file.tellg();
if (length <= 0)
{
return {};
}
file.seekg(0, std::ios::beg);
std::vector<char> buffer(static_cast<std::size_t>(length));
if (!file.read(buffer.data(), length))
{
return {};
}
return buffer;
}
/// Parses one ELF class (32 or 64-bit); Ehdr/Shdr/Sym is
/// Elf32_* or Elf64_* depending on the caller, matched against
/// e_ident[EI_CLASS] before this is ever instantiated.
template <typename Ehdr, typename Shdr, typename Sym, typename SymType, typename SymBind>
std::vector<ExportedSymbol> parseElf(
const std::vector<char>& file, void* loadedBase, SymType symType, SymBind symBind)
{
std::vector<ExportedSymbol> result;
if (file.size() < sizeof(Ehdr))
{
return result;
}
const auto* ehdr = reinterpret_cast<const Ehdr*>(file.data());
if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0)
{
return result; // no section header table -- see file comment above
}
if (static_cast<std::uint64_t>(ehdr->e_shoff) + static_cast<std::uint64_t>(ehdr->e_shnum) * sizeof(Shdr) >
file.size())
{
return result; // truncated/corrupt file
}
const auto* sections = reinterpret_cast<const Shdr*>(file.data() + ehdr->e_shoff);
// Section header string table, to find sections by name.
if (ehdr->e_shstrndx >= ehdr->e_shnum)
{
return result;
}
const char* shstrtab = file.data() + sections[ehdr->e_shstrndx].sh_offset;
const Shdr* dynsymSection = nullptr;
const Shdr* dynstrSection = nullptr;
for (std::size_t i = 0; i < ehdr->e_shnum; ++i)
{
const char* name = shstrtab + sections[i].sh_name;
if (std::strcmp(name, ".dynsym") == 0)
{
dynsymSection = &sections[i];
}
else if (std::strcmp(name, ".dynstr") == 0)
{
dynstrSection = &sections[i];
}
}
if (dynsymSection == nullptr || dynstrSection == nullptr)
{
return result;
}
const char* dynstr = file.data() + dynstrSection->sh_offset;
std::size_t symbolCount = dynsymSection->sh_size / sizeof(Sym);
const auto* symbols = reinterpret_cast<const Sym*>(file.data() + dynsymSection->sh_offset);
for (std::size_t i = 0; i < symbolCount; ++i)
{
const Sym& sym = symbols[i];
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
{
continue; // imported/unresolved, not defined here
}
unsigned char bind = symBind(sym.st_info);
if (bind != STB_GLOBAL && bind != STB_WEAK)
{
continue; // not part of this module's visible surface
}
ExportedSymbol exported;
exported.name = dynstr + sym.st_name;
exported.address = static_cast<char*>(loadedBase) + sym.st_value;
exported.size = static_cast<std::size_t>(sym.st_size);
bool isFuncType = symType(sym.st_info) == STT_FUNC;
bool sectionExecutable = false;
if (sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON && sym.st_shndx < ehdr->e_shnum)
{
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
}
exported.isFunction = isFuncType && sectionExecutable;
result.push_back(std::move(exported));
}
return result;
}
}
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase)
{
std::vector<char> file = readWholeFile(imagePath);
if (file.size() < EI_NIDENT)
{
return {};
}
unsigned char elfClass = static_cast<unsigned char>(file[EI_CLASS]);
if (elfClass == ELFCLASS64)
{
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(
file, loadedBase, [](unsigned char info) { return ELF64_ST_TYPE(info); },
[](unsigned char info) { return ELF64_ST_BIND(info); });
}
if (elfClass == ELFCLASS32)
{
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(
file, loadedBase, [](unsigned char info) { return ELF32_ST_TYPE(info); },
[](unsigned char info) { return ELF32_ST_BIND(info); });
}
return {};
}
#endif
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
namespace hot_reload::detail
{
/// One exported symbol found in a loaded module, already classified.
struct ExportedSymbol
{
std::string name;
void* address = nullptr;
/// The single load-bearing safety gate behind the "only
/// executable code changes" guarantee in hot_reload.h: false
/// for anything not mechanically identified as code (a data
/// object, a forwarder export, or a symbol kind this reader
/// does not recognize at all). HotReloadContext never patches
/// a symbol where this is false, on either side of a reload.
bool isFunction = false;
/// Compiled size in bytes if known, 0 if not. ELF gives this
/// exactly (the symbol table's st_size field); PE gives no
/// such field, so this holds a conservative estimate derived
/// from the distance to the next exported function in the
/// same section -- see the file comment in SymbolTable.cpp
/// for exactly what that heuristic can and cannot catch.
std::size_t size = 0;
};
/// Reads every exported, defined symbol belonging to the module
/// mapped at loadedBase (the base address dlopen()/LoadLibrary()
/// returned for it), classifying each one as code or data.
///
/// On POSIX this parses imagePath on disk (its ELF .dynsym /
/// .dynstr / section-header table) and adds loadedBase as the
/// runtime load bias. On Windows this ignores imagePath and reads
/// directly from the already-mapped image at loadedBase (its PE
/// export directory and section table) -- see the per-platform
/// implementation blocks in SymbolTable.cpp for why each platform
/// reads from a different place.
///
/// Returns an empty vector, never throws, if the image could not
/// be parsed as this platform's native format (including the rare
/// case of a .so with its section header table deliberately
/// stripped -- see the file comment in SymbolTable.cpp).
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase);
}
+48
View File
@@ -0,0 +1,48 @@
---
# source/HotReload/src/c-api/.clang-tidy
#
# hot_reload.h's implementation (HotReloadApi.cpp) is Part II (C API)
# style -- snake_case, per style guide §13 -- even though it is a
# .cpp file living inside a C++ library. Copied from the repo-root
# c-api/.clang-tidy template (see that file's own header comment for
# the full explanation of how clang-tidy's directory-scoped config
# resolution makes this work); the sibling directory one level up
# (source/HotReload/src/) has no override of its own, so its Part I
# (C++) files keep inheriting the repo-root .clang-tidy's PascalCase/
# camelCase rules exactly as normal.
InheritParentConfig: true
Checks: >
readability-identifier-naming,
bugprone-exception-escape
CheckOptions:
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.LocalVariableCase
value: lower_case
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalVariableCase
value: lower_case
- key: readability-identifier-naming.GlobalVariablePrefix
value: g_
- key: readability-identifier-naming.StaticVariableCase
value: lower_case
- key: readability-identifier-naming.StaticVariablePrefix
value: s_
+90
View File
@@ -0,0 +1,90 @@
/* Demangle.cpp -- hot_reload_demangle() / hot_reload_free_string().
*
* Pure string utility, no HotReloadContext involved, so it gets its
* own file alongside HotReloadApi.cpp rather than crowding that one's
* single responsibility (the opaque-pointer shim). Part II (C API)
* style, like everything else in this directory -- see .clang-tidy
* here and cpp-style-guide.md section 13.
*
* Every C++ symbol name in this library's events/lookups is always
* the REAL symbol-table name (whatever hot_reload_get_symbol() needs
* to find it again) -- this function is an opt-in convenience for
* DISPLAYING that name, never applied automatically, so there is
* never a mismatch between what an event reports and what you'd pass
* back in to look the symbol up.
*/
#include "hot_reload/hot_reload.h"
#include <cstring>
#include <string>
#if defined(__GNUC__) || defined(__clang__)
// Covers GCC and Clang on every OS this library targets, MinGW
// included: what selects the demangling scheme is the C++ ABI the
// COMPILER implements (Itanium here), not the operating system --
// MinGW is still GCC/Clang generating Itanium-mangled names even
// though the OS is Windows. MSVC, on the same OS, uses its own,
// unrelated scheme (the #elif below). See README.md's "Hot
// reloading C++ symbols" section for why this distinction matters
// beyond just this function.
#include <cxxabi.h>
#elif defined(_MSC_VER)
#include <dbghelp.h>
#include <windows.h>
#endif
extern "C"
{
char* hot_reload_demangle(const char* name)
{
if (name == nullptr)
{
return nullptr;
}
std::string text = name; // fallback: unchanged, for anything that isn't a mangled C++ name at all
try
{
#if defined(__GNUC__) || defined(__clang__)
int status = 0;
char* demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status);
if (status == 0 && demangled != nullptr)
{
text = demangled;
}
std::free(demangled); // __cxa_demangle always malloc()s or leaves this null either way
#elif defined(_MSC_VER)
// UnDecorateSymbolName operates on the string alone (no
// SymInitialize/session needed, unlike most of
// DbgHelp) -- but DbgHelp as a whole is documented as not
// thread-safe, so serialize calls to this function
// yourself if you call it from more than one thread.
// Written against Microsoft's documented behavior; unlike
// most of this library, this specific function was not
// exercised on real Windows -- see README.md's toolchain
// verification notes.
char buffer[2048];
if (UnDecorateSymbolName(name, buffer, sizeof(buffer), UNDNAME_COMPLETE) != 0)
{
text = buffer;
}
#endif
char* result = new char[text.size() + 1];
std::memcpy(result, text.c_str(), text.size() + 1);
return result;
}
catch (...)
{
return nullptr;
}
}
void hot_reload_free_string(char* str)
{
delete[] str;
}
}
+130
View File
@@ -0,0 +1,130 @@
#include "hot_reload/hot_reload.h"
#include "../HotReloadContext.hpp"
namespace
{
/// The opaque ::HotReloadContext the public header declares is
/// never actually instantiated as itself -- every instance really
/// is a hot_reload::HotReloadContext, following the same
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
/// example uses. This is the one place that cast happens.
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
{
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
}
}
// Per cpp-style-guide.md section 11: a C++ exception must never cross
// an extern "C" boundary -- doing so is undefined behavior for any
// caller that is not itself compiled to expect one, which a plain C
// caller of this API never is. Every function here is a thin,
// non-throwing wrapper for exactly that reason: std::string/vector/
// unordered_map construction is the only thing in hot_reload::
// HotReloadContext that could throw at all (std::bad_alloc under
// memory pressure), so catching (...) and mapping it to this API's
// existing failure values is sufficient without needing per-exception-
// type handling.
extern "C"
{
HotReloadContext* hot_reload_context_create(const HotReloadOptions* options)
{
try
{
return reinterpret_cast<::HotReloadContext*>(new hot_reload::HotReloadContext(options));
}
catch (...)
{
return nullptr;
}
}
void hot_reload_context_destroy(HotReloadContext* ctx)
{
delete impl(ctx);
}
HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path)
{
if (ctx == nullptr || module_name == nullptr || path == nullptr)
{
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
}
try
{
return impl(ctx)->load(module_name, path);
}
catch (...)
{
return HOT_RELOAD_ERROR_LOAD_FAILED;
}
}
HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path)
{
if (ctx == nullptr || module_name == nullptr || new_path == nullptr)
{
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
}
try
{
return impl(ctx)->reload(module_name, new_path);
}
catch (...)
{
return HOT_RELOAD_ERROR_LOAD_FAILED;
}
}
void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name)
{
if (ctx == nullptr || module_name == nullptr || symbol_name == nullptr)
{
return nullptr;
}
try
{
return impl(ctx)->getSymbol(module_name, symbol_name);
}
catch (...)
{
return nullptr;
}
}
const char* hot_reload_last_error(HotReloadContext* ctx)
{
if (ctx == nullptr)
{
return "";
}
try
{
return impl(ctx)->lastError().c_str();
}
catch (...)
{
return "";
}
}
const char* hot_reload_status_string(HotReloadStatus status)
{
switch (status)
{
case HOT_RELOAD_OK:
return "ok";
case HOT_RELOAD_ERROR_INVALID_ARGUMENT:
return "invalid argument";
case HOT_RELOAD_ERROR_LOAD_FAILED:
return "load failed";
case HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION:
return "no previous version loaded for this module name";
case HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE:
return "no patch backend for this CPU architecture";
}
return "unknown hot reload status";
}
}