Add project files.
This commit is contained in:
commit
3087fcea8a
71 files changed
+8325
No files matched your search
@@ -0,0 +1,14 @@
|
||||
# The hot-reload runtime is infrastructure the host application links
|
||||
# against directly. Unlike the plugins it loads at runtime (see
|
||||
# source/examples/HotReloadDemo), this library is never itself meant
|
||||
# to be hot-reloaded, so TYPE STATIC is a deliberate choice here,
|
||||
# independent of the project-wide BUILD_SHARED_LIBS default.
|
||||
add_lib_module(hotreload_lib TYPE STATIC OUTPUT_NAME "hotreload")
|
||||
|
||||
if(NOT WIN32)
|
||||
# dlopen/dlclose/dlinfo (ModuleHandle.cpp). PUBLIC, not PRIVATE:
|
||||
# for a STATIC library this needs to reach whatever executable
|
||||
# ultimately links hotreload_lib, not just hotreload_lib's own
|
||||
# (nonexistent, for a static lib) link step.
|
||||
target_link_libraries(hotreload_lib PUBLIC ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
@@ -0,0 +1,396 @@
|
||||
# Hot Reload
|
||||
|
||||
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.
|
||||
|
||||
If you just want to see it run:
|
||||
|
||||
```sh
|
||||
cmake -S . -B out -DBUILD_EXAMPLES=ON
|
||||
cmake --build out
|
||||
./out/bin/examples/hotreload_demo_host
|
||||
```
|
||||
|
||||
Then open `source/examples/HotReloadDemo/PluginV1/src/Plugin.cpp` and
|
||||
`PluginV2/src/Plugin.cpp` side by side against the output -- they're
|
||||
written to stand in for "the file before your edit" and "after". The
|
||||
same two directories' `CppWidget.cpp` do the same for a real C++
|
||||
class with no `extern "C"` anywhere -- see "Hot reloading C++ symbols"
|
||||
below.
|
||||
|
||||
Both of those exist to prove things deterministically, though -- for
|
||||
actually playing with this yourself, see
|
||||
`source/examples/HotReloadLiveDemo/README.md` instead: a host that
|
||||
stays running, and one plugin file you edit and rebuild as many times
|
||||
as you want while it's live.
|
||||
|
||||
## How it works, in one paragraph
|
||||
|
||||
`hot_reload_load()` loads a shared library and reads its exported
|
||||
symbol table. `hot_reload_reload()` loads a *new* build of that same
|
||||
library, reads its symbol table too, and for every name both builds
|
||||
export as a function, overwrites the first few bytes of the *old*
|
||||
function with an unconditional jump to the *new* one's address. Every
|
||||
existing call site -- direct calls, calls through the PLT/GOT, raw
|
||||
function pointers already sitting in some struct -- reaches the old
|
||||
address and immediately redirects, because that's now all that's
|
||||
there. Full byte-level detail, verified against a real assembler
|
||||
rather than transcribed from memory, is in
|
||||
`src/Architecture.cpp`'s file comment.
|
||||
|
||||
## The two guarantees this library asked to make
|
||||
|
||||
**"Only executable code changes, never data."** This isn't a naming
|
||||
convention you have to follow -- it's mechanical. `src/SymbolTable.cpp`
|
||||
classifies every exported symbol using the loader's own metadata (an
|
||||
ELF symbol's type + its section's executable flag; a PE export's
|
||||
containing section's characteristics), and `HotReloadContext` refuses
|
||||
to patch anything not classified as a function, on *either* side of a
|
||||
reload. `source/examples/HotReloadDemo` proves this concretely:
|
||||
`plugin_build_marker` is an exported `int` sitting right next to real
|
||||
functions, changes value between V1 and V2 exactly like a function's
|
||||
behavior would, and the demo reads its original address again *after*
|
||||
reloading to show it's untouched.
|
||||
|
||||
**"Handle deletion gracefully."** A function missing from the new
|
||||
build is reported through `HotReloadOptions::on_symbol_event`
|
||||
(`HOT_RELOAD_SYMBOL_REMOVED`) and, by default, its old implementation
|
||||
is simply left running -- nothing is ever patched to a null or
|
||||
garbage address. If you want stale calls to be loud instead,
|
||||
`on_deleted_symbol` lets you redirect it to a stub of your own. Both
|
||||
policies are exercised in this repo:
|
||||
`source/examples/HotReloadDemo/Host` supplies a stub;
|
||||
`source/tests/HotReloadTests` covers the default.
|
||||
|
||||
Neither guarantee depends on anything the hot-reloadable module's
|
||||
author has to remember to do correctly -- see "What this cannot
|
||||
detect" below for the one important thing that's true in reverse.
|
||||
|
||||
## Hot reloading C++ symbols (classes, virtual methods, operators)
|
||||
|
||||
Everything above works for a real, unmodified C++ mangled name with
|
||||
**no changes to this library at all** -- classification only ever
|
||||
looks at a symbol's type and section (see `src/SymbolTable.cpp`),
|
||||
never at what its name looks like, so a class's ordinary method, a
|
||||
virtual method, and an overloaded operator are, mechanically, no
|
||||
different from an `extern "C"` function. This isn't a theoretical
|
||||
claim: `source/examples/HotReloadDemo/Shared/CppWidgetApi.h` declares
|
||||
a class with all three, exported with no `extern "C"` anywhere, living
|
||||
in the *same* plugin build as the plain-C demo, and
|
||||
`source/examples/HotReloadDemo/Host/Main.cpp` hot-reloads it -- run
|
||||
the demo (see the top of this file) and watch the `CppWidget::` lines
|
||||
in the output.
|
||||
|
||||
**Virtual methods work automatically, including through a real
|
||||
vtable.** A virtual call is dispatched by loading a function pointer
|
||||
out of the object's vtable and calling through it -- and that pointer
|
||||
holds exactly the same address a direct call would, since the plugin's
|
||||
own code populated the vtable with it. Patch that address, and *every*
|
||||
path that reaches it redirects: a raw function pointer, a direct call,
|
||||
and a vtable slot are all just different ways of holding the same
|
||||
address. The demo proves this directly: it calls `widget->virtualTick(x)`
|
||||
through ordinary polymorphic dispatch, once before `hot_reload_reload()`
|
||||
and once after, on the *same* pre-existing object, and the second call
|
||||
runs the new build's code.
|
||||
|
||||
That said, a vtable is data, and changing its *layout* -- adding,
|
||||
removing, or reordering virtual functions, or changing base classes --
|
||||
is exactly the data-layout hazard "What this cannot detect" describes
|
||||
below, just in a C++-specific shape: a pre-existing object's vptr
|
||||
still points at the *old* build's vtable, with the old slot count and
|
||||
order, so new code that assumes the new layout while running against
|
||||
an old object's vtable will read the wrong slot. A newly-constructed
|
||||
object (created after the reload) gets the new vtable and is fine.
|
||||
This library does not check for this -- it isn't something a symbol
|
||||
table carries any information about -- so keep a hot-reloadable
|
||||
class's virtual function set unchanged across a reload, the same
|
||||
discipline as keeping a free function's signature unchanged.
|
||||
|
||||
**Operators are just functions with unusual mangled names** and need
|
||||
nothing special either -- `CppWidgetApi.h`'s `operator+` patches
|
||||
exactly like `tick()` below.
|
||||
|
||||
**The one real requirement, restated for C++:** a function only exists
|
||||
to be patched at all if the compiler emitted an actual out-of-line
|
||||
body for it somewhere addressable. This was already true for
|
||||
`extern "C"` functions (see "Using it in your own module" below), but
|
||||
C++ makes it much easier to violate by accident:
|
||||
|
||||
- A method defined *inside* a class body (`struct S { int f() { ...
|
||||
} };`) is implicitly `inline`, and a caller that includes the header
|
||||
will very often get the body inlined directly into its own compiled
|
||||
code, with no separate symbol left anywhere to patch. This is not
|
||||
hypothetical -- built and inspected while writing this feature:
|
||||
```cpp
|
||||
struct InlineWidget { int tick(int x) { return x * 2; } };
|
||||
int caller(int x) { InlineWidget w; return w.tick(x); }
|
||||
```
|
||||
compiled with optimization on, `nm -D` on the resulting library shows
|
||||
**no `InlineWidget::tick` symbol at all** -- the compiler deleted the
|
||||
separate function entirely, so there is nothing any binary-patching
|
||||
tool, this one included, could ever redirect. `caller`'s own compiled
|
||||
code has to be replaced by reloading `caller` itself.
|
||||
- Templates are instantiated per translation unit and, unless
|
||||
explicitly instantiated once elsewhere, typically get inlined at
|
||||
each call site for the same reason.
|
||||
- The fix, where it applies, is the same one C already needed: declare
|
||||
in a header, define exactly once out-of-line in a `.cpp` compiled
|
||||
into the reloadable module, and don't let a caller outside that
|
||||
module see the definition (so the compiler never has the option to
|
||||
inline it away). `CppWidgetApi.h`'s methods are declared in the
|
||||
header and defined in `PluginV1/src/CppWidget.cpp` /
|
||||
`PluginV2/src/CppWidget.cpp` for exactly this reason.
|
||||
|
||||
**Mangled names are only stable within one ABI family.** GCC and
|
||||
Clang implement the same (Itanium) C++ ABI and mangle the demo's
|
||||
`CppWidget` identically -- confirmed directly: the demo was built and
|
||||
run with both compilers, and separately cross-compiled with MinGW and
|
||||
run under Wine, all using the exact same hardcoded mangled-name string
|
||||
in `Host/Main.cpp`. MSVC uses its own, unrelated scheme. This isn't a
|
||||
new limitation this library introduces -- you were always going to
|
||||
reload with the same toolchain you started a session with -- but it's
|
||||
worth stating plainly for mangled names specifically, since unlike an
|
||||
`extern "C"` name, a mangled one simply won't match at all across the
|
||||
two families.
|
||||
|
||||
**Looking up a mangled name is genuinely more annoying than an
|
||||
`extern "C"` one**, which is the practical reason to prefer the
|
||||
latter for anything the *host* needs to call by a human-typed string.
|
||||
There's no portable way to ask the compiler for another declaration's
|
||||
mangled name at compile time; the demo gets it the same way you would
|
||||
in practice, with `nm -D --defined-only <library> | c++filt`. Two ways
|
||||
around this, both shown in the demo:
|
||||
1. Keep one `extern "C"` factory/entry function (`make_cpp_widget()`
|
||||
in the demo) and reach everything else through it -- a returned
|
||||
object's *virtual* methods then need no lookup at all (see above).
|
||||
2. For a method that isn't virtual, call `hot_reload_demangle()` on
|
||||
whatever name you *do* have (e.g. for logging -- see
|
||||
`HotReloadOptions::on_symbol_event`'s output in the demo, where
|
||||
`_ZN9CppWidget4tickEi` prints as `CppWidget::tick(int)`), or accept
|
||||
the manual `nm | c++filt` lookup as a one-time cost, same as the
|
||||
demo does for `CppWidget::tick`.
|
||||
|
||||
`hot_reload_demangle()` renders a real mangled name for display (via
|
||||
`__cxa_demangle` on GCC/Clang/MinGW, `UnDecorateSymbolName` on MSVC)
|
||||
and returns anything else -- including a plain `extern "C"` name --
|
||||
completely unchanged, so it's safe to call on every
|
||||
`HotReloadSymbolEventInfo::name` unconditionally, as the demo does.
|
||||
It never changes what a name means for lookup purposes; it exists
|
||||
purely so a log is readable.
|
||||
|
||||
**One general C++-plugin caveat, not specific to hot-reloading:**
|
||||
exceptions and RTTI (`dynamic_cast`, `typeid`) generally should not
|
||||
cross the boundary into or out of a dynamically-loaded module at
|
||||
all -- this is already true of any `dlopen()`/`LoadLibrary()`-based
|
||||
plugin architecture, for the same reasons cpp-style-guide.md section
|
||||
11 gives for the plain C boundary, and hot-reloading a module doesn't
|
||||
change it either way. The demo's `delete widget` relies on `new` and
|
||||
`delete` resolving to the same allocator across the boundary, which
|
||||
holds on Linux/macOS as tested here; it's worth double-checking on
|
||||
Windows if you link the CRT in an unusual way (e.g. mismatched
|
||||
static/dynamic CRT between the host and a plugin).
|
||||
|
||||
## Scope: what's actually implemented and checked
|
||||
|
||||
| | x86 | x86-64 | ARM64 | ARM32 |
|
||||
|---|---|---|---|---|
|
||||
| Jump-patch backend | yes | yes | yes | not implemented |
|
||||
| Landing pad handled | ENDBR32 | ENDBR64 | BTI | -- |
|
||||
|
||||
*"Any architecture"* was the ask; ARM32 is the one mainstream target
|
||||
deliberately left out, because it's the one case this project's
|
||||
testing couldn't back up with confidence -- see below.
|
||||
|
||||
**Compilers.** GCC and Clang share one backend (including MinGW,
|
||||
which is one or the other targeting Windows); MSVC has its own
|
||||
`/hotpatch`-based path in `cmake/HotReload.cmake` and its own PE
|
||||
export-table reader in `SymbolTable.cpp`.
|
||||
|
||||
**What was actually run, not just compiled**, while building this:
|
||||
- Native GCC and Clang on Linux/x86-64: the full demo, both compilers,
|
||||
including a real ENDBR64 landing pad this toolchain turned out to
|
||||
emit by default -- see `src/Architecture.cpp`'s file comment.
|
||||
- Cross-compiled to ARM64 and run under `qemu-aarch64`: the full demo,
|
||||
twice -- once plain, once with `-mbranch-protection=bti` -- to
|
||||
exercise the BTI-landing-pad path on real (emulated) ARM64
|
||||
semantics, not just have it compile.
|
||||
- Cross-compiled with MinGW and run under Wine: the full demo,
|
||||
exercising the real Win32 `LoadLibrary`/`VirtualProtect`/
|
||||
`FlushInstructionCache` calls and the PE export-table parser. This
|
||||
run is also *why* the demo plugins now set
|
||||
`WINDOWS_EXPORT_ALL_SYMBOLS ON` -- the first attempt silently
|
||||
exported nothing under an MSVC-style build until that was added.
|
||||
Re-run after adding `CppWidgetApi.h`'s C++ class: confirmed the PE
|
||||
reader also correctly classifies a vtable and typeinfo as data (not
|
||||
code) on Windows, not just on ELF, and that GCC and Clang produce
|
||||
identical mangled names for the same class (the demo's hardcoded
|
||||
`_ZN9CppWidget4tickEi` lookup string was derived from a native GCC
|
||||
build but also worked unmodified against this MinGW/Wine build and
|
||||
the separate native Clang build).
|
||||
- **MSVC itself was not run anywhere in this process** -- there is no
|
||||
Windows machine or MSVC install available in the environment this
|
||||
was built in. The MSVC-specific code (`/hotpatch`,
|
||||
`IMAGE_EXPORT_DIRECTORY` parsing, `VirtualProtect`,
|
||||
`FlushInstructionCache`) is written directly against Microsoft's
|
||||
documented structures and behavior, and the equivalent PE-reading
|
||||
code was compiled clean against real `<windows.h>` headers via
|
||||
MinGW, but treat the MSVC path as unverified until you've built it
|
||||
on an actual Windows box. If it doesn't work, `SymbolTable.cpp`'s
|
||||
PE branch and `cmake/HotReload.cmake`'s `/hotpatch` branch are the
|
||||
two places to look first.
|
||||
- **32-bit ARM was not implemented at all**, deliberately: it's the
|
||||
one architecture in this list where getting it wrong is easy and
|
||||
getting it verified is hard. Its instructions are either 4 bytes
|
||||
(ARM mode) or a 2/4-byte mix (Thumb mode), and a function pointer's
|
||||
low bit is the only thing that says which -- get the interworking
|
||||
wrong and you don't get a clean crash, you get a CPU that's decoding
|
||||
the wrong instruction stream. Nothing in this environment could
|
||||
exercise that on real or emulated ARM32 hardware, so rather than
|
||||
ship an ARM32 backend on the strength of a manual reading of the
|
||||
ISA reference alone, it's left out. Adding it is a matter of
|
||||
implementing one more branch of `src/Architecture.cpp`'s interface;
|
||||
test it for real before trusting it.
|
||||
- **macOS (Mach-O) was not implemented.** Nothing in the request named
|
||||
it, and it would need a third format reader alongside
|
||||
`SymbolTable.cpp`'s ELF and PE branches. `readExportedSymbols()` is
|
||||
the one function that would need it.
|
||||
|
||||
## What this cannot detect
|
||||
|
||||
**A changed function signature.** Patching redirects control flow at a
|
||||
fixed address; it has no way to know from a symbol table alone that
|
||||
`void tick(int)` became `void tick(int, int)`. If a function's
|
||||
signature changes, give it a new exported name instead of reusing the
|
||||
old one -- the old name is then correctly reported as
|
||||
`HOT_RELOAD_SYMBOL_REMOVED` and the new one as `HOT_RELOAD_SYMBOL_ADDED`,
|
||||
rather than being patched into a mismatched calling convention.
|
||||
|
||||
**A data layout change reached only through code that still gets
|
||||
patched.** The library guarantees it never *touches* a data symbol
|
||||
directly. It cannot guarantee that a patched function is safe to run
|
||||
against a `struct`/class instance that already exists in memory with
|
||||
the *old* layout -- e.g. a game object allocated before the reload,
|
||||
if the new code adds a field to its type. This is a correctness
|
||||
property of your own code across the reload boundary, not something a
|
||||
symbol table (which carries no field-offset information) can check.
|
||||
If you need this checked, the extension point is the same
|
||||
place: `HotReloadContext::reload()` -- diffing the two builds' debug
|
||||
info (DWARF or PDB) for layout changes in types touched by patched
|
||||
functions is real work, genuinely useful, and out of scope for what's
|
||||
here now.
|
||||
|
||||
**Being called concurrently with the code it's patching.** The
|
||||
intended use is the same as every other tool in this space (Unreal's
|
||||
Live Coding, Handmade Hero's hot reload, etc.): call
|
||||
`hot_reload_reload()` from a quiescent point -- your main thread
|
||||
between frames or ticks -- where nothing else is actively calling into
|
||||
the module you're reloading. The write order in
|
||||
`src/Architecture.cpp` (payload bytes before the opcode that arms
|
||||
them, with a release fence between) protects the realistic case where
|
||||
a call arrives right at that boundary. It does not protect a thread
|
||||
that is genuinely, actively executing through the exact bytes being
|
||||
patched at that moment -- doing that in general on a variable-length
|
||||
ISA needs a signal-handler-based retry protocol, a substantially
|
||||
bigger mechanism than a dev-loop tool like this one calls for. See
|
||||
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.
|
||||
- **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
|
||||
"not enough room," never prove "enough" -- see `SymbolTable.cpp`'s
|
||||
file comment. `/hotpatch` (applied automatically by
|
||||
`hot_reload_enable_patchable_functions()`) is the real safety
|
||||
mechanism on Windows; treat the size estimate as a bonus, not a
|
||||
guarantee.
|
||||
- **A forwarder PE export** (one that re-exports another DLL's
|
||||
function) is classified as non-function and left alone, not
|
||||
followed.
|
||||
|
||||
## Diagnosing a failed load
|
||||
|
||||
`HOT_RELOAD_ERROR_LOAD_FAILED` alone doesn't say *why* -- a missing
|
||||
file, a permissions problem, a corrupt or wrong-architecture image,
|
||||
and a genuinely undefined symbol all look the same from the status
|
||||
code alone. Call `hot_reload_last_error(ctx)` right after a failing
|
||||
`hot_reload_load()`/`hot_reload_reload()` for the platform's own
|
||||
description (`dlerror()` on POSIX, a formatted `GetLastError()` on
|
||||
Windows).
|
||||
|
||||
The specific case that motivated adding this, reproduced directly
|
||||
while writing it: a **non-inline `static` class member that is
|
||||
declared but never given an out-of-class definition**.
|
||||
|
||||
```cpp
|
||||
class Randomizer
|
||||
{
|
||||
static std::mt19937& gen() { return m_gen; } // ODR-uses m_gen
|
||||
static std::mt19937 m_gen; // declared here -- never defined anywhere
|
||||
};
|
||||
```
|
||||
|
||||
This compiles cleanly and links cleanly into a shared library --
|
||||
unlike an executable, a shared library is allowed to keep undefined
|
||||
symbols by default, on the assumption that whatever eventually loads
|
||||
it will supply them. It only fails once `dlopen(..., RTLD_NOW)` (this
|
||||
library always uses `RTLD_NOW`, deliberately -- see the comment in
|
||||
`ModuleHandle.cpp` on why a clean, catchable failure right here beats
|
||||
a deferred, uncatchable one under `RTLD_LAZY`) tries to eagerly
|
||||
resolve every symbol and can't find this one anywhere in the process.
|
||||
Confirmed directly: building the same class with the code path that
|
||||
uses it left uncalled produces a `.so` with no trace of the missing
|
||||
symbol at all (the compiler drops the unused, never-instantiated
|
||||
inline method entirely); the moment something calls it,
|
||||
`hot_reload_last_error()` reports exactly
|
||||
`undefined symbol: _ZN10Randomizer4m_genE`. The fix is the one C++17
|
||||
already offers: mark it `inline static` (with its initializer, if it
|
||||
has one) instead of a bare `static` declaration, the same way you'd
|
||||
fix this in any shared library, hot-reloaded or not.
|
||||
|
||||
## Using it in your own module
|
||||
|
||||
1. Every function you want reloadable needs C linkage --
|
||||
`extern "C"` -- for the same reason any `dlopen()`'d interface
|
||||
needs it (see `hot_reload.h`'s file comment and style guide §11).
|
||||
2. Build it as its own `SHARED` library target and call
|
||||
`hot_reload_enable_patchable_functions(<target>)`
|
||||
(`cmake/HotReload.cmake`) on it, so every exported function has
|
||||
guaranteed room for a jump. Without this, reload still works for
|
||||
any function whose compiled body happens to be large enough on its
|
||||
own -- functions that aren't get skipped
|
||||
(`HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL`), not corrupted.
|
||||
3. On Windows, also set `WINDOWS_EXPORT_ALL_SYMBOLS ON` on that
|
||||
target, or add explicit `__declspec(dllexport)` to each function
|
||||
yourself -- MSVC exports nothing by default. (Prefer explicit
|
||||
per-symbol export in a real project, even though the demo uses the
|
||||
blanket property to match this repo's existing `add_lib_module()`
|
||||
convention: exporting everything means anything your build happens
|
||||
to pull in -- including, as this project's own Windows testing
|
||||
turned up, incidental runtime-library internals -- becomes part of
|
||||
your reloadable surface too, redirected right along with the
|
||||
functions you actually meant.)
|
||||
4. Have your host `hot_reload_load()` it once at startup and
|
||||
`hot_reload_reload()` it after every rebuild, from a point where
|
||||
nothing else is calling into it (see "What this cannot detect"
|
||||
above).
|
||||
5. Rebuilding to the *same output path* every time is fine --
|
||||
`HotReloadContext` stages every load through a fresh, uniquely-named
|
||||
copy internally specifically so your build system's exact
|
||||
write pattern can't cause a stale reload (a known gotcha for this
|
||||
style of tool; see `HotReloadContext.cpp`'s
|
||||
`stagePathForLoad()`).
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# source/HotReload/include/hot_reload/.clang-tidy
|
||||
#
|
||||
# hot_reload.h itself is Part II (C API) style -- snake_case, per style
|
||||
# guide §13. Copied from the repo-root c-api/.clang-tidy template (see
|
||||
# that file's own header comment for how clang-tidy's directory-scoped
|
||||
# config resolution makes this work); see also
|
||||
# source/HotReload/src/c-api/.clang-tidy for this same override applied
|
||||
# to the C API's .cpp implementation, one directory tree over.
|
||||
InheritParentConfig: true
|
||||
|
||||
Checks: >
|
||||
readability-identifier-naming,
|
||||
bugprone-exception-escape
|
||||
|
||||
CheckOptions:
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.LocalVariableCase
|
||||
value: lower_case
|
||||
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: UPPER_CASE
|
||||
|
||||
- key: readability-identifier-naming.GlobalConstantCase
|
||||
value: UPPER_CASE
|
||||
- key: readability-identifier-naming.MacroDefinitionCase
|
||||
value: UPPER_CASE
|
||||
|
||||
- key: readability-identifier-naming.GlobalVariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.GlobalVariablePrefix
|
||||
value: g_
|
||||
- key: readability-identifier-naming.StaticVariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.StaticVariablePrefix
|
||||
value: s_
|
||||
@@ -0,0 +1,292 @@
|
||||
/* hot_reload.h
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* 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().
|
||||
*/
|
||||
#ifndef HOT_RELOAD_H
|
||||
#define HOT_RELOAD_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef HOT_RELOAD_BUILD_SHARED
|
||||
#define HOT_RELOAD_API __declspec(dllexport)
|
||||
#elif defined(HOT_RELOAD_USE_SHARED)
|
||||
#define HOT_RELOAD_API __declspec(dllimport)
|
||||
#else
|
||||
#define HOT_RELOAD_API
|
||||
#endif
|
||||
#else
|
||||
#if defined(HOT_RELOAD_BUILD_SHARED) || defined(HOT_RELOAD_USE_SHARED)
|
||||
#define HOT_RELOAD_API __attribute__((visibility("default")))
|
||||
#else
|
||||
#define HOT_RELOAD_API
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
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. */
|
||||
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"). */
|
||||
typedef enum HotReloadStatus
|
||||
{
|
||||
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. */
|
||||
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. */
|
||||
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_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. */
|
||||
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. */
|
||||
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. */
|
||||
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(). */
|
||||
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
|
||||
* 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. */
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
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). */
|
||||
typedef struct HotReloadSymbolEventInfo
|
||||
{
|
||||
const char* name;
|
||||
HotReloadSymbolEvent kind;
|
||||
void* old_address;
|
||||
void* new_address;
|
||||
} HotReloadSymbolEventInfo;
|
||||
|
||||
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. */
|
||||
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". */
|
||||
typedef struct HotReloadOptions
|
||||
{
|
||||
HotReloadSymbolEventFn on_symbol_event; /* nullable */
|
||||
HotReloadDeletedSymbolFn on_deleted_symbol; /* nullable */
|
||||
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. */
|
||||
HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options);
|
||||
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx);
|
||||
|
||||
/* 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(). */
|
||||
HOT_RELOAD_API char* hot_reload_demangle(const char* name);
|
||||
|
||||
HOT_RELOAD_API void hot_reload_free_string(char* str);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HOT_RELOAD_H */
|
||||
@@ -0,0 +1,228 @@
|
||||
#include "Architecture.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
/* WHY THERE CAN BE A "LANDING PAD" IN FRONT OF A FUNCTION
|
||||
* -----------------------------------------------------------
|
||||
* Modern toolchains can emit a marker instruction as the very first
|
||||
* instruction of a function, required by a CPU-level indirect-branch
|
||||
* protection feature: x86 CET ("Control-flow Enforcement Technology",
|
||||
* ENDBR64/ENDBR32) and ARM64 BTI ("Branch Target Identification").
|
||||
* When the CPU enforces either, an indirect call/jump (which is
|
||||
* exactly how a call through a function pointer, or a call across a
|
||||
* shared-library boundary via the PLT/GOT, actually reaches a
|
||||
* function) is only allowed to land on one of these marker
|
||||
* instructions -- landing anywhere else raises a fault.
|
||||
*
|
||||
* This matters here because it is genuinely on by default in
|
||||
* mainstream environments, not a rare opt-in: this exact toolchain
|
||||
* (Ubuntu 24.04's system GCC 13, x86-64) emits ENDBR64 at every
|
||||
* function's entry with no extra flags beyond -fpatchable-function-entry,
|
||||
* confirmed empirically while building this library --
|
||||
* `gcc -O2 -fpatchable-function-entry=16,0` alone was enough to produce
|
||||
* an `endbr64` as the very first instruction, ahead of the requested
|
||||
* NOP padding. Overwriting it unconditionally would silently work on
|
||||
* a CPU/kernel combination that does not enforce IBT and silently
|
||||
* crash on one that does -- exactly the kind of environment-dependent
|
||||
* failure this library exists to avoid.
|
||||
*
|
||||
* So every backend below first checks the live bytes at the function's
|
||||
* entry for its architecture's landing-pad encoding and, if present,
|
||||
* treats it as a fixed, immovable prefix: the jump is written
|
||||
* immediately after it rather than over it. Since a landing pad is
|
||||
* defined to otherwise behave as a NOP, falling through it into our
|
||||
* jump is always correct whether or not the CPU actually enforces the
|
||||
* protection it marks.
|
||||
*
|
||||
* WRITE ORDERING (AND WHAT IT DOES / DOES NOT PROTECT AGAINST)
|
||||
* -----------------------------------------------------------
|
||||
* Every backend below writes the target-address bytes first and the
|
||||
* leading opcode/instruction word that "arms" the jump last, with a
|
||||
* release fence in between. This protects the realistic case this
|
||||
* library is designed for: hot_reload_reload() runs at a quiescent
|
||||
* point (e.g. your main thread between frames) where nothing is
|
||||
* actively calling INTO the function while it is being patched, and
|
||||
* this ordering guarantees that any call arriving right at that
|
||||
* boundary sees either the fully-old or the fully-new jump, never a
|
||||
* target address that is half-old-half-new. It does NOT make the
|
||||
* write safe against a thread that is concurrently, actively
|
||||
* executing through the exact bytes being patched -- doing that in
|
||||
* general on a variable-length ISA needs a signal-handler-based retry
|
||||
* protocol (a much bigger mechanism, well beyond a dev-loop tool's
|
||||
* requirements). See the README for this precondition stated plainly.
|
||||
*/
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
|
||||
constexpr std::size_t kLandingPadBytes = 4;
|
||||
constexpr unsigned char kEndbr64[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFA};
|
||||
// jmp qword ptr [rip+0]; <8-byte absolute address> -- reaches
|
||||
// any 64-bit address regardless of how far apart the old and
|
||||
// new modules were loaded, unlike a 5-byte relative jump
|
||||
// (E9 rel32) which only reaches +/-2GB. Verified byte-for-byte
|
||||
// against `as`/objdump output while building this library.
|
||||
constexpr std::size_t kJumpBytes = 14;
|
||||
|
||||
bool hasLandingPad(const unsigned char* entry)
|
||||
{
|
||||
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
|
||||
}
|
||||
|
||||
void writeJumpAt(unsigned char* site, const void* target)
|
||||
{
|
||||
std::uint64_t targetValue = 0;
|
||||
std::memcpy(&targetValue, &target, sizeof(target));
|
||||
|
||||
// Payload first: the absolute address (site+6) and the
|
||||
// constant zero rip-displacement (site+2) are only ever
|
||||
// consumed as DATA once the opcode below is in place, so
|
||||
// writing them early cannot be misinterpreted as a
|
||||
// different, fresh instruction stream by anything that
|
||||
// might fall through this region in its old, all-NOP state.
|
||||
std::memcpy(site + 6, &targetValue, sizeof(targetValue));
|
||||
std::memcpy(site + 2, "\x00\x00\x00\x00", 4);
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
|
||||
// Arm last: until these 2 bytes land, site+0 still reads as
|
||||
// whatever it was (a NOP, on first patch; a previous FF 25
|
||||
// on a later reload -- either way harmless/inert).
|
||||
unsigned char opcode[2] = {0xFF, 0x25};
|
||||
std::memcpy(site, opcode, sizeof(opcode));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
}
|
||||
|
||||
#elif defined(__i386__) || defined(_M_IX86)
|
||||
|
||||
constexpr std::size_t kLandingPadBytes = 4;
|
||||
constexpr unsigned char kEndbr32[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFB};
|
||||
// e9 rel32 -- a plain relative near jump. On 32-bit x86 the
|
||||
// entire address space fits in the signed 32-bit displacement
|
||||
// (mod 2^32 wraparound gives the right answer regardless of
|
||||
// "distance"), so unlike x86-64 there is no need for an
|
||||
// absolute/indirect form here.
|
||||
constexpr std::size_t kJumpBytes = 5;
|
||||
|
||||
bool hasLandingPad(const unsigned char* entry)
|
||||
{
|
||||
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
|
||||
}
|
||||
|
||||
void writeJumpAt(unsigned char* site, const void* target)
|
||||
{
|
||||
std::int32_t rel32 = static_cast<std::int32_t>(
|
||||
reinterpret_cast<std::intptr_t>(target) - reinterpret_cast<std::intptr_t>(site) - 5);
|
||||
|
||||
// Payload (the displacement) first, opcode byte last --
|
||||
// same reasoning as the x86-64 backend above.
|
||||
std::memcpy(site + 1, &rel32, sizeof(rel32));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
site[0] = 0xE9;
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||||
|
||||
constexpr std::size_t kLandingPadBytes = 4;
|
||||
|
||||
// Matches any BTI variant (plain/C/J/JC): bits [31:6] are fixed
|
||||
// at 0b010101000000001001001, differing only in bits [6:5]
|
||||
// (the qualifier) -- equivalently, the low byte is one of
|
||||
// 0x1F/0x5F/0x9F/0xDF and the next 3 bytes are always
|
||||
// 24 03 D5. Verified with `as`/objdump for all 4 variants
|
||||
// while building this library.
|
||||
bool isBti(const unsigned char* entry)
|
||||
{
|
||||
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
|
||||
}
|
||||
|
||||
bool hasLandingPad(const unsigned char* entry)
|
||||
{
|
||||
return isBti(entry);
|
||||
}
|
||||
|
||||
// ldr x17, #8 ; br x17 ; <8-byte absolute address>. x16/x17
|
||||
// are AArch64's designated "intra-procedure-call" scratch
|
||||
// registers (IP0/IP1) -- the AAPCS64 explicitly reserves them
|
||||
// for exactly this kind of linker/veneer-style long branch, so
|
||||
// clobbering them at a function's entry point, before any of
|
||||
// its real prologue has executed, is always safe. Byte values
|
||||
// verified with `as`/objdump while building this library.
|
||||
constexpr std::size_t kJumpBytes = 16;
|
||||
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
|
||||
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
|
||||
|
||||
void writeJumpAt(unsigned char* site, const void* target)
|
||||
{
|
||||
std::uint64_t targetValue = 0;
|
||||
std::memcpy(&targetValue, &target, sizeof(target));
|
||||
|
||||
// Payload (the literal address) first. ARM64 instructions
|
||||
// are fixed-width and naturally aligned, so -- unlike the
|
||||
// variable-length x86 case -- each 4-byte slot here is
|
||||
// unambiguous: it is either the old NOP, the old
|
||||
// instruction, or the new one, never a byte-misaligned
|
||||
// mix. Writing high-to-low keeps the two real instructions
|
||||
// (which is what any in-flight fall-through NOP execution
|
||||
// could actually reach) inert for as long as possible.
|
||||
std::memcpy(site + 8, &targetValue, sizeof(targetValue));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
std::memcpy(site + 4, kBrX17, sizeof(kBrX17));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
|
||||
// Arm last: until this word lands, site+0 still reads as a
|
||||
// NOP (or a previous ldr, on a later reload) and execution
|
||||
// never reaches the br at site+4 via fall-through.
|
||||
std::memcpy(site, kLdrX17Pc8, sizeof(kLdrX17Pc8));
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isArchitectureSupported()
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
defined(_M_ARM64)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
PatchLayout planPatch(const void* entryAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
defined(_M_ARM64)
|
||||
const unsigned char* entry = static_cast<const unsigned char*>(entryAddress);
|
||||
PatchLayout layout;
|
||||
layout.patchOffset = hasLandingPad(entry) ? kLandingPadBytes : 0;
|
||||
layout.totalBytes = layout.patchOffset + kJumpBytes;
|
||||
return layout;
|
||||
#else
|
||||
(void) entryAddress;
|
||||
return PatchLayout{};
|
||||
#endif
|
||||
}
|
||||
|
||||
bool writeJump(void* entryAddress, const void* targetAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
defined(_M_ARM64)
|
||||
PatchLayout layout = planPatch(entryAddress);
|
||||
unsigned char* site = static_cast<unsigned char*>(entryAddress) + layout.patchOffset;
|
||||
writeJumpAt(site, targetAddress);
|
||||
return true;
|
||||
#else
|
||||
(void) entryAddress;
|
||||
(void) targetAddress;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// What planPatch() found at one specific function's entry point --
|
||||
/// how many bytes from the entry address must be safe to overwrite,
|
||||
/// and how many of those (if any) are a landing pad that must be
|
||||
/// preserved rather than overwritten. See Architecture.cpp's file
|
||||
/// comment for why a landing pad can be present at all.
|
||||
struct PatchLayout
|
||||
{
|
||||
/// Total bytes from the function's entry address that this
|
||||
/// patch touches, landing pad included -- compare this against
|
||||
/// the symbol's compiled size before writing anything.
|
||||
std::size_t totalBytes = 0;
|
||||
|
||||
/// How many leading bytes are a landing pad and must be left
|
||||
/// untouched; the jump itself is written starting at
|
||||
/// entryAddress + patchOffset.
|
||||
std::size_t patchOffset = 0;
|
||||
};
|
||||
|
||||
/// True if this build has a jump-patch backend for the CPU
|
||||
/// architecture it was compiled for (decided at compile time --
|
||||
/// see the top of Architecture.cpp). x86, x86-64, and ARM64 are
|
||||
/// supported; anything else (including 32-bit ARM, whose ARM/Thumb
|
||||
/// interworking this library does not attempt -- see the README)
|
||||
/// returns false here, and HotReloadContext surfaces that as
|
||||
/// HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
||||
bool isArchitectureSupported();
|
||||
|
||||
/// Inspects the bytes already sitting at entryAddress to determine
|
||||
/// the real patch layout for THIS function, accounting for a
|
||||
/// possible leading landing pad. Safe to call even when
|
||||
/// isArchitectureSupported() is false (returns a zeroed PatchLayout).
|
||||
PatchLayout planPatch(const void* entryAddress);
|
||||
|
||||
/// Overwrites entryAddress so that control transfers unconditionally
|
||||
/// to targetAddress, following the layout planPatch() would return
|
||||
/// for this same entryAddress. The caller must already have made
|
||||
/// [entryAddress, entryAddress + planPatch(entryAddress).totalBytes)
|
||||
/// writable (see MemoryProtection.hpp) and must flush the
|
||||
/// instruction cache over that same range afterwards -- this
|
||||
/// function does neither.
|
||||
///
|
||||
/// Only meaningful precondition: the caller has already checked the
|
||||
/// target symbol's compiled size against planPatch(entryAddress)
|
||||
/// .totalBytes; this function does not re-check it and will happily
|
||||
/// overwrite whatever comes next in memory if asked to.
|
||||
///
|
||||
/// Returns false only if isArchitectureSupported() is false.
|
||||
bool writeJump(void* entryAddress, const void* targetAddress);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "HotReloadContext.hpp"
|
||||
|
||||
#include "Architecture.hpp"
|
||||
#include "MemoryProtection.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool copyFile(const std::string& from, const std::string& to)
|
||||
{
|
||||
std::ifstream in(from, std::ios::binary);
|
||||
if (!in)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::ofstream out(to, std::ios::binary | std::ios::trunc);
|
||||
if (!out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out << in.rdbuf();
|
||||
return static_cast<bool>(out);
|
||||
}
|
||||
}
|
||||
|
||||
HotReloadContext::HotReloadContext(const HotReloadOptions* options)
|
||||
: m_onSymbolEvent(nullptr)
|
||||
, m_onDeletedSymbol(nullptr)
|
||||
, m_userData(nullptr)
|
||||
{
|
||||
if (options != nullptr)
|
||||
{
|
||||
m_onSymbolEvent = options->on_symbol_event;
|
||||
m_onDeletedSymbol = options->on_deleted_symbol;
|
||||
m_userData = options->user_data;
|
||||
}
|
||||
}
|
||||
|
||||
void HotReloadContext::emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress)
|
||||
{
|
||||
if (m_onSymbolEvent == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HotReloadSymbolEventInfo info;
|
||||
info.name = name.c_str();
|
||||
info.kind = kind;
|
||||
info.old_address = oldAddress;
|
||||
info.new_address = newAddress;
|
||||
m_onSymbolEvent(&info, m_userData);
|
||||
}
|
||||
|
||||
std::string HotReloadContext::stagePathForLoad(
|
||||
LoadedModule& module, const std::string& moduleName, const std::string& sourcePath)
|
||||
{
|
||||
// Every load (first and subsequent) goes through a freshly,
|
||||
// uniquely-named copy rather than dlopen()/LoadLibrary()'ing
|
||||
// sourcePath directly. Without this, a build that overwrites
|
||||
// the SAME path in place for every rebuild (rather than the
|
||||
// more common write-temp-then-rename pattern) can hit
|
||||
// dlopen's device+inode-based caching and hand back the
|
||||
// already-mapped OLD content instead of genuinely reloading --
|
||||
// a well-known gotcha for this style of tool. Copying to a
|
||||
// name guaranteed never reused sidesteps it regardless of how
|
||||
// the build produced sourcePath.
|
||||
++module.reloadCounter;
|
||||
std::string staged = sourcePath + ".hotreload." + moduleName + "." + std::to_string(module.reloadCounter);
|
||||
if (!copyFile(sourcePath, staged))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return staged;
|
||||
}
|
||||
|
||||
HotReloadStatus HotReloadContext::load(const std::string& moduleName, const std::string& path)
|
||||
{
|
||||
m_lastError.clear();
|
||||
|
||||
if (m_modules.find(moduleName) != m_modules.end())
|
||||
{
|
||||
// Already loaded -- call reload() to load a new build of
|
||||
// an existing name, rather than silently discarding
|
||||
// whatever was already patched into the first one.
|
||||
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
LoadedModule module;
|
||||
std::string staged = stagePathForLoad(module, moduleName, path);
|
||||
if (staged.empty())
|
||||
{
|
||||
m_lastError = "could not stage '" + path + "' for loading (source unreadable, or destination path unwritable)";
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
}
|
||||
|
||||
detail::ModuleHandle handle = detail::ModuleHandle::load(staged);
|
||||
if (!handle.isValid())
|
||||
{
|
||||
m_lastError = handle.errorMessage();
|
||||
std::remove(staged.c_str());
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
}
|
||||
|
||||
module.currentSymbols = detail::readExportedSymbols(staged, handle.loadedBase());
|
||||
std::remove(staged.c_str()); // best-effort tidy-up; the mapping survives regardless (see stagePathForLoad)
|
||||
module.versions.push_back(std::move(handle));
|
||||
|
||||
m_modules.emplace(moduleName, std::move(module));
|
||||
return HOT_RELOAD_OK;
|
||||
}
|
||||
|
||||
HotReloadStatus HotReloadContext::reload(const std::string& moduleName, const std::string& newPath)
|
||||
{
|
||||
m_lastError.clear();
|
||||
|
||||
auto it = m_modules.find(moduleName);
|
||||
if (it == m_modules.end())
|
||||
{
|
||||
return HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION;
|
||||
}
|
||||
if (!detail::isArchitectureSupported())
|
||||
{
|
||||
return HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE;
|
||||
}
|
||||
|
||||
LoadedModule& module = it->second;
|
||||
|
||||
std::string staged = stagePathForLoad(module, moduleName, newPath);
|
||||
if (staged.empty())
|
||||
{
|
||||
m_lastError =
|
||||
"could not stage '" + newPath + "' for loading (source unreadable, or destination path unwritable)";
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
}
|
||||
|
||||
detail::ModuleHandle newHandle = detail::ModuleHandle::load(staged);
|
||||
if (!newHandle.isValid())
|
||||
{
|
||||
m_lastError = newHandle.errorMessage();
|
||||
std::remove(staged.c_str());
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED; // nothing touched -- the previous version keeps running
|
||||
}
|
||||
|
||||
std::vector<detail::ExportedSymbol> newSymbols = detail::readExportedSymbols(staged, newHandle.loadedBase());
|
||||
std::remove(staged.c_str());
|
||||
|
||||
std::unordered_map<std::string, const detail::ExportedSymbol*> newByName;
|
||||
newByName.reserve(newSymbols.size());
|
||||
for (const detail::ExportedSymbol& symbol : newSymbols)
|
||||
{
|
||||
newByName[symbol.name] = &symbol;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> matchedNewNames;
|
||||
for (const detail::ExportedSymbol& oldSymbol : module.currentSymbols)
|
||||
{
|
||||
auto found = newByName.find(oldSymbol.name);
|
||||
if (found == newByName.end())
|
||||
{
|
||||
handleRemoved(oldSymbol);
|
||||
continue;
|
||||
}
|
||||
|
||||
const detail::ExportedSymbol& newSymbol = *found->second;
|
||||
matchedNewNames.insert(oldSymbol.name);
|
||||
|
||||
if (!oldSymbol.isFunction || !newSymbol.isFunction)
|
||||
{
|
||||
// Present on both sides, but classified as data (or
|
||||
// something this library does not recognize as code)
|
||||
// on at least one side -- the mechanical half of the
|
||||
// "only executable code changes" guarantee. Never
|
||||
// touched, in either direction.
|
||||
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
continue;
|
||||
}
|
||||
|
||||
patchOne(oldSymbol, newSymbol);
|
||||
}
|
||||
|
||||
for (const detail::ExportedSymbol& newSymbol : newSymbols)
|
||||
{
|
||||
if (matchedNewNames.find(newSymbol.name) == matchedNewNames.end())
|
||||
{
|
||||
emitEvent(HOT_RELOAD_SYMBOL_ADDED, newSymbol.name, nullptr, newSymbol.address);
|
||||
}
|
||||
}
|
||||
|
||||
module.currentSymbols = std::move(newSymbols);
|
||||
module.versions.push_back(std::move(newHandle));
|
||||
return HOT_RELOAD_OK;
|
||||
}
|
||||
|
||||
void* HotReloadContext::getSymbol(const std::string& moduleName, const std::string& symbolName) const
|
||||
{
|
||||
auto it = m_modules.find(moduleName);
|
||||
if (it == m_modules.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (const detail::ExportedSymbol& symbol : it->second.currentSymbols)
|
||||
{
|
||||
if (symbol.name == symbolName)
|
||||
{
|
||||
return symbol.address;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void HotReloadContext::patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol)
|
||||
{
|
||||
detail::PatchLayout layout = detail::planPatch(oldSymbol.address);
|
||||
if (oldSymbol.size != 0 && oldSymbol.size < layout.totalBytes)
|
||||
{
|
||||
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
return;
|
||||
}
|
||||
|
||||
detail::ScopedCodeWriteAccess writable(oldSymbol.address, layout.totalBytes);
|
||||
if (!writable.isValid())
|
||||
{
|
||||
emitEvent(HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
return;
|
||||
}
|
||||
|
||||
detail::writeJump(oldSymbol.address, newSymbol.address);
|
||||
writable.finishWrite();
|
||||
emitEvent(HOT_RELOAD_SYMBOL_PATCHED, oldSymbol.name, oldSymbol.address, newSymbol.address);
|
||||
}
|
||||
|
||||
void HotReloadContext::handleRemoved(const detail::ExportedSymbol& oldSymbol)
|
||||
{
|
||||
// Ask for a stub at most once, and only for an old symbol that
|
||||
// was itself real code -- there is no sensible "redirect" for
|
||||
// a removed data symbol. A returned NULL (including because no
|
||||
// callback was supplied at all) means the default: leave the
|
||||
// old implementation running untouched.
|
||||
void* stub = nullptr;
|
||||
if (oldSymbol.isFunction && m_onDeletedSymbol != nullptr)
|
||||
{
|
||||
stub = m_onDeletedSymbol(oldSymbol.name.c_str(), m_userData);
|
||||
}
|
||||
|
||||
if (stub != nullptr)
|
||||
{
|
||||
detail::PatchLayout layout = detail::planPatch(oldSymbol.address);
|
||||
bool sizeOk = oldSymbol.size == 0 || oldSymbol.size >= layout.totalBytes;
|
||||
if (sizeOk)
|
||||
{
|
||||
detail::ScopedCodeWriteAccess writable(oldSymbol.address, layout.totalBytes);
|
||||
if (writable.isValid())
|
||||
{
|
||||
detail::writeJump(oldSymbol.address, stub);
|
||||
writable.finishWrite();
|
||||
}
|
||||
// If the OS refused write access, or the function was
|
||||
// too small to safely hold the stub redirect, this
|
||||
// silently falls back to leaving the old implementation
|
||||
// running -- the same safe default as not supplying a
|
||||
// stub at all, rather than adding a second kind of
|
||||
// event on top of HOT_RELOAD_SYMBOL_REMOVED below for
|
||||
// what is already a rare, best-effort extra.
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(HOT_RELOAD_SYMBOL_REMOVED, oldSymbol.name, oldSymbol.address, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModuleHandle.hpp"
|
||||
#include "SymbolTable.hpp"
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
/// The real type behind the opaque ::HotReloadContext the public C
|
||||
/// API hands out -- see HotReloadApi.cpp for the extern "C" shim
|
||||
/// that casts between the two, following the same opaque-pointer
|
||||
/// PIMPL pattern as cpp-style-guide.md's own NetManager example.
|
||||
class HotReloadContext
|
||||
{
|
||||
public:
|
||||
explicit HotReloadContext(const HotReloadOptions* options);
|
||||
|
||||
HotReloadContext(const HotReloadContext&) = delete;
|
||||
HotReloadContext& operator=(const HotReloadContext&) = delete;
|
||||
|
||||
/// See hot_reload_load()'s doc comment in hot_reload.h.
|
||||
HotReloadStatus load(const std::string& moduleName, const std::string& path);
|
||||
|
||||
/// See hot_reload_reload()'s doc comment in hot_reload.h.
|
||||
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
|
||||
|
||||
/// See hot_reload_get_symbol()'s doc comment in hot_reload.h.
|
||||
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
|
||||
|
||||
/// See hot_reload_last_error()'s doc comment in hot_reload.h.
|
||||
const std::string& lastError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Everything tracked for one logical module name. Has no
|
||||
/// meaning outside HotReloadContext, so it lives here rather
|
||||
/// than in its own header -- see cpp-style-guide.md's carve-out
|
||||
/// for small types with no use outside their owning type.
|
||||
struct LoadedModule
|
||||
{
|
||||
// Every version ever loaded for this name, kept alive for
|
||||
// the rest of the process's lifetime: once a function is
|
||||
// patched, anything that already holds its old address
|
||||
// must stay valid memory forever, even though it now holds
|
||||
// nothing but a jump. versions.back() is the active one.
|
||||
std::vector<detail::ModuleHandle> versions;
|
||||
std::vector<detail::ExportedSymbol> currentSymbols;
|
||||
int reloadCounter = 0;
|
||||
};
|
||||
|
||||
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
|
||||
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
|
||||
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
|
||||
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
|
||||
|
||||
std::unordered_map<std::string, LoadedModule> m_modules;
|
||||
HotReloadSymbolEventFn m_onSymbolEvent;
|
||||
HotReloadDeletedSymbolFn m_onDeletedSymbol;
|
||||
void* m_userData;
|
||||
std::string m_lastError;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "MemoryProtection.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::size_t systemPageSize()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SYSTEM_INFO info;
|
||||
GetSystemInfo(&info);
|
||||
return static_cast<std::size_t>(info.dwPageSize);
|
||||
#else
|
||||
long size = sysconf(_SC_PAGESIZE);
|
||||
return size > 0 ? static_cast<std::size_t>(size) : 4096;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ScopedCodeWriteAccess::ScopedCodeWriteAccess(void* address, std::size_t length)
|
||||
: m_pageAddress(nullptr)
|
||||
, m_pageLength(0)
|
||||
, m_rangeAddress(address)
|
||||
, m_rangeLength(length)
|
||||
, m_previousProtection(0)
|
||||
, m_valid(false)
|
||||
{
|
||||
const std::size_t page = systemPageSize();
|
||||
const std::uintptr_t start = reinterpret_cast<std::uintptr_t>(address);
|
||||
const std::uintptr_t alignedStart = start - (start % page);
|
||||
const std::size_t frontSlack = static_cast<std::size_t>(start - alignedStart);
|
||||
const std::size_t alignedLength = ((frontSlack + length + page - 1) / page) * page;
|
||||
|
||||
m_pageAddress = reinterpret_cast<void*>(alignedStart);
|
||||
m_pageLength = alignedLength;
|
||||
|
||||
#if defined(_WIN32)
|
||||
DWORD oldProtect = 0;
|
||||
if (VirtualProtect(m_pageAddress, m_pageLength, PAGE_EXECUTE_READWRITE, &oldProtect))
|
||||
{
|
||||
m_previousProtection = oldProtect;
|
||||
m_valid = true;
|
||||
}
|
||||
#else
|
||||
if (mprotect(m_pageAddress, m_pageLength, PROT_READ | PROT_WRITE | PROT_EXEC) == 0)
|
||||
{
|
||||
m_previousProtection = PROT_READ | PROT_EXEC;
|
||||
m_valid = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ScopedCodeWriteAccess::~ScopedCodeWriteAccess()
|
||||
{
|
||||
if (!m_valid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(m_pageAddress, m_pageLength, static_cast<DWORD>(m_previousProtection), &ignored);
|
||||
#else
|
||||
mprotect(m_pageAddress, m_pageLength, static_cast<int>(m_previousProtection));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScopedCodeWriteAccess::finishWrite()
|
||||
{
|
||||
if (!m_valid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
FlushInstructionCache(GetCurrentProcess(), m_rangeAddress, m_rangeLength);
|
||||
#else
|
||||
char* begin = static_cast<char*>(m_rangeAddress);
|
||||
__builtin___clear_cache(begin, begin + m_rangeLength);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// RAII guard: makes [address, address + length) writable for its
|
||||
/// lifetime (the OS only grants permissions in whole pages, so the
|
||||
/// guard rounds out to the enclosing page range internally) and
|
||||
/// restores the original protection when it goes out of scope.
|
||||
///
|
||||
/// Assumes the range started out READ+EXECUTE, as every platform's
|
||||
/// loader maps a .text/code section -- restoring to exactly that,
|
||||
/// rather than querying and restoring whatever the previous
|
||||
/// protection actually was, since POSIX mprotect() has no portable
|
||||
/// way to ask for the current protection of a range and code pages
|
||||
/// are never anything else in practice.
|
||||
class ScopedCodeWriteAccess
|
||||
{
|
||||
public:
|
||||
ScopedCodeWriteAccess(void* address, std::size_t length);
|
||||
~ScopedCodeWriteAccess();
|
||||
|
||||
ScopedCodeWriteAccess(const ScopedCodeWriteAccess&) = delete;
|
||||
ScopedCodeWriteAccess& operator=(const ScopedCodeWriteAccess&) = delete;
|
||||
|
||||
/// False if the platform call to relax protection failed --
|
||||
/// when false, nothing was changed and the range must not be
|
||||
/// written to (the destructor is still safe to run either way).
|
||||
bool isValid() const
|
||||
{
|
||||
return m_valid;
|
||||
}
|
||||
|
||||
/// Call exactly once, after writing the new bytes and before
|
||||
/// this guard is destroyed: flushes the instruction cache over
|
||||
/// the original [address, address + length) range so every
|
||||
/// core sees the new instructions rather than a stale fetch
|
||||
/// (required on ARM64, where instruction and data caches are
|
||||
/// not automatically coherent; effectively a formality on
|
||||
/// x86/x86-64, called anyway for correctness on that
|
||||
/// architecture too).
|
||||
void finishWrite();
|
||||
|
||||
private:
|
||||
void* m_pageAddress;
|
||||
std::size_t m_pageLength;
|
||||
void* m_rangeAddress;
|
||||
std::size_t m_rangeLength;
|
||||
unsigned long m_previousProtection;
|
||||
bool m_valid;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#include "ModuleHandle.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#include <link.h>
|
||||
#endif
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
#if defined(_WIN32)
|
||||
namespace
|
||||
{
|
||||
/// GetLastError() alone is just a number; this renders it the
|
||||
/// way a person would want to read it, the same information
|
||||
/// dlerror() gives directly on POSIX. Written against
|
||||
/// documented Win32 behavior; not exercised on real Windows --
|
||||
/// see source/HotReload/README.md's verification notes.
|
||||
std::string formatLastWindowsError()
|
||||
{
|
||||
DWORD errorCode = GetLastError();
|
||||
LPSTR buffer = nullptr;
|
||||
DWORD length = FormatMessageA(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
|
||||
errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<LPSTR>(&buffer), 0, nullptr);
|
||||
|
||||
std::string message;
|
||||
if (length > 0 && buffer != nullptr)
|
||||
{
|
||||
message.assign(buffer, length);
|
||||
while (!message.empty() && (message.back() == '\n' || message.back() == '\r'))
|
||||
{
|
||||
message.pop_back();
|
||||
}
|
||||
}
|
||||
if (buffer != nullptr)
|
||||
{
|
||||
LocalFree(buffer);
|
||||
}
|
||||
if (message.empty())
|
||||
{
|
||||
message = "LoadLibrary failed (error code " + std::to_string(errorCode) + ")";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ModuleHandle::~ModuleHandle()
|
||||
{
|
||||
if (m_native != nullptr)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
FreeLibrary(static_cast<HMODULE>(m_native));
|
||||
#else
|
||||
dlclose(m_native);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
|
||||
: m_native(other.m_native)
|
||||
, m_loadedBase(other.m_loadedBase)
|
||||
{
|
||||
other.m_native = nullptr;
|
||||
other.m_loadedBase = nullptr;
|
||||
}
|
||||
|
||||
ModuleHandle& ModuleHandle::operator=(ModuleHandle&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
if (m_native != nullptr)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
FreeLibrary(static_cast<HMODULE>(m_native));
|
||||
#else
|
||||
dlclose(m_native);
|
||||
#endif
|
||||
}
|
||||
m_native = other.m_native;
|
||||
m_loadedBase = other.m_loadedBase;
|
||||
other.m_native = nullptr;
|
||||
other.m_loadedBase = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ModuleHandle ModuleHandle::load(const std::string& path)
|
||||
{
|
||||
ModuleHandle handle;
|
||||
|
||||
#if defined(_WIN32)
|
||||
HMODULE module = LoadLibraryA(path.c_str());
|
||||
if (module != nullptr)
|
||||
{
|
||||
handle.m_native = module;
|
||||
handle.m_loadedBase = module; // the HMODULE already IS the mapped base on Windows
|
||||
}
|
||||
else
|
||||
{
|
||||
handle.m_errorMessage = formatLastWindowsError();
|
||||
}
|
||||
#else
|
||||
// RTLD_LOCAL: keep each version's symbols out of the global
|
||||
// symbol namespace. Hot reload deliberately resolves symbols
|
||||
// itself (see SymbolTable.cpp) rather than through the dynamic
|
||||
// linker's own global name lookup, and RTLD_GLOBAL would let
|
||||
// an old and a new build of the same plugin -- which export
|
||||
// the very same names on purpose -- collide there.
|
||||
//
|
||||
// RTLD_NOW (eager symbol resolution), not RTLD_LAZY: this
|
||||
// means a plugin with a genuinely undefined symbol (e.g. a
|
||||
// non-inline static class member declared but never defined,
|
||||
// only ODR-used once some previously-dead code path becomes
|
||||
// reachable -- a real case found while building this feature)
|
||||
// fails right here, cleanly and reportably, INSTEAD of
|
||||
// crashing the whole host process the moment that code path
|
||||
// first runs under RTLD_LAZY's deferred binding. For a
|
||||
// long-running host, a clean, catchable load failure is far
|
||||
// preferable to a delayed, uncatchable one.
|
||||
void* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (module == nullptr)
|
||||
{
|
||||
const char* error = dlerror();
|
||||
handle.m_errorMessage = error != nullptr ? error : "dlopen failed with no further detail";
|
||||
}
|
||||
else
|
||||
{
|
||||
struct link_map* map = nullptr;
|
||||
if (dlinfo(module, RTLD_DI_LINKMAP, &map) == 0 && map != nullptr)
|
||||
{
|
||||
handle.m_native = module;
|
||||
handle.m_loadedBase = reinterpret_cast<void*>(map->l_addr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Captured before dlclose() below, which could
|
||||
// otherwise overwrite dlerror()'s state first.
|
||||
const char* error = dlerror();
|
||||
handle.m_errorMessage =
|
||||
error != nullptr ? error : "dlinfo(RTLD_DI_LINKMAP) failed with no further detail";
|
||||
dlclose(module);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// RAII wrapper around dlopen()/LoadLibrary(). Move-only, closes
|
||||
/// the underlying module on destruction (or on being replaced by a
|
||||
/// move-assignment).
|
||||
class ModuleHandle
|
||||
{
|
||||
public:
|
||||
ModuleHandle() = default;
|
||||
~ModuleHandle();
|
||||
|
||||
ModuleHandle(const ModuleHandle&) = delete;
|
||||
ModuleHandle& operator=(const ModuleHandle&) = delete;
|
||||
ModuleHandle(ModuleHandle&& other) noexcept;
|
||||
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
|
||||
|
||||
/// Loads path. On failure the returned handle's isValid() is
|
||||
/// false and loadedBase() is null.
|
||||
static ModuleHandle load(const std::string& path);
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
return m_native != nullptr;
|
||||
}
|
||||
|
||||
/// The address to pass as SymbolTable's loadedBase: on Windows
|
||||
/// this is simply the HMODULE; on POSIX it is the runtime load
|
||||
/// bias (struct link_map::l_addr) that turns an ELF symbol's
|
||||
/// file-relative st_value into a real address.
|
||||
void* loadedBase() const
|
||||
{
|
||||
return m_loadedBase;
|
||||
}
|
||||
|
||||
/// Only meaningful when isValid() is false: the underlying
|
||||
/// platform's own description of why loading failed --
|
||||
/// dlerror() on POSIX, the formatted message for
|
||||
/// GetLastError() on Windows. Captured immediately inside
|
||||
/// load()'s failing branch, before any other dl*/Win32 call
|
||||
/// that could otherwise overwrite it first. Empty if load()
|
||||
/// was never attempted, or if the platform gave no message.
|
||||
const std::string& errorMessage() const
|
||||
{
|
||||
return m_errorMessage;
|
||||
}
|
||||
|
||||
private:
|
||||
void* m_native = nullptr;
|
||||
void* m_loadedBase = nullptr;
|
||||
std::string m_errorMessage;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "SymbolTable.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
|
||||
* ---------------------------------------------------------
|
||||
* hot_reload.h promises that data -- globals, class instances, vtables,
|
||||
* anything that isn't executable code -- is never patched. That
|
||||
* guarantee is enforced in exactly one place: here. Every symbol this
|
||||
* file reports carries a real classification pulled from the loader's
|
||||
* own metadata (ELF's symbol type + section flags, or a PE export's
|
||||
* containing section's characteristics), never inferred from the
|
||||
* symbol's name or any convention the hot-reloadable module is
|
||||
* expected to follow. HotReloadContext (the only caller) simply
|
||||
* refuses to patch anything this file marks isFunction = false, on
|
||||
* either side of a reload -- so nothing downstream has to be trusted
|
||||
* to double-check that part.
|
||||
*/
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// PE / Windows: read straight out of the image already mapped at
|
||||
// loadedBase. Unlike ELF (below), the OS loader has already fully
|
||||
// relocated this image and handed us its exact base address, so
|
||||
// there is no separate "load bias" to compute -- every RVA in the
|
||||
// headers just gets added directly to loadedBase.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct SectionInfo
|
||||
{
|
||||
std::uint32_t rvaBegin;
|
||||
std::uint32_t rvaEnd;
|
||||
bool executable;
|
||||
};
|
||||
|
||||
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
|
||||
{
|
||||
return reinterpret_cast<const char*>(base + rva);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ExportedSymbol> readExportedSymbols(const std::string& /*imagePath*/, void* loadedBase)
|
||||
{
|
||||
std::vector<ExportedSymbol> result;
|
||||
if (loadedBase == nullptr)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint8_t* base = static_cast<std::uint8_t*>(loadedBase);
|
||||
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Section table for the executable-section check, following
|
||||
// the same "code lives in a section marked executable" rule
|
||||
// as the ELF path's SHF_EXECINSTR check below.
|
||||
std::vector<SectionInfo> sections;
|
||||
const auto* sectionHeaders = IMAGE_FIRST_SECTION(nt);
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i)
|
||||
{
|
||||
const IMAGE_SECTION_HEADER& section = sectionHeaders[i];
|
||||
SectionInfo info;
|
||||
info.rvaBegin = section.VirtualAddress;
|
||||
info.rvaEnd = section.VirtualAddress + std::max(section.Misc.VirtualSize, section.SizeOfRawData);
|
||||
info.executable = (section.Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
|
||||
sections.push_back(info);
|
||||
}
|
||||
|
||||
auto sectionFor = [§ions](std::uint32_t rva) -> const SectionInfo*
|
||||
{
|
||||
for (const SectionInfo& section : sections)
|
||||
{
|
||||
if (rva >= section.rvaBegin && rva < section.rvaEnd)
|
||||
{
|
||||
return §ion;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
if (nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const IMAGE_DATA_DIRECTORY& exportDirEntry = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (exportDirEntry.VirtualAddress == 0 || exportDirEntry.Size == 0)
|
||||
{
|
||||
return result; // no exports at all
|
||||
}
|
||||
|
||||
const auto* exportDir = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(base + exportDirEntry.VirtualAddress);
|
||||
const auto* nameRvas = reinterpret_cast<const DWORD*>(base + exportDir->AddressOfNames);
|
||||
const auto* nameOrdinals = reinterpret_cast<const WORD*>(base + exportDir->AddressOfNameOrdinals);
|
||||
const auto* functionRvas = reinterpret_cast<const DWORD*>(base + exportDir->AddressOfFunctions);
|
||||
|
||||
const std::uint32_t exportDirBegin = exportDirEntry.VirtualAddress;
|
||||
const std::uint32_t exportDirEnd = exportDirEntry.VirtualAddress + exportDirEntry.Size;
|
||||
|
||||
struct Candidate
|
||||
{
|
||||
std::string name;
|
||||
std::uint32_t rva;
|
||||
bool isFunction;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
|
||||
for (DWORD i = 0; i < exportDir->NumberOfNames; ++i)
|
||||
{
|
||||
const char* name = rvaToPointer(base, nameRvas[i]);
|
||||
WORD ordinal = nameOrdinals[i];
|
||||
if (ordinal >= exportDir->NumberOfFunctions)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::uint32_t functionRva = functionRvas[ordinal];
|
||||
if (functionRva == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A forwarder export's "RVA" actually points at a string
|
||||
// like "OtherDll.OtherFunction" living inside the export
|
||||
// directory itself, not at real code -- never classified
|
||||
// as a patchable function.
|
||||
bool isForwarder = functionRva >= exportDirBegin && functionRva < exportDirEnd;
|
||||
|
||||
const SectionInfo* section = sectionFor(functionRva);
|
||||
bool isFunction = !isForwarder && section != nullptr && section->executable;
|
||||
|
||||
candidates.push_back(Candidate{name, functionRva, isFunction});
|
||||
}
|
||||
|
||||
// Best-effort size estimate: distance to the next function
|
||||
// export in the same section. This can OVER-estimate (there
|
||||
// may be non-exported code, or alignment padding, between two
|
||||
// exported functions, so a large gap does not guarantee a
|
||||
// large real function) -- it only ever gives a hard "not
|
||||
// enough room" answer when the gap itself is already small.
|
||||
// PE has no per-export size field the way ELF does (see
|
||||
// below), so /hotpatch (applied automatically by
|
||||
// cmake/HotReload.cmake) is the primary safety mechanism on
|
||||
// Windows; this is a secondary backstop, not a substitute.
|
||||
std::vector<std::uint32_t> functionRvasSorted;
|
||||
for (const Candidate& candidate : candidates)
|
||||
{
|
||||
if (candidate.isFunction)
|
||||
{
|
||||
functionRvasSorted.push_back(candidate.rva);
|
||||
}
|
||||
}
|
||||
std::sort(functionRvasSorted.begin(), functionRvasSorted.end());
|
||||
|
||||
for (const Candidate& candidate : candidates)
|
||||
{
|
||||
ExportedSymbol symbol;
|
||||
symbol.name = candidate.name;
|
||||
symbol.address = base + candidate.rva;
|
||||
symbol.isFunction = candidate.isFunction;
|
||||
symbol.size = 0;
|
||||
|
||||
if (candidate.isFunction)
|
||||
{
|
||||
const SectionInfo* section = sectionFor(candidate.rva);
|
||||
std::uint32_t upperBound = section != nullptr ? section->rvaEnd : candidate.rva;
|
||||
|
||||
auto it = std::upper_bound(functionRvasSorted.begin(), functionRvasSorted.end(), candidate.rva);
|
||||
if (it != functionRvasSorted.end())
|
||||
{
|
||||
upperBound = std::min(upperBound, *it);
|
||||
}
|
||||
symbol.size = upperBound > candidate.rva ? (upperBound - candidate.rva) : 0;
|
||||
}
|
||||
|
||||
result.push_back(std::move(symbol));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// ELF / POSIX: read imagePath on disk rather than the mapped
|
||||
// image. Reading from disk means walking the ordinary section
|
||||
// header table (.dynsym / .dynstr, plus every section's sh_flags
|
||||
// for the executable check) instead of re-deriving the same
|
||||
// information from the .dynamic section's DT_* tags the way the
|
||||
// dynamic linker itself does internally -- simpler to get right,
|
||||
// at the cost of one known gap: a shared object with its section
|
||||
// header table deliberately stripped (rare -- an explicit, extra
|
||||
// step beyond an ordinary `strip`, essentially never done to a
|
||||
// library you are actively iterating on) will read back as having
|
||||
// no exported functions at all, handled the same as any other
|
||||
// unparseable image: an empty result, never a crash.
|
||||
//
|
||||
// st_value in the file is relative to the module's own link-time
|
||||
// base, so it is loadedBase (the actual runtime load address --
|
||||
// the "load bias" ModuleHandle reads via dlinfo) that turns it
|
||||
// into a real, callable address.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
#include <elf.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::vector<char> readWholeFile(const std::string& path)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
std::streamsize length = file.tellg();
|
||||
if (length <= 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length));
|
||||
if (!file.read(buffer.data(), length))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// Parses one ELF class (32 or 64-bit); Ehdr/Shdr/Sym is
|
||||
/// Elf32_* or Elf64_* depending on the caller, matched against
|
||||
/// e_ident[EI_CLASS] before this is ever instantiated.
|
||||
template <typename Ehdr, typename Shdr, typename Sym, typename SymType, typename SymBind>
|
||||
std::vector<ExportedSymbol> parseElf(
|
||||
const std::vector<char>& file, void* loadedBase, SymType symType, SymBind symBind)
|
||||
{
|
||||
std::vector<ExportedSymbol> result;
|
||||
if (file.size() < sizeof(Ehdr))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto* ehdr = reinterpret_cast<const Ehdr*>(file.data());
|
||||
if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0)
|
||||
{
|
||||
return result; // no section header table -- see file comment above
|
||||
}
|
||||
if (static_cast<std::uint64_t>(ehdr->e_shoff) + static_cast<std::uint64_t>(ehdr->e_shnum) * sizeof(Shdr) >
|
||||
file.size())
|
||||
{
|
||||
return result; // truncated/corrupt file
|
||||
}
|
||||
|
||||
const auto* sections = reinterpret_cast<const Shdr*>(file.data() + ehdr->e_shoff);
|
||||
|
||||
// Section header string table, to find sections by name.
|
||||
if (ehdr->e_shstrndx >= ehdr->e_shnum)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
const char* shstrtab = file.data() + sections[ehdr->e_shstrndx].sh_offset;
|
||||
|
||||
const Shdr* dynsymSection = nullptr;
|
||||
const Shdr* dynstrSection = nullptr;
|
||||
for (std::size_t i = 0; i < ehdr->e_shnum; ++i)
|
||||
{
|
||||
const char* name = shstrtab + sections[i].sh_name;
|
||||
if (std::strcmp(name, ".dynsym") == 0)
|
||||
{
|
||||
dynsymSection = §ions[i];
|
||||
}
|
||||
else if (std::strcmp(name, ".dynstr") == 0)
|
||||
{
|
||||
dynstrSection = §ions[i];
|
||||
}
|
||||
}
|
||||
if (dynsymSection == nullptr || dynstrSection == nullptr)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* dynstr = file.data() + dynstrSection->sh_offset;
|
||||
std::size_t symbolCount = dynsymSection->sh_size / sizeof(Sym);
|
||||
const auto* symbols = reinterpret_cast<const Sym*>(file.data() + dynsymSection->sh_offset);
|
||||
|
||||
for (std::size_t i = 0; i < symbolCount; ++i)
|
||||
{
|
||||
const Sym& sym = symbols[i];
|
||||
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
|
||||
{
|
||||
continue; // imported/unresolved, not defined here
|
||||
}
|
||||
unsigned char bind = symBind(sym.st_info);
|
||||
if (bind != STB_GLOBAL && bind != STB_WEAK)
|
||||
{
|
||||
continue; // not part of this module's visible surface
|
||||
}
|
||||
|
||||
ExportedSymbol exported;
|
||||
exported.name = dynstr + sym.st_name;
|
||||
exported.address = static_cast<char*>(loadedBase) + sym.st_value;
|
||||
exported.size = static_cast<std::size_t>(sym.st_size);
|
||||
|
||||
bool isFuncType = symType(sym.st_info) == STT_FUNC;
|
||||
bool sectionExecutable = false;
|
||||
if (sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON && sym.st_shndx < ehdr->e_shnum)
|
||||
{
|
||||
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
|
||||
}
|
||||
exported.isFunction = isFuncType && sectionExecutable;
|
||||
|
||||
result.push_back(std::move(exported));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase)
|
||||
{
|
||||
std::vector<char> file = readWholeFile(imagePath);
|
||||
if (file.size() < EI_NIDENT)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
unsigned char elfClass = static_cast<unsigned char>(file[EI_CLASS]);
|
||||
if (elfClass == ELFCLASS64)
|
||||
{
|
||||
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(
|
||||
file, loadedBase, [](unsigned char info) { return ELF64_ST_TYPE(info); },
|
||||
[](unsigned char info) { return ELF64_ST_BIND(info); });
|
||||
}
|
||||
if (elfClass == ELFCLASS32)
|
||||
{
|
||||
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(
|
||||
file, loadedBase, [](unsigned char info) { return ELF32_ST_TYPE(info); },
|
||||
[](unsigned char info) { return ELF32_ST_BIND(info); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// One exported symbol found in a loaded module, already classified.
|
||||
struct ExportedSymbol
|
||||
{
|
||||
std::string name;
|
||||
void* address = nullptr;
|
||||
|
||||
/// The single load-bearing safety gate behind the "only
|
||||
/// executable code changes" guarantee in hot_reload.h: false
|
||||
/// for anything not mechanically identified as code (a data
|
||||
/// object, a forwarder export, or a symbol kind this reader
|
||||
/// does not recognize at all). HotReloadContext never patches
|
||||
/// a symbol where this is false, on either side of a reload.
|
||||
bool isFunction = false;
|
||||
|
||||
/// Compiled size in bytes if known, 0 if not. ELF gives this
|
||||
/// exactly (the symbol table's st_size field); PE gives no
|
||||
/// such field, so this holds a conservative estimate derived
|
||||
/// from the distance to the next exported function in the
|
||||
/// same section -- see the file comment in SymbolTable.cpp
|
||||
/// for exactly what that heuristic can and cannot catch.
|
||||
std::size_t size = 0;
|
||||
};
|
||||
|
||||
/// Reads every exported, defined symbol belonging to the module
|
||||
/// mapped at loadedBase (the base address dlopen()/LoadLibrary()
|
||||
/// returned for it), classifying each one as code or data.
|
||||
///
|
||||
/// On POSIX this parses imagePath on disk (its ELF .dynsym /
|
||||
/// .dynstr / section-header table) and adds loadedBase as the
|
||||
/// runtime load bias. On Windows this ignores imagePath and reads
|
||||
/// directly from the already-mapped image at loadedBase (its PE
|
||||
/// export directory and section table) -- see the per-platform
|
||||
/// implementation blocks in SymbolTable.cpp for why each platform
|
||||
/// reads from a different place.
|
||||
///
|
||||
/// Returns an empty vector, never throws, if the image could not
|
||||
/// be parsed as this platform's native format (including the rare
|
||||
/// case of a .so with its section header table deliberately
|
||||
/// stripped -- see the file comment in SymbolTable.cpp).
|
||||
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# source/HotReload/src/c-api/.clang-tidy
|
||||
#
|
||||
# hot_reload.h's implementation (HotReloadApi.cpp) is Part II (C API)
|
||||
# style -- snake_case, per style guide §13 -- even though it is a
|
||||
# .cpp file living inside a C++ library. Copied from the repo-root
|
||||
# c-api/.clang-tidy template (see that file's own header comment for
|
||||
# the full explanation of how clang-tidy's directory-scoped config
|
||||
# resolution makes this work); the sibling directory one level up
|
||||
# (source/HotReload/src/) has no override of its own, so its Part I
|
||||
# (C++) files keep inheriting the repo-root .clang-tidy's PascalCase/
|
||||
# camelCase rules exactly as normal.
|
||||
InheritParentConfig: true
|
||||
|
||||
Checks: >
|
||||
readability-identifier-naming,
|
||||
bugprone-exception-escape
|
||||
|
||||
CheckOptions:
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.LocalVariableCase
|
||||
value: lower_case
|
||||
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: UPPER_CASE
|
||||
|
||||
- key: readability-identifier-naming.GlobalConstantCase
|
||||
value: UPPER_CASE
|
||||
- key: readability-identifier-naming.MacroDefinitionCase
|
||||
value: UPPER_CASE
|
||||
|
||||
- key: readability-identifier-naming.GlobalVariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.GlobalVariablePrefix
|
||||
value: g_
|
||||
- key: readability-identifier-naming.StaticVariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.StaticVariablePrefix
|
||||
value: s_
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Demangle.cpp -- hot_reload_demangle() / hot_reload_free_string().
|
||||
*
|
||||
* Pure string utility, no HotReloadContext involved, so it gets its
|
||||
* own file alongside HotReloadApi.cpp rather than crowding that one's
|
||||
* single responsibility (the opaque-pointer shim). Part II (C API)
|
||||
* style, like everything else in this directory -- see .clang-tidy
|
||||
* here and cpp-style-guide.md section 13.
|
||||
*
|
||||
* Every C++ symbol name in this library's events/lookups is always
|
||||
* the REAL symbol-table name (whatever hot_reload_get_symbol() needs
|
||||
* to find it again) -- this function is an opt-in convenience for
|
||||
* DISPLAYING that name, never applied automatically, so there is
|
||||
* never a mismatch between what an event reports and what you'd pass
|
||||
* back in to look the symbol up.
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
// Covers GCC and Clang on every OS this library targets, MinGW
|
||||
// included: what selects the demangling scheme is the C++ ABI the
|
||||
// COMPILER implements (Itanium here), not the operating system --
|
||||
// MinGW is still GCC/Clang generating Itanium-mangled names even
|
||||
// though the OS is Windows. MSVC, on the same OS, uses its own,
|
||||
// unrelated scheme (the #elif below). See README.md's "Hot
|
||||
// reloading C++ symbols" section for why this distinction matters
|
||||
// beyond just this function.
|
||||
#include <cxxabi.h>
|
||||
#elif defined(_MSC_VER)
|
||||
#include <dbghelp.h>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
char* hot_reload_demangle(const char* name)
|
||||
{
|
||||
if (name == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string text = name; // fallback: unchanged, for anything that isn't a mangled C++ name at all
|
||||
|
||||
try
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
int status = 0;
|
||||
char* demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status);
|
||||
if (status == 0 && demangled != nullptr)
|
||||
{
|
||||
text = demangled;
|
||||
}
|
||||
std::free(demangled); // __cxa_demangle always malloc()s or leaves this null either way
|
||||
#elif defined(_MSC_VER)
|
||||
// UnDecorateSymbolName operates on the string alone (no
|
||||
// SymInitialize/session needed, unlike most of
|
||||
// DbgHelp) -- but DbgHelp as a whole is documented as not
|
||||
// thread-safe, so serialize calls to this function
|
||||
// yourself if you call it from more than one thread.
|
||||
// Written against Microsoft's documented behavior; unlike
|
||||
// most of this library, this specific function was not
|
||||
// exercised on real Windows -- see README.md's toolchain
|
||||
// verification notes.
|
||||
char buffer[2048];
|
||||
if (UnDecorateSymbolName(name, buffer, sizeof(buffer), UNDNAME_COMPLETE) != 0)
|
||||
{
|
||||
text = buffer;
|
||||
}
|
||||
#endif
|
||||
|
||||
char* result = new char[text.size() + 1];
|
||||
std::memcpy(result, text.c_str(), text.size() + 1);
|
||||
return result;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void hot_reload_free_string(char* str)
|
||||
{
|
||||
delete[] str;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include "../HotReloadContext.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
/// The opaque ::HotReloadContext the public header declares is
|
||||
/// never actually instantiated as itself -- every instance really
|
||||
/// is a hot_reload::HotReloadContext, following the same
|
||||
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
|
||||
/// example uses. This is the one place that cast happens.
|
||||
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
|
||||
{
|
||||
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Per cpp-style-guide.md section 11: a C++ exception must never cross
|
||||
// an extern "C" boundary -- doing so is undefined behavior for any
|
||||
// caller that is not itself compiled to expect one, which a plain C
|
||||
// caller of this API never is. Every function here is a thin,
|
||||
// non-throwing wrapper for exactly that reason: std::string/vector/
|
||||
// unordered_map construction is the only thing in hot_reload::
|
||||
// HotReloadContext that could throw at all (std::bad_alloc under
|
||||
// memory pressure), so catching (...) and mapping it to this API's
|
||||
// existing failure values is sufficient without needing per-exception-
|
||||
// type handling.
|
||||
extern "C"
|
||||
{
|
||||
|
||||
HotReloadContext* hot_reload_context_create(const HotReloadOptions* options)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reinterpret_cast<::HotReloadContext*>(new hot_reload::HotReloadContext(options));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void hot_reload_context_destroy(HotReloadContext* ctx)
|
||||
{
|
||||
delete impl(ctx);
|
||||
}
|
||||
|
||||
HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path)
|
||||
{
|
||||
if (ctx == nullptr || module_name == nullptr || path == nullptr)
|
||||
{
|
||||
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->load(module_name, path);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path)
|
||||
{
|
||||
if (ctx == nullptr || module_name == nullptr || new_path == nullptr)
|
||||
{
|
||||
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->reload(module_name, new_path);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HOT_RELOAD_ERROR_LOAD_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name)
|
||||
{
|
||||
if (ctx == nullptr || module_name == nullptr || symbol_name == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->getSymbol(module_name, symbol_name);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const char* hot_reload_last_error(HotReloadContext* ctx)
|
||||
{
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
try
|
||||
{
|
||||
return impl(ctx)->lastError().c_str();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char* hot_reload_status_string(HotReloadStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case HOT_RELOAD_OK:
|
||||
return "ok";
|
||||
case HOT_RELOAD_ERROR_INVALID_ARGUMENT:
|
||||
return "invalid argument";
|
||||
case HOT_RELOAD_ERROR_LOAD_FAILED:
|
||||
return "load failed";
|
||||
case HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION:
|
||||
return "no previous version loaded for this module name";
|
||||
case HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE:
|
||||
return "no patch backend for this CPU architecture";
|
||||
}
|
||||
return "unknown hot reload status";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user