Added ARM32 support
This commit is contained in:
1 parent
df792729ef
commit
1212cb9dbe
36 files changed
+2381
-783
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @file Architecture.cpp
|
||||
* @brief Per-architecture jump-patch backends for x86, x86-64, and ARM64.
|
||||
* @brief Per-architecture jump-patch backends for x86, x86-64, ARM64, and ARM32 (both the ARM
|
||||
* and Thumb-2 instruction sets).
|
||||
*/
|
||||
#include "Architecture.hpp"
|
||||
|
||||
@@ -218,13 +219,151 @@ namespace hot_reload::detail
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
}
|
||||
|
||||
#elif defined(__arm__) || defined(_M_ARM)
|
||||
|
||||
/* 32-bit ARM is a genuinely different shape from the three backends above: every
|
||||
* other architecture here picks one instruction encoding and a single fixed
|
||||
* kJumpBytes. ARM32 cannot, for a reason specific to this architecture: a CPU in
|
||||
* this family is always in one of two incompatible instruction-decoding states, ARM
|
||||
* (fixed 4-byte instructions) or Thumb (2-byte and 4-byte Thumb-2 instructions), and
|
||||
* a function's own compiled state -- never a free choice this library gets to make,
|
||||
* only something to detect and match -- decides which encoding is even legal to
|
||||
* write at its address. The ARM EABI records this on every function symbol: bit 0 of
|
||||
* its value is set if and only if the function is Thumb code (so that loading the
|
||||
* symbol's own value into pc, exactly what this backend's jump does, switches the
|
||||
* CPU to the right state automatically) -- SymbolTable.cpp's `loadedBase + st_value`
|
||||
* already preserves that bit unmodified for every architecture, so detecting the
|
||||
* mode here is as simple as reading it back off the address this backend is handed.
|
||||
*
|
||||
* A second, smaller wrinkle is specific to Thumb state alone: its `ldr pc, [...]`
|
||||
* literal-load form can only read from a 4-byte-aligned address, computed from the
|
||||
* instruction's OWN address rounded down to 4 bytes -- but a Thumb function is only
|
||||
* ever guaranteed 2-byte alignment, not 4 (confirmed directly: building a plugin
|
||||
* with -falign-functions=1 reliably produces functions starting 2 bytes off a
|
||||
* 4-byte boundary). So Thumb state alone needs a one-instruction fork: a 2-byte NOP
|
||||
* realigns first when the entry isn't already on a 4-byte boundary, nothing extra
|
||||
* when it already is. ARM state needs no such fork -- that state requires 4-byte
|
||||
* alignment architecture-wide, so every ARM-mode function already has it.
|
||||
*
|
||||
* No landing pad exists on this architecture in any mainstream toolchain or CPU
|
||||
* extension in common use (unlike x86 CET or ARM64 BTI) -- confirmed directly:
|
||||
* -fpatchable-function-entry's reserved padding here is plain NOPs in both states,
|
||||
* nothing else, so there is never anything to detect or preserve in front of a
|
||||
* function's real body.
|
||||
*
|
||||
* Every byte sequence below was derived the same way as the other three backends:
|
||||
* assembled with `as` from the exact mnemonics named in each comment, and the
|
||||
* resulting bytes read back with objdump -- including, for the Thumb realignment
|
||||
* case specifically, assembling it at both a 4-byte-aligned and a 2-mod-4 address
|
||||
* and confirming objdump's own computed literal-load target lands exactly on the
|
||||
* embedded address both times.
|
||||
*/
|
||||
|
||||
/// No landing pad exists on this architecture (see the file comment above), so unlike
|
||||
/// the three backends above, there is no hasLandingPad()/kLandingPadBytes pair here:
|
||||
/// the outer planPatch() below sets PatchLayout::patchOffset to 0 directly instead.
|
||||
|
||||
/// @param address A function's address as this library carries it: `loadedBase +
|
||||
/// st_value`, Thumb's mode bit (bit 0) included unmodified.
|
||||
/// @return True if @p address names Thumb code, per the ARM EABI convention.
|
||||
bool isThumb(const void* address)
|
||||
{
|
||||
return (reinterpret_cast<std::uintptr_t>(address) & std::uintptr_t{1}) != 0;
|
||||
}
|
||||
|
||||
/// @param address A function's address, Thumb mode bit possibly included.
|
||||
/// @return The real, even byte address of its first instruction -- bit 0 is a mode
|
||||
/// marker on this architecture, never part of the actual memory location.
|
||||
unsigned char* realAddress(const void* address)
|
||||
{
|
||||
return reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(address) & ~std::uintptr_t{1});
|
||||
}
|
||||
|
||||
/// `ldr pc, [pc, #-4]`: ARM state's literal-load jump. ARM state is always 4-byte
|
||||
/// aligned architecture-wide, so this one fixed form always applies; the embedded
|
||||
/// address follows immediately, 8 bytes total.
|
||||
constexpr unsigned char kArmJump[4] = {0x04, 0xF0, 0x1F, 0xE5};
|
||||
|
||||
/// `ldr.w pc, [pc, #0]`: Thumb-2's literal-load jump, valid only when this
|
||||
/// instruction's own address is 4-byte aligned (see the file comment above); the
|
||||
/// embedded address follows immediately, 8 bytes total from wherever this is placed.
|
||||
constexpr unsigned char kThumbJump[4] = {0xDF, 0xF8, 0x00, 0xF0};
|
||||
|
||||
/// The Thumb-2 `nop` hint instruction -- matches the padding GCC's own
|
||||
/// -fpatchable-function-entry emits on this target -- used only to realign kThumbJump
|
||||
/// onto a 4-byte boundary when the function's own entry isn't already on one.
|
||||
constexpr unsigned char kThumbNop[2] = {0x00, 0xBF};
|
||||
|
||||
/// @param entryAddress The function's entry address, Thumb bit included.
|
||||
/// @return 8 for ARM state or an already-4-byte-aligned Thumb entry; 10 for a Thumb
|
||||
/// entry starting 2 bytes off a 4-byte boundary (room for the one extra
|
||||
/// realigning NOP in front of kThumbJump -- see the file comment above).
|
||||
/// Unlike every other backend in this file, this one genuinely depends on
|
||||
/// the entry address itself, not just the architecture.
|
||||
std::size_t jumpBytes(const void* entryAddress)
|
||||
{
|
||||
if (!isThumb(entryAddress))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
bool misaligned = (reinterpret_cast<std::uintptr_t>(realAddress(entryAddress)) & std::uintptr_t{2}) != 0;
|
||||
return misaligned ? 10 : 8;
|
||||
}
|
||||
|
||||
/// Writes the ARM32 jump at @p site: the ARM-state form if @p site is ARM code, the
|
||||
/// Thumb-2 form (with a realigning NOP first, if needed) if it is Thumb -- @p site's
|
||||
/// own bit 0 says which, exactly as isThumb() reads it, and there is no separate
|
||||
/// landing pad to skip past on this architecture (see hasLandingPad()), so @p site is
|
||||
/// always the function's own entry address, unmodified.
|
||||
/// @param site Where the jump goes.
|
||||
/// @param target Address the jump transfers control to, Thumb bit preserved so the
|
||||
/// `ldr` that loads it into pc correctly switches state at the destination,
|
||||
/// too, exactly as calling this address normally would.
|
||||
void writeJumpAt(const void* site, const void* target)
|
||||
{
|
||||
std::uint32_t targetValue = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(target));
|
||||
unsigned char* bytes = realAddress(site);
|
||||
|
||||
if (!isThumb(site))
|
||||
{
|
||||
// Payload first, opcode last -- same reasoning as every other backend above.
|
||||
std::memcpy(bytes + 4, &targetValue, sizeof(targetValue));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
std::memcpy(bytes, kArmJump, sizeof(kArmJump));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
bool misaligned = (reinterpret_cast<std::uintptr_t>(bytes) & std::uintptr_t{2}) != 0;
|
||||
unsigned char* jumpAt = misaligned ? bytes + 2 : bytes;
|
||||
|
||||
// The realigning NOP, if there is one, is pure filler -- inert whether a reader
|
||||
// sees it before or after this write, and byte-for-byte identical on every
|
||||
// reload of this same (fixed) address, so it is written first, unconditionally,
|
||||
// with no fence of its own: it never participates in the old-jump/new-jump
|
||||
// transition the fences below protect.
|
||||
if (misaligned)
|
||||
{
|
||||
std::memcpy(bytes, kThumbNop, sizeof(kThumbNop));
|
||||
}
|
||||
|
||||
// Payload next, opcode last -- same reasoning as every other backend above: until
|
||||
// kThumbJump lands, jumpAt still reads as whatever it was (old NOP padding, or a
|
||||
// previous ldr.w, on a later reload), and the fully-written literal sitting
|
||||
// ahead of it is inert until something actually loads it.
|
||||
std::memcpy(jumpAt + 4, &targetValue, sizeof(targetValue));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
std::memcpy(jumpAt, kThumbJump, sizeof(kThumbJump));
|
||||
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)
|
||||
defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
@@ -233,7 +372,14 @@ namespace hot_reload::detail
|
||||
|
||||
PatchLayout planPatch(const void* entryAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
#if defined(__arm__) || defined(_M_ARM)
|
||||
// ARM32's own kJumpBytes genuinely depends on entryAddress (ARM state vs. Thumb state,
|
||||
// and for Thumb, alignment) -- see Architecture.cpp's ARM32 file comment above -- so it
|
||||
// cannot share the single-constant shape the other three architectures below do; there
|
||||
// is never a landing pad to skip past on this architecture either (hasLandingPad() is
|
||||
// always false), so patchOffset is always 0.
|
||||
return PatchLayout{jumpBytes(entryAddress), 0};
|
||||
#elif 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;
|
||||
@@ -248,7 +394,14 @@ namespace hot_reload::detail
|
||||
|
||||
bool writeJump(void* entryAddress, const void* targetAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
#if defined(__arm__) || defined(_M_ARM)
|
||||
// entryAddress is passed through unmodified (not cast to unsigned char* the way the
|
||||
// generic path below does): its own bit 0, the Thumb/ARM mode marker, is exactly what
|
||||
// writeJumpAt() needs to pick the right encoding -- see Architecture.cpp's ARM32 file
|
||||
// comment above. There is no landing pad on this architecture to offset past first.
|
||||
writeJumpAt(entryAddress, targetAddress);
|
||||
return true;
|
||||
#elif 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;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* The jump-patch backends (`Architecture.hpp`), the page-protection helper
|
||||
* (`MemoryProtection.hpp`), the loader wrapper (`ModuleHandle.hpp`) and the symbol-table readers
|
||||
* (`SymbolTable.hpp`) all live here. They are implementation details of
|
||||
* hot_reload::HotReloadContext and may change freely; the supported surface is the C API in
|
||||
* `hot_reload.h`.
|
||||
* hot_reload::Context and may change freely; the supported surface is hot_reload::Context itself
|
||||
* and the C API in `hot_reload.h`.
|
||||
*/
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
@@ -44,11 +44,11 @@ namespace hot_reload::detail
|
||||
* @brief Whether this build has a jump-patch backend for the current CPU architecture.
|
||||
*
|
||||
* 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 "Scope" section of `source/HotReload/README.md`) returns false here, and
|
||||
* `HotReloadContext` surfaces that as
|
||||
* ::HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
||||
* x86-64, ARM64, and ARM32 (both its ARM and Thumb-2 instruction
|
||||
* sets) are supported; anything else returns false here, and
|
||||
* `hot_reload::Context::reload()` surfaces that as
|
||||
* `Status::UnsupportedArchitecture` (`::HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE` through
|
||||
* the C API).
|
||||
*
|
||||
* @return True if writeJump() can succeed on this architecture.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* @file Context.cpp
|
||||
* @brief Context::Impl -- the diff-and-patch orchestration logic: load, reload, per-symbol
|
||||
* policy, and the reader-writer lock that makes all three safe to call concurrently.
|
||||
*/
|
||||
#include "hot_reload/Context.hpp"
|
||||
|
||||
#include "Architecture.hpp"
|
||||
#include "MemoryProtection.hpp"
|
||||
#include "ModuleHandle.hpp"
|
||||
#include "SymbolTable.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Copies a file byte for byte, overwriting the destination.
|
||||
/// @param from The file to read.
|
||||
/// @param to The file to create (or truncate).
|
||||
/// @return True if the whole copy succeeded. An empty source counts as a failure (inserting
|
||||
/// zero characters into an output stream sets its failbit) -- harmless, since an
|
||||
/// empty file is never a loadable module anyway.
|
||||
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);
|
||||
}
|
||||
|
||||
/// The module name as it appears inside a staging copy's file name.
|
||||
///
|
||||
/// moduleName is an arbitrary caller-chosen key (Context::load()'s own doc comment
|
||||
/// promises it need not match anything on disk), so it may contain characters that are
|
||||
/// not legal in a file name -- a '/' in "engine/renderer", say. Everything outside a
|
||||
/// conservative set is replaced by '_'. stagePathForLoad() and
|
||||
/// cleanupOrphanedStagingFiles() must agree on the result, so both call this. Names made
|
||||
/// only of letters, digits, '-' and '_' are unchanged.
|
||||
///
|
||||
/// @param moduleName The logical module name.
|
||||
/// @return @p moduleName with every character outside `[A-Za-z0-9_-]` replaced by '_'.
|
||||
std::string stagingTag(std::string_view moduleName)
|
||||
{
|
||||
std::string tag(moduleName);
|
||||
for (char& character : tag)
|
||||
{
|
||||
const bool isLower = character >= 'a' && character <= 'z';
|
||||
const bool isUpper = character >= 'A' && character <= 'Z';
|
||||
const bool isDigit = character >= '0' && character <= '9';
|
||||
const bool isDashOrUnderscore = character == '-' || character == '_';
|
||||
const bool safe = isLower || isUpper || isDigit || isDashOrUnderscore;
|
||||
if (!safe)
|
||||
{
|
||||
character = '_';
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/// Best-effort: deletes any staging copy left over from a PREVIOUS run of this same
|
||||
/// host/module -- see stagePathForLoad()'s own comment for why those copies exist at
|
||||
/// all.
|
||||
///
|
||||
/// On POSIX this normally finds nothing (an active run's own copies are already gone by
|
||||
/// the time it makes its next one -- see the std::remove() calls after each
|
||||
/// ModuleHandle::load() below). On Windows, a copy backing a still-loaded module cannot
|
||||
/// be deleted for as long as that module stays mapped -- which, by this library's own
|
||||
/// design, is for as long as the owning Context lives, usually the rest of the CURRENT
|
||||
/// process's lifetime -- so within a single long-running session some accumulation is
|
||||
/// expected and unavoidable; this specifically cleans up the PREVIOUS session's
|
||||
/// leftovers once that process has exited and released whatever lock Windows held.
|
||||
/// Called once, at the first load() for a given name -- not on every reload() -- since a
|
||||
/// copy from earlier in the SAME session would still be locked for the same reason and
|
||||
/// retrying wouldn't change that.
|
||||
///
|
||||
/// @param sourcePath The path the module is being loaded from; the copies live next to it.
|
||||
/// @param moduleName The module's logical name.
|
||||
void cleanupOrphanedStagingFiles(const std::string& sourcePath, std::string_view moduleName)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path source(sourcePath);
|
||||
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
std::string prefix = source.filename().string() + ".hotreload." + tag + ".";
|
||||
|
||||
std::error_code dirError;
|
||||
fs::directory_iterator entries(dir, dirError);
|
||||
if (dirError)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const fs::directory_entry& entry : entries)
|
||||
{
|
||||
std::error_code fileTypeError;
|
||||
if (!entry.is_regular_file(fileTypeError) || fileTypeError)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string filename = entry.path().filename().string();
|
||||
if (filename.compare(0, prefix.size(), prefix) == 0)
|
||||
{
|
||||
std::error_code removeError;
|
||||
fs::remove(entry.path(), removeError); // best-effort; a failure here just means try again next launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Context's real implementation; see `Context.hpp`'s own doc comment for the public
|
||||
* contract this upholds (including its thread-safety guarantee, which m_mutex is what
|
||||
* actually provides: a `std::shared_mutex` locked exclusively for load()/reload() and
|
||||
* shared for findSymbol(), so concurrent lookups run in parallel with each other while a
|
||||
* reload briefly excludes all of them).
|
||||
*
|
||||
* Forward-declared only in Context.hpp (never defined there), which is what lets that header
|
||||
* stay free of every detail:: header this file includes -- a caller who only `#include
|
||||
* "hot_reload/Context.hpp"` never sees ModuleHandle.hpp, SymbolTable.hpp, or either of their
|
||||
* own dependencies, and a change to any of those never forces that caller to recompile.
|
||||
*/
|
||||
struct Context::Impl
|
||||
{
|
||||
/// Everything tracked for one logical module name. Has no meaning outside Impl, 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 as long as the owning
|
||||
/// Context lives (in practice usually the rest of the process): once a function is
|
||||
/// patched, anything that already holds its old address must stay valid memory,
|
||||
/// even though it now holds nothing but a jump. versions.back() is the active one.
|
||||
std::vector<detail::ModuleHandle> versions;
|
||||
|
||||
/// The exported symbols of the newest build (versions.back()), as reported by
|
||||
/// detail::readExportedSymbols(); what the next reload() diffs against.
|
||||
std::vector<detail::ExportedSymbol> currentSymbols;
|
||||
|
||||
/// How many staging copies have been made for this name so far; makes each copy's
|
||||
/// file name unique (see stagePathForLoad()).
|
||||
int reloadCounter = 0;
|
||||
};
|
||||
|
||||
explicit Impl(Options options)
|
||||
: m_options(std::move(options))
|
||||
{
|
||||
}
|
||||
|
||||
Result load(std::string_view moduleName, const std::filesystem::path& path)
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
|
||||
const std::string key(moduleName);
|
||||
if (m_modules.find(key) != 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 Result::failure(Status::InvalidArgument);
|
||||
}
|
||||
|
||||
const std::string sourcePath = path.string();
|
||||
cleanupOrphanedStagingFiles(sourcePath, moduleName);
|
||||
|
||||
LoadedModule module;
|
||||
std::string staged = stagePathForLoad(module, moduleName, sourcePath);
|
||||
if (staged.empty())
|
||||
{
|
||||
return Result::failure(
|
||||
Status::LoadFailed,
|
||||
"could not stage '" + sourcePath + "' for loading (source unreadable, or destination path unwritable)");
|
||||
}
|
||||
|
||||
detail::ModuleHandle handle = detail::ModuleHandle::load(staged);
|
||||
if (!handle.isValid())
|
||||
{
|
||||
std::string message = handle.errorMessage();
|
||||
std::remove(staged.c_str());
|
||||
return Result::failure(Status::LoadFailed, std::move(message));
|
||||
}
|
||||
|
||||
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(key, std::move(module));
|
||||
return Result::success();
|
||||
}
|
||||
|
||||
Result reload(std::string_view moduleName, const std::filesystem::path& newPath)
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
|
||||
const std::string key(moduleName);
|
||||
auto it = m_modules.find(key);
|
||||
if (it == m_modules.end())
|
||||
{
|
||||
return Result::failure(Status::NoPreviousVersion);
|
||||
}
|
||||
if (!detail::isArchitectureSupported())
|
||||
{
|
||||
return Result::failure(Status::UnsupportedArchitecture);
|
||||
}
|
||||
|
||||
LoadedModule& module = it->second;
|
||||
|
||||
const std::string sourcePath = newPath.string();
|
||||
std::string staged = stagePathForLoad(module, moduleName, sourcePath);
|
||||
if (staged.empty())
|
||||
{
|
||||
return Result::failure(
|
||||
Status::LoadFailed,
|
||||
"could not stage '" + sourcePath + "' for loading (source unreadable, or destination path unwritable)");
|
||||
}
|
||||
|
||||
detail::ModuleHandle newHandle = detail::ModuleHandle::load(staged);
|
||||
if (!newHandle.isValid())
|
||||
{
|
||||
std::string message = newHandle.errorMessage();
|
||||
std::remove(staged.c_str());
|
||||
return Result::failure(Status::LoadFailed, std::move(message)); // 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(SymbolEvent::SkippedNotCode, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
continue;
|
||||
}
|
||||
|
||||
patchOne(oldSymbol, newSymbol);
|
||||
}
|
||||
|
||||
for (const detail::ExportedSymbol& newSymbol : newSymbols)
|
||||
{
|
||||
if (matchedNewNames.find(newSymbol.name) == matchedNewNames.end())
|
||||
{
|
||||
emitEvent(SymbolEvent::Added, newSymbol.name, nullptr, newSymbol.address);
|
||||
}
|
||||
}
|
||||
|
||||
module.currentSymbols = std::move(newSymbols);
|
||||
module.versions.push_back(std::move(newHandle));
|
||||
return Result::success();
|
||||
}
|
||||
|
||||
void* findSymbol(std::string_view moduleName, std::string_view symbolName) const noexcept
|
||||
{
|
||||
std::shared_lock lock(m_mutex);
|
||||
|
||||
auto it = m_modules.find(std::string(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;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Stages a source file through a fresh, uniquely-named copy.
|
||||
*
|
||||
* Called before a path is ever passed to detail::ModuleHandle::load(): 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.
|
||||
*
|
||||
* @param module The module the copy is for; its reloadCounter is advanced.
|
||||
* @param moduleName The module's logical name (used, sanitized, in the copy's file name).
|
||||
* @param sourcePath The file to copy.
|
||||
* @return The copy's path, or an empty string if copying failed.
|
||||
*/
|
||||
static std::string stagePathForLoad(LoadedModule& module, std::string_view moduleName, const std::string& sourcePath)
|
||||
{
|
||||
++module.reloadCounter;
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
const std::string counter = std::to_string(module.reloadCounter);
|
||||
std::string staged = sourcePath + ".hotreload." + tag + "." + counter;
|
||||
if (!copyFile(sourcePath, staged))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return staged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Redirects one matched function pair, or emits a SKIPPED_* event instead if
|
||||
* that isn't safe.
|
||||
*
|
||||
* Patches @p oldSymbol's entry to jump to @p newSymbol's address; emits exactly one
|
||||
* event describing the outcome either way.
|
||||
*
|
||||
* @param oldSymbol The function in the previous build.
|
||||
* @param newSymbol The same-named function in the new build.
|
||||
*/
|
||||
void 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(SymbolEvent::SkippedTooSmall, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
return;
|
||||
}
|
||||
|
||||
detail::ScopedCodeWriteAccess writable(oldSymbol.address, layout.totalBytes);
|
||||
if (!writable.isValid())
|
||||
{
|
||||
emitEvent(SymbolEvent::SkippedUnwritable, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
return;
|
||||
}
|
||||
|
||||
detail::writeJump(oldSymbol.address, newSymbol.address);
|
||||
writable.finishWrite();
|
||||
emitEvent(SymbolEvent::Patched, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles a symbol present in the old build but missing from the new one.
|
||||
*
|
||||
* Emits SymbolEvent::Removed, and first redirects to a caller-supplied stub if
|
||||
* Options::onDeletedSymbol provides one and it's safe to do so.
|
||||
*
|
||||
* @param oldSymbol The symbol the new build no longer exports.
|
||||
*/
|
||||
void 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_options.onDeletedSymbol)
|
||||
{
|
||||
stub = m_options.onDeletedSymbol(oldSymbol.name);
|
||||
}
|
||||
|
||||
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
|
||||
// SymbolEvent::Removed below for what is already a rare, best-effort extra.
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(SymbolEvent::Removed, oldSymbol.name, oldSymbol.address, nullptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Invokes Options::onSymbolEvent, if one was supplied.
|
||||
* @param kind What happened to the symbol.
|
||||
* @param name The symbol's real (possibly mangled) name.
|
||||
* @param oldAddress The symbol's address in the previous build, or null.
|
||||
* @param newAddress The symbol's address in the new build, or null.
|
||||
*/
|
||||
void emitEvent(SymbolEvent kind, std::string_view name, void* oldAddress, void* newAddress)
|
||||
{
|
||||
if (!m_options.onSymbolEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SymbolEventInfo info;
|
||||
info.name = name;
|
||||
info.kind = kind;
|
||||
info.oldAddress = oldAddress;
|
||||
info.newAddress = newAddress;
|
||||
m_options.onSymbolEvent(info);
|
||||
}
|
||||
|
||||
/// Guards every member below. Exclusive (`std::unique_lock`) for load()/reload(),
|
||||
/// shared (`std::shared_lock`) for findSymbol() -- see this class's own doc comment.
|
||||
/// `mutable` because findSymbol() is logically const but still needs to lock it.
|
||||
mutable std::shared_mutex m_mutex;
|
||||
|
||||
/// Every module loaded so far, by logical name. Guarded by m_mutex.
|
||||
std::unordered_map<std::string, LoadedModule> m_modules;
|
||||
|
||||
/// Copied out of the constructor's argument once; never changes after that, so reading
|
||||
/// it (onSymbolEvent/onDeletedSymbol, both only ever called from inside a load()/
|
||||
/// reload() call that already holds m_mutex) needs no additional synchronization of its
|
||||
/// own.
|
||||
Options m_options;
|
||||
};
|
||||
|
||||
Context::Context(Options options)
|
||||
: m_impl(std::make_unique<Impl>(std::move(options)))
|
||||
{
|
||||
}
|
||||
|
||||
Context::~Context() = default;
|
||||
|
||||
Context::Context(Context&& other) noexcept = default;
|
||||
|
||||
Context& Context::operator=(Context&& other) noexcept = default;
|
||||
|
||||
Result Context::load(std::string_view moduleName, const std::filesystem::path& path)
|
||||
{
|
||||
return m_impl->load(moduleName, path);
|
||||
}
|
||||
|
||||
Result Context::reload(std::string_view moduleName, const std::filesystem::path& newPath)
|
||||
{
|
||||
return m_impl->reload(moduleName, newPath);
|
||||
}
|
||||
|
||||
void* Context::findSymbol(std::string_view moduleName, std::string_view symbolName) const noexcept
|
||||
{
|
||||
return m_impl->findSymbol(moduleName, symbolName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @file Demangle.cpp
|
||||
* @brief hot_reload::demangle() -- the real demangling logic (hot_reload_demangle() wraps this).
|
||||
*/
|
||||
#include "hot_reload/Demangle.hpp"
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
// _MSC_VER, checked before __GNUC__/__clang__ below, and deliberately so: Clang defines
|
||||
// _MSC_VER too, alongside its own __clang__, whenever it is targeting the MSVC ABI (its
|
||||
// default on Windows unless pointed at a MinGW sysroot instead) -- confirmed directly by a
|
||||
// real failure this ordering used to produce, building with clang++.exe against the MSVC
|
||||
// runtime (_DLL/_MT/--dependent-lib=msvcrt on its own command line are the tell): checking
|
||||
// __clang__ first took the #else branch below and failed to find <cxxabi.h>, which does not
|
||||
// exist in that configuration at all -- Clang-targeting-MSVC emits MSVC-mangled names, the
|
||||
// same as real cl.exe, not Itanium ones, so UnDecorateSymbolName is what it actually needs.
|
||||
// _MSC_VER is the right thing to key this on precisely because it tracks the ABI/mangling
|
||||
// scheme in use, which is what this file cares about, rather than which compiler frontend
|
||||
// produced it, which on its own does not say that.
|
||||
//
|
||||
// windows.h must come first: dbghelp.h uses types (HANDLE, DWORD,
|
||||
// BOOL, ...) it assumes are already defined rather than including
|
||||
// their defining headers itself -- reversing this order fails to
|
||||
// compile. NOMINMAX prevents windows.h from defining min/max
|
||||
// macros that would otherwise clash with std::min/std::max and
|
||||
// anything else named min/max in this translation unit. Both
|
||||
// fixes below came from an actual Windows build/run of this
|
||||
// library, not just documentation -- see the top-level README's
|
||||
// "What's actually been tested" section.
|
||||
//
|
||||
// WIN32_LEAN_AND_MEAN: see MemoryProtection.cpp's comment for the
|
||||
// full reasoning. dbghelp.h itself needs only the core Win32 types
|
||||
// (HANDLE, DWORD, BOOL, ...) windows.h still provides with this
|
||||
// defined -- not the COM/OLE headers it strips.
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
// Belt-and-suspenders for anyone building this file outside this
|
||||
// project's own CMake (source/HotReload/CMakeLists.txt links
|
||||
// dbghelp explicitly for every MSVC-family build, which is the
|
||||
// primary fix and the one actually required -- confirmed directly
|
||||
// that omitting it leaves UnDecorateSymbolName undefined at link
|
||||
// time regardless of this pragma).
|
||||
#pragma comment(lib, "dbghelp.lib")
|
||||
#include <mutex>
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
// GCC, or Clang NOT targeting the MSVC ABI (the #if above already claimed that case) --
|
||||
// real Linux/macOS builds, and MinGW, where Clang/GCC both still generate Itanium-mangled
|
||||
// names even though the OS is Windows, exactly because _MSC_VER is never defined there.
|
||||
// See the "Hot reloading C++ symbols" section of source/HotReload/README.md for why this
|
||||
// distinction matters beyond just this function.
|
||||
#include <cstdlib>
|
||||
#include <cxxabi.h>
|
||||
#include <memory>
|
||||
#endif
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
namespace
|
||||
{
|
||||
/// DbgHelp -- UnDecorateSymbolName included, by Microsoft's own documentation -- is not
|
||||
/// safe to call from more than one thread at once. Demangle.cpp's own callers (the C++
|
||||
/// API directly, and hot_reload_demangle() through it) make no promise about which
|
||||
/// thread calls this, so this serializes every MSVC-path call through one mutex rather
|
||||
/// than pushing that requirement onto every caller. The GCC/Clang path below needs
|
||||
/// nothing equivalent: `abi::__cxa_demangle` keeps no shared mutable state of its own.
|
||||
std::mutex& dbgHelpMutex()
|
||||
{
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string demangle(std::string_view name)
|
||||
{
|
||||
std::string text(name); // fallback: unchanged, for anything that isn't a mangled C++ name at all
|
||||
|
||||
try
|
||||
{
|
||||
#if defined(_MSC_VER)
|
||||
std::string nameCopy(name); // UnDecorateSymbolName also needs a NUL-terminated buffer
|
||||
char buffer[2048];
|
||||
std::lock_guard<std::mutex> lock(dbgHelpMutex());
|
||||
if (UnDecorateSymbolName(nameCopy.c_str(), buffer, sizeof(buffer), UNDNAME_COMPLETE) != 0)
|
||||
{
|
||||
text = buffer;
|
||||
}
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
// __cxa_demangle requires a NUL-terminated C string; name is not guaranteed to be
|
||||
// one (a string_view over a substring, say), so this always goes through a
|
||||
// std::string first rather than risk reading past a non-terminated view.
|
||||
//
|
||||
// __cxa_demangle always malloc()s its result (or returns null), which Deleter below
|
||||
// exists to pair back up via RAII -- std::free itself isn't a type, so an explicit
|
||||
// function-pointer deleter type is what makes unique_ptr able to hold it at all.
|
||||
using Deleter = void (*)(void*);
|
||||
int status = 0;
|
||||
std::unique_ptr<char, Deleter> demangled(
|
||||
abi::__cxa_demangle(text.c_str(), nullptr, nullptr, &status), &std::free);
|
||||
if (status == 0 && demangled != nullptr)
|
||||
{
|
||||
text = demangled.get();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Out of memory while demangling, most plausibly -- text is still whatever it was
|
||||
// going in, so fall through and return that rather than propagate.
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
/**
|
||||
* @file HotReloadContext.cpp
|
||||
* @brief The diff-and-patch orchestration logic: load, reload, and per-symbol policy.
|
||||
*/
|
||||
#include "HotReloadContext.hpp"
|
||||
|
||||
#include "Architecture.hpp"
|
||||
#include "MemoryProtection.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
namespace
|
||||
{
|
||||
/// Copies a file byte for byte, overwriting the destination.
|
||||
/// @param from The file to read.
|
||||
/// @param to The file to create (or truncate).
|
||||
/// @return True if the whole copy succeeded. An empty source counts as a failure (inserting
|
||||
/// zero characters into an output stream sets its failbit) -- harmless, since an
|
||||
/// empty file is never a loadable module anyway.
|
||||
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);
|
||||
}
|
||||
|
||||
/// The module name as it appears inside a staging copy's file name.
|
||||
///
|
||||
/// module_name is an arbitrary caller-chosen key (hot_reload.h promises it need not match
|
||||
/// anything on disk), so it may contain characters that are not legal in a file name --
|
||||
/// a '/' in "engine/renderer", say. Everything outside a conservative set is replaced by
|
||||
/// '_'. stagePathForLoad() and cleanupOrphanedStagingFiles() must agree on the result, so
|
||||
/// both call this. Names made only of letters, digits, '-' and '_' are unchanged.
|
||||
///
|
||||
/// @param moduleName The logical module name.
|
||||
/// @return @p moduleName with every character outside `[A-Za-z0-9_-]` replaced by '_'.
|
||||
std::string stagingTag(const std::string& moduleName)
|
||||
{
|
||||
std::string tag = moduleName;
|
||||
for (char& character : tag)
|
||||
{
|
||||
const bool isLower = character >= 'a' && character <= 'z';
|
||||
const bool isUpper = character >= 'A' && character <= 'Z';
|
||||
const bool isDigit = character >= '0' && character <= '9';
|
||||
const bool isDashOrUnderscore = character == '-' || character == '_';
|
||||
const bool safe = isLower || isUpper || isDigit || isDashOrUnderscore;
|
||||
if (!safe)
|
||||
{
|
||||
character = '_';
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/// Best-effort: deletes any staging copy left over from a
|
||||
/// PREVIOUS run of this same host/module -- see
|
||||
/// stagePathForLoad()'s own comment for why those copies exist
|
||||
/// at all.
|
||||
///
|
||||
/// On POSIX this normally finds nothing (an active
|
||||
/// run's own copies are already gone by the time it makes its
|
||||
/// next one -- see the std::remove() calls after each
|
||||
/// ModuleHandle::load() below). On Windows, a copy backing a
|
||||
/// still-loaded module cannot be deleted for as long as that
|
||||
/// module stays mapped -- which, by this library's own design,
|
||||
/// is for as long as the owning context lives, usually the rest
|
||||
/// of the CURRENT process's lifetime (see HotReloadContext.hpp's
|
||||
/// LoadedModule::versions) -- so within a
|
||||
/// single long-running session some accumulation is expected
|
||||
/// and unavoidable; this specifically cleans up the PREVIOUS
|
||||
/// session's leftovers once that process has exited and
|
||||
/// released whatever lock Windows held. Called once, at
|
||||
/// the first hot_reload_load() for a given name -- not on every
|
||||
/// reload() -- since a copy from earlier in the SAME session
|
||||
/// would still be locked for the same reason and retrying
|
||||
/// wouldn't change that.
|
||||
///
|
||||
/// @param sourcePath The path the module is being loaded from; the copies live next to it.
|
||||
/// @param moduleName The module's logical name.
|
||||
void cleanupOrphanedStagingFiles(const std::string& sourcePath, const std::string& moduleName)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path source(sourcePath);
|
||||
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
std::string prefix = source.filename().string() + ".hotreload." + tag + ".";
|
||||
|
||||
std::error_code dirError;
|
||||
fs::directory_iterator entries(dir, dirError);
|
||||
if (dirError)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const fs::directory_entry& entry : entries)
|
||||
{
|
||||
std::error_code fileTypeError;
|
||||
if (!entry.is_regular_file(fileTypeError) || fileTypeError)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string filename = entry.path().filename().string();
|
||||
if (filename.compare(0, prefix.size(), prefix) == 0)
|
||||
{
|
||||
std::error_code removeError;
|
||||
fs::remove(entry.path(), removeError); // best-effort; a failure here just means try again next launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
const std::string counter = std::to_string(module.reloadCounter);
|
||||
std::string staged = sourcePath + ".hotreload." + tag + "." + counter;
|
||||
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;
|
||||
}
|
||||
|
||||
cleanupOrphanedStagingFiles(path, moduleName);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModuleHandle.hpp"
|
||||
#include "SymbolTable.hpp"
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @namespace hot_reload
|
||||
* @brief The C++ implementation behind the C API declared in `hot_reload.h`.
|
||||
*
|
||||
* The one type meant to be used from outside is HotReloadContext, and even that only through the
|
||||
* opaque ::HotReloadContext pointer the C API hands out. The building blocks it is made of live in
|
||||
* hot_reload::detail.
|
||||
*/
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 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.
|
||||
*
|
||||
* A context owns every version of every module loaded through it. Destroying the context
|
||||
* closes all of them, after which any function or data pointer obtained from any of those
|
||||
* modules -- including old, patched entry points that now only jump into a newer version --
|
||||
* is dangling.
|
||||
*
|
||||
* @note Not thread-safe: the class holds no locks, so all calls on one context (and any
|
||||
* reload it performs) must be serialized by the caller.
|
||||
*/
|
||||
class HotReloadContext
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Creates an empty context.
|
||||
* @param options Callbacks and user data to use, copied out immediately; may be null,
|
||||
* meaning "no callbacks".
|
||||
*/
|
||||
explicit HotReloadContext(const HotReloadOptions* options);
|
||||
|
||||
HotReloadContext(const HotReloadContext&) = delete;
|
||||
HotReloadContext& operator=(const HotReloadContext&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Implements hot_reload_load().
|
||||
* @param moduleName The logical name to register the module under.
|
||||
* @param path Path to the shared library to load.
|
||||
* @return The status hot_reload_load() reports; see its doc comment in `hot_reload.h`.
|
||||
*/
|
||||
HotReloadStatus load(const std::string& moduleName, const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief Implements hot_reload_reload().
|
||||
* @param moduleName The logical name the module was loaded under.
|
||||
* @param newPath Path to the new build of the shared library.
|
||||
* @return The status hot_reload_reload() reports; see its doc comment in `hot_reload.h`.
|
||||
*/
|
||||
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
|
||||
|
||||
/**
|
||||
* @brief Implements hot_reload_get_symbol().
|
||||
* @param moduleName The logical name the module was loaded under.
|
||||
* @param symbolName The symbol's real (possibly mangled) name.
|
||||
* @return The symbol's address in the module's newest build, or null if the module or
|
||||
* symbol is unknown; see hot_reload_get_symbol()'s doc comment in `hot_reload.h`.
|
||||
*/
|
||||
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
|
||||
|
||||
/**
|
||||
* @brief Implements hot_reload_last_error().
|
||||
* @return The platform's message for the most recent load()/reload() failure on this
|
||||
* context; empty if there was none. Valid until the next load()/reload().
|
||||
*/
|
||||
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 as long as the owning
|
||||
/// context lives (in practice usually the rest of the process): once a function is
|
||||
/// patched, anything that already holds its old address must stay valid memory,
|
||||
/// even though it now holds nothing but a jump. versions.back() is the active one.
|
||||
std::vector<detail::ModuleHandle> versions;
|
||||
|
||||
/// The exported symbols of the newest build (versions.back()), as reported by
|
||||
/// detail::readExportedSymbols(); what the next reload() diffs against.
|
||||
std::vector<detail::ExportedSymbol> currentSymbols;
|
||||
|
||||
/// How many staging copies have been made for this name so far; makes each copy's
|
||||
/// file name unique (see stagePathForLoad()).
|
||||
int reloadCounter = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stages a source file through a fresh, uniquely-named copy.
|
||||
*
|
||||
* Called before a path is ever passed to detail::ModuleHandle::load() -- see the .cpp
|
||||
* definition's comment for why (a dlopen caching gotcha on POSIX; a Windows file-locking
|
||||
* side effect that turns out to be handled by the same fix).
|
||||
*
|
||||
* @param module The module the copy is for; its reloadCounter is advanced.
|
||||
* @param moduleName The module's logical name (used, sanitized, in the copy's file name).
|
||||
* @param sourcePath The file to copy.
|
||||
* @return The copy's path, or an empty string if copying failed.
|
||||
*/
|
||||
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
|
||||
|
||||
/**
|
||||
* @brief Redirects one matched function pair, or emits a SKIPPED_* event instead if that
|
||||
* isn't safe.
|
||||
*
|
||||
* Patches @p oldSymbol's entry to jump to @p newSymbol's address; emits exactly one event
|
||||
* describing the outcome either way.
|
||||
*
|
||||
* @param oldSymbol The function in the previous build.
|
||||
* @param newSymbol The same-named function in the new build.
|
||||
*/
|
||||
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
|
||||
|
||||
/**
|
||||
* @brief Handles a symbol present in the old build but missing from the new one.
|
||||
*
|
||||
* Emits REMOVED, and first redirects to a caller-supplied stub if
|
||||
* HotReloadOptions::on_deleted_symbol provides one and it's safe to do so.
|
||||
*
|
||||
* @param oldSymbol The symbol the new build no longer exports.
|
||||
*/
|
||||
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
|
||||
|
||||
/**
|
||||
* @brief Invokes HotReloadOptions::on_symbol_event, if one was supplied.
|
||||
* @param kind What happened to the symbol.
|
||||
* @param name The symbol's real (possibly mangled) name.
|
||||
* @param oldAddress The symbol's address in the previous build, or null.
|
||||
* @param newAddress The symbol's address in the new build, or null.
|
||||
*/
|
||||
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
|
||||
|
||||
/// Every module loaded so far, by logical name.
|
||||
std::unordered_map<std::string, LoadedModule> m_modules;
|
||||
|
||||
/// HotReloadOptions::on_symbol_event, or null.
|
||||
HotReloadSymbolEventFn m_onSymbolEvent;
|
||||
|
||||
/// HotReloadOptions::on_deleted_symbol, or null.
|
||||
HotReloadDeletedSymbolFn m_onDeletedSymbol;
|
||||
|
||||
/// HotReloadOptions::user_data, handed back to both callbacks untouched.
|
||||
void* m_userData;
|
||||
|
||||
/// The message lastError() returns.
|
||||
std::string m_lastError;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -11,9 +11,27 @@
|
||||
// would otherwise clash with std::min/std::max and anything else
|
||||
// named min/max in this translation unit -- found via an actual
|
||||
// Windows build/run of this library, not just documentation.
|
||||
//
|
||||
// WIN32_LEAN_AND_MEAN: without it, windows.h also pulls in COM/OLE
|
||||
// headers (ole2.h, combaseapi.h, objidlbase.h, servprov.h, ...)
|
||||
// this library has no use for at all. A real build report (GNU-style
|
||||
// clang++ 22.1 targeting the MSVC ABI, Windows SDK 10.0.26100)
|
||||
// failed inside those headers' own __uuidof()-based
|
||||
// QueryInterface<Q>() template helper, unrelated to anything this
|
||||
// library does itself. Defining this keeps windows.h to the core
|
||||
// Win32 surface this library actually needs (LoadLibrary,
|
||||
// VirtualProtect, the PE/COFF IMAGE_* structures, ...) and keeps
|
||||
// that whole header chain out of the translation unit -- the
|
||||
// standard fix for this class of windows.h/COM interaction. What
|
||||
// has and has not been checked about it is recorded in the
|
||||
// top-level README's "What's actually been tested" table, under the
|
||||
// Clang-targeting-the-MSVC-ABI row.
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
#include <utility>
|
||||
|
||||
#if defined(_WIN32)
|
||||
// NOMINMAX: see MemoryProtection.cpp's identical comment -- same
|
||||
// windows.h, same reason, needed in every translation unit that
|
||||
// includes it, not just once for the library as a whole.
|
||||
// NOMINMAX, WIN32_LEAN_AND_MEAN: see MemoryProtection.cpp's identical
|
||||
// comment -- same windows.h, same reasons, needed in every
|
||||
// translation unit that includes it, not just once for the library
|
||||
// as a whole.
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
@@ -148,7 +152,7 @@ namespace hot_reload::detail
|
||||
// would fail with a baffling "cannot open shared object file"
|
||||
// for a file that is sitting right there. Everything this
|
||||
// library loads is a real file on disk (the staged copy
|
||||
// HotReloadContext makes), so always force path semantics.
|
||||
// Context makes), so always force path semantics.
|
||||
const std::string loadPath = path.find('/') == std::string::npos ? "./" + path : path;
|
||||
void* module = dlopen(loadPath.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (module == nullptr)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file Result.cpp
|
||||
* @brief Result::success() and Result::failure() -- the only two ways a Result is ever constructed.
|
||||
*/
|
||||
#include "hot_reload/Result.hpp"
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
Result Result::success() noexcept
|
||||
{
|
||||
return Result(Status::Ok, {});
|
||||
}
|
||||
|
||||
Result Result::failure(Status status, std::string message) noexcept
|
||||
{
|
||||
return Result(status, std::move(message));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @file Status.cpp
|
||||
* @brief toString(Status) -- one switch, kept in sync with Status.hpp by -Wswitch.
|
||||
*/
|
||||
#include "hot_reload/Status.hpp"
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
std::string_view toString(Status status) noexcept
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case Status::Ok:
|
||||
return "ok";
|
||||
case Status::InvalidArgument:
|
||||
return "invalid argument";
|
||||
case Status::LoadFailed:
|
||||
return "load failed";
|
||||
case Status::NoPreviousVersion:
|
||||
return "no previous version loaded for this module name";
|
||||
case Status::UnsupportedArchitecture:
|
||||
return "no patch backend for this CPU architecture";
|
||||
}
|
||||
return "unknown hot reload status"; // unreachable for any enumerator; -Wswitch catches a missing one above
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file SymbolEvent.cpp
|
||||
* @brief toString(SymbolEvent) -- one switch, kept in sync with SymbolEvent.hpp by -Wswitch.
|
||||
*/
|
||||
#include "hot_reload/SymbolEvent.hpp"
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
std::string_view toString(SymbolEvent event) noexcept
|
||||
{
|
||||
switch (event)
|
||||
{
|
||||
case SymbolEvent::Patched:
|
||||
return "patched";
|
||||
case SymbolEvent::Added:
|
||||
return "added";
|
||||
case SymbolEvent::Removed:
|
||||
return "removed";
|
||||
case SymbolEvent::SkippedNotCode:
|
||||
return "skipped (not code)";
|
||||
case SymbolEvent::SkippedTooSmall:
|
||||
return "skipped (too small)";
|
||||
case SymbolEvent::SkippedUnwritable:
|
||||
return "skipped (unwritable)";
|
||||
}
|
||||
return "unknown symbol event"; // unreachable for any enumerator; -Wswitch catches a missing one above
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file SymbolEventInfo.cpp
|
||||
* @brief SymbolEventInfo::summary() -- a one-line, human-readable rendering of one event.
|
||||
*/
|
||||
#include "hot_reload/SymbolEventInfo.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/// @param address An address to render, possibly null.
|
||||
/// @return @p address as `0x...` hex, or `"(null)"`.
|
||||
std::string hex(const void* address)
|
||||
{
|
||||
if (address == nullptr)
|
||||
{
|
||||
return "(null)";
|
||||
}
|
||||
std::ostringstream stream;
|
||||
stream << address;
|
||||
return stream.str();
|
||||
}
|
||||
}
|
||||
|
||||
std::string SymbolEventInfo::summary() const
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << toString(kind) << " '" << name << "'";
|
||||
switch (kind)
|
||||
{
|
||||
case SymbolEvent::Patched:
|
||||
stream << " (" << hex(oldAddress) << " -> " << hex(newAddress) << ")";
|
||||
break;
|
||||
case SymbolEvent::Added:
|
||||
stream << " (" << hex(newAddress) << ")";
|
||||
break;
|
||||
case SymbolEvent::Removed:
|
||||
case SymbolEvent::SkippedNotCode:
|
||||
case SymbolEvent::SkippedTooSmall:
|
||||
case SymbolEvent::SkippedUnwritable:
|
||||
stream << " (" << hex(oldAddress) << ")";
|
||||
break;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* 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
|
||||
* expected to follow. Context (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.
|
||||
@@ -47,9 +47,19 @@ namespace hot_reload::detail
|
||||
// qualified name, and expands it regardless) and the file fails to
|
||||
// compile on MSVC. Found via an actual Windows build/run of this
|
||||
// library, not just documentation.
|
||||
//
|
||||
// WIN32_LEAN_AND_MEAN: see MemoryProtection.cpp's comment for the
|
||||
// full reasoning. This is the file the reported Clang-on-the-MSVC-ABI
|
||||
// build actually failed in first (inside windows.h's own COM headers,
|
||||
// reached through ole2.h -- nothing this file itself does): it needs
|
||||
// only the PE/COFF IMAGE_* structures, which come from winnt.h and
|
||||
// stay in place under WIN32_LEAN_AND_MEAN.
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace hot_reload::detail
|
||||
* 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
|
||||
* not recognize at all). `Context` never patches a
|
||||
* symbol where this is false, on either side of a reload.
|
||||
*/
|
||||
bool isFunction = false;
|
||||
|
||||
@@ -1,60 +1,21 @@
|
||||
/**
|
||||
* @file Demangle.cpp
|
||||
* @brief hot_reload_demangle() / hot_reload_free_string() implementation.
|
||||
* @brief hot_reload_demangle() / hot_reload_free_string() -- a thin C wrapper over
|
||||
* hot_reload::demangle(), which carries the real logic (see that function's own file,
|
||||
* `src/Demangle.cpp`, directly under `source/HotReload/src/`, not this directory).
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include "hot_reload/Demangle.hpp"
|
||||
|
||||
#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 the "Hot reloading C++
|
||||
// symbols" section of source/HotReload/README.md for why this
|
||||
// distinction matters beyond just this function.
|
||||
#include <cxxabi.h>
|
||||
#elif defined(_MSC_VER)
|
||||
// windows.h must come first: dbghelp.h uses types (HANDLE, DWORD,
|
||||
// BOOL, ...) it assumes are already defined rather than including
|
||||
// their defining headers itself -- reversing this order fails to
|
||||
// compile. NOMINMAX prevents windows.h from defining min/max
|
||||
// macros that would otherwise clash with std::min/std::max and
|
||||
// anything else named min/max in this translation unit. Both
|
||||
// fixes below came from an actual Windows build/run of this
|
||||
// library, not just documentation -- see README.md's toolchain
|
||||
// verification notes.
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
// Belt-and-suspenders for anyone building this file outside this
|
||||
// project's own CMake (source/HotReload/CMakeLists.txt links
|
||||
// dbghelp explicitly for every MSVC-family build, which is the
|
||||
// primary fix and the one actually required -- confirmed directly
|
||||
// that omitting it leaves UnDecorateSymbolName undefined at link
|
||||
// time regardless of this pragma).
|
||||
#pragma comment(lib, "dbghelp.lib")
|
||||
#endif
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
@@ -64,40 +25,11 @@ extern "C"
|
||||
{
|
||||
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.
|
||||
// Exercised on a real Windows build (see README.md's
|
||||
// toolchain verification notes) -- getting the
|
||||
// windows.h/dbghelp.h include order and NOMINMAX right
|
||||
// (see the #include block above) and linking dbghelp
|
||||
// explicitly (source/HotReload/CMakeLists.txt) were both
|
||||
// needed to get here.
|
||||
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);
|
||||
std::string demangled = hot_reload::demangle(name);
|
||||
char* result = new char[demangled.size() + 1];
|
||||
std::memcpy(result, demangled.c_str(), demangled.size() + 1);
|
||||
return result;
|
||||
}
|
||||
catch (...)
|
||||
|
||||
@@ -1,23 +1,194 @@
|
||||
/**
|
||||
* @file HotReloadApi.cpp
|
||||
* @brief The extern "C" shim implementing hot_reload.h by delegating to hot_reload::HotReloadContext.
|
||||
* @brief The extern "C" shim implementing hot_reload.h on top of hot_reload::Context.
|
||||
*
|
||||
* Deliberately loose coupling, not the tight opaque-pointer-reinterpret_cast PIMPL
|
||||
* cpp-style-guide.md's own NetManager example uses elsewhere in this codebase: `::HotReloadContext`
|
||||
* (the struct this file defines, matching the opaque forward declaration in `hot_reload.h`) *owns*
|
||||
* a `hot_reload::Context` by value, and every function below reaches it only through Context's own
|
||||
* public methods -- exactly as any other C++ caller of `Context.hpp` would, with no special access
|
||||
* and no reinterpret_cast between unrelated types. `hot_reload::Context` has no idea this file, or
|
||||
* the C API it implements, exists at all.
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include "../HotReloadContext.hpp"
|
||||
#include "hot_reload/Context.hpp"
|
||||
#include "hot_reload/Result.hpp"
|
||||
#include "hot_reload/Status.hpp"
|
||||
#include "hot_reload/SymbolEventInfo.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/// Defines the type `hot_reload.h` only forward-declares (`typedef struct HotReloadContext
|
||||
/// HotReloadContext;`) -- every function in this file sees the real definition, so ordinary
|
||||
/// member access replaces the reinterpret_cast a true opaque pointer would otherwise need.
|
||||
struct HotReloadContext
|
||||
{
|
||||
explicit HotReloadContext(hot_reload::Options options)
|
||||
: impl(std::move(options))
|
||||
{
|
||||
}
|
||||
|
||||
/// A process-wide, never-reused identifier for this context, used only to key
|
||||
/// t_lastErrorByContextId() below -- see that function's own comment for why a raw `this`
|
||||
/// pointer is not used for that instead.
|
||||
std::uint64_t id = nextId();
|
||||
|
||||
/// The real implementation. Composition, not inheritance or a cast: this file uses it
|
||||
/// exactly the way any other hot_reload::Context caller would.
|
||||
hot_reload::Context impl;
|
||||
|
||||
private:
|
||||
static std::uint64_t nextId()
|
||||
{
|
||||
static std::atomic<std::uint64_t> counter{1};
|
||||
return counter.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
/// @param ctx A context previously returned by hot_reload_context_create(), or null.
|
||||
/// @return The same object viewed as the C++ type it really is (null if @p ctx is null).
|
||||
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
|
||||
/// hot_reload.h's `hot_reload_last_error()` is a stateful call: "whatever the most recent
|
||||
/// load()/reload() on this ctx failed with". hot_reload::Result has no equivalent shared
|
||||
/// state at all -- each call's outcome comes back in its own Result -- which is exactly what
|
||||
/// makes the C++ API safe to use concurrently without two threads' errors ever being
|
||||
/// confused (see Context.hpp's thread-safety note). Reintroducing that stateful call for the
|
||||
/// C API's own backward compatibility, now safely, means tracking it per *(thread, context)*
|
||||
/// pair: a plain `thread_local` map needs no lock at all, since no other thread can ever see
|
||||
/// this one's instance of it.
|
||||
///
|
||||
/// Keyed by HotReloadContext::id rather than the context's own address: a destroyed
|
||||
/// context's memory can be reused by an unrelated, later one, and keying by address would
|
||||
/// risk a brand new context momentarily reading a stale error string left behind by
|
||||
/// whatever used to live at that same address.
|
||||
std::unordered_map<std::uint64_t, std::string>& t_lastErrorByContextId()
|
||||
{
|
||||
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
|
||||
thread_local std::unordered_map<std::uint64_t, std::string> errors;
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// Records @p result's message, if it has one, as this thread's last error for @p ctx.
|
||||
/// @param ctx The context the call was made on.
|
||||
/// @param result The call's own outcome.
|
||||
void recordLastError(const HotReloadContext& ctx, const hot_reload::Result& result)
|
||||
{
|
||||
if (result.message().empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
t_lastErrorByContextId()[ctx.id] = result.message();
|
||||
}
|
||||
|
||||
/// @param status A hot_reload::Status.
|
||||
/// @return The equivalent HotReloadStatus.
|
||||
HotReloadStatus toCStatus(hot_reload::Status status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case hot_reload::Status::Ok:
|
||||
return HOT_RELOAD_OK;
|
||||
case hot_reload::Status::InvalidArgument:
|
||||
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
||||
case hot_reload::Status::LoadFailed:
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
case hot_reload::Status::NoPreviousVersion:
|
||||
return HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION;
|
||||
case hot_reload::Status::UnsupportedArchitecture:
|
||||
return HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE;
|
||||
}
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED; // unreachable for any enumerator; -Wswitch catches a missing one above
|
||||
}
|
||||
|
||||
/// @param status A HotReloadStatus.
|
||||
/// @return The equivalent hot_reload::Status.
|
||||
hot_reload::Status toCppStatus(HotReloadStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case HOT_RELOAD_OK:
|
||||
return hot_reload::Status::Ok;
|
||||
case HOT_RELOAD_ERROR_INVALID_ARGUMENT:
|
||||
return hot_reload::Status::InvalidArgument;
|
||||
case HOT_RELOAD_ERROR_LOAD_FAILED:
|
||||
return hot_reload::Status::LoadFailed;
|
||||
case HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION:
|
||||
return hot_reload::Status::NoPreviousVersion;
|
||||
case HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE:
|
||||
return hot_reload::Status::UnsupportedArchitecture;
|
||||
}
|
||||
return hot_reload::Status::LoadFailed; // unreachable for any enumerator; -Wswitch catches a missing one above
|
||||
}
|
||||
|
||||
/// @param kind A hot_reload::SymbolEvent.
|
||||
/// @return The equivalent HotReloadSymbolEvent.
|
||||
HotReloadSymbolEvent toCEvent(hot_reload::SymbolEvent kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case hot_reload::SymbolEvent::Patched:
|
||||
return HOT_RELOAD_SYMBOL_PATCHED;
|
||||
case hot_reload::SymbolEvent::Added:
|
||||
return HOT_RELOAD_SYMBOL_ADDED;
|
||||
case hot_reload::SymbolEvent::Removed:
|
||||
return HOT_RELOAD_SYMBOL_REMOVED;
|
||||
case hot_reload::SymbolEvent::SkippedNotCode:
|
||||
return HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE;
|
||||
case hot_reload::SymbolEvent::SkippedTooSmall:
|
||||
return HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL;
|
||||
case hot_reload::SymbolEvent::SkippedUnwritable:
|
||||
return HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE;
|
||||
}
|
||||
return HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE; // unreachable for any enumerator; -Wswitch catches one missing above
|
||||
}
|
||||
|
||||
/// Builds the hot_reload::Options this file's two C callback adapters below run through,
|
||||
/// from a (possibly null) HotReloadOptions*.
|
||||
/// @param options The options hot_reload_context_create() was called with; null means none.
|
||||
/// @return Options wrapping @p options's two callbacks (each a no-op if @p options is null,
|
||||
/// or if the specific callback field itself is null), capturing nothing but the
|
||||
/// plain C function pointer and user_data they were given, so this is safe to store
|
||||
/// for the Context's whole lifetime.
|
||||
hot_reload::Options toCppOptions(const HotReloadOptions* options)
|
||||
{
|
||||
hot_reload::Options result;
|
||||
if (options == nullptr)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options->on_symbol_event != nullptr)
|
||||
{
|
||||
HotReloadSymbolEventFn callback = options->on_symbol_event;
|
||||
void* userData = options->user_data;
|
||||
result.onSymbolEvent = [callback, userData](const hot_reload::SymbolEventInfo& event)
|
||||
{
|
||||
// name.data() is safe to hand out as a NUL-terminated const char* here: it
|
||||
// ultimately points into a detail::ExportedSymbol's own std::string (see
|
||||
// Context.cpp's emitEvent()), which is always NUL-terminated.
|
||||
HotReloadSymbolEventInfo cEvent;
|
||||
cEvent.name = event.name.data();
|
||||
cEvent.kind = toCEvent(event.kind);
|
||||
cEvent.old_address = event.oldAddress;
|
||||
cEvent.new_address = event.newAddress;
|
||||
callback(&cEvent, userData);
|
||||
};
|
||||
}
|
||||
|
||||
if (options->on_deleted_symbol != nullptr)
|
||||
{
|
||||
HotReloadDeletedSymbolFn callback = options->on_deleted_symbol;
|
||||
void* userData = options->user_data;
|
||||
result.onDeletedSymbol = [callback, userData](std::string_view deletedSymbolName) -> void*
|
||||
{
|
||||
// Same reasoning as above for passing .data() directly as a C string.
|
||||
return callback(deletedSymbolName.data(), userData);
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +197,11 @@ namespace
|
||||
// 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.
|
||||
// unordered_map construction (on the hot_reload::Context side of this
|
||||
// boundary) is the only thing 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"
|
||||
{
|
||||
|
||||
@@ -38,7 +209,7 @@ extern "C"
|
||||
{
|
||||
try
|
||||
{
|
||||
return reinterpret_cast<::HotReloadContext*>(new hot_reload::HotReloadContext(options));
|
||||
return new HotReloadContext(toCppOptions(options));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -48,7 +219,17 @@ extern "C"
|
||||
|
||||
void hot_reload_context_destroy(HotReloadContext* ctx)
|
||||
{
|
||||
delete impl(ctx);
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Best-effort: removes this (calling) thread's own entry for ctx, if it has one. Another
|
||||
// thread's thread_local entry for the same id, if it made calls on this ctx too, is left
|
||||
// in place -- there is no way to reach another thread's thread_local storage from here --
|
||||
// a small, bounded-by-distinct-destroyed-context-count leak, accepted as the cost of
|
||||
// hot_reload_last_error() needing no lock at all on the hot (every-call) path.
|
||||
t_lastErrorByContextId().erase(ctx->id);
|
||||
delete ctx;
|
||||
}
|
||||
|
||||
HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path)
|
||||
@@ -59,7 +240,9 @@ extern "C"
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->load(module_name, path);
|
||||
hot_reload::Result result = ctx->impl.load(module_name, path);
|
||||
recordLastError(*ctx, result);
|
||||
return toCStatus(result.status());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -75,7 +258,9 @@ extern "C"
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->reload(module_name, new_path);
|
||||
hot_reload::Result result = ctx->impl.reload(module_name, new_path);
|
||||
recordLastError(*ctx, result);
|
||||
return toCStatus(result.status());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -91,7 +276,7 @@ extern "C"
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->getSymbol(module_name, symbol_name);
|
||||
return ctx->impl.findSymbol(module_name, symbol_name);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -107,7 +292,9 @@ extern "C"
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->lastError().c_str();
|
||||
auto& errors = t_lastErrorByContextId();
|
||||
auto found = errors.find(ctx->id);
|
||||
return found == errors.end() ? "" : found->second.c_str();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -117,20 +304,7 @@ extern "C"
|
||||
|
||||
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";
|
||||
return hot_reload::toString(toCppStatus(status)).data(); // points into a static string literal either way
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user