Fix documentation

This commit is contained in:
alejo committed 2026-09-25 03:20:35 -03:00
1 parent a317c07124
commit ab21b3f30d
27 files changed
+2137 -1043

No files matched your search

+26 -14
View File
@@ -3,12 +3,18 @@
Patches a running process's own code in place so that a new build of a
function takes effect immediately -- no restart, and every existing
caller (including a raw C function pointer someone cached ten minutes
ago) picks up the change automatically. `hot_reload.h` is the API;
`source/examples/HotReloadDemo` is a full worked example; this file is
the overview `hot_reload.h`'s own doc comments don't have room for,
especially the parts you should know are *not* guaranteed.
ago) picks up the change automatically. This is the full technical
reference; see this project's top-level `README.md` for a quicker
overview, feature summary, and build/run instructions if you haven't
already.
If you just want to see it run:
`hot_reload.h` is the API (also fully Doxygen-documented -- see the
top-level README's "Documentation" section for how to build that
locally); `source/examples/HotReloadDemo` is a full worked example;
this file is the overview `hot_reload.h`'s own doc comments don't have
room for, especially the parts you should know are *not* guaranteed.
To see it run:
```sh
cmake -S . -B out -DBUILD_EXAMPLES=ON
@@ -367,15 +373,21 @@ that file's comment for the full reasoning.
## Known, accepted gaps
- **A `.so` with its section header table deliberately stripped**
reads back as exporting no functions at all (empty, not a crash) --
`SymbolTable.cpp`'s ELF reader uses section headers to find
`.dynsym`/`.dynstr`, which is simpler and easier to get right than
walking `.dynamic`'s `DT_*` tags the way the dynamic linker itself
does, at the cost of this one edge case. Stripping section headers
from a `.so` you are actively hot-reloading is an unusual, separate
step from an ordinary `strip` and essentially never happens by
accident.
- **A `.so` with its section header table deliberately stripped,
truncated, or otherwise corrupted** reads back as exporting no
functions at all (empty, not a crash) -- `SymbolTable.cpp`'s ELF
reader uses section headers to find `.dynsym`/`.dynstr`, which is
simpler and easier to get right than walking `.dynamic`'s `DT_*` tags
the way the dynamic linker itself does, at the cost of this one edge
case for a deliberately-stripped file. Every offset, size, and index
this reader trusts is bounds-checked against the file's actual size
before use -- confirmed directly by fuzzing the section header table
of a real plugin build: 1500 randomly corrupted variants (each still
a file the OS loader itself accepts via `dlopen()`), zero crashes. An
earlier version of this same reader, without those checks, crashed on
69 of an initial 300. Stripping section headers from a `.so` you are
actively hot-reloading is, separately, an unusual step essentially
never taken by accident.
- **PE exports have no per-symbol size.** Unlike ELF's `st_size`,
which backs a real safety check, the Windows path estimates size
from the gap to the next exported function and can only ever prove
+451 -200
View File
@@ -1,79 +1,126 @@
/* hot_reload.h
/**
* @file hot_reload.h
* @brief Public C API for the hot-reload library.
*
* Public C API for the hot-reload library -- see cpp-style-guide.md
* Part II (C API Style) for the naming/formatting rules this header
* follows, and section 11 ("Runtime Loading") for why this boundary is
* plain C in the first place: anything crossing a dlopen()/LoadLibrary()
* boundary must have C linkage, so this is exactly the kind of surface
* that section calls out.
* See @c cpp-style-guide.md Part II (C API Style) for the naming/formatting
* rules this header follows, and section 11 ("Runtime Loading") for why
* this boundary is plain C in the first place: anything crossing a
* `dlopen()`/`LoadLibrary()` boundary must have C linkage, so this is
* exactly the kind of surface that section calls out.
*
* WHAT THIS LIBRARY DOES
* -----------------------
* hot_reload_load() loads a shared library (.so/.dll) under a logical
* name. hot_reload_reload() loads a NEW build of that same shared
* library and, for every function the two builds have in common,
* rewrites the FIRST FEW BYTES of the old function in memory into a
* jump to the new function -- so every existing call site (anyone who
* already resolved the old function's address, including via a plain
* C function pointer) transparently starts running the new code,
* without restarting the process.
* @section hotreload_what_it_does What this library does
*
* WHAT THIS LIBRARY DELIBERATELY DOES NOT DO
* --------------------------------------------
* It never patches anything that isn't executable code. A symbol is
* only ever a patch candidate if the loader's own symbol table marks
* it as a function (ELF STT_FUNC / a PE export whose address falls
* inside an executable section) -- see the design note in
* SymbolTable.hpp for exactly how that's determined. Global variables,
* class/struct instances, vtables, and anything else classified as
* data are always left alone, on both the old and the new module --
* there is no code path in this library that ever writes to a data
* symbol. This is a mechanical guarantee from how symbols are
* classified, not a naming convention you opt into.
* hot_reload_load() loads a shared library (`.so`/`.dll`) under a logical
* name. hot_reload_reload() loads a *new* build of that same shared
* library and, for every function the two builds have in common, rewrites
* the first few bytes of the old function in memory into a jump to the
* new function -- so every existing call site (anyone who already
* resolved the old function's address, including via a plain C function
* pointer) transparently starts running the new code, without restarting
* the process.
*
* It also cannot verify that a hot-reloadable function's SIGNATURE
* @section hotreload_what_it_wont What this library deliberately does not do
*
* It never patches anything that isn't executable code. A symbol is only
* ever a patch candidate if the loader's own symbol table marks it as a
* function (ELF `STT_FUNC` / a PE export whose address falls inside an
* executable section) -- see the design note at the top of
* `SymbolTable.cpp` (and detail::ExportedSymbol::isFunction) for exactly
* how that's determined. Global variables, class/struct
* instances, vtables, and anything else classified as data are always
* left alone, on both the old and the new module -- there is no code path
* in this library that ever writes to a data symbol. This is a mechanical
* guarantee from how symbols are classified, not a naming convention you
* opt into.
*
* It also cannot verify that a hot-reloadable function's *signature*
* hasn't changed. Patching only ever redirects control flow at a fixed
* entry point; it has no way to know, from a symbol table alone, that
* `void tick(int)` became `void tick(int, int)` between builds. If a
* function's signature changes, give it a new exported name (so the
* old symbol is treated as removed and the new one as added -- see
* HOT_RELOAD_SYMBOL_REMOVED below) rather than reusing the old name.
* See the top-level README for the full list of hazards this library
* cannot detect for you.
* function's signature changes, give it a new exported name (so the old
* symbol is treated as removed and the new one as added -- see
* ::HOT_RELOAD_SYMBOL_REMOVED) rather than reusing the old name. See the
* "Limitations" section of the top-level README (and "What this cannot
* detect" in `source/HotReload/README.md`) for the full list of hazards
* this library cannot detect for you.
*
* @section hotreload_requirements Requirements on a hot-reloadable module
*
* REQUIREMENTS ON A HOT-RELOADABLE MODULE
* ------------------------------------------
* Matching an old build's function up with the new build's requires a
* name that is stable across recompiles. extern "C" linkage -- see
* cpp-style-guide.md section 11 -- is the simplest way to get one: no
* name that is stable across recompiles. `extern "C"` linkage -- see
* `cpp-style-guide.md` section 11 -- is the simplest way to get one: no
* mangling at all, so the name never depends on the compiler, its
* version, or the function's signature.
*
* A real, unmodified C++ mangled name works too, and needs no special
* handling by this library -- classification only ever looks at a
* symbol's type and section, never at what its name looks like, so an
* ordinary out-of-line method, a virtual method (redirected wherever
* it's reached from -- directly or through a vtable slot, since both
* hold the same address), and an operator overload all patch exactly
* like any extern "C" function. What extern "C" actually buys you is
* a name you can type by hand into hot_reload_get_symbol() and a
* guarantee that it won't change if you rebuild with a different
* compiler; a mangled name gives up both of those, only stays stable
* within one ABI family (GCC and Clang share the Itanium C++ ABI;
* MSVC does not), and -- the one hazard that has nothing to do with
* this library and everything to do with what "hot-reloadable" can
* even mean -- only exists to patch at all if the compiler actually
* emitted an out-of-line body for it, which an implicitly-inline
* method defined in a class body, or a template instantiated at the
* call site, generally will not. See the top-level README's "Hot
* reloading C++ symbols" section for the full picture, including how
* to get a readable name for logging with hot_reload_demangle().
* ordinary out-of-line method, a virtual method (redirected wherever it's
* reached from -- directly or through a vtable slot, since both hold the
* same address), and an operator overload all patch exactly like any
* `extern "C"` function. What `extern "C"` actually buys you is a name
* you can type by hand into hot_reload_get_symbol() and a guarantee that
* it won't change if you rebuild with a different compiler; a mangled
* name gives up both of those, only stays stable within one ABI family
* (GCC and Clang share the Itanium C++ ABI; MSVC does not), and -- the
* one hazard that has nothing to do with this library and everything to
* do with what "hot-reloadable" can even mean -- only exists to patch at
* all if the compiler actually emitted an out-of-line body for it, which
* an implicitly-inline method defined in a class body, or a template
* instantiated at the call site, generally will not. See the "Hot
* reloading C++ symbols" section of `source/HotReload/README.md` for the
* full picture, including how to get a readable name for logging with
* hot_reload_demangle().
*
* Every function the module exports needs room at its entry point for the
* jump this library writes over it. Compile the module with
* `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to
* guarantee that; without it, a function too small to hold the jump is
* skipped and reported as ::HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than
* patched. Also avoid identical-code-folding linker options (MSVC
* `/OPT:ICF`, `--icf` in gold/lld) for the module: two different exported
* functions folded into one address can only be patched to one target.
*
* @section hotreload_lifetime What carries over a reload -- and what does not
*
* Only *code* is redirected. Every build of the module is a separate
* mapping with its own copy of everything else, so:
* - Global and `static` variables are not carried over. Code that runs
* after a reload uses the *new* build's copies, freshly initialised;
* keep state you need to survive in memory the host owns and pass it in.
* - Static constructors of the new build run when it is loaded (on every
* reload); static destructors of every version run when the context is
* destroyed.
* - Every version stays loaded until hot_reload_context_destroy(). Old
* entry points keep working (they are now jumps), one extra hop per
* reload; destroying the context unloads them all, so no pointer into
* any of the modules may be used afterwards.
*
* @section hotreload_threads Threading
*
* A context is not thread-safe: serialize all calls that use one context.
* Call hot_reload_reload() from a quiescent point -- one where no thread is
* executing inside the module's functions -- since a function that is being
* executed while its entry bytes are rewritten cannot be protected.
*/
#ifndef HOT_RELOAD_H
#define HOT_RELOAD_H
#include <stddef.h>
/**
* @def HOT_RELOAD_API
* @brief Marks every function of this library's own C API.
*
* Expands to nothing in the default configuration, where the library is linked statically.
* Define `HOT_RELOAD_BUILD_SHARED` while building the library as a shared library, and
* `HOT_RELOAD_USE_SHARED` while compiling code that uses such a build, to get
* `__declspec(dllexport)`/`__declspec(dllimport)` on Windows or default symbol visibility
* elsewhere.
*
* Not to be confused with ::HOT_RELOAD_EXPORT, which marks symbols in a hot-reloadable
* *module* rather than in this library.
*/
#ifdef _WIN32
#ifdef HOT_RELOAD_BUILD_SHARED
#define HOT_RELOAD_API __declspec(dllexport)
@@ -90,23 +137,29 @@
#endif
#endif
/* Marks a single declaration for export under
* hot_reload_restrict_exports() (cmake/HotReload.cmake) without
* otherwise changing its linkage: pair with extern "C" yourself for a
* free function you want a stable, human-typable name for (see this
* file's own comment on when you'd want that) -- extern "C" first,
* then this macro, then the return type, e.g.
* `extern "C" HOT_RELOAD_EXPORT int my_function(...)`, which is the
* one ordering GCC/Clang accept without a warning here. Leave the
* extern "C" off to keep a C++ class or method's real mangled name,
* e.g. a virtual method a host only ever reaches through a vtable
* slot (see the top-level README's "Hot reloading C++ symbols"
* section).
/**
* @def HOT_RELOAD_EXPORT
* @brief Marks a single declaration in a hot-reloadable *module* for export
* under `hot_reload_restrict_exports()` (`cmake/HotReload.cmake`).
*
* hot_reload_restrict_exports() is a narrower export surface, not a
* quieter one: see the README's "Why do std::string/std::vector
* symbols show up as PATCHED?" section for why it does not, and
* structurally cannot, suppress those specifically. */
* This is for the plugins you hot-reload, not for this library's own API
* above (::HOT_RELOAD_API). It does not otherwise change a declaration's
* linkage: pair it with `extern "C"` yourself for a free function you want
* a stable, human-typable name for (see @ref hotreload_requirements for
* when you'd want that) -- `extern "C"` first, then this macro, then the
* return type, e.g. `extern "C" HOT_RELOAD_EXPORT int my_function(...)`,
* which is the one ordering GCC/Clang accept without a warning here. Leave
* the `extern "C"` off to keep a C++ class or method's real mangled name,
* e.g. a virtual method a host only ever reaches through a vtable slot
* (see the "Hot reloading C++ symbols" section of
* `source/HotReload/README.md`).
*
* @note `hot_reload_restrict_exports()` is a *narrower* export surface,
* not a *quieter* one: see the "Why do std::string/std::vector
* symbols show up as PATCHED?" section of `source/HotReload/README.md`
* for why it does not, and structurally cannot, suppress those
* specifically.
*/
#ifdef _WIN32
#define HOT_RELOAD_EXPORT __declspec(dllexport)
#else
@@ -118,196 +171,394 @@ extern "C"
{
#endif
/* Opaque -- see cpp-style-guide.md section 16 ("Do: use opaque
* pointers ... to hide implementation details"). The real definition
* is the C++ class hot_reload::HotReloadContext (HotReloadContext.hpp),
* never visible from this header. */
/**
* @defgroup hotreload_types Types and callbacks
* @brief The status codes, event/option structs, and callback signatures
* used throughout the rest of this API.
* @{
*/
/**
* @brief Opaque handle to a hot-reload context.
*
* See `cpp-style-guide.md` section 16 ("Do: use opaque pointers ... to
* hide implementation details"). The real definition is the C++ class
* `hot_reload::HotReloadContext` (`HotReloadContext.hpp`), never visible
* from this header.
*/
typedef struct HotReloadContext HotReloadContext;
/* Every fallible function returns one of these -- see section 16
* ("Do: return an integer error code / status enum from every
* fallible function"). */
/**
* @brief Result of any fallible call in this API.
*
* See `cpp-style-guide.md` section 16 ("Do: return an integer error
* code / status enum from every fallible function"). Render one of these
* as text with hot_reload_status_string(); for a fuller, platform-specific
* explanation of a ::HOT_RELOAD_ERROR_LOAD_FAILED, see
* hot_reload_last_error().
*/
typedef enum HotReloadStatus
{
/** Success. */
HOT_RELOAD_OK = 0,
/* ctx, module_name, or path/new_path was NULL, or module_name
* was not found for hot_reload_reload / hot_reload_get_symbol. */
/**
* A required argument (@c ctx, @c module_name, or @c path / @c new_path)
* was `NULL`, or hot_reload_load() was called for a @c module_name that is
* already loaded -- call hot_reload_reload() for that name instead.
*/
HOT_RELOAD_ERROR_INVALID_ARGUMENT,
/* dlopen()/LoadLibrary() (or the equivalent parse of the file on
* disk) failed for the given path. On hot_reload_reload(),
* this is non-destructive: the previously loaded version is left
* running untouched, exactly as if reload had not been called --
* see the file comment above HotReloadContext::reload(). Call
* hot_reload_last_error() for the platform's own description of
* why -- this one status code covers everything from a missing
* file to a genuinely corrupt image to (a real case, found while
* building this feature) a shared library with an unresolved
* symbol reference, and those need very different fixes. */
/**
* `dlopen()`/`LoadLibrary()` (or the equivalent parse of the file on
* disk) failed for the given path.
*
* On hot_reload_reload(), this is non-destructive: the previously
* loaded version is left running untouched, exactly as if reload had
* not been called. Call hot_reload_last_error() for the
* platform's own description of why -- this one status code covers
* everything from a missing file to a genuinely corrupt image to (a
* real case, found while building this feature) a shared library
* with an unresolved symbol reference, and those need very different
* fixes.
*
* It is also reported when the file could not be copied to its staging
* name (see hot_reload_load()): the source is unreadable, or the directory
* it lives in is not writable.
*/
HOT_RELOAD_ERROR_LOAD_FAILED,
/* hot_reload_reload() was called for a module_name that was never
* hot_reload_load()'ed first, so there is no "old" version to
* diff against. Call hot_reload_load() once, up front. */
/**
* hot_reload_reload() was called for a @c module_name that was never
* hot_reload_load()'ed first, so there is no "old" version to diff
* against. Call hot_reload_load() once, up front.
*/
HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION,
/* This process's CPU architecture has no patch backend compiled
* in (see Architecture.hpp -- currently x86, x86-64, and ARM64).
* Nothing was patched; hot_reload_load() still succeeded, you
* just cannot hot_reload_reload() on this target. */
/**
* This process's CPU architecture has no patch backend compiled in
* (see `Architecture.hpp` -- currently x86, x86-64, and ARM64).
* Nothing was patched; hot_reload_load() still succeeded, you just
* cannot hot_reload_reload() on this target.
*/
HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE
} HotReloadStatus;
/* Reported once per matched/unmatched symbol during hot_reload_reload()
* via HotReloadOptions::on_symbol_event, so the host application can
* log (or assert on, in a test) exactly what happened -- nothing here
* is silent. */
/**
* @brief What happened to one symbol during a hot_reload_reload() call.
*
* Reported once per matched/unmatched symbol via
* HotReloadOptions::on_symbol_event, so the host application can log (or
* assert on, in a test) exactly what happened -- nothing here is silent.
*/
typedef enum HotReloadSymbolEvent
{
/* Existed in both builds and was classified as code in both --
* the old function's entry now jumps to the new one. */
/**
* Existed in both builds and was classified as code in both -- the
* old function's entry now jumps to the new one.
*/
HOT_RELOAD_SYMBOL_PATCHED,
/* Exists in the new build only. Nothing to patch -- it becomes
* reachable the normal way, e.g. via hot_reload_get_symbol(). */
/**
* Exists in the new build only. Nothing to patch -- it becomes
* reachable the normal way, e.g. via hot_reload_get_symbol().
*/
HOT_RELOAD_SYMBOL_ADDED,
/* Existed in the old build, missing from the new one. Per
* cpp-style-guide.md's spirit of never leaving a caller with an
/**
* Existed in the old build, missing from the new one.
*
* Per `cpp-style-guide.md`'s spirit of never leaving a caller with an
* ambiguous, half-updated state: the old implementation is left
* running as-is unless HotReloadOptions::on_deleted_symbol
* supplies a stub to redirect to instead -- see that field. */
* running as-is unless HotReloadOptions::on_deleted_symbol supplies a
* stub to redirect to instead -- see that field.
*/
HOT_RELOAD_SYMBOL_REMOVED,
/* Present in both builds by name, but the symbol table classifies
* it as data (or something else non-function) in either build --
* e.g. an accidentally-exported global. Never touched, in either
* direction. This is the mechanical half of the "only executable
* code changes" guarantee described at the top of this file. */
/**
* Present in both builds by name, but the symbol table classifies it
* as data (or something else non-function) in either build -- e.g.
* an accidentally-exported global.
*
* Never touched, in either direction. This is the mechanical half of
* the "only executable code changes" guarantee described in
* @ref hotreload_what_it_wont.
*/
HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE,
/* Classified as a function in both builds, but the OLD function's
* compiled size is smaller than the number of bytes this
* platform's jump sequence needs to write -- patching it would
* overwrite whatever comes after it in memory. Skipped rather
* than guessed; see hot_reload_enable_patchable_functions() in
* cmake/HotReload.cmake for how to stop this from happening. */
/**
* Classified as a function in both builds, but the old function's
* compiled size is smaller than the number of bytes this platform's
* jump sequence needs to write -- patching it would overwrite
* whatever comes after it in memory.
*
* Skipped rather than guessed. Compile the module with
* `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to
* stop this from happening; `hot_reload_enable_patchable_functions()` in
* `cmake/HotReload.cmake` applies whichever one your compiler needs.
*/
HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL,
/* Classified as a function in both builds, correctly sized, but
* the OS refused to make its page temporarily writable (mprotect /
* VirtualProtect failed -- e.g. a hardened environment that denies
* W^X transitions). Rare in a normal developer setup; reported
* rather than silently ignored either way. */
/**
* Classified as a function in both builds, correctly sized, but the
* OS refused to make its page temporarily writable (`mprotect` /
* `VirtualProtect` failed -- e.g. a hardened environment that denies
* W^X transitions).
*
* Rare in a normal developer setup; reported rather than silently
* ignored either way.
*/
HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE
} HotReloadSymbolEvent;
/* One event report, passed to HotReloadOptions::on_symbol_event.
* old_address/new_address are NULL when not applicable (e.g.
* old_address is NULL for HOT_RELOAD_SYMBOL_ADDED). */
/**
* @brief One symbol's outcome, passed to HotReloadOptions::on_symbol_event.
*/
typedef struct HotReloadSymbolEventInfo
{
const char* name;
HotReloadSymbolEvent kind;
void* old_address;
void* new_address;
const char* name; /**< The symbol's real name (mangled, if it's a C++ symbol -- see hot_reload_demangle()). */
HotReloadSymbolEvent kind; /**< What happened to it -- see ::HotReloadSymbolEvent. */
void* old_address; /**< The symbol's address in the previous build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_ADDED). */
void* new_address; /**< The symbol's address in the new build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_REMOVED). */
} HotReloadSymbolEventInfo;
/**
* @brief Callback invoked once per symbol on every hot_reload_reload() call.
*
* Called synchronously, on the thread that called hot_reload_reload(), while the reload is in
* progress.
*
* @param event Describes one symbol's outcome; valid only for the duration of the call.
* @param user_data Whatever was passed as HotReloadOptions::user_data, untouched.
*
* @warning Do not call hot_reload_load() or hot_reload_reload() on the same context from inside
* this callback.
*/
typedef void (*HotReloadSymbolEventFn)(const HotReloadSymbolEventInfo* event, void* user_data);
/* Called instead of patching, once per HOT_RELOAD_SYMBOL_REMOVED
* event, so the host can catch stale references during development
* instead of them silently continuing to run old code forever. Return
* the address of a replacement to redirect the old function to (its
* only safe use is a fixed-signature stub -- e.g. `void stub(void)`--
* that logs/asserts; this library has no way to know the deleted
* function's real signature, so redirecting to anything that reads
* arguments the caller didn't pass is undefined behavior, exactly
* like calling any function through the wrong function-pointer type).
* Return NULL (or pass a NULL callback in HotReloadOptions to begin
* with) to leave the old implementation running untouched -- the
* default, and the safe choice unless you specifically want deleted
* functions to be loud. */
/**
* @brief Callback invoked instead of patching, once per ::HOT_RELOAD_SYMBOL_REMOVED event.
*
* Lets the host catch stale references during development instead of them
* silently continuing to run old code forever.
*
* Called synchronously, on the thread that called hot_reload_reload(), while the reload is in
* progress; like HotReloadSymbolEventFn, it must not call hot_reload_load() or
* hot_reload_reload() on the same context.
*
* @param symbol_name The removed function's real (possibly mangled) name.
* @param user_data Whatever was passed as HotReloadOptions::user_data, untouched.
* @return The address of a replacement to redirect the old function to, or
* `NULL` to leave the old implementation running untouched (the
* default, and the safe choice unless you specifically want
* deleted functions to be loud).
*
* @warning A non-`NULL` return's only safe use is a fixed-signature stub
* (e.g. `void stub(void)`) that logs/asserts; this library has
* no way to know the deleted function's real signature, so
* redirecting to anything that reads arguments the caller didn't
* pass is undefined behavior, exactly like calling any function
* through the wrong function-pointer type.
*/
typedef void* (*HotReloadDeletedSymbolFn)(const char* symbol_name, void* user_data);
/* Passed to hot_reload_context_create(); NULL is a valid options
* pointer meaning "no callbacks, default policy for everything". */
/**
* @brief Options passed to hot_reload_context_create().
*
* A `NULL` options pointer is valid and means "no callbacks, default
* policy for everything."
*/
typedef struct HotReloadOptions
{
HotReloadSymbolEventFn on_symbol_event; /* nullable */
HotReloadDeletedSymbolFn on_deleted_symbol; /* nullable */
void* user_data; /* passed back to both callbacks, untouched */
HotReloadSymbolEventFn on_symbol_event; /**< Nullable. See ::HotReloadSymbolEventFn. */
HotReloadDeletedSymbolFn on_deleted_symbol; /**< Nullable. See ::HotReloadDeletedSymbolFn. */
void* user_data; /**< Passed back to both callbacks, untouched. */
} HotReloadOptions;
/* Caller owns the returned pointer and must call
* hot_reload_context_destroy() -- see cpp-style-guide.md section 16
* ("Document ownership transfer explicitly"). options may be NULL. */
/** @} */ /* end of hotreload_types */
/**
* @defgroup hotreload_lifecycle Context lifecycle
* @brief Creating and destroying a hot-reload context.
* @{
*/
/**
* @brief Creates a new hot-reload context.
* @param options Nullable; see ::HotReloadOptions. Its contents are copied, so it need not
* outlive this call.
* @return A new context, or `NULL` if allocation failed. The caller owns it and must call
* hot_reload_context_destroy() (see `cpp-style-guide.md` section 16,
* "Document ownership transfer explicitly").
*/
HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options);
/**
* @brief Destroys a context created by hot_reload_context_create().
*
* Unloads every version of every module the context loaded (running their static destructors).
* From then on, every pointer obtained through this context -- functions, data, and the old
* entry points that had been patched to jump into newer versions -- is dangling and must not be
* used. Destroy a context only when nothing can still call into its modules.
*
* @param ctx The context to destroy. Safe to pass `NULL`.
*/
HOT_RELOAD_API void hot_reload_context_destroy(HotReloadContext* ctx);
/* Loads path for the first time under module_name -- an arbitrary
* caller-chosen key used to look the module back up in later
* hot_reload_reload()/hot_reload_get_symbol() calls; it does not need
* to match anything on disk. Returns HOT_RELOAD_ERROR_INVALID_ARGUMENT
* if module_name was already loaded -- call hot_reload_reload()
* instead of hot_reload_load() again for the same name. */
/** @} */ /* end of hotreload_lifecycle */
/**
* @defgroup hotreload_loading Loading and reloading
* @brief The core operations: register a module, hot-reload it, and
* resolve its symbols.
* @{
*/
/**
* @brief Loads a module for the first time under a logical name.
*
* The file is not loaded in place: it is first copied to a uniquely named sibling
* (`<path>.hotreload.<module_name>.<n>`) and that copy is loaded, which keeps a rebuild that
* overwrites @c path in place from being mistaken for the already-loaded version. The copy is
* deleted again right after loading on POSIX; on Windows it stays until the module is unloaded
* (see the top-level README). This means the directory containing @c path must be writable.
*
* The module's static constructors run as part of loading it.
*
* @note If the module's symbol table cannot be read (for instance a `.so` whose section headers
* were stripped), loading still succeeds but the module exports nothing as far as this
* library is concerned: hot_reload_get_symbol() finds nothing and a later
* hot_reload_reload() has nothing to patch.
*
* @param ctx The context to load into.
* @param module_name An arbitrary caller-chosen key used to look the
* module back up in later hot_reload_reload()/
* hot_reload_get_symbol() calls; it does not need to match
* anything on disk.
* @param path Path to the shared library (`.so`/`.dll`) to load. Always treated as a file path:
* a bare file name such as `plugin.so` means the file of that name in the current
* working directory (it is never looked up through the library search path).
* @return ::HOT_RELOAD_OK, or ::HOT_RELOAD_ERROR_INVALID_ARGUMENT if
* an argument was `NULL` or @c module_name was already loaded (call
* hot_reload_reload() instead of hot_reload_load() again for the same
* name), or ::HOT_RELOAD_ERROR_LOAD_FAILED (see hot_reload_last_error()
* for why).
*/
HOT_RELOAD_API HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path);
/* Loads a NEW build of the module previously registered under
* module_name, diffs its exported functions against the previous
* version, and patches every one that is safe to patch in place --
* see HotReloadSymbolEvent above for exactly what happens to each
* kind of change. On any HOT_RELOAD_ERROR_* return, nothing was
* changed: the previous version keeps running exactly as it was. */
/**
* @brief Hot-reloads a module previously registered with hot_reload_load().
*
* Loads a new build of the module, diffs its exported functions against
* the previous version, and patches every one that is safe to patch in
* place -- see ::HotReloadSymbolEvent for exactly what happens to each
* kind of change. The previous version stays loaded (its entry points now
* jump into the new one), and the new build's static constructors run.
* If the new build exports no functions at all -- including when its symbol
* table could not be read -- every previously exported function is reported
* as ::HOT_RELOAD_SYMBOL_REMOVED.
*
* Call this from a quiescent point (see @ref hotreload_threads), and see
* @ref hotreload_lifetime for what does not carry over. @c new_path follows
* the same rules as @c path in hot_reload_load().
*
* @param ctx The context @c module_name was hot_reload_load()'ed into.
* @param module_name The same logical name passed to hot_reload_load().
* @param new_path Path to the new build of the shared library.
* @return ::HOT_RELOAD_OK, or an `HOT_RELOAD_ERROR_*` code. On any error,
* nothing was changed: the previous version keeps running exactly
* as it was.
*/
HOT_RELOAD_API HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path);
/* Resolves symbol_name in module_name's CURRENT build (i.e. always
* the freshest one loaded, patched or not) -- for calling a function
* that did not exist yet when the host first started, such as one
* reported via HOT_RELOAD_SYMBOL_ADDED. Returns NULL if module_name
* is unknown or symbol_name is not an exported function in it. */
/**
* @brief Resolves a symbol in a module's current build.
*
* Always resolves against the freshest build loaded for @c module_name,
* patched or not -- useful for calling a function that did not exist yet
* when the host first started, such as one reported via
* ::HOT_RELOAD_SYMBOL_ADDED. Any exported symbol resolves, data included
* (a global variable, say); only functions are ever patched.
*
* @param ctx The context @c module_name was hot_reload_load()'ed into.
* @param module_name The module to resolve @c symbol_name in.
* @param symbol_name The symbol's real (possibly mangled) name.
* @return The symbol's current address, or `NULL` if an argument is `NULL`,
* @c module_name is unknown, or @c symbol_name is not exported by
* the module's newest build.
*/
HOT_RELOAD_API void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name);
/* Only meaningful immediately after hot_reload_load() or
* hot_reload_reload() on this SAME ctx returned something other than
* HOT_RELOAD_OK: the underlying platform's own description of why --
* dlerror() on POSIX, the formatted message for GetLastError() on
* Windows -- e.g. "undefined symbol: _ZN6Widget4tickEi" for a shared
* library with an unresolved reference (a real, confusing-until-you-
* see-this-message case: a non-inline static class member that is
* declared but never given an out-of-class definition compiles fine
* and links fine into a shared library -- the linker allows a shared
* library to keep undefined symbols by default -- and only fails,
* right here, once something ODR-uses it and the loader tries to
* resolve it for real). Overwritten by the next
* hot_reload_load()/hot_reload_reload() call on this ctx; copy it out
* immediately if you need to keep it past that point (e.g. for a log
* line). Empty if the platform gave no further detail, or if the
* failure never reached the platform loader at all (e.g. a NULL
* argument, which is HOT_RELOAD_ERROR_INVALID_ARGUMENT before
* anything platform-specific happens). A static string internally --
* never free() this. */
/** @} */ /* end of hotreload_loading */
/**
* @defgroup hotreload_diagnostics Diagnostics
* @brief Turning a status code or a mangled name into something readable.
* @{
*/
/**
* @brief The platform's own explanation for the most recent failure on this context.
*
* Only meaningful immediately after hot_reload_load() or
* hot_reload_reload() on this *same* @c ctx returned something other than
* ::HOT_RELOAD_OK: the underlying platform's own description of why --
* `dlerror()` on POSIX, the formatted message for `GetLastError()` on
* Windows -- e.g. `"undefined symbol: _ZN6Widget4tickEi"` for a shared
* library with an unresolved reference (a real, confusing-until-you-see-
* this-message case: a non-inline static class member that is declared
* but never given an out-of-class definition compiles fine and links fine
* into a shared library -- the linker allows a shared library to keep
* undefined symbols by default -- and only fails, right here, once
* something ODR-uses it and the loader tries to resolve it for real).
*
* @param ctx The context to query.
* @return The platform's error message. Empty if it gave no further
* detail, or if the failure never reached the platform loader at
* all (e.g. a `NULL` argument, which is
* ::HOT_RELOAD_ERROR_INVALID_ARGUMENT before anything
* platform-specific happens). Owned by @c ctx -- never `free()`
* this. It stays valid only until the next hot_reload_load()/
* hot_reload_reload() call on this @c ctx, or until the context is
* destroyed; copy it out immediately if you need to keep it past
* that point (e.g. for a log line).
*/
HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx);
/* A static string literal -- never free() this. */
/**
* @brief Renders a ::HotReloadStatus as text.
* @param status The status to render.
* @return A static string literal -- never `free()` this.
*/
HOT_RELOAD_API const char* hot_reload_status_string(HotReloadStatus status);
/* Renders name for a human to read: a real C++ mangled name (Itanium
* on GCC/Clang/MinGW, MSVC's own scheme under MSVC -- whichever this
* library itself was built with) becomes something like
* "Widget::tick(int)"; anything else, including an ordinary extern "C"
* name, comes back unchanged. Safe to call unconditionally on any
* HotReloadSymbolEventInfo::name or hot_reload_get_symbol() key --
* this never changes what name means for lookup purposes, it is only
* ever applied when you ask for it, purely for display. Returns NULL
* only if name itself is NULL or allocation fails; caller must free
* a non-NULL result with hot_reload_free_string(). */
/**
* @brief Renders a symbol name for a human to read.
*
* A real C++ mangled name (Itanium on GCC/Clang/MinGW, MSVC's own scheme
* under MSVC -- whichever this library itself was built with) becomes
* something like `"Widget::tick(int)"`; anything else, including an
* ordinary `extern "C"` name, comes back unchanged. Safe to call
* unconditionally on any HotReloadSymbolEventInfo::name or
* hot_reload_get_symbol() key -- this never changes what a name means for
* lookup purposes, it is only ever applied when you ask for it, purely
* for display.
*
* @param name The name to render.
* @return The rendered name, or `NULL` if @c name itself is `NULL` or
* allocation fails. Caller must free a non-`NULL` result with
* hot_reload_free_string().
*/
HOT_RELOAD_API char* hot_reload_demangle(const char* name);
/**
* @brief Frees a string returned by hot_reload_demangle().
* @param str The string to free. Safe to pass `NULL`.
*/
HOT_RELOAD_API void hot_reload_free_string(char* str);
/** @} */ /* end of hotreload_diagnostics */
#ifdef __cplusplus
}
#endif
+60 -24
View File
@@ -1,3 +1,7 @@
/**
* @file Architecture.cpp
* @brief Per-architecture jump-patch backends for x86, x86-64, and ARM64.
*/
#include "Architecture.hpp"
#include <atomic>
@@ -51,7 +55,8 @@
* 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.
* requirements). See the "Limitations" section of the top-level README
* for this precondition stated plainly.
*/
namespace hot_reload::detail
@@ -60,20 +65,30 @@ namespace hot_reload::detail
{
#if defined(__x86_64__) || defined(_M_X64)
/// Size in bytes of the CET landing pad (`endbr64`) a function may start with.
constexpr std::size_t kLandingPadBytes = 4;
/// Encoding of `endbr64`, the x86-64 CET indirect-branch landing pad.
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.
/// Size in bytes of the jump written over a function's entry:
/// `jmp qword ptr [rip+0]` followed by an 8-byte absolute address. That 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;
/// Whether the function at @p entry starts with an `endbr64` landing pad.
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
/// @return True if the first bytes are `endbr64`.
bool hasLandingPad(const unsigned char* entry)
{
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
}
/// Writes the x86-64 absolute jump at @p site, payload first and the opcode last.
/// @param site Where the jump goes (just past the landing pad, if there is one).
/// @param target Address the jump transfers control to.
void writeJumpAt(unsigned char* site, const void* target)
{
std::uint64_t targetValue = 0;
@@ -99,20 +114,29 @@ namespace hot_reload::detail
#elif defined(__i386__) || defined(_M_IX86)
/// Size in bytes of the CET landing pad (`endbr32`) a function may start with.
constexpr std::size_t kLandingPadBytes = 4;
/// Encoding of `endbr32`, the 32-bit x86 CET indirect-branch landing pad.
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.
/// Size in bytes of the jump written over a function's entry: `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;
/// Whether the function at @p entry starts with an `endbr32` landing pad.
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
/// @return True if the first bytes are `endbr32`.
bool hasLandingPad(const unsigned char* entry)
{
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
}
/// Writes the x86 relative jump at @p site, displacement first and the opcode last.
/// @param site Where the jump goes (just past the landing pad, if there is one).
/// @param target Address the jump transfers control to.
void writeJumpAt(unsigned char* site, const void* target)
{
std::int32_t rel32 = static_cast<std::int32_t>(
@@ -128,35 +152,47 @@ namespace hot_reload::detail
#elif defined(__aarch64__) || defined(_M_ARM64)
/// Size in bytes of the BTI landing pad a function may start with (one instruction word).
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.
/// Whether the instruction word at @p entry is 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.
/// @param entry The function's entry address (at least 4 readable bytes).
/// @return True if the first instruction is a BTI landing pad.
bool isBti(const unsigned char* entry)
{
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
}
/// Whether the function at @p entry starts with a BTI landing pad.
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
/// @return True if the first instruction is a BTI landing pad.
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.
/// Size in bytes of the jump written over a function's 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;
/// Encoding of `ldr x17, #8` (loads the literal address stored 8 bytes further on).
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
/// Encoding of `br x17` (branches to the address `ldr x17, #8` loaded).
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
/// Writes the ARM64 `ldr`/`br` pair plus literal address at @p site, payload first and
/// the leading `ldr` last.
/// @param site Where the jump goes (just past the landing pad, if there is one).
/// @param target Address the jump transfers control to.
void writeJumpAt(unsigned char* site, const void* target)
{
std::uint64_t targetValue = 0;
+71 -36
View File
@@ -2,56 +2,91 @@
#include <cstddef>
/**
* @namespace hot_reload::detail
* @brief Internal building blocks of the hot-reload runtime -- not part of the public API.
*
* 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`.
*/
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.
/**
* @brief 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.
/**
* 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.
/**
* 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.
/**
* @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.
*
* @return True if writeJump() can succeed on this 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).
/**
* @brief Determines the real patch layout for one specific function.
*
* Inspects the bytes already sitting at @p entryAddress, accounting
* for a possible leading landing pad. Safe to call even when
* isArchitectureSupported() is false (returns a zeroed PatchLayout).
*
* @param entryAddress The function's entry address.
* @return The layout to check against the symbol's compiled size
* before writing anything, and to pass to writeJump().
*/
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.
/**
* @brief Overwrites a function's entry so it jumps to another address.
*
* Follows the layout planPatch() would return for this same
* @p entryAddress.
*
* @param entryAddress The function entry to overwrite.
* @param targetAddress Where control should transfer to instead.
* @return False only if isArchitectureSupported() is false.
*
* @pre The caller has already made
* `[entryAddress, entryAddress + planPatch(entryAddress).totalBytes)`
* writable (see `MemoryProtection.hpp`) and will flush the
* instruction cache over that same range afterwards -- this
* function does neither.
* @pre 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.
*/
bool writeJump(void* entryAddress, const void* targetAddress);
}
+68 -21
View File
@@ -1,3 +1,7 @@
/**
* @file HotReloadContext.cpp
* @brief The diff-and-patch orchestration logic: load, reload, and per-symbol policy.
*/
#include "HotReloadContext.hpp"
#include "Architecture.hpp"
@@ -12,6 +16,12 @@ 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);
@@ -28,32 +38,67 @@ namespace hot_reload
return static_cast<bool>(out);
}
// 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 the rest of the CURRENT process's lifetime (see
// HotReloadContext.hpp's LoadedModule comment) -- 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, here, 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.
/// 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(".");
std::string prefix = source.filename().string() + ".hotreload." + moduleName + ".";
const std::string tag = stagingTag(moduleName);
std::string prefix = source.filename().string() + ".hotreload." + tag + ".";
std::error_code dirError;
fs::directory_iterator entries(dir, dirError);
@@ -120,7 +165,9 @@ namespace hot_reload
// 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);
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 {};
+115 -17
View File
@@ -8,62 +8,160 @@
#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
{
/// 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.
/**
* @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;
/// See hot_reload_load()'s doc comment in hot_reload.h.
/**
* @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);
/// See hot_reload_reload()'s doc comment in hot_reload.h.
/**
* @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);
/// See hot_reload_get_symbol()'s doc comment in hot_reload.h.
/**
* @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;
/// See hot_reload_last_error()'s doc comment in hot_reload.h.
/**
* @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.
/// 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.
/// 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;
};
@@ -1,3 +1,7 @@
/**
* @file MemoryProtection.cpp
* @brief Code-page write-access RAII guard implementation (mprotect/VirtualProtect).
*/
#include "MemoryProtection.hpp"
#include <cstdint>
@@ -20,6 +24,8 @@ namespace hot_reload::detail
{
namespace
{
/// The operating system's virtual-memory page size.
/// @return The page size in bytes (4096 if the platform could not report one).
std::size_t systemPageSize()
{
#if defined(_WIN32)
+61 -22
View File
@@ -5,50 +5,89 @@
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.
/**
* @brief RAII guard that temporarily makes a code range writable.
*
* 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.
*
* @note 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.
*
* @note The change applies to whole pages and is process-wide: every thread sees the
* enclosing pages as writable (and still executable) while the guard is alive.
*/
class ScopedCodeWriteAccess
{
public:
/**
* @brief Attempts to make a range writable immediately.
*
* Check isValid() afterwards: the platform call can be refused (for example by a
* hardened environment that denies writable+executable transitions).
*
* @param address Start of the range.
* @param length Length of the range in bytes.
*/
ScopedCodeWriteAccess(void* address, std::size_t length);
/**
* @brief Restores the range's original protection (does nothing if isValid() is false).
*/
~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).
/**
* @brief Whether the platform call to relax protection succeeded.
*
* When false, nothing was changed and the range must not be
* written to (the destructor is still safe to run either way).
*
* @return True if the range is currently writable.
*/
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).
/**
* @brief Flushes the instruction cache over the patched range.
*
* Call exactly once, after writing the new bytes and before this
* guard is destroyed, 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:
/// Start of the page-aligned region whose protection was changed.
void* m_pageAddress;
/// Length in bytes of that page-aligned region (a whole number of pages).
std::size_t m_pageLength;
/// Start of the range the caller actually asked to write to.
void* m_rangeAddress;
/// Length in bytes of the range the caller actually asked to write to.
std::size_t m_rangeLength;
/// The protection to restore on destruction, in the platform's own encoding
/// (`PROT_*` flags on POSIX, a `PAGE_*` constant on Windows).
unsigned long m_previousProtection;
/// Whether the constructor's protection change succeeded.
bool m_valid;
};
+25 -4
View File
@@ -1,6 +1,11 @@
/**
* @file ModuleHandle.cpp
* @brief RAII dlopen()/LoadLibrary() wrapper implementation.
*/
#include "ModuleHandle.hpp"
#include <string>
#include <utility>
#if defined(_WIN32)
// NOMINMAX: see MemoryProtection.cpp's identical comment -- same
@@ -23,9 +28,12 @@ namespace hot_reload::detail
{
/// 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.
/// dlerror() gives directly on POSIX. Exercised under Wine (MinGW cross-build) -- see
/// "What's actually been tested" in the top-level README; not verified against a real
/// Windows/MSVC build.
/// @return The system's message for the calling thread's last Win32 error, without
/// trailing line breaks, or a generic message naming the error code if the
/// system has none.
std::string formatLastWindowsError()
{
DWORD errorCode = GetLastError();
@@ -71,6 +79,7 @@ namespace hot_reload::detail
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
: m_native(other.m_native)
, m_loadedBase(other.m_loadedBase)
, m_errorMessage(std::move(other.m_errorMessage))
{
other.m_native = nullptr;
other.m_loadedBase = nullptr;
@@ -90,6 +99,7 @@ namespace hot_reload::detail
}
m_native = other.m_native;
m_loadedBase = other.m_loadedBase;
m_errorMessage = std::move(other.m_errorMessage);
other.m_native = nullptr;
other.m_loadedBase = nullptr;
}
@@ -129,7 +139,18 @@ namespace hot_reload::detail
// 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);
//
// The "./" prefix: dlopen() only treats its argument as a file
// path if it contains a '/'. A bare name (e.g. "plugin.so") is
// instead searched for like a library name -- LD_LIBRARY_PATH,
// ld.so.cache, the default directories -- and is never looked
// for in the current directory, so a plain relative file name
// 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.
const std::string loadPath = path.find('/') == std::string::npos ? "./" + path : path;
void* module = dlopen(loadPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (module == nullptr)
{
const char* error = dlerror();
+66 -16
View File
@@ -5,53 +5,103 @@
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).
/**
* @brief RAII wrapper around `dlopen()`/`LoadLibrary()`.
*
* Move-only, closes the underlying module on destruction (or on
* being replaced by a move-assignment).
*
* @note Closing a module unmaps its code (unless the platform keeps it resident, e.g. a
* library that defines C++ unique symbols). Any function pointer into a closed module
* -- including old, patched entry points that jump into it -- dangles from then on.
*/
class ModuleHandle
{
public:
/** @brief Creates an empty handle that owns no module (isValid() is false). */
ModuleHandle() = default;
/** @brief Closes the module, if this handle owns one. */
~ModuleHandle();
ModuleHandle(const ModuleHandle&) = delete;
ModuleHandle& operator=(const ModuleHandle&) = delete;
/**
* @brief Takes ownership of @p other's module (and its error message), leaving @p other
* empty.
* @param other The handle to move from.
*/
ModuleHandle(ModuleHandle&& other) noexcept;
/**
* @brief Closes this handle's current module, then takes ownership of @p other's module
* (and its error message), leaving @p other empty.
* @param other The handle to move from.
* @return This handle.
*/
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
/// Loads path. On failure the returned handle's isValid() is
/// false and loadedBase() is null.
/**
* @brief Loads a shared library.
*
* @param path Path to the shared library to load. Always treated as a filesystem path: a
* bare file name such as `plugin.so` means the file of that name in the current
* working directory (on POSIX it is loaded as `./plugin.so`, because `dlopen()`
* would otherwise search the library path for it instead).
* @return The loaded handle. On failure, isValid() is false and
* loadedBase() is null -- check errorMessage() for why.
*/
static ModuleHandle load(const std::string& path);
/**
* @brief Whether load() succeeded.
* @return True if this handle owns a loaded module.
*/
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.
/**
* @brief 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.
*
* @return The value described above, or null if isValid() is false.
*/
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.
/**
* @brief Why load() failed, if 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 POSIX dl-family or
* Win32 call that could otherwise overwrite it first.
*
* @return The platform's message, or empty if load() was never
* attempted or the platform gave no message.
*/
const std::string& errorMessage() const
{
return m_errorMessage;
}
private:
/// The platform's own handle (`HMODULE` or the `dlopen()` handle); null if none.
void* m_native = nullptr;
/// See loadedBase().
void* m_loadedBase = nullptr;
/// See errorMessage().
std::string m_errorMessage;
};
+160 -37
View File
@@ -1,3 +1,7 @@
/**
* @file SymbolTable.cpp
* @brief Reads and classifies a module's exported symbols (ELF and PE).
*/
#include "SymbolTable.hpp"
#include <algorithm>
@@ -5,6 +9,7 @@
#include <cstring>
#include <fstream>
#include <map>
#include <string_view>
#include <vector>
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
@@ -49,13 +54,18 @@ namespace hot_reload::detail
namespace
{
/// One PE section, reduced to what the function/data classification needs.
struct SectionInfo
{
std::uint32_t rvaBegin;
std::uint32_t rvaEnd;
bool executable;
std::uint32_t rvaBegin; /**< First RVA covered by the section. */
std::uint32_t rvaEnd; /**< One past the last RVA covered by the section. */
bool executable; /**< Whether the section is marked as containing code. */
};
/// Turns a relative virtual address inside the mapped image into a string pointer.
/// @param base The image's base address.
/// @param rva A relative virtual address inside the image.
/// @return `base + rva`, viewed as a C string.
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
{
return reinterpret_cast<const char*>(base + rva);
@@ -170,9 +180,11 @@ namespace hot_reload::detail
// 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.
// below), so /hotpatch (applied by calling
// hot_reload_enable_patchable_functions() from
// cmake/HotReload.cmake on the plugin target) 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)
{
@@ -236,6 +248,10 @@ namespace hot_reload::detail
namespace
{
/// Reads a whole file into memory.
/// @param path The file to read.
/// @return The file's bytes, or an empty vector if it could not be opened, is empty, or
/// could not be read completely.
std::vector<char> readWholeFile(const std::string& path)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
@@ -257,83 +273,191 @@ namespace hot_reload::detail
return buffer;
}
/// Whether a range read out of an ELF file lies entirely inside that file.
///
/// Every offset and length in an ELF header is untrusted input until proven otherwise:
/// the dynamic loader validates the program headers it maps, but nothing validates the
/// section header table this reader walks. Written so the sum can never overflow.
///
/// @param offset Start of the range, as read from the file.
/// @param length Length of the range, as read from the file.
/// @param fileSize Size of the whole file in bytes.
/// @return True if `[offset, offset + length)` is inside `[0, fileSize)`.
bool rangeInFile(std::uint64_t offset, std::uint64_t length, std::size_t fileSize)
{
return offset <= fileSize && length <= fileSize - offset;
}
/// Copies one `T` out of the file, if it lies entirely inside it.
///
/// A `memcpy` rather than a `reinterpret_cast`, so an offset read from a corrupt file
/// can never produce a misaligned access.
///
/// @tparam T A trivially copyable ELF structure (`Elf64_Ehdr`, `Elf32_Sym`, ...).
/// @param file The whole file.
/// @param offset Where the structure starts.
/// @param[out] out Receives the structure.
/// @return False (leaving @p out untouched) if the structure would extend past the end.
template <typename T>
bool readStruct(const std::vector<char>& file, std::uint64_t offset, T& out)
{
if (!rangeInFile(offset, sizeof(T), file.size()))
{
return false;
}
std::memcpy(&out, file.data() + offset, sizeof(T));
return true;
}
/// Looks up a NUL-terminated string in an ELF string table, without ever reading past it.
/// @param table Start of the string table.
/// @param tableSize Size of the string table in bytes.
/// @param offset The string's offset within the table, as read from the file.
/// @return The string, or an empty view if @p offset is outside the table or the string
/// is not NUL-terminated inside it.
std::string_view stringAt(const char* table, std::uint64_t tableSize, std::uint64_t offset)
{
if (offset >= tableSize)
{
return {};
}
const char* begin = table + offset;
const auto remaining = static_cast<std::size_t>(tableSize - offset);
const void* end = std::memchr(begin, '\0', remaining);
if (end == nullptr)
{
return {};
}
const auto* endPtr = static_cast<const char*>(end);
return std::string_view(begin, static_cast<std::size_t>(endPtr - begin));
}
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
/// @return The symbol's type (`STT_*`).
unsigned char elfSymType(unsigned char info)
{
return static_cast<unsigned char>(ELF32_ST_TYPE(info));
}
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
/// @return The symbol's binding (`STB_*`).
unsigned char elfSymBind(unsigned char info)
{
return static_cast<unsigned char>(ELF32_ST_BIND(info));
}
/// 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)
///
/// Every offset, length and index read from the file is bounds-checked first (see
/// rangeInFile()), so a damaged image produces a shorter result rather than an
/// out-of-bounds read.
///
/// @tparam Ehdr The ELF file header type (`Elf32_Ehdr` or `Elf64_Ehdr`).
/// @tparam Shdr The ELF section header type (`Elf32_Shdr` or `Elf64_Shdr`).
/// @tparam Sym The ELF symbol type (`Elf32_Sym` or `Elf64_Sym`).
/// @param file The whole shared object, as read from disk.
/// @param loadedBase The runtime load bias to add to every symbol's `st_value`.
/// @return Every defined, global or weak symbol in `.dynsym`, classified; empty if the
/// file's section headers or symbol tables are missing or inconsistent.
template <typename Ehdr, typename Shdr, typename Sym>
std::vector<ExportedSymbol> parseElf(const std::vector<char>& file, void* loadedBase)
{
std::vector<ExportedSymbol> result;
if (file.size() < sizeof(Ehdr))
Ehdr ehdr{};
if (!readStruct(file, 0, ehdr))
{
return result;
}
const auto* ehdr = reinterpret_cast<const Ehdr*>(file.data());
if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0)
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())
const auto sectionTableBytes = static_cast<std::uint64_t>(ehdr.e_shnum) * sizeof(Shdr);
const bool sectionsFit = rangeInFile(ehdr.e_shoff, sectionTableBytes, file.size());
if (ehdr.e_shentsize != sizeof(Shdr) || !sectionsFit)
{
return result; // truncated/corrupt file
}
const auto* sections = reinterpret_cast<const Shdr*>(file.data() + ehdr->e_shoff);
std::vector<Shdr> sections(ehdr.e_shnum);
const auto sectionBytes = sections.size() * sizeof(Shdr);
std::memcpy(sections.data(), file.data() + ehdr.e_shoff, sectionBytes);
// Section header string table, to find sections by name.
if (ehdr->e_shstrndx >= ehdr->e_shnum)
if (ehdr.e_shstrndx >= ehdr.e_shnum)
{
return result;
}
const char* shstrtab = file.data() + sections[ehdr->e_shstrndx].sh_offset;
const Shdr& shstrSection = sections[ehdr.e_shstrndx];
if (!rangeInFile(shstrSection.sh_offset, shstrSection.sh_size, file.size()))
{
return result;
}
const char* shstrtab = file.data() + shstrSection.sh_offset;
const Shdr* dynsymSection = nullptr;
const Shdr* dynstrSection = nullptr;
for (std::size_t i = 0; i < ehdr->e_shnum; ++i)
for (const Shdr& section : sections)
{
const char* name = shstrtab + sections[i].sh_name;
if (std::strcmp(name, ".dynsym") == 0)
const auto nameOffset = section.sh_name;
const std::string_view name = stringAt(shstrtab, shstrSection.sh_size, nameOffset);
if (name == ".dynsym")
{
dynsymSection = &sections[i];
dynsymSection = &section;
}
else if (std::strcmp(name, ".dynstr") == 0)
else if (name == ".dynstr")
{
dynstrSection = &sections[i];
dynstrSection = &section;
}
}
if (dynsymSection == nullptr || dynstrSection == nullptr)
{
return result;
}
if (!rangeInFile(dynsymSection->sh_offset, dynsymSection->sh_size, file.size()) ||
!rangeInFile(dynstrSection->sh_offset, dynstrSection->sh_size, file.size()))
{
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);
const auto symBytes = static_cast<std::size_t>(dynsymSection->sh_size);
const std::size_t symbolCount = symBytes / sizeof(Sym);
for (std::size_t i = 0; i < symbolCount; ++i)
{
const Sym& sym = symbols[i];
Sym sym{};
const char* symPtr = file.data() + dynsymSection->sh_offset + i * sizeof(Sym);
std::memcpy(&sym, symPtr, sizeof(Sym));
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
{
continue; // imported/unresolved, not defined here
}
unsigned char bind = symBind(sym.st_info);
unsigned char bind = elfSymBind(sym.st_info);
if (bind != STB_GLOBAL && bind != STB_WEAK)
{
continue; // not part of this module's visible surface
}
const auto symNameOffset = sym.st_name;
const auto dynstrSize = dynstrSection->sh_size;
const std::string_view name = stringAt(dynstr, dynstrSize, symNameOffset);
if (name.empty())
{
continue; // name offset outside .dynstr (corrupt file)
}
ExportedSymbol exported;
exported.name = dynstr + sym.st_name;
exported.name = std::string(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 isFuncType = elfSymType(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)
const bool realSection = sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON;
if (realSection && sym.st_shndx < ehdr.e_shnum)
{
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
}
@@ -354,18 +478,17 @@ namespace hot_reload::detail
return {};
}
// ELF32_ST_TYPE/BIND and ELF64_ST_TYPE/BIND decode the same bit layout (the System V ABI
// defines st_info identically for both classes) -- elfSymType()/elfSymBind() above use the
// ELF32 macros for both, so one pair of extractors serves both instantiations below.
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); });
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(file, loadedBase);
}
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 parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(file, loadedBase);
}
return {};
}
+46 -31
View File
@@ -7,45 +7,60 @@
namespace hot_reload::detail
{
/// One exported symbol found in a loaded module, already classified.
/**
* @brief One exported symbol found in a loaded module, already classified.
*/
struct ExportedSymbol
{
std::string name;
void* address = nullptr;
std::string name; /**< The symbol's real (possibly mangled) name. */
void* address = nullptr; /**< The symbol's runtime address. */
/// 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.
/**
* 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.
/**
* 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).
/**
* @brief Reads and classifies every exported, defined symbol in a loaded module.
*
* On POSIX this parses @p imagePath on disk (its ELF `.dynsym` /
* `.dynstr` / section-header table) and adds @p loadedBase as the
* runtime load bias. On Windows this ignores @p imagePath and reads
* directly from the already-mapped image at @p 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.
*
* A file that is truncated or otherwise corrupt in the parts this reader relies on (the
* section header table and the tables it points at) is treated like any other unparseable
* image: every offset and length read from the file is range-checked against the file's size
* before it is used, so a damaged image yields fewer (or no) symbols rather than an
* out-of-bounds read.
*
* @param imagePath Path to the module on disk (POSIX only -- see above).
* @param loadedBase The base address `dlopen()`/`LoadLibrary()` returned for it.
* @return Every exported symbol, classified via ExportedSymbol::isFunction.
* Empty (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);
}
+7 -4
View File
@@ -1,4 +1,6 @@
/* Demangle.cpp -- hot_reload_demangle() / hot_reload_free_string().
/**
* @file Demangle.cpp
* @brief hot_reload_demangle() / hot_reload_free_string() implementation.
*
* Pure string utility, no HotReloadContext involved, so it gets its
* own file alongside HotReloadApi.cpp rather than crowding that one's
@@ -15,6 +17,7 @@
*/
#include "hot_reload/hot_reload.h"
#include <cstdlib>
#include <cstring>
#include <string>
@@ -24,9 +27,9 @@
// 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.
// 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,
@@ -1,3 +1,7 @@
/**
* @file HotReloadApi.cpp
* @brief The extern "C" shim implementing hot_reload.h by delegating to hot_reload::HotReloadContext.
*/
#include "hot_reload/hot_reload.h"
#include "../HotReloadContext.hpp"
@@ -9,6 +13,8 @@ namespace
/// 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)
{
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);