# Volkan A cross-platform, cross-compiler C/C++ library that patches a running process's own code in place, so a new build of a function takes effect immediately — no restart, and every existing caller (including a raw C function pointer cached somewhere) picks it up automatically. ```cpp // host.cpp -- loads a plugin, calls into it, then hot-reloads it later hot_reload::Context ctx; ctx.load("plugin", "plugin.so"); auto tick = reinterpret_cast(ctx.findSymbol("plugin", "tick")); tick(1); // runs V1's code ctx.reload("plugin", "plugin.so"); // plugin.so was rebuilt in between tick(1); // same pointer, now runs V2's code ``` This is `hot_reload::Context`, the primary, modern C++ API ([`Context.hpp`](source/HotReload/include/hot_reload/Context.hpp)) -- RAII, a `Result` instead of a status-code-plus-side-channel, and safe to call from multiple threads at once. A separate, independently stable C API ([`hot_reload.h`](source/HotReload/include/hot_reload/hot_reload.h)) wraps it for anything that needs plain C linkage; see ["Pick an API"](#pick-an-api) below. ## Features - **Works on a real function pointer, not just a lookup table.** Patching happens at the function's own address, so *every* path that reaches it — a direct call, a call through the PLT/GOT, a raw function pointer cached in a struct, a C++ vtable slot — picks up the new code, with nothing to re-resolve. - **Only executable code is ever touched — mechanically, not by convention.** A symbol is a patch candidate only if the loader's own metadata says it's a function in an executable section (ELF `STT_FUNC`+`SHF_EXECINSTR`, or a PE export in an executable section). Data — globals, class instances, vtables, RTTI — is structurally excluded, never something you have to remember to avoid exporting. - **Deletion is handled, not just tolerated.** A function missing from a new build is reported through an event callback, and by default its old implementation just keeps running — never a crash, never a jump to garbage. Redirect it to your own stub instead if you want stale calls to be loud during development. - **Real C++ works, not just `extern "C"`.** A real, unmodified mangled name — an ordinary method, a virtual method (redirected wherever it's reached from, direct call or vtable slot, since both hold the same address), an overloaded operator — patches exactly like a plain C function, with no special handling needed. See ["Hot reloading C++ symbols"](source/HotReload/README.md#hot-reloading-c-symbols-classes-virtual-methods-operators) for the one real limit this runs into (inlining) and how to work with it. - **Failures are diagnosable, not a black box.** `hot_reload_last_error()` returns the platform's own explanation (`dlerror()` / a formatted `GetLastError()`) for why a load failed — a missing file, a permissions problem, and a shared library with a genuinely undefined symbol all look identical from the status code alone, and don't need to. A corrupted or truncated module is handled with the same care: every offset the ELF reader trusts is bounds-checked against the file's actual size, confirmed by feeding it 1500 randomly corrupted variants of a real plugin — see "What's actually been tested" below. - **x86, x86-64, ARM64, and ARM32 (both its ARM and Thumb-2 instruction sets), across GCC, Clang, MinGW, and MSVC.** See "What's actually been tested" below for exactly how each combination was verified — natively, under QEMU, or under Wine, not just reasoned about. - **Safe to call from multiple threads at once, verified under ThreadSanitizer, not just locked and hoped.** `hot_reload::Context` uses a reader-writer lock internally: concurrent `findSymbol()` calls run in parallel with each other, while `load()`/`reload()` briefly take exclusive access. This covers the library's own bookkeeping — it does not, and architecturally cannot, make it safe to reload a module some other thread is at that exact moment executing inside; see ["Pick an API"](#pick-an-api) for the precise guarantee and `source/HotReload/README.md`'s "What this cannot detect" for why that one specific case is out of reach for any tool like this one. ## Documentation - **[`source/HotReload/README.md`](source/HotReload/README.md)** is the full reference: exactly how patching works, both safety guarantees in detail, the complete C++ interoperability story, per-platform scope and how each was verified, and a growing list of real bugs found (and fixed) by actually running this — on Windows/MSVC, under fuzzing, under ThreadSanitizer, and on every other toolchain in the table below — read this before relying on any specific edge case. - **[`Context.hpp`](source/HotReload/include/hot_reload/Context.hpp) is the primary, modern C++ API**, with the rest of its surface -- [`Result`](source/HotReload/include/hot_reload/Result.hpp), [`Status`](source/HotReload/include/hot_reload/Status.hpp), [`Options`](source/HotReload/include/hot_reload/Options.hpp), [`SymbolEvent`](source/HotReload/include/hot_reload/SymbolEvent.hpp), [`SymbolEventInfo`](source/HotReload/include/hot_reload/SymbolEventInfo.hpp), [`demangle()`](source/HotReload/include/hot_reload/Demangle.hpp) -- one type or function per header, each with its own matching `.cpp`. `hot_reload.h`'s C API is a separate, loosely coupled wrapper built on top of this one's public methods only (`c-api/HotReloadApi.cpp`), not the other way around -- see that file's own file comment, and ["Pick an API"](#pick-an-api) below for which to reach for. - **Every source file, not just the header, is Doxygen-documented** — `hot_reload.h`'s public API (every function, status code, and event kind has a `@brief`/`@param`/`@return`) and every internal class and helper behind it, down to why each one exists. Build it locally: ```sh scripts/setup.sh # one-time: fetches a pinned Doxygen scripts/docs.sh hotreload_lib ``` then open `out/docs/hotreload_lib/html/index.html`. Verified to generate with zero warnings — for every module this project documents, library, examples, and tests alike — even reconfigured with `-DDOXYGEN_WARN_IF_UNDOCUMENTED=YES -DDOXYGEN_EXTRACT_PRIVATE=YES`, the strictest combination `cmake/Documentation.cmake` exposes. - **[`CONTRIBUTING.md`](CONTRIBUTING.md)** covers the CMake build system this project itself uses (module conventions, `scripts/`, output layout) — only relevant if you're extending *this* repository, not for using the library in your own. ## How to use it ### Build it ```sh scripts/build.sh # library only scripts/build.sh Release -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON # + both demos + the test suite ``` ```bat scripts\build.bat scripts\build.bat Release -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON ``` This produces `hotreload_lib`, a `STATIC` library exposing both a modern C++ API (`Context.hpp` and the headers alongside it) and a separate, independently stable plain C API (`hot_reload.h`) built on top of it. Link your host application against it (`target_link_libraries(your_app PRIVATE hotreload_lib)` if you're using CMake; see [`source/HotReload/CMakeLists.txt`](source/HotReload/CMakeLists.txt) for what it links on each platform if you're not). ### Pick an API Both are first-class, both stay supported, and both end up patching the exact same way underneath (the C API calls the C++ one's own public methods, nothing more privileged) -- this is about which fits what you're writing: - **`hot_reload::Context` (`Context.hpp`)** — a C++ host. RAII (no separate create/destroy calls), a `Result` returned directly from `load()`/`reload()` instead of a status code plus a separate `last_error()` side-channel to remember to check, `std::function` callbacks instead of a raw function pointer plus manually-threaded `void*` user data, and the thread-safety guarantee described above — there is no equivalent of the C API's stateful `last_error()` to race on in the first place, since every call's outcome comes back in its own `Result`. Prefer this for any new C++ code. - **`hot_reload.h`** — plain C linkage itself is the requirement: a C host, or a different language's FFI boundary (most FFI story the way a C header does; few can call a C++ class directly at all). The opaque `HotReloadContext*` it hands out is a small wrapper struct (`c-api/HotReloadApi.cpp`) that owns a real `hot_reload::Context` by composition — never a cast between unrelated types — and `hot_reload_last_error()` keeps working exactly as documented by tracking each calling thread's own most recent error internally, so two threads' errors can never be confused with each other even though the underlying `Context` itself keeps none of that state. ### See it work — a scripted, deterministic demo ```sh ./out/build/bin/examples/hotreload_demo_host ``` Loads one plugin, calls into it, hot-reloads a second build of it, and calls the *same, never-re-resolved* function pointers again — proving every guarantee above in one run. Open [`source/examples/HotReloadDemo/PluginV1/src/Plugin.cpp`](source/examples/HotReloadDemo/PluginV1/src/Plugin.cpp) and its `PluginV2` counterpart side by side against the output. ### Try it yourself — a persistent, interactive demo ```sh ./out/build/bin/examples/hotreload_live_demo_host # leave this running cmake --build out/build --target hotreload_live_plugin # in another terminal, or: source/examples/HotReloadLiveDemo/watch.sh out/build # rebuild automatically on save ``` Edit [`source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp`](source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp) — its own comment walks through several concrete things worth trying — and watch the *running* process pick up your change within about a second, with no restart. See [`source/examples/HotReloadLiveDemo/README.md`](source/examples/HotReloadLiveDemo/README.md) for the full walkthrough, including two Windows-specific gotchas (`Access is denied` on "Rebuild", and why leftover `.hotreload.N` files next to your plugin are expected, not a leak) that are easy to mistake for bugs the first time you hit them. ### Use it in your own module 1. `extern "C"` isn't required — a real C++ mangled name works too — but it's the simplest choice for anything you want to look up by a plain, human-typed name via `hot_reload_get_symbol()`. 2. Call `hot_reload_enable_patchable_functions()` (`cmake/HotReload.cmake`) on your plugin's CMake target, so every exported function has guaranteed room for a jump. 3. Optionally call `hot_reload_restrict_exports()` and mark just your intended entry points with `HOT_RELOAD_EXPORT`, instead of exporting everything by default — keeps your own internal helpers out of the reloadable surface. See `source/HotReload/README.md`'s "Using it in your own module" for the full version of this list, including why this specific step doesn't (and can't) quiet `std::string`/ `std::vector` noise, and why that's fine. 4. Have your host `load()` it once at startup (`hot_reload::Context`, or `hot_reload_load()` through the C API) and `reload()` it after every rebuild, from a point where nothing else is actively calling into it. A path with no `/` in it is still resolved against the current working directory (never a system library path), so a plain `plugin.so` next to your host works exactly as you'd expect. ## Limitations Two things this library fundamentally cannot detect, because a symbol table doesn't carry the information needed to: - **A changed function signature.** Patching redirects control flow at a fixed address; it has no way to know that `void tick(int)` became `void tick(int, int)` between builds. Give a changed signature a new exported name instead of reusing the old one. - **A data layout change reached through code that still gets patched.** This library guarantees it never *touches* a data symbol directly. It cannot guarantee that a patched function is safe to run against an object that already exists in memory with the *old* layout — e.g. a C++ vtable gaining a new virtual function, or a struct gaining a field, while an old instance is still alive. One thing it doesn't fully solve, by design rather than by omission: calling `hot_reload_reload()` **concurrently with active execution** of the code being replaced isn't guaranteed safe on every architecture — the intended pattern (shared by every tool in this space) is reloading from a quiescent point, such as your main thread between frames. See `source/HotReload/README.md`'s "What this cannot detect" for the full reasoning, including exactly which write-ordering protections *are* in place and what they do and don't cover. ## What's actually been tested Not just built for, actually run to completion, verified directly rather than assumed: | Toolchain | How it was verified | |---|---| | GCC + Clang, native Linux/x86-64 | Both compilers, the full demo and both test suites (`hotreload_tests` for the C API, `context_tests` for `hot_reload::Context` directly) run to completion, including a CET (`endbr64`) landing pad this toolchain turns out to emit by default, and again explicitly forcing one with `-fcf-protection=full` | | GCC, native Linux/x86 (32-bit) | Full demo and test suite run to completion under `-m32`. This is also where the suite's own "too small to patch" plugin function turned out not to be small enough at `-O2` on this one target — a real, previously-unverified gap between what the test assumed and what the compiler actually emitted, closed by making that function require no instructions at all | | ARM64 (GCC cross-compiled) | Run under `qemu-aarch64` — twice, once plain and once with `-mbranch-protection=standard`, to exercise the BTI landing-pad path on real ARM64 semantics. Re-run, both test suites included, after `hot_reload::Context` and its reader-writer lock were introduced, to confirm a genuinely multi-threaded C++ host works correctly under real (if emulated) ARM64 hardware too, not just natively | | ARM32 (GCC cross-compiled, hard-float ABI) | Run under `qemu-arm` — both instruction sets (the default Thumb-2, and ARM state forced with `-marm`), across `-O0` through `-O3`. This architecture needed a genuinely new backend, not just a recompile: a CPU in this family is always in one of two incompatible instruction-decoding states, detected from the same bit the ARM EABI already marks every function symbol with. The one case that depends on runtime layout rather than just compiler flags was built and run deliberately, not just reasoned about: Thumb-2's literal-load jump needs its own address 4-byte aligned, which a Thumb function is only ever guaranteed to be 2-byte aligned for, so a plugin with a function confirmed landing 2 bytes off that boundary was built and reloaded under qemu to prove the one-instruction realignment this backend adds for exactly that case actually works. Also found here: the suite's own "too small to patch" function needed a third implementation (hand-written, not compiler-generated) — even an empty C++ body's unoptimized frame-pointer prologue reaches this architecture's own jump size at `-O0` | | MinGW | Cross-compiled and run under Wine, including the PE export-table parser and a real `LoadLibrary` failure (confirmed `hot_reload_last_error()` reports "Bad EXE format" correctly) | | MSVC | Built and live-tested on real Windows by someone using this library — not by the environment that wrote it. Four real fixes came out of that and are folded in: `windows.h`/`dbghelp.h` include order, `NOMINMAX`, explicit `dbghelp` linking, and a reload-retry widening for Windows' slower post-rebuild file-availability timing. The same person has since reported it still working after the rewrite to `hot_reload::Context` (reported, not independently re-run here) | | Clang (GNU-style `clang++`) targeting the MSVC ABI, Windows | **Two real build failures reported against this configuration (Clang 22.1, Windows SDK 10.0.26100), both fixed — but not yet confirmed by a successful end-to-end build on that exact toolchain.** Nothing in the environment this was written in can build for Windows natively, so everything below is a proxy check, not that confirmation. (1) `Demangle.cpp` tested `__clang__` before `_MSC_VER`, but Clang defines both when targeting the MSVC ABI, so it went looking for ``, which that configuration does not have; it now keys on `_MSC_VER` first, since that tracks the mangling scheme in use rather than which compiler frontend produced it. (2) `windows.h` without `WIN32_LEAN_AND_MEAN` pulls in COM headers (`ole2.h` → `combaseapi.h` → …) that failed to compile there; all four files that include it now define it. A third was found by reading the same build log rather than waiting for it to fail: the CMake condition that links `dbghelp` asked whether the compiler takes MSVC *command-line syntax* (the `MSVC` variable, false for GNU-style `clang++`) when it needed to ask whether it produces the MSVC *ABI* — `CMAKE_CXX_SIMULATE_ID`, which CMake sets from the very `_MSC_VER` the C++ code now keys on. What was actually checked: each branch condition under simulated `_MSC_VER` and under plain Clang/GCC; every library source compiled for Windows with mingw-w64 (plus `Demangle.cpp` with its MSVC branch forced on); that `WIN32_LEAN_AND_MEAN` removes exactly the `ole2.h`/`combaseapi.h`/`objidlbase.h`/`servprov.h`/`urlmon.h` chain from the failing log (against mingw-w64's headers, not the real SDK's); the new CMake condition under four compiler configurations, including a `clang++` made to define `_MSC_VER` so CMake reports `MSVC` false and `SIMULATE_ID` `MSVC`, as that log implies; and that Clang accepts `-fpatchable-function-entry=32,0` for the Windows/MSVC target and emits the entry padding this library needs (while `-fms-hotpatch` emits none on x64, so it is not a substitute). The actual `__uuidof` failure itself could not be reproduced here | | AddressSanitizer + UndefinedBehaviorSanitizer, and Valgrind with self-modifying-code checking enabled | Both test suites, clean under all three — the self-modifying-code checking matters specifically here, since patching a function's own bytes at runtime is exactly the pattern a memory checker's JIT otherwise mishandles by default | | ThreadSanitizer | `context_tests`' own concurrency stress test (several threads calling `findSymbol()` against others calling `reload()`, some on the very same module name as each other, plus several more each `load()`-ing their own distinct module name, all against one shared `Context`) -- five separate runs, to cover more than one possible thread interleaving, zero races reported each time. Exactly what `hot_reload::Context`'s reader-writer lock exists to make true, confirmed rather than assumed, the same standard this project holds every other claim in this table to | | Fuzzing (ELF section-header parsing) | 1500 randomly corrupted variants of a real plugin's section header table — each still a file the OS loader itself accepts via `dlopen()` — all handled cleanly. An earlier version of this same reader crashed on 69 of an initial 300 | Three more real, reproducible bugs came out of this same pass, each confirmed broken before the fix and confirmed fixed after: a `hot_reload_load()`/`hot_reload_reload()` path with no `/` in it (e.g. a plain `"plugin.so"`) silently failed to load on POSIX, since `dlopen()` treats a bare name as a library-search request rather than a file path; a module name containing a `/` or other unusual character (the header never promised it had to look like a file name) broke the internal staging path used to make each reload load from a fresh copy; and the ELF section-header hardening above. **Deliberately not implemented, rather than implemented and untested:** macOS/Mach-O (never requested, and would need a third image format reader alongside the existing ELF and PE ones) — a documented, bounded gap; see `source/HotReload/README.md`'s "Scope" section for the reasoning and the extension point if you need it.