Add project files.
This commit is contained in:
commit
3087fcea8a
71 files changed
+8325
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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 = [§ions](std::uint32_t rva) -> const SectionInfo*
|
||||
{
|
||||
for (const SectionInfo& section : sections)
|
||||
{
|
||||
if (rva >= section.rvaBegin && rva < section.rvaEnd)
|
||||
{
|
||||
return §ion;
|
||||
}
|
||||
}
|
||||
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 = §ions[i];
|
||||
}
|
||||
else if (std::strcmp(name, ".dynstr") == 0)
|
||||
{
|
||||
dynstrSection = §ions[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
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user