Fix documentation
This commit is contained in:
1 parent
a317c07124
commit
ab21b3f30d
27 files changed
+2137
-1043
No files matched your search
+560
@@ -0,0 +1,560 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
This covers the CMake module system this project's build is organized
|
||||||
|
around, `scripts/` for the everyday commands, and how the pieces fit
|
||||||
|
together -- everything you'd need to add a module, understand the
|
||||||
|
build output layout, or extend the tooling. None of it is specific to
|
||||||
|
hot-reloading; if you're here to *use* the library, [`README.md`](README.md)
|
||||||
|
and [`source/HotReload/README.md`](source/HotReload/README.md) are the
|
||||||
|
ones to read instead.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
CMakeLists.txt project setup, options, install(), discovery -- edit by hand
|
||||||
|
Configuration.cmake all output-path configuration -- edit to relocate anything
|
||||||
|
cmake/BuildHelpers.cmake module macros + discovery mechanism -- edit rarely
|
||||||
|
cmake/Dependencies.cmake vendoring / installed-library / FetchContent helpers -- edit rarely
|
||||||
|
cmake/Documentation.cmake per-module Doxygen integration -- only ever LOOKS for a pinned Doxygen
|
||||||
|
cmake/DoxygenPin.cmake shared version/URL/hash parameters for the pinned Doxygen
|
||||||
|
cmake/FetchDoxygen.cmake standalone downloader (run via `cmake -P`, not during normal configure)
|
||||||
|
cmake/NinjaPin.cmake shared version/URL/hash parameters for the pinned Ninja
|
||||||
|
cmake/FetchNinja.cmake standalone downloader (run via `cmake -P`, not during normal configure)
|
||||||
|
cmake/HotReload.cmake hot_reload_enable_patchable_functions() / hot_reload_restrict_exports()
|
||||||
|
source/HotReload/ the library -- library module "hotreload_lib" (STATIC)
|
||||||
|
source/examples/HotReloadDemo/ worked example: a host + two builds of one hot-reloadable plugin
|
||||||
|
source/examples/HotReloadLiveDemo/ interactive: a persistent host + one plugin file you edit yourself
|
||||||
|
source/tests/HotReloadTests/ the automated test suite
|
||||||
|
source/tests/UndefinedSymbolFixture/ a fixture the test suite loads, deliberately broken -- see its own comment
|
||||||
|
scripts/setup.sh, setup.bat one-time: fetch pinned Ninja + Doxygen, check tooling, activate git hook
|
||||||
|
scripts/build.sh, build.bat configure + build -- prefers a pinned Ninja, falls back gracefully
|
||||||
|
scripts/run.sh, run.bat run a module by its CMake target name
|
||||||
|
scripts/install.sh, install.bat install this project's own artifacts only
|
||||||
|
scripts/docs.sh, docs.bat build documentation for every module (or one) -- needs setup.sh first
|
||||||
|
scripts/refactor.sh, refactor.bat auto-fix formatting/naming across this project's own code
|
||||||
|
.clang-format, .clang-tidy C++ style guide tooling (see "Code style & tooling" below)
|
||||||
|
.editorconfig, .gitattributes -- same
|
||||||
|
.gitignore Visual Studio template + a block auto-synced from Configuration.cmake
|
||||||
|
c-api/.clang-tidy naming override template for a C API subtree -- applied for real at
|
||||||
|
source/HotReload/src/c-api/ and source/HotReload/include/hot_reload/
|
||||||
|
.githooks/pre-commit formatting/lint pre-commit hook
|
||||||
|
cpp-style-guide.md, TOOLING.md the style guide itself + tooling docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything generated -- the build tree, install output, docs, and reserved
|
||||||
|
packaging output -- lives under one `out/` directory (see "Output paths"
|
||||||
|
below); nothing under `out/` is source-controlled.
|
||||||
|
|
||||||
|
Each module's `CMakeLists.txt` is (conventionally) one line:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# source/HotReload/CMakeLists.txt
|
||||||
|
add_lib_module(hotreload_lib TYPE STATIC OUTPUT_NAME "hotreload")
|
||||||
|
```
|
||||||
|
|
||||||
|
A module declares its own dependencies in its own `CMakeLists.txt` --
|
||||||
|
same "minimal tool intervention" principle as module registration:
|
||||||
|
nothing to add in the root file. This project's own modules don't
|
||||||
|
currently need any third-party dependency, so the table in "Three ways
|
||||||
|
to bring in a dependency" below is showing you machinery this project
|
||||||
|
isn't using itself right now -- it's still there, exercised, and ready
|
||||||
|
if you add a module that needs it.
|
||||||
|
|
||||||
|
## Output paths (`Configuration.cmake`)
|
||||||
|
|
||||||
|
Every generated path is a cache variable, overridable with `-D<NAME>=...`
|
||||||
|
at configure time instead of editing any file:
|
||||||
|
|
||||||
|
| Variable | Default | What it controls |
|
||||||
|
|---|---|---|
|
||||||
|
| `OUT_DIR` | `out/` | Root of everything generated; override this to relocate all of the below at once |
|
||||||
|
| `INSTALL_OUTPUT_DIR` | `out/install` | Root of the `cmake --install` tree, one subdirectory per `DIST_TARGET` |
|
||||||
|
| `DIST_OUTPUT_DIR` | `out/dist` | Reserved for packaged/archived output (e.g. a future CPack integration) -- not populated by this project yet, but created and ready |
|
||||||
|
| `DOCS_OUTPUT_DIR` | `out/docs` | Root of generated Doxygen documentation, one subdirectory per module |
|
||||||
|
| `TOOLCACHE_DIR` | `.cache/tools` | Where downloaded, pinned build tools (currently Ninja and Doxygen) are cached -- kept outside `OUT_DIR` so `rm -rf out/` doesn't force a re-download |
|
||||||
|
| `COMPILE_COMMANDS_DESTINATION` | `compile_commands.json` | Where `compile_commands.json` is copied to after every build (see "compile_commands.json / clang tooling" below) |
|
||||||
|
| `RUNTIME_OUTPUT_SUBDIR` | `bin` | Subdirectory (inside whatever the build dir is) for executables/`.dll`/`.so` |
|
||||||
|
| `LIBRARY_OUTPUT_SUBDIR` | `bin` | Subdirectory for shared-library artifacts (kept alongside executables so `$ORIGIN` RPATH resolves them) |
|
||||||
|
| `ARCHIVE_OUTPUT_SUBDIR` | `lib` | Subdirectory for static/import-library artifacts |
|
||||||
|
|
||||||
|
The build directory itself (`CMAKE_BINARY_DIR`, where `CMakeCache.txt`
|
||||||
|
lives) isn't a `Configuration.cmake` variable -- it's fixed by whatever you
|
||||||
|
pass to `-B` when configuring. `scripts/build.sh`/`build.bat` default to
|
||||||
|
`out/build`, so the resulting layout is:
|
||||||
|
|
||||||
|
```
|
||||||
|
out/
|
||||||
|
build/ CMAKE_BINARY_DIR -- out/build/bin, out/build/lib during normal builds
|
||||||
|
install/<DIST_TARGET>/{bin,lib} what `cmake --install` / scripts/install.sh populates
|
||||||
|
dist/ reserved for future packaging output
|
||||||
|
docs/<module>/html/, docs/index.html generated documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
Example -- build into a different location entirely:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -G Ninja -S . -B /tmp/my-build -DOUT_DIR=/tmp/my-out
|
||||||
|
cmake --build /tmp/my-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.gitignore` stays in sync automatically
|
||||||
|
|
||||||
|
`Configuration.cmake` is the single source of truth for every path above
|
||||||
|
-- `.gitignore` doesn't hardcode its own copy of them. Every configure,
|
||||||
|
a marked block near the bottom of `.gitignore` is regenerated from the
|
||||||
|
*current* value of each path variable:
|
||||||
|
|
||||||
|
```
|
||||||
|
# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>>
|
||||||
|
.cache/tools/
|
||||||
|
compile_commands.json
|
||||||
|
out/
|
||||||
|
out/dist/
|
||||||
|
out/docs/
|
||||||
|
out/install/
|
||||||
|
# <<< Configuration.cmake generated paths -- END <<<
|
||||||
|
```
|
||||||
|
|
||||||
|
Change `OUT_DIR` (or any of the others, permanently in `Configuration.cmake`
|
||||||
|
or per-configure with `-D...=...`) and this block updates to match on the
|
||||||
|
next configure -- nothing to keep in sync by hand. Everything outside the
|
||||||
|
block is untouched: `.gitignore` ships based on GitHub's
|
||||||
|
[`VisualStudio.gitignore`](https://github.com/github/gitignore/blob/main/VisualStudio.gitignore)
|
||||||
|
template, and that content is never rewritten. A path is only added if it
|
||||||
|
resolves *inside* the repo -- pointing `TOOLCACHE_DIR` somewhere else
|
||||||
|
entirely just omits it, since `.gitignore` has nothing useful to say about
|
||||||
|
a location outside the repository it lives in. The file is only rewritten
|
||||||
|
when the computed block actually differs from what's already there, so a
|
||||||
|
normal reconfigure doesn't spuriously touch its contents or mtime.
|
||||||
|
|
||||||
|
## Three ways to bring in a dependency (`cmake/Dependencies.cmake`)
|
||||||
|
|
||||||
|
Not currently used by any module in this project (`hotreload_lib` and
|
||||||
|
everything under `source/examples/`/`source/tests/` are entirely
|
||||||
|
self-contained -- no network access is needed to build any of it), but
|
||||||
|
fully exercised machinery, ready for a module that does need one:
|
||||||
|
|
||||||
|
| Helper | For | Usage |
|
||||||
|
|---|---|---|
|
||||||
|
| `add_vendor_header_only(NAME [INCLUDE_DIR <dir>])` | A buildless header-only drop under `vendor/<NAME>/include` | `add_vendor_header_only(some_header_lib)` |
|
||||||
|
| `add_vendor_subdirectory(SUBDIR)` | A vendored dependency with its own `CMakeLists.txt`, under `vendor/<SUBDIR>` | `add_vendor_subdirectory(some_vendored_lib)` |
|
||||||
|
| `add_dependency(NAME <FetchContent_Declare args>)` | An installed package (`FIND_PACKAGE_ARGS ...`) with a fetch-from-source fallback | `add_dependency(fmt GIT_REPOSITORY ... FIND_PACKAGE_ARGS NAMES fmt)` |
|
||||||
|
|
||||||
|
All three are safe to call more than once for the same name (e.g. two
|
||||||
|
modules that need the same dependency) and mark the dependency's headers
|
||||||
|
`SYSTEM`, so `-Wall -Wextra -Wpedantic`/`/W4` -- which we only ever apply
|
||||||
|
per-target inside our own module macros, never globally -- doesn't fire on
|
||||||
|
code you don't control. Their targets are also dropped from
|
||||||
|
`compile_commands.json` (see below).
|
||||||
|
|
||||||
|
`add_dependency` is CMake's native find-then-fetch integration: it tries
|
||||||
|
`find_package()` first (an already-installed apt/vcpkg/conan copy) via
|
||||||
|
`FIND_PACKAGE_ARGS`, and only downloads + builds from source if nothing is
|
||||||
|
found. Omit `FIND_PACKAGE_ARGS` to always fetch. Requires network access
|
||||||
|
the first time a dependency isn't already installed.
|
||||||
|
|
||||||
|
A module `DEPENDS` on whatever target the chosen path defines exactly
|
||||||
|
like depending on another module -- `add_lib_module`/`add_exe_module`
|
||||||
|
don't need to know or care which of the three supplied it.
|
||||||
|
|
||||||
|
## Documentation (automatic per module, pinned Doxygen, entirely opt-in)
|
||||||
|
|
||||||
|
Every module registered via `add_lib_module`/`add_exe_module`/
|
||||||
|
`add_test_module`/`add_example_module` gets its own
|
||||||
|
[Doxygen](https://www.doxygen.nl/) documentation target for free -- nothing
|
||||||
|
to add to a module's own `CMakeLists.txt`. This project uses a pinned
|
||||||
|
Doxygen (currently **1.16.1**, see `DOXYGEN_PINNED_VERSION` in
|
||||||
|
`cmake/DoxygenPin.cmake`) -- it deliberately never looks for or uses a
|
||||||
|
system-installed `doxygen`, so every machine building this project
|
||||||
|
generates identical output regardless of what (if anything) is on `PATH`.
|
||||||
|
|
||||||
|
**Building the project never needs Doxygen.** `cmake/Documentation.cmake`
|
||||||
|
only ever *looks* for an already-fetched pinned Doxygen under
|
||||||
|
`.cache/tools/` (`TOOLCACHE_DIR`) -- it never downloads anything itself
|
||||||
|
during a normal configure, so `scripts/build.sh` has no Doxygen/network
|
||||||
|
dependency at all. Fetching it is a separate, explicit, one-time step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/setup.sh # see "Setup" below -- fetches the pinned Doxygen once
|
||||||
|
```
|
||||||
|
|
||||||
|
Until you've run that, `scripts/docs.sh` prints a clear message instead of
|
||||||
|
building anything -- everything else (the actual C++ project) builds
|
||||||
|
completely normally either way:
|
||||||
|
|
||||||
|
```
|
||||||
|
Documentation isn't set up yet (or ENABLE_DOXYGEN=OFF). Run scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure.
|
||||||
|
```
|
||||||
|
|
||||||
|
Once set up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/docs.sh # build docs for every module, prints where to open it
|
||||||
|
scripts/docs.sh hotreload_lib # build docs for just one module (docs_<target>)
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\docs.bat
|
||||||
|
scripts\docs.bat hotreload_lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Output goes to `out/docs/<module>/html/index.html` (a sibling of
|
||||||
|
`out/build/`, not nested inside it -- see "Output paths" above), plus a
|
||||||
|
landing page at `out/docs/index.html` linking to every module.
|
||||||
|
Documentation is **not** built as part of a normal `scripts/build.sh` --
|
||||||
|
it's opt-in, the same way tests and examples are.
|
||||||
|
|
||||||
|
**Writing docs for a module** is just normal Doxygen comments in that
|
||||||
|
module's headers -- e.g. `source/HotReload/include/hot_reload/hot_reload.h`,
|
||||||
|
which is written this way throughout (`@brief`/`@param`/`@return`, and
|
||||||
|
`@defgroup` sections grouping related declarations -- see that file for
|
||||||
|
the pattern to follow in a new one).
|
||||||
|
|
||||||
|
Even with zero comments, a module's docs are still generated and useful --
|
||||||
|
file lists, function/class signatures, and (if [Graphviz](https://graphviz.org/)
|
||||||
|
is installed on your system -- this one enhancement, unlike Doxygen
|
||||||
|
itself, is still detected rather than pinned, since it's optional) call
|
||||||
|
graphs are produced regardless, since `DOXYGEN_EXTRACT_ALL` is on by
|
||||||
|
default. Comments just make it richer. Drop a `README.md` in a module's
|
||||||
|
own directory and it's automatically used as that module's Doxygen main
|
||||||
|
page -- `source/HotReload/README.md` already does this.
|
||||||
|
|
||||||
|
**Configuring it** -- nothing is required, but everything is overridable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Disable entirely (docs targets print a message instead of building):
|
||||||
|
cmake -S . -B out/build -DENABLE_DOXYGEN=OFF
|
||||||
|
|
||||||
|
# Pin a different Doxygen version (also update the two SHA256 hashes in
|
||||||
|
# cmake/DoxygenPin.cmake to match that release's assets -- a stale hash
|
||||||
|
# fails scripts/setup.sh loudly, on purpose, rather than silently
|
||||||
|
# accepting an unexpected file -- then re-run scripts/setup.sh):
|
||||||
|
cmake -S . -B out/build -DDOXYGEN_PINNED_VERSION=1.17.0
|
||||||
|
|
||||||
|
# Any Doxyfile tag can be set as a CMake variable, e.g. stricter output
|
||||||
|
# (this is exactly what was used to verify every Doxygen comment in this
|
||||||
|
# project while writing them -- zero warnings, checked directly, not assumed):
|
||||||
|
cmake -S . -B out/build -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES -DDOXYGEN_WARN_NO_PARAMDOC=YES -DDOXYGEN_QUIET=NO
|
||||||
|
```
|
||||||
|
|
||||||
|
A single module can opt out with `NO_DOCS`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# some/internal/module/CMakeLists.txt
|
||||||
|
add_lib_module(internal_only TYPE STATIC NO_DOCS)
|
||||||
|
```
|
||||||
|
|
||||||
|
Vendored and fetched dependencies (`cmake/Dependencies.cmake`) are never
|
||||||
|
auto-documented -- same reasoning as their `compile_commands.json`
|
||||||
|
exclusion, it isn't your code.
|
||||||
|
|
||||||
|
## compile_commands.json / clang tooling
|
||||||
|
|
||||||
|
`CMAKE_EXPORT_COMPILE_COMMANDS` is on by default, and every build copies
|
||||||
|
`compile_commands.json` from the build directory to the project root so
|
||||||
|
clangd/clang-tidy/IWYU find it without extra configuration (CMake only
|
||||||
|
ever writes it into the build directory; most editors look for it at the
|
||||||
|
repo root). Vendored and fetched dependencies are excluded from it, so it
|
||||||
|
only reflects code this project actually owns. The project also builds
|
||||||
|
cleanly with clang directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -G Ninja -S . -B out/build -DCMAKE_CXX_COMPILER=clang++ -DDIST_TARGET=linux-x64-clang
|
||||||
|
cmake --build out/build
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified, not assumed, throughout this project's own development: every
|
||||||
|
module here (the library, both example demos, and the test suite) was
|
||||||
|
built and its tests/demos run to completion under both GCC and Clang.
|
||||||
|
|
||||||
|
## Code style & tooling
|
||||||
|
|
||||||
|
This project follows [`cpp-style-guide.md`](cpp-style-guide.md) (tabs,
|
||||||
|
Allman braces, `PascalCase` file names, `camelCase` functions/variables,
|
||||||
|
`m_`/`s_`/`g_` member/static/global prefixes, plus a distinct Part II
|
||||||
|
style -- `snake_case`, library-prefixed names -- for any C API boundary;
|
||||||
|
see the file itself for the full rules and rationale), enforced by the
|
||||||
|
tooling described in [`TOOLING.md`](TOOLING.md):
|
||||||
|
|
||||||
|
| File | Enforces |
|
||||||
|
|---|---|
|
||||||
|
| `.clang-format` | All formatting: tabs, Allman braces, pointer alignment, include order, line length |
|
||||||
|
| `.clang-tidy` | Naming conventions and a subset of language-feature rules, for this project's own C++ code |
|
||||||
|
| `c-api/.clang-tidy` | A naming override (`snake_case`) for a C API subtree -- see below for where this is actually applied |
|
||||||
|
| `.editorconfig` | Baseline tabs/whitespace for editors that don't run clang-format live |
|
||||||
|
| `.gitattributes` | Forces LF line endings for source files on every OS |
|
||||||
|
| `.githooks/pre-commit` | Runs clang-format + clang-tidy on staged files before each commit; warns and skips (never blocks) if a tool isn't installed |
|
||||||
|
|
||||||
|
**Fixing violations automatically**, instead of hunting them down by hand:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refactor.sh # every file under source/ and c-api/ by default
|
||||||
|
scripts/refactor.sh source/HotReload # just one file or directory
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\refactor.bat
|
||||||
|
scripts\refactor.bat source\HotReload
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs `clang-format -i` (always) and `clang-tidy --fix` (if
|
||||||
|
`compile_commands.json` exists -- build once first if it doesn't) across
|
||||||
|
the given scope. The pre-commit hook only *checks*; this is the "just fix
|
||||||
|
it" counterpart -- review the result with `git diff` afterward, same as
|
||||||
|
any auto-formatter.
|
||||||
|
|
||||||
|
**Be especially careful reviewing `git diff` on a file with more than one
|
||||||
|
architecture/OS branch** (`#if defined(__x86_64__) ... #elif ... #endif`,
|
||||||
|
several of which exist under `source/HotReload/src/` -- see
|
||||||
|
`Architecture.cpp`, `ModuleHandle.cpp`, `MemoryProtection.cpp`,
|
||||||
|
`SymbolTable.cpp`). `clang-tidy` only ever analyzes the ONE branch active
|
||||||
|
for whichever platform you're running it on -- it cannot see, and will
|
||||||
|
not touch, the source text inside the other, inactive `#elif` branches at
|
||||||
|
all. Verified directly while building this project: running `--fix` on
|
||||||
|
`Architecture.cpp` from an x86-64 Linux machine renamed a local constant
|
||||||
|
inside the x86-64 branch only, then updated a later line shared by *all
|
||||||
|
three* architecture branches to match the new name -- silently breaking
|
||||||
|
the build for the other two architectures, which still declared the old
|
||||||
|
name. Caught only by reviewing the diff and separately cross-compiling
|
||||||
|
every branch, which is exactly the review step this paragraph is asking
|
||||||
|
you to actually do on a file shaped like this, not skip.
|
||||||
|
|
||||||
|
With no argument, only `source/` and `c-api/` are ever scanned -- `out/`,
|
||||||
|
`.cache/`, and `.git/` are never walked, so this can't accidentally spend
|
||||||
|
minutes reformatting/analyzing a fetched dependency's entire source tree
|
||||||
|
(this project doesn't currently have one under `out/build/_deps/`, but the
|
||||||
|
exclusion is unconditional, not conditional on one existing). Passing an
|
||||||
|
explicit scope takes it as given: `refactor.sh` still excludes
|
||||||
|
`out`/`.cache`/`.git` defensively if your scope happens to overlap with
|
||||||
|
them; `refactor.bat` does not (its Windows batch equivalent of that filter
|
||||||
|
turned out to be unreliable to verify, so it was removed rather than
|
||||||
|
shipped uncertain -- pick a scope that doesn't overlap with those
|
||||||
|
directories, which is the normal case anyway, e.g. `source\HotReload`).
|
||||||
|
|
||||||
|
**One-time setup per clone** to activate the hook -- `scripts/setup.sh` /
|
||||||
|
`setup.bat` does this for you (see "Setup" below) along with fetching the
|
||||||
|
pinned Doxygen and checking for clang-format/clang-tidy; the two commands
|
||||||
|
it runs for the hook specifically are:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x .githooks/pre-commit
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
```
|
||||||
|
|
||||||
|
**clang-tidy needs `compile_commands.json`** to resolve `#include`s --
|
||||||
|
this project already generates and copies it to the repo root on every
|
||||||
|
build (see above), so as long as you've built at least once, the hook's
|
||||||
|
clang-tidy check works with no extra setup.
|
||||||
|
|
||||||
|
**This project genuinely has a C API subtree**, unlike the generic
|
||||||
|
template this was adapted from: `source/HotReload/include/hot_reload/`
|
||||||
|
(the public header) and `source/HotReload/src/c-api/` (its `extern "C"`
|
||||||
|
implementation) each carry a copy of `c-api/.clang-tidy`, positioned at
|
||||||
|
the root of the subtree they apply to -- `clang-tidy` always uses the
|
||||||
|
closest `.clang-tidy` file up the directory tree, so files under either
|
||||||
|
directory pick up `snake_case` naming instead of the rest of this
|
||||||
|
project's `PascalCase`/`camelCase`, while `source/HotReload/src/`'s own
|
||||||
|
Part I (C++) files, one level up, keep inheriting this repo-root
|
||||||
|
`.clang-tidy`'s rules exactly as normal. See either copy's own header
|
||||||
|
comment for the full explanation of how clang-tidy's directory-scoped
|
||||||
|
config resolution makes that work.
|
||||||
|
|
||||||
|
## How module discovery works
|
||||||
|
|
||||||
|
The root `CMakeLists.txt` calls `_discover_module_subdirectories()` on
|
||||||
|
three roots, in this order:
|
||||||
|
|
||||||
|
| Root | What it holds |
|
||||||
|
|---|---|
|
||||||
|
| `source/*` | library AND app/executable modules, side by side |
|
||||||
|
| `source/tests/*` | test modules |
|
||||||
|
| `source/examples/*` | example modules |
|
||||||
|
|
||||||
|
(`source/tests` and `source/examples` aren't picked up by the first call
|
||||||
|
-- they have no `CMakeLists.txt` directly inside them, only their own
|
||||||
|
children do.)
|
||||||
|
|
||||||
|
Any immediate child directory that contains its own `CMakeLists.txt` gets
|
||||||
|
`add_subdirectory()`'d automatically (`vendor/` is not one of these roots
|
||||||
|
-- vendored dependencies are opted into explicitly by the modules that
|
||||||
|
need them, not auto-built by default). This uses `CONFIGURE_DEPENDS`, so
|
||||||
|
the *next build* reconfigures on its own after you add or remove a module
|
||||||
|
directory -- no manual `cmake` re-run needed. A module may `DEPENDS` on a
|
||||||
|
module discovered later in this list, or reference one via
|
||||||
|
`add_dependencies()`/a generator expression before that other module's
|
||||||
|
own `CMakeLists.txt` has even run -- order doesn't affect linking or
|
||||||
|
target existence (CMake resolves target names, and dependency edges to
|
||||||
|
not-yet-declared targets, across the whole project at generate time --
|
||||||
|
verified directly with an isolated, minimal reproduction while building
|
||||||
|
this project, not assumed).
|
||||||
|
|
||||||
|
A module directory can itself contain more than one CMake target when its
|
||||||
|
shape doesn't fit the "one target per module" default -- see
|
||||||
|
`source/examples/HotReloadDemo/PluginV1/CMakeLists.txt`'s own comment for
|
||||||
|
a real example (a hot-reloadable plugin needs to exist as two separate
|
||||||
|
builds of the same exported names, which `add_lib_module` doesn't assume)
|
||||||
|
and `source/examples/HotReloadDemo/CMakeLists.txt` for the small grouping
|
||||||
|
file that pattern needs, one level up, purely so discovery (which only
|
||||||
|
looks one level deep into `source/examples/`) finds it at all.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
CMake >= 3.25, a C++20 compiler (GCC/Clang/MSVC). **No network access is
|
||||||
|
needed to build this project** -- every module here is self-contained,
|
||||||
|
with no `add_dependency()`-fetched third-party library anywhere (see
|
||||||
|
"Three ways to bring in a dependency" above). Ninja and Doxygen are both
|
||||||
|
pinned and fetched automatically (see "Setup" below) if you want them,
|
||||||
|
not system dependencies, and `scripts/build.sh` warns rather than fails
|
||||||
|
if Ninja isn't available at all (see "Build" below).
|
||||||
|
[Graphviz](https://graphviz.org/) is optional for Doxygen call graphs, and
|
||||||
|
[clang-format](https://clang.llvm.org/docs/ClangFormat.html)/
|
||||||
|
[clang-tidy](https://clang.llvm.org/extra/clang-tidy/) are optional for
|
||||||
|
the pre-commit hook and `scripts/refactor.sh` (see "Code style & tooling"
|
||||||
|
above) -- both are detected, not pinned, since they're editor/workflow
|
||||||
|
tools rather than part of the build itself. `scripts/setup.sh` (below)
|
||||||
|
checks for both and prints install guidance if either is missing.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Optional, and separate from building (see "Requirements", "Build", and
|
||||||
|
"Documentation" above) -- run once per clone, or any time you want to
|
||||||
|
pick up a newer pinned version after bumping `NINJA_PINNED_VERSION` or
|
||||||
|
`DOXYGEN_PINNED_VERSION`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/setup.sh
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\setup.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Does four things, each independent of the others:
|
||||||
|
|
||||||
|
1. Fetches the pinned Ninja (`cmake/FetchNinja.cmake`) -- used by
|
||||||
|
`scripts/build.sh`/`build.bat` if present (see "Build" below); not
|
||||||
|
required, a system Ninja works fine too.
|
||||||
|
2. Fetches the pinned Doxygen (`cmake/FetchDoxygen.cmake`) -- needed for
|
||||||
|
`scripts/docs.sh`, not for building the project.
|
||||||
|
3. Checks for `clang-format`/`clang-tidy` on `PATH` and prints
|
||||||
|
OS-appropriate install guidance if either is missing (never
|
||||||
|
auto-installs -- that would need assumptions about your package
|
||||||
|
manager and elevated privileges this script shouldn't assume it has).
|
||||||
|
4. Activates the pre-commit hook for this clone, if it's a git repository
|
||||||
|
(`git config core.hooksPath .githooks`).
|
||||||
|
|
||||||
|
Safe to re-run any time -- each step is a no-op if there's nothing to do
|
||||||
|
(e.g. the pinned Ninja/Doxygen are already cached).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/build.sh # Release (default), library + tests only if BUILD_TESTS was set before
|
||||||
|
scripts/build.sh Release -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON # + the automated tests + both demos
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\build.bat
|
||||||
|
scripts\build.bat Release -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON
|
||||||
|
```
|
||||||
|
|
||||||
|
`BUILD_TESTS` and `BUILD_EXAMPLES` both default to `OFF` (see
|
||||||
|
`CMakeLists.txt`) -- plain `scripts/build.sh` with no extra arguments
|
||||||
|
builds only `hotreload_lib` itself. Anything after the build type is
|
||||||
|
passed straight through to `cmake`'s configure step (tested directly:
|
||||||
|
`scripts/build.sh Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON` builds the
|
||||||
|
library, both demos, and the test suite together), and CMake caches `-D`
|
||||||
|
options across reconfigures, so passing them once and calling
|
||||||
|
`scripts/build.sh` plain afterward for incremental builds keeps them set
|
||||||
|
-- no need to repeat them on every call.
|
||||||
|
|
||||||
|
`build.bat` parses its arguments through `for /f "tokens=1,*"` rather
|
||||||
|
than `%1`/`%2`/etc. directly, deliberately -- `cmd.exe`'s own parameter
|
||||||
|
parser treats `=` (along with `,` and `;`) as an argument separator, so
|
||||||
|
`-DBUILD_EXAMPLES=ON` typed as one argument would otherwise arrive
|
||||||
|
split into two, silently corrupting the value before the script ever
|
||||||
|
sees it. Verified directly under Wine's `cmd.exe` against exactly that
|
||||||
|
failure, not just reasoned through -- see the script's own comment.
|
||||||
|
|
||||||
|
Picks a generator in this order, each falling back to the next:
|
||||||
|
|
||||||
|
1. **A pinned Ninja**, if `scripts/setup.sh`/`setup.bat` has fetched one --
|
||||||
|
passed to CMake explicitly via `-DCMAKE_MAKE_PROGRAM=...` so it's used
|
||||||
|
even if a different Ninja also happens to be on `PATH` (same "always
|
||||||
|
the pinned copy" preference as Doxygen -- see "Documentation" above).
|
||||||
|
2. **A system Ninja**, if one is on `PATH` and no pinned copy was found.
|
||||||
|
3. **CMake's own default generator** for the platform, if neither Ninja is
|
||||||
|
available anywhere -- `scripts/build.sh`/`build.bat` print a warning
|
||||||
|
and continue rather than fail. (`CMakeLists.txt` separately warns too,
|
||||||
|
once CMake reports which generator it actually picked -- this is the
|
||||||
|
pre-existing "This project targets Ninja" message, unrelated to this
|
||||||
|
script.)
|
||||||
|
|
||||||
|
Verified concretely, not assumed: with neither a pinned nor a system Ninja
|
||||||
|
present anywhere, `scripts/build.sh` still successfully configures, builds,
|
||||||
|
and produces working binaries -- it just uses whatever CMake's default
|
||||||
|
generator is for the platform (Unix Makefiles on Linux, typically NMake or
|
||||||
|
a Visual Studio generator on Windows) instead.
|
||||||
|
|
||||||
|
**If you're building inside Visual Studio's own CMake integration**
|
||||||
|
(open-folder, not a generated `.sln`) rather than via these scripts:
|
||||||
|
"Rebuild" does a full clean first, which will fail with `Access is
|
||||||
|
denied` on any `.exe` that's currently running -- e.g. the live demo's
|
||||||
|
host (see `source/examples/HotReloadLiveDemo/README.md`). This is a
|
||||||
|
Windows constraint (a running process's own image file can't be
|
||||||
|
deleted), not a bug in this project; a plain "Build", or building one
|
||||||
|
specific target, never touches an unrelated running executable and
|
||||||
|
isn't affected.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/run.sh hotreload_demo_host
|
||||||
|
scripts/run.sh hotreload_live_demo_host
|
||||||
|
scripts/run.sh # no args: lists available targets
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\run.bat hotreload_demo_host
|
||||||
|
scripts\run.bat hotreload_live_demo_host
|
||||||
|
```
|
||||||
|
|
||||||
|
`run.sh`/`run.bat` resolve the CMake **target id** (not the binary's
|
||||||
|
`OUTPUT_NAME`) to the actual binary via a manifest CMake generates at
|
||||||
|
configure time, so these scripts never need editing when a module is
|
||||||
|
added, removed, or renamed. Requires `-DBUILD_EXAMPLES=ON` to have been
|
||||||
|
set at configure time for either of the two hot-reload targets above,
|
||||||
|
since they live under `source/examples/`.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/install.sh
|
||||||
|
```
|
||||||
|
```bat
|
||||||
|
scripts\install.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Always use the install script (or pass `--component hotreload` yourself if
|
||||||
|
calling `cmake --install` directly). A vendored/fetched dependency's own
|
||||||
|
`CMakeLists.txt` usually has `install()` rules of its own with no
|
||||||
|
component tag; installing without `--component` would also run those,
|
||||||
|
typically dumping files under `CMAKE_INSTALL_PREFIX` (e.g. `/usr/local`).
|
||||||
|
Tagging this project's own `install(TARGETS ...)` calls with `COMPONENT
|
||||||
|
hotreload` and always filtering on it is what keeps `cmake --install`
|
||||||
|
scoped to just this project's own artifacts -- currently just
|
||||||
|
`hotreload_lib` itself, since examples and tests are opt-in, dev-time
|
||||||
|
targets rather than something you'd install.
|
||||||
|
|
||||||
|
## Adding a module
|
||||||
|
|
||||||
|
1. Create a directory under the right root (see the discovery table above).
|
||||||
|
2. Add a `CMakeLists.txt` inside it with one `add_lib_module` /
|
||||||
|
`add_exe_module` / `add_test_module` / `add_example_module` call, plus
|
||||||
|
any `add_vendor_*`/`add_dependency` calls it needs.
|
||||||
|
3. Add your `.cpp`/`.h` files inside that same directory.
|
||||||
|
4. Rebuild (`scripts/build.sh`) -- no other file needs touching.
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# source/MyTool/CMakeLists.txt
|
||||||
|
add_exe_module(my_tool_exe OUTPUT_NAME "my-tool" DEPENDS hotreload_lib)
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/run.sh my_tool_exe` picks it up automatically once it's built,
|
||||||
|
and `scripts/docs.sh my_tool_exe` documents it automatically too -- both
|
||||||
|
with zero edits anywhere outside `source/MyTool/`.
|
||||||
@@ -1,575 +1,213 @@
|
|||||||
<div align="center">
|
# Hot Reload
|
||||||
<img src="https://github.com/AcruxDevelopment/Volkan/blob/main/res/icon.png" alt="Logo" width="auto" height="80">
|
|
||||||
|
|
||||||
# Volkan
|
|
||||||
|
|
||||||
**C++ Hot-Reload library**
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
A cross-platform, cross-compiler C/C++ library that patches a running
|
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
|
process's own code in place, so a new build of a function takes effect
|
||||||
immediately, without restarting -- see
|
immediately — no restart, and every existing caller (including a raw C
|
||||||
[`source/HotReload/README.md`](source/HotReload/README.md) for what it
|
function pointer cached somewhere) picks it up automatically.
|
||||||
does, what it guarantees, and its honest limitations. This file covers
|
|
||||||
the project around it: the CMake module system, its tooling, and how
|
```cpp
|
||||||
the examples and tests fit together.
|
// host.cpp -- loads a plugin, calls into it, then hot-reloads it later
|
||||||
|
HotReloadContext* ctx = hot_reload_context_create(nullptr);
|
||||||
|
hot_reload_load(ctx, "plugin", "plugin.so");
|
||||||
|
|
||||||
|
auto tick = reinterpret_cast<int (*)(int)>(hot_reload_get_symbol(ctx, "plugin", "tick"));
|
||||||
|
tick(1); // runs V1's code
|
||||||
|
|
||||||
|
hot_reload_reload(ctx, "plugin", "plugin.so"); // plugin.so was rebuilt in between
|
||||||
|
tick(1); // same pointer, now runs V2's code
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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, and ARM64, 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.
|
||||||
|
|
||||||
|
## 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, and
|
||||||
|
on every other toolchain in the table below — read this before relying
|
||||||
|
on any specific edge case.
|
||||||
|
- **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
|
```sh
|
||||||
scripts/setup.sh # optional, one-time -- see "Setup" below
|
scripts/build.sh # library only
|
||||||
scripts/build.sh Release -DBUILD_EXAMPLES=ON
|
scripts/build.sh Release -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON # + both demos + the test suite
|
||||||
./out/build/bin/examples/hotreload_demo_host
|
|
||||||
```
|
|
||||||
|
|
||||||
## What's here
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| [`source/HotReload/`](source/HotReload/README.md) | The library itself: `hotreload_lib`, a `STATIC` library with a plain C API (`hot_reload.h`) over a C++ implementation |
|
|
||||||
| [`source/examples/HotReloadDemo/`](source/examples/HotReloadDemo) | A scripted, deterministic demo: a host + two builds of one plugin, exercising every guarantee the library makes |
|
|
||||||
| [`source/examples/HotReloadLiveDemo/`](source/examples/HotReloadLiveDemo/README.md) | An interactive demo: a host that stays running, and one plugin file you edit and rebuild yourself |
|
|
||||||
| [`source/tests/HotReloadTests/`](source/tests/HotReloadTests) | An automated `ctest` suite covering the library end to end, including a regression test for a real bug found while building this |
|
|
||||||
|
|
||||||
The rest of this file is about the project *around* that: a per-module
|
|
||||||
CMake setup adapted from a generic template (each module owns a tiny,
|
|
||||||
self-contained `CMakeLists.txt`; the root `CMakeLists.txt` discovers
|
|
||||||
them automatically), `scripts/` for the everyday commands, and the
|
|
||||||
code-style tooling. None of it is specific to hot-reloading -- if
|
|
||||||
you're only here for the library, `source/HotReload/README.md` is the
|
|
||||||
one to read.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
CMakeLists.txt project setup, options, install(), discovery -- edit by hand
|
|
||||||
Configuration.cmake all output-path configuration -- edit to relocate anything
|
|
||||||
cmake/BuildHelpers.cmake module macros + discovery mechanism -- edit rarely
|
|
||||||
cmake/Dependencies.cmake vendoring / installed-library / FetchContent helpers -- edit rarely
|
|
||||||
cmake/Documentation.cmake per-module Doxygen integration -- only ever LOOKS for a pinned Doxygen
|
|
||||||
cmake/DoxygenPin.cmake shared version/URL/hash parameters for the pinned Doxygen
|
|
||||||
cmake/FetchDoxygen.cmake standalone downloader (run via `cmake -P`, not during normal configure)
|
|
||||||
cmake/NinjaPin.cmake shared version/URL/hash parameters for the pinned Ninja
|
|
||||||
cmake/FetchNinja.cmake standalone downloader (run via `cmake -P`, not during normal configure)
|
|
||||||
cmake/HotReload.cmake hot_reload_enable_patchable_functions() / hot_reload_restrict_exports()
|
|
||||||
source/HotReload/ the library -- library module "hotreload_lib" (STATIC)
|
|
||||||
source/examples/HotReloadDemo/ worked example: a host + two builds of one hot-reloadable plugin
|
|
||||||
source/examples/HotReloadLiveDemo/ interactive: a persistent host + one plugin file you edit yourself
|
|
||||||
source/tests/HotReloadTests/ the automated test suite
|
|
||||||
source/tests/UndefinedSymbolFixture/ a fixture the test suite loads, deliberately broken -- see its own comment
|
|
||||||
scripts/setup.sh, setup.bat one-time: fetch pinned Ninja + Doxygen, check tooling, activate git hook
|
|
||||||
scripts/build.sh, build.bat configure + build -- prefers a pinned Ninja, falls back gracefully
|
|
||||||
scripts/run.sh, run.bat run a module by its CMake target name
|
|
||||||
scripts/install.sh, install.bat install this project's own artifacts only
|
|
||||||
scripts/docs.sh, docs.bat build documentation for every module (or one) -- needs setup.sh first
|
|
||||||
scripts/refactor.sh, refactor.bat auto-fix formatting/naming across this project's own code
|
|
||||||
.clang-format, .clang-tidy C++ style guide tooling (see "Code style & tooling" below)
|
|
||||||
.editorconfig, .gitattributes -- same
|
|
||||||
.gitignore Visual Studio template + a block auto-synced from Configuration.cmake
|
|
||||||
c-api/.clang-tidy naming override template for a C API subtree -- applied for real at
|
|
||||||
source/HotReload/src/c-api/ and source/HotReload/include/hot_reload/
|
|
||||||
.githooks/pre-commit formatting/lint pre-commit hook
|
|
||||||
cpp-style-guide.md, TOOLING.md the style guide itself + tooling docs
|
|
||||||
```
|
|
||||||
|
|
||||||
Everything generated -- the build tree, install output, docs, and reserved
|
|
||||||
packaging output -- lives under one `out/` directory (see "Output paths"
|
|
||||||
below); nothing under `out/` is source-controlled.
|
|
||||||
|
|
||||||
Each module's `CMakeLists.txt` is (conventionally) one line:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
# source/HotReload/CMakeLists.txt
|
|
||||||
add_lib_module(hotreload_lib TYPE STATIC OUTPUT_NAME "hotreload")
|
|
||||||
```
|
|
||||||
|
|
||||||
A module declares its own dependencies in its own `CMakeLists.txt` --
|
|
||||||
same "minimal tool intervention" principle as module registration:
|
|
||||||
nothing to add in the root file. This project's own modules don't
|
|
||||||
currently need any third-party dependency, so the table in "Three ways
|
|
||||||
to bring in a dependency" below is showing you machinery this project
|
|
||||||
isn't using itself right now -- it's still there, exercised, and ready
|
|
||||||
if you add a module that needs it.
|
|
||||||
|
|
||||||
## Output paths (`Configuration.cmake`)
|
|
||||||
|
|
||||||
Every generated path is a cache variable, overridable with `-D<NAME>=...`
|
|
||||||
at configure time instead of editing any file:
|
|
||||||
|
|
||||||
| Variable | Default | What it controls |
|
|
||||||
|---|---|---|
|
|
||||||
| `OUT_DIR` | `out/` | Root of everything generated; override this to relocate all of the below at once |
|
|
||||||
| `INSTALL_OUTPUT_DIR` | `out/install` | Root of the `cmake --install` tree, one subdirectory per `DIST_TARGET` |
|
|
||||||
| `DIST_OUTPUT_DIR` | `out/dist` | Reserved for packaged/archived output (e.g. a future CPack integration) -- not populated by this project yet, but created and ready |
|
|
||||||
| `DOCS_OUTPUT_DIR` | `out/docs` | Root of generated Doxygen documentation, one subdirectory per module |
|
|
||||||
| `TOOLCACHE_DIR` | `.cache/tools` | Where downloaded, pinned build tools (currently Ninja and Doxygen) are cached -- kept outside `OUT_DIR` so `rm -rf out/` doesn't force a re-download |
|
|
||||||
| `COMPILE_COMMANDS_DESTINATION` | `compile_commands.json` | Where `compile_commands.json` is copied to after every build (see "compile_commands.json / clang tooling" below) |
|
|
||||||
| `RUNTIME_OUTPUT_SUBDIR` | `bin` | Subdirectory (inside whatever the build dir is) for executables/`.dll`/`.so` |
|
|
||||||
| `LIBRARY_OUTPUT_SUBDIR` | `bin` | Subdirectory for shared-library artifacts (kept alongside executables so `$ORIGIN` RPATH resolves them) |
|
|
||||||
| `ARCHIVE_OUTPUT_SUBDIR` | `lib` | Subdirectory for static/import-library artifacts |
|
|
||||||
|
|
||||||
The build directory itself (`CMAKE_BINARY_DIR`, where `CMakeCache.txt`
|
|
||||||
lives) isn't a `Configuration.cmake` variable -- it's fixed by whatever you
|
|
||||||
pass to `-B` when configuring. `scripts/build.sh`/`build.bat` default to
|
|
||||||
`out/build`, so the resulting layout is:
|
|
||||||
|
|
||||||
```
|
|
||||||
out/
|
|
||||||
build/ CMAKE_BINARY_DIR -- out/build/bin, out/build/lib during normal builds
|
|
||||||
install/<DIST_TARGET>/{bin,lib} what `cmake --install` / scripts/install.sh populates
|
|
||||||
dist/ reserved for future packaging output
|
|
||||||
docs/<module>/html/, docs/index.html generated documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
Example -- build into a different location entirely:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cmake -G Ninja -S . -B /tmp/my-build -DOUT_DIR=/tmp/my-out
|
|
||||||
cmake --build /tmp/my-build
|
|
||||||
```
|
|
||||||
|
|
||||||
### `.gitignore` stays in sync automatically
|
|
||||||
|
|
||||||
`Configuration.cmake` is the single source of truth for every path above
|
|
||||||
-- `.gitignore` doesn't hardcode its own copy of them. Every configure,
|
|
||||||
a marked block near the bottom of `.gitignore` is regenerated from the
|
|
||||||
*current* value of each path variable:
|
|
||||||
|
|
||||||
```
|
|
||||||
# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>>
|
|
||||||
.cache/tools/
|
|
||||||
compile_commands.json
|
|
||||||
out/
|
|
||||||
out/dist/
|
|
||||||
out/docs/
|
|
||||||
out/install/
|
|
||||||
# <<< Configuration.cmake generated paths -- END <<<
|
|
||||||
```
|
|
||||||
|
|
||||||
Change `OUT_DIR` (or any of the others, permanently in `Configuration.cmake`
|
|
||||||
or per-configure with `-D...=...`) and this block updates to match on the
|
|
||||||
next configure -- nothing to keep in sync by hand. Everything outside the
|
|
||||||
block is untouched: `.gitignore` ships based on GitHub's
|
|
||||||
[`VisualStudio.gitignore`](https://github.com/github/gitignore/blob/main/VisualStudio.gitignore)
|
|
||||||
template, and that content is never rewritten. A path is only added if it
|
|
||||||
resolves *inside* the repo -- pointing `TOOLCACHE_DIR` somewhere else
|
|
||||||
entirely just omits it, since `.gitignore` has nothing useful to say about
|
|
||||||
a location outside the repository it lives in. The file is only rewritten
|
|
||||||
when the computed block actually differs from what's already there, so a
|
|
||||||
normal reconfigure doesn't spuriously touch its contents or mtime.
|
|
||||||
|
|
||||||
## Three ways to bring in a dependency (`cmake/Dependencies.cmake`)
|
|
||||||
|
|
||||||
Not currently used by any module in this project (`hotreload_lib` and
|
|
||||||
everything under `source/examples/`/`source/tests/` are entirely
|
|
||||||
self-contained -- no network access is needed to build any of it), but
|
|
||||||
fully exercised machinery, ready for a module that does need one:
|
|
||||||
|
|
||||||
| Helper | For | Usage |
|
|
||||||
|---|---|---|
|
|
||||||
| `add_vendor_header_only(NAME [INCLUDE_DIR <dir>])` | A buildless header-only drop under `vendor/<NAME>/include` | `add_vendor_header_only(some_header_lib)` |
|
|
||||||
| `add_vendor_subdirectory(SUBDIR)` | A vendored dependency with its own `CMakeLists.txt`, under `vendor/<SUBDIR>` | `add_vendor_subdirectory(some_vendored_lib)` |
|
|
||||||
| `add_dependency(NAME <FetchContent_Declare args>)` | An installed package (`FIND_PACKAGE_ARGS ...`) with a fetch-from-source fallback | `add_dependency(fmt GIT_REPOSITORY ... FIND_PACKAGE_ARGS NAMES fmt)` |
|
|
||||||
|
|
||||||
All three are safe to call more than once for the same name (e.g. two
|
|
||||||
modules that need the same dependency) and mark the dependency's headers
|
|
||||||
`SYSTEM`, so `-Wall -Wextra -Wpedantic`/`/W4` -- which we only ever apply
|
|
||||||
per-target inside our own module macros, never globally -- doesn't fire on
|
|
||||||
code you don't control. Their targets are also dropped from
|
|
||||||
`compile_commands.json` (see below).
|
|
||||||
|
|
||||||
`add_dependency` is CMake's native find-then-fetch integration: it tries
|
|
||||||
`find_package()` first (an already-installed apt/vcpkg/conan copy) via
|
|
||||||
`FIND_PACKAGE_ARGS`, and only downloads + builds from source if nothing is
|
|
||||||
found. Omit `FIND_PACKAGE_ARGS` to always fetch. Requires network access
|
|
||||||
the first time a dependency isn't already installed.
|
|
||||||
|
|
||||||
A module `DEPENDS` on whatever target the chosen path defines exactly
|
|
||||||
like depending on another module -- `add_lib_module`/`add_exe_module`
|
|
||||||
don't need to know or care which of the three supplied it.
|
|
||||||
|
|
||||||
## Documentation (automatic per module, pinned Doxygen, entirely opt-in)
|
|
||||||
|
|
||||||
Every module registered via `add_lib_module`/`add_exe_module`/
|
|
||||||
`add_test_module`/`add_example_module` gets its own
|
|
||||||
[Doxygen](https://www.doxygen.nl/) documentation target for free -- nothing
|
|
||||||
to add to a module's own `CMakeLists.txt`. This project uses a pinned
|
|
||||||
Doxygen (currently **1.16.1**, see `DOXYGEN_PINNED_VERSION` in
|
|
||||||
`cmake/DoxygenPin.cmake`) -- it deliberately never looks for or uses a
|
|
||||||
system-installed `doxygen`, so every machine building this project
|
|
||||||
generates identical output regardless of what (if anything) is on `PATH`.
|
|
||||||
|
|
||||||
**Building the project never needs Doxygen.** `cmake/Documentation.cmake`
|
|
||||||
only ever *looks* for an already-fetched pinned Doxygen under
|
|
||||||
`.cache/tools/` (`TOOLCACHE_DIR`) -- it never downloads anything itself
|
|
||||||
during a normal configure, so `scripts/build.sh` has no Doxygen/network
|
|
||||||
dependency at all. Fetching it is a separate, explicit, one-time step:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/setup.sh # see "Setup" below -- fetches the pinned Doxygen once
|
|
||||||
```
|
|
||||||
|
|
||||||
Until you've run that, `scripts/docs.sh` prints a clear message instead of
|
|
||||||
building anything -- everything else (the actual C++ project) builds
|
|
||||||
completely normally either way:
|
|
||||||
|
|
||||||
```
|
|
||||||
Documentation isn't set up yet (or ENABLE_DOXYGEN=OFF). Run scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure.
|
|
||||||
```
|
|
||||||
|
|
||||||
Once set up:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/docs.sh # build docs for every module, prints where to open it
|
|
||||||
scripts/docs.sh hotreload_lib # build docs for just one module (docs_<target>)
|
|
||||||
```
|
|
||||||
```bat
|
|
||||||
scripts\docs.bat
|
|
||||||
scripts\docs.bat hotreload_lib
|
|
||||||
```
|
|
||||||
|
|
||||||
Output goes to `out/docs/<module>/html/index.html` (a sibling of
|
|
||||||
`out/build/`, not nested inside it -- see "Output paths" above), plus a
|
|
||||||
landing page at `out/docs/index.html` linking to every module.
|
|
||||||
Documentation is **not** built as part of a normal `scripts/build.sh` --
|
|
||||||
it's opt-in, the same way tests and examples are.
|
|
||||||
|
|
||||||
**Writing docs for a module** is just normal Doxygen comments in that
|
|
||||||
module's headers -- e.g. `source/HotReload/include/hot_reload/hot_reload.h`,
|
|
||||||
which is already written this way throughout.
|
|
||||||
|
|
||||||
Even with zero comments, a module's docs are still generated and useful --
|
|
||||||
file lists, function/class signatures, and (if [Graphviz](https://graphviz.org/)
|
|
||||||
is installed on your system -- this one enhancement, unlike Doxygen
|
|
||||||
itself, is still detected rather than pinned, since it's optional) call
|
|
||||||
graphs are produced regardless, since `DOXYGEN_EXTRACT_ALL` is on by
|
|
||||||
default. Comments just make it richer. Drop a `README.md` in a module's
|
|
||||||
own directory and it's automatically used as that module's Doxygen main
|
|
||||||
page -- `source/HotReload/README.md` already does this.
|
|
||||||
|
|
||||||
**Configuring it** -- nothing is required, but everything is overridable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Disable entirely (docs targets print a message instead of building):
|
|
||||||
cmake -S . -B out/build -DENABLE_DOXYGEN=OFF
|
|
||||||
|
|
||||||
# Pin a different Doxygen version (also update the two SHA256 hashes in
|
|
||||||
# cmake/DoxygenPin.cmake to match that release's assets -- a stale hash
|
|
||||||
# fails scripts/setup.sh loudly, on purpose, rather than silently
|
|
||||||
# accepting an unexpected file -- then re-run scripts/setup.sh):
|
|
||||||
cmake -S . -B out/build -DDOXYGEN_PINNED_VERSION=1.17.0
|
|
||||||
|
|
||||||
# Any Doxyfile tag can be set as a CMake variable, e.g. stricter output:
|
|
||||||
cmake -S . -B out/build -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES -DDOXYGEN_QUIET=NO
|
|
||||||
```
|
|
||||||
|
|
||||||
A single module can opt out with `NO_DOCS`:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
# some/internal/module/CMakeLists.txt
|
|
||||||
add_lib_module(internal_only TYPE STATIC NO_DOCS)
|
|
||||||
```
|
|
||||||
|
|
||||||
Vendored and fetched dependencies (`cmake/Dependencies.cmake`) are never
|
|
||||||
auto-documented -- same reasoning as their `compile_commands.json`
|
|
||||||
exclusion, it isn't your code.
|
|
||||||
|
|
||||||
## compile_commands.json / clang tooling
|
|
||||||
|
|
||||||
`CMAKE_EXPORT_COMPILE_COMMANDS` is on by default, and every build copies
|
|
||||||
`compile_commands.json` from the build directory to the project root so
|
|
||||||
clangd/clang-tidy/IWYU find it without extra configuration (CMake only
|
|
||||||
ever writes it into the build directory; most editors look for it at the
|
|
||||||
repo root). Vendored and fetched dependencies are excluded from it, so it
|
|
||||||
only reflects code this project actually owns. The project also builds
|
|
||||||
cleanly with clang directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cmake -G Ninja -S . -B out/build -DCMAKE_CXX_COMPILER=clang++ -DDIST_TARGET=linux-x64-clang
|
|
||||||
cmake --build out/build
|
|
||||||
```
|
|
||||||
|
|
||||||
Verified, not assumed, throughout this project's own development: every
|
|
||||||
module here (the library, both example demos, and the test suite) was
|
|
||||||
built and its tests/demos run to completion under both GCC and Clang.
|
|
||||||
|
|
||||||
## Code style & tooling
|
|
||||||
|
|
||||||
This project follows [`cpp-style-guide.md`](cpp-style-guide.md) (tabs,
|
|
||||||
Allman braces, `PascalCase` file names, `camelCase` functions/variables,
|
|
||||||
`m_`/`s_`/`g_` member/static/global prefixes, plus a distinct Part II
|
|
||||||
style -- `snake_case`, library-prefixed names -- for any C API boundary;
|
|
||||||
see the file itself for the full rules and rationale), enforced by the
|
|
||||||
tooling described in [`TOOLING.md`](TOOLING.md):
|
|
||||||
|
|
||||||
| File | Enforces |
|
|
||||||
|---|---|
|
|
||||||
| `.clang-format` | All formatting: tabs, Allman braces, pointer alignment, include order, line length |
|
|
||||||
| `.clang-tidy` | Naming conventions and a subset of language-feature rules, for this project's own C++ code |
|
|
||||||
| `c-api/.clang-tidy` | A naming override (`snake_case`) for a C API subtree -- see below for where this is actually applied |
|
|
||||||
| `.editorconfig` | Baseline tabs/whitespace for editors that don't run clang-format live |
|
|
||||||
| `.gitattributes` | Forces LF line endings for source files on every OS |
|
|
||||||
| `.githooks/pre-commit` | Runs clang-format + clang-tidy on staged files before each commit; warns and skips (never blocks) if a tool isn't installed |
|
|
||||||
|
|
||||||
**Fixing violations automatically**, instead of hunting them down by hand:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/refactor.sh # every file under source/ and c-api/ by default
|
|
||||||
scripts/refactor.sh source/HotReload # just one file or directory
|
|
||||||
```
|
|
||||||
```bat
|
|
||||||
scripts\refactor.bat
|
|
||||||
scripts\refactor.bat source\HotReload
|
|
||||||
```
|
|
||||||
|
|
||||||
Runs `clang-format -i` (always) and `clang-tidy --fix` (if
|
|
||||||
`compile_commands.json` exists -- build once first if it doesn't) across
|
|
||||||
the given scope. The pre-commit hook only *checks*; this is the "just fix
|
|
||||||
it" counterpart -- review the result with `git diff` afterward, same as
|
|
||||||
any auto-formatter.
|
|
||||||
|
|
||||||
**Be especially careful reviewing `git diff` on a file with more than one
|
|
||||||
architecture/OS branch** (`#if defined(__x86_64__) ... #elif ... #endif`,
|
|
||||||
several of which exist under `source/HotReload/src/` -- see
|
|
||||||
`Architecture.cpp`, `ModuleHandle.cpp`, `MemoryProtection.cpp`,
|
|
||||||
`SymbolTable.cpp`). `clang-tidy` only ever analyzes the ONE branch active
|
|
||||||
for whichever platform you're running it on -- it cannot see, and will
|
|
||||||
not touch, the source text inside the other, inactive `#elif` branches at
|
|
||||||
all. Verified directly while building this project: running `--fix` on
|
|
||||||
`Architecture.cpp` from this x86-64 Linux machine renamed a local
|
|
||||||
constant inside the x86-64 branch only, then updated a later line shared
|
|
||||||
by *all three* architecture branches to match the new name -- silently
|
|
||||||
breaking the build for the other two architectures, which still declared
|
|
||||||
the old name. Caught only by reviewing the diff and separately
|
|
||||||
cross-compiling every branch, which is exactly the review step this
|
|
||||||
paragraph is asking you to actually do on a file shaped like this, not
|
|
||||||
skip.
|
|
||||||
|
|
||||||
With no argument, only `source/` and `c-api/` are ever scanned -- `out/`,
|
|
||||||
`.cache/`, and `.git/` are never walked, so this can't accidentally spend
|
|
||||||
minutes reformatting/analyzing a fetched dependency's entire source tree
|
|
||||||
(this project doesn't currently have one under `out/build/_deps/`, but the
|
|
||||||
exclusion is unconditional, not conditional on one existing). Passing an
|
|
||||||
explicit scope takes it as given: `refactor.sh` still excludes
|
|
||||||
`out`/`.cache`/`.git` defensively if your scope happens to overlap with
|
|
||||||
them; `refactor.bat` does not (its Windows batch equivalent of that filter
|
|
||||||
turned out to be unreliable to verify, so it was removed rather than
|
|
||||||
shipped uncertain -- pick a scope that doesn't overlap with those
|
|
||||||
directories, which is the normal case anyway, e.g. `source\HotReload`).
|
|
||||||
|
|
||||||
**One-time setup per clone** to activate the hook -- `scripts/setup.sh` /
|
|
||||||
`setup.bat` does this for you (see "Setup" below) along with fetching the
|
|
||||||
pinned Doxygen and checking for clang-format/clang-tidy; the two commands
|
|
||||||
it runs for the hook specifically are:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x .githooks/pre-commit
|
|
||||||
git config core.hooksPath .githooks
|
|
||||||
```
|
|
||||||
|
|
||||||
**clang-tidy needs `compile_commands.json`** to resolve `#include`s --
|
|
||||||
this project already generates and copies it to the repo root on every
|
|
||||||
build (see above), so as long as you've built at least once, the hook's
|
|
||||||
clang-tidy check works with no extra setup.
|
|
||||||
|
|
||||||
**This project genuinely has a C API subtree**, unlike the generic
|
|
||||||
template this was adapted from: `source/HotReload/include/hot_reload/`
|
|
||||||
(the public header) and `source/HotReload/src/c-api/` (its `extern "C"`
|
|
||||||
implementation) each carry a copy of `c-api/.clang-tidy`, positioned at
|
|
||||||
the root of the subtree they apply to -- `clang-tidy` always uses the
|
|
||||||
closest `.clang-tidy` file up the directory tree, so files under either
|
|
||||||
directory pick up `snake_case` naming instead of the rest of this
|
|
||||||
project's `PascalCase`/`camelCase`, while `source/HotReload/src/`'s own
|
|
||||||
Part I (C++) files, one level up, keep inheriting this repo-root
|
|
||||||
`.clang-tidy`'s rules exactly as normal. See either copy's own header
|
|
||||||
comment for the full explanation of how clang-tidy's directory-scoped
|
|
||||||
config resolution makes that work.
|
|
||||||
|
|
||||||
## How module discovery works
|
|
||||||
|
|
||||||
The root `CMakeLists.txt` calls `_discover_module_subdirectories()` on
|
|
||||||
three roots, in this order:
|
|
||||||
|
|
||||||
| Root | What it holds |
|
|
||||||
|---|---|
|
|
||||||
| `source/*` | library AND app/executable modules, side by side |
|
|
||||||
| `source/tests/*` | test modules |
|
|
||||||
| `source/examples/*` | example modules |
|
|
||||||
|
|
||||||
(`source/tests` and `source/examples` aren't picked up by the first call
|
|
||||||
-- they have no `CMakeLists.txt` directly inside them, only their own
|
|
||||||
children do.)
|
|
||||||
|
|
||||||
Any immediate child directory that contains its own `CMakeLists.txt` gets
|
|
||||||
`add_subdirectory()`'d automatically (`vendor/` is not one of these roots
|
|
||||||
-- vendored dependencies are opted into explicitly by the modules that
|
|
||||||
need them, not auto-built by default). This uses `CONFIGURE_DEPENDS`, so
|
|
||||||
the *next build* reconfigures on its own after you add or remove a module
|
|
||||||
directory -- no manual `cmake` re-run needed. A module may `DEPENDS` on a
|
|
||||||
module discovered later in this list, or reference one via
|
|
||||||
`add_dependencies()`/a generator expression before that other module's
|
|
||||||
own `CMakeLists.txt` has even run -- order doesn't affect linking or
|
|
||||||
target existence (CMake resolves target names, and dependency edges to
|
|
||||||
not-yet-declared targets, across the whole project at generate time --
|
|
||||||
verified directly with an isolated, minimal reproduction while building
|
|
||||||
this project, not assumed).
|
|
||||||
|
|
||||||
A module directory can itself contain more than one CMake target when its
|
|
||||||
shape doesn't fit the "one target per module" default -- see
|
|
||||||
`source/examples/HotReloadDemo/PluginV1/CMakeLists.txt`'s own comment for
|
|
||||||
a real example (a hot-reloadable plugin needs to exist as two separate
|
|
||||||
builds of the same exported names, which `add_lib_module` doesn't assume)
|
|
||||||
and `source/examples/HotReloadDemo/CMakeLists.txt` for the small grouping
|
|
||||||
file that pattern needs, one level up, purely so discovery (which only
|
|
||||||
looks one level deep into `source/examples/`) finds it at all.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
CMake >= 3.25, a C++20 compiler (GCC/Clang/MSVC). **No network access is
|
|
||||||
needed to build this project** -- every module here is self-contained,
|
|
||||||
with no `add_dependency()`-fetched third-party library anywhere (see
|
|
||||||
"Three ways to bring in a dependency" above). Ninja and Doxygen are both
|
|
||||||
pinned and fetched automatically (see "Setup" below) if you want them,
|
|
||||||
not system dependencies, and `scripts/build.sh` warns rather than fails
|
|
||||||
if Ninja isn't available at all (see "Build" below).
|
|
||||||
[Graphviz](https://graphviz.org/) is optional for Doxygen call graphs, and
|
|
||||||
[clang-format](https://clang.llvm.org/docs/ClangFormat.html)/
|
|
||||||
[clang-tidy](https://clang.llvm.org/extra/clang-tidy/) are optional for
|
|
||||||
the pre-commit hook and `scripts/refactor.sh` (see "Code style & tooling"
|
|
||||||
above) -- both are detected, not pinned, since they're editor/workflow
|
|
||||||
tools rather than part of the build itself. `scripts/setup.sh` (below)
|
|
||||||
checks for both and prints install guidance if either is missing.
|
|
||||||
|
|
||||||
To actually try hot-reloading something (rather than just build the
|
|
||||||
library), you'll also want a way to rebuild one file while another
|
|
||||||
process keeps running -- covered in
|
|
||||||
[`source/examples/HotReloadLiveDemo/README.md`](source/examples/HotReloadLiveDemo/README.md).
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Optional, and separate from building (see "Requirements", "Build", and
|
|
||||||
"Documentation" above) -- run once per clone, or any time you want to
|
|
||||||
pick up a newer pinned version after bumping `NINJA_PINNED_VERSION` or
|
|
||||||
`DOXYGEN_PINNED_VERSION`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/setup.sh
|
|
||||||
```
|
|
||||||
```bat
|
|
||||||
scripts\setup.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
Does four things, each independent of the others:
|
|
||||||
|
|
||||||
1. Fetches the pinned Ninja (`cmake/FetchNinja.cmake`) -- used by
|
|
||||||
`scripts/build.sh`/`build.bat` if present (see "Build" below); not
|
|
||||||
required, a system Ninja works fine too.
|
|
||||||
2. Fetches the pinned Doxygen (`cmake/FetchDoxygen.cmake`) -- needed for
|
|
||||||
`scripts/docs.sh`, not for building the project.
|
|
||||||
3. Checks for `clang-format`/`clang-tidy` on `PATH` and prints
|
|
||||||
OS-appropriate install guidance if either is missing (never
|
|
||||||
auto-installs -- that would need assumptions about your package
|
|
||||||
manager and elevated privileges this script shouldn't assume it has).
|
|
||||||
4. Activates the pre-commit hook for this clone, if it's a git repository
|
|
||||||
(`git config core.hooksPath .githooks`).
|
|
||||||
|
|
||||||
Safe to re-run any time -- each step is a no-op if there's nothing to do
|
|
||||||
(e.g. the pinned Ninja/Doxygen are already cached).
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/build.sh # Release (default), library + tests only if BUILD_TESTS was set before
|
|
||||||
scripts/build.sh Release -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON # + the automated tests + both demos
|
|
||||||
```
|
```
|
||||||
```bat
|
```bat
|
||||||
scripts\build.bat
|
scripts\build.bat
|
||||||
scripts\build.bat Release -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON
|
scripts\build.bat Release -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
|
||||||
```
|
```
|
||||||
|
|
||||||
`BUILD_TESTS` and `BUILD_EXAMPLES` both default to `OFF` (see
|
This produces `hotreload_lib`, a `STATIC` library with a plain C API
|
||||||
`CMakeLists.txt`) -- plain `scripts/build.sh` with no extra arguments
|
(`hot_reload.h`) over a C++ implementation. Link your host application
|
||||||
builds only `hotreload_lib` itself. Anything after the build type is
|
against it (`target_link_libraries(your_app PRIVATE hotreload_lib)` if
|
||||||
passed straight through to `cmake`'s configure step (tested directly:
|
you're using CMake; see [`source/HotReload/CMakeLists.txt`](source/HotReload/CMakeLists.txt)
|
||||||
`scripts/build.sh Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON` builds the
|
for what it links on each platform if you're not).
|
||||||
library, both demos, and the test suite together), and CMake caches `-D`
|
|
||||||
options across reconfigures, so passing them once and calling
|
|
||||||
`scripts/build.sh` plain afterward for incremental builds keeps them set
|
|
||||||
-- no need to repeat them on every call.
|
|
||||||
|
|
||||||
Picks a generator in this order, each falling back to the next:
|
### See it work — a scripted, deterministic demo
|
||||||
|
|
||||||
1. **A pinned Ninja**, if `scripts/setup.sh`/`setup.bat` has fetched one --
|
```sh
|
||||||
passed to CMake explicitly via `-DCMAKE_MAKE_PROGRAM=...` so it's used
|
./out/build/bin/examples/hotreload_demo_host
|
||||||
even if a different Ninja also happens to be on `PATH` (same "always
|
|
||||||
the pinned copy" preference as Doxygen -- see "Documentation" above).
|
|
||||||
2. **A system Ninja**, if one is on `PATH` and no pinned copy was found.
|
|
||||||
3. **CMake's own default generator** for the platform, if neither Ninja is
|
|
||||||
available anywhere -- `scripts/build.sh`/`build.bat` print a warning
|
|
||||||
and continue rather than fail. (`CMakeLists.txt` separately warns too,
|
|
||||||
once CMake reports which generator it actually picked -- this is the
|
|
||||||
pre-existing "This project targets Ninja" message, unrelated to this
|
|
||||||
script.)
|
|
||||||
|
|
||||||
Verified concretely, not assumed: with neither a pinned nor a system Ninja
|
|
||||||
present anywhere, `scripts/build.sh` still successfully configures, builds,
|
|
||||||
and produces working binaries -- it just uses whatever CMake's default
|
|
||||||
generator is for the platform (Unix Makefiles on Linux, typically NMake or
|
|
||||||
a Visual Studio generator on Windows) instead.
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/run.sh hotreload_demo_host
|
|
||||||
scripts/run.sh hotreload_live_demo_host
|
|
||||||
scripts/run.sh # no args: lists available targets
|
|
||||||
```
|
|
||||||
```bat
|
|
||||||
scripts\run.bat hotreload_demo_host
|
|
||||||
scripts\run.bat hotreload_live_demo_host
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`run.sh`/`run.bat` resolve the CMake **target id** (not the binary's
|
Loads one plugin, calls into it, hot-reloads a second build of it, and
|
||||||
`OUTPUT_NAME`) to the actual binary via a manifest CMake generates at
|
calls the *same, never-re-resolved* function pointers again — proving
|
||||||
configure time, so these scripts never need editing when a module is
|
every guarantee above in one run. Open
|
||||||
added, removed, or renamed. Requires `-DBUILD_EXAMPLES=ON` to have been
|
[`source/examples/HotReloadDemo/PluginV1/src/Plugin.cpp`](source/examples/HotReloadDemo/PluginV1/src/Plugin.cpp)
|
||||||
set at configure time for either of the two hot-reload targets above,
|
and its `PluginV2` counterpart side by side against the output.
|
||||||
since they live under `source/examples/`.
|
|
||||||
|
|
||||||
## Install
|
### Try it yourself — a persistent, interactive demo
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
scripts/install.sh
|
./out/build/bin/examples/hotreload_live_demo_host # leave this running
|
||||||
```
|
cmake --build out/build --target hotreload_live_plugin # in another terminal, or:
|
||||||
```bat
|
source/examples/HotReloadLiveDemo/watch.sh out/build # rebuild automatically on save
|
||||||
scripts\install.bat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Always use the install script (or pass `--component hotreload` yourself if
|
Edit [`source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp`](source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp)
|
||||||
calling `cmake --install` directly). A vendored/fetched dependency's own
|
— its own comment walks through several concrete things worth trying —
|
||||||
`CMakeLists.txt` usually has `install()` rules of its own with no
|
and watch the *running* process pick up your change within about a
|
||||||
component tag; installing without `--component` would also run those,
|
second, with no restart. See
|
||||||
typically dumping files under `CMAKE_INSTALL_PREFIX` (e.g. `/usr/local`).
|
[`source/examples/HotReloadLiveDemo/README.md`](source/examples/HotReloadLiveDemo/README.md)
|
||||||
Tagging this project's own `install(TARGETS ...)` calls with `COMPONENT
|
for the full walkthrough, including two Windows-specific gotchas
|
||||||
hotreload` and always filtering on it is what keeps `cmake --install`
|
(`Access is denied` on "Rebuild", and why leftover `.hotreload.N` files
|
||||||
scoped to just this project's own artifacts -- currently just
|
next to your plugin are expected, not a leak) that are easy to mistake
|
||||||
`hotreload_lib` itself, since examples and tests are opt-in, dev-time
|
for bugs the first time you hit them.
|
||||||
targets rather than something you'd install.
|
|
||||||
|
|
||||||
## Adding a module
|
### Use it in your own module
|
||||||
|
|
||||||
1. Create a directory under the right root (see the discovery table above).
|
1. `extern "C"` isn't required — a real C++ mangled name works too — but
|
||||||
2. Add a `CMakeLists.txt` inside it with one `add_lib_module` /
|
it's the simplest choice for anything you want to look up by a plain,
|
||||||
`add_exe_module` / `add_test_module` / `add_example_module` call, plus
|
human-typed name via `hot_reload_get_symbol()`.
|
||||||
any `add_vendor_*`/`add_dependency` calls it needs.
|
2. Call `hot_reload_enable_patchable_functions(<target>)`
|
||||||
3. Add your `.cpp`/`.h` files inside that same directory.
|
(`cmake/HotReload.cmake`) on your plugin's CMake target, so every
|
||||||
4. Rebuild (`scripts/build.sh`) -- no other file needs touching.
|
exported function has guaranteed room for a jump.
|
||||||
|
3. Optionally call `hot_reload_restrict_exports(<target>)` 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 `hot_reload_load()` it once at startup and
|
||||||
|
`hot_reload_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.
|
||||||
|
|
||||||
```cmake
|
## Limitations
|
||||||
# source/MyTool/CMakeLists.txt
|
|
||||||
add_exe_module(my_tool_exe OUTPUT_NAME "my-tool" DEPENDS hotreload_lib)
|
|
||||||
```
|
|
||||||
|
|
||||||
`scripts/run.sh my_tool_exe` picks it up automatically once it's built,
|
Two things this library fundamentally cannot detect, because a symbol
|
||||||
and `scripts/docs.sh my_tool_exe` documents it automatically too -- both
|
table doesn't carry the information needed to:
|
||||||
with zero edits anywhere outside `source/MyTool/`.
|
|
||||||
|
- **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, full demo suite and test suite 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 |
|
||||||
|
| 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 |
|
||||||
|
| AddressSanitizer + UndefinedBehaviorSanitizer, and Valgrind with self-modifying-code checking enabled | Full test suite, 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 |
|
||||||
|
| 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:**
|
||||||
|
32-bit ARM (its ARM/Thumb instruction-mode ambiguity fails silently if
|
||||||
|
gotten wrong, and there was no way to verify it on real or emulated
|
||||||
|
hardware) and macOS/Mach-O (never requested, and would need a third image
|
||||||
|
format reader alongside the existing ELF and PE ones). Both are
|
||||||
|
documented, bounded gaps — see `source/HotReload/README.md`'s "Scope"
|
||||||
|
section for the reasoning behind each, and the extension point if you
|
||||||
|
need either.
|
||||||
+26
-14
@@ -3,12 +3,18 @@
|
|||||||
Patches a running process's own code in place so that a new build of a
|
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
|
function takes effect immediately -- no restart, and every existing
|
||||||
caller (including a raw C function pointer someone cached ten minutes
|
caller (including a raw C function pointer someone cached ten minutes
|
||||||
ago) picks up the change automatically. `hot_reload.h` is the API;
|
ago) picks up the change automatically. This is the full technical
|
||||||
`source/examples/HotReloadDemo` is a full worked example; this file is
|
reference; see this project's top-level `README.md` for a quicker
|
||||||
the overview `hot_reload.h`'s own doc comments don't have room for,
|
overview, feature summary, and build/run instructions if you haven't
|
||||||
especially the parts you should know are *not* guaranteed.
|
already.
|
||||||
|
|
||||||
If you just want to see it run:
|
`hot_reload.h` is the API (also fully Doxygen-documented -- see the
|
||||||
|
top-level README's "Documentation" section for how to build that
|
||||||
|
locally); `source/examples/HotReloadDemo` is a full worked example;
|
||||||
|
this file is the overview `hot_reload.h`'s own doc comments don't have
|
||||||
|
room for, especially the parts you should know are *not* guaranteed.
|
||||||
|
|
||||||
|
To see it run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cmake -S . -B out -DBUILD_EXAMPLES=ON
|
cmake -S . -B out -DBUILD_EXAMPLES=ON
|
||||||
@@ -367,15 +373,21 @@ that file's comment for the full reasoning.
|
|||||||
|
|
||||||
## Known, accepted gaps
|
## Known, accepted gaps
|
||||||
|
|
||||||
- **A `.so` with its section header table deliberately stripped**
|
- **A `.so` with its section header table deliberately stripped,
|
||||||
reads back as exporting no functions at all (empty, not a crash) --
|
truncated, or otherwise corrupted** reads back as exporting no
|
||||||
`SymbolTable.cpp`'s ELF reader uses section headers to find
|
functions at all (empty, not a crash) -- `SymbolTable.cpp`'s ELF
|
||||||
`.dynsym`/`.dynstr`, which is simpler and easier to get right than
|
reader uses section headers to find `.dynsym`/`.dynstr`, which is
|
||||||
walking `.dynamic`'s `DT_*` tags the way the dynamic linker itself
|
simpler and easier to get right than walking `.dynamic`'s `DT_*` tags
|
||||||
does, at the cost of this one edge case. Stripping section headers
|
the way the dynamic linker itself does, at the cost of this one edge
|
||||||
from a `.so` you are actively hot-reloading is an unusual, separate
|
case for a deliberately-stripped file. Every offset, size, and index
|
||||||
step from an ordinary `strip` and essentially never happens by
|
this reader trusts is bounds-checked against the file's actual size
|
||||||
accident.
|
before use -- confirmed directly by fuzzing the section header table
|
||||||
|
of a real plugin build: 1500 randomly corrupted variants (each still
|
||||||
|
a file the OS loader itself accepts via `dlopen()`), zero crashes. An
|
||||||
|
earlier version of this same reader, without those checks, crashed on
|
||||||
|
69 of an initial 300. Stripping section headers from a `.so` you are
|
||||||
|
actively hot-reloading is, separately, an unusual step essentially
|
||||||
|
never taken by accident.
|
||||||
- **PE exports have no per-symbol size.** Unlike ELF's `st_size`,
|
- **PE exports have no per-symbol size.** Unlike ELF's `st_size`,
|
||||||
which backs a real safety check, the Windows path estimates 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
|
from the gap to the next exported function and can only ever prove
|
||||||
|
|||||||
@@ -1,79 +1,126 @@
|
|||||||
/* hot_reload.h
|
/**
|
||||||
|
* @file hot_reload.h
|
||||||
|
* @brief Public C API for the hot-reload library.
|
||||||
*
|
*
|
||||||
* Public C API for the hot-reload library -- see cpp-style-guide.md
|
* See @c cpp-style-guide.md Part II (C API Style) for the naming/formatting
|
||||||
* Part II (C API Style) for the naming/formatting rules this header
|
* rules this header follows, and section 11 ("Runtime Loading") for why
|
||||||
* follows, and section 11 ("Runtime Loading") for why this boundary is
|
* this boundary is plain C in the first place: anything crossing a
|
||||||
* plain C in the first place: anything crossing a dlopen()/LoadLibrary()
|
* `dlopen()`/`LoadLibrary()` boundary must have C linkage, so this is
|
||||||
* boundary must have C linkage, so this is exactly the kind of surface
|
* exactly the kind of surface that section calls out.
|
||||||
* that section calls out.
|
|
||||||
*
|
*
|
||||||
* WHAT THIS LIBRARY DOES
|
* @section hotreload_what_it_does 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
|
* hot_reload_load() loads a shared library (`.so`/`.dll`) under a logical
|
||||||
* --------------------------------------------
|
* name. hot_reload_reload() loads a *new* build of that same shared
|
||||||
* It never patches anything that isn't executable code. A symbol is
|
* library and, for every function the two builds have in common, rewrites
|
||||||
* only ever a patch candidate if the loader's own symbol table marks
|
* the first few bytes of the old function in memory into a jump to the
|
||||||
* it as a function (ELF STT_FUNC / a PE export whose address falls
|
* new function -- so every existing call site (anyone who already
|
||||||
* inside an executable section) -- see the design note in
|
* resolved the old function's address, including via a plain C function
|
||||||
* SymbolTable.hpp for exactly how that's determined. Global variables,
|
* pointer) transparently starts running the new code, without restarting
|
||||||
* class/struct instances, vtables, and anything else classified as
|
* the process.
|
||||||
* 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
|
* @section hotreload_what_it_wont What this library deliberately does not do
|
||||||
|
*
|
||||||
|
* It never patches anything that isn't executable code. A symbol is only
|
||||||
|
* ever a patch candidate if the loader's own symbol table marks it as a
|
||||||
|
* function (ELF `STT_FUNC` / a PE export whose address falls inside an
|
||||||
|
* executable section) -- see the design note at the top of
|
||||||
|
* `SymbolTable.cpp` (and detail::ExportedSymbol::isFunction) for exactly
|
||||||
|
* how that's determined. Global variables, class/struct
|
||||||
|
* instances, vtables, and anything else classified as data are always
|
||||||
|
* left alone, on both the old and the new module -- there is no code path
|
||||||
|
* in this library that ever writes to a data symbol. This is a mechanical
|
||||||
|
* guarantee from how symbols are classified, not a naming convention you
|
||||||
|
* opt into.
|
||||||
|
*
|
||||||
|
* It also cannot verify that a hot-reloadable function's *signature*
|
||||||
* hasn't changed. Patching only ever redirects control flow at a fixed
|
* 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
|
* 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
|
* `void tick(int)` became `void tick(int, int)` between builds. If a
|
||||||
* function's signature changes, give it a new exported name (so the
|
* function's signature changes, give it a new exported name (so the old
|
||||||
* old symbol is treated as removed and the new one as added -- see
|
* symbol is treated as removed and the new one as added -- see
|
||||||
* HOT_RELOAD_SYMBOL_REMOVED below) rather than reusing the old name.
|
* ::HOT_RELOAD_SYMBOL_REMOVED) rather than reusing the old name. See the
|
||||||
* See the top-level README for the full list of hazards this library
|
* "Limitations" section of the top-level README (and "What this cannot
|
||||||
* cannot detect for you.
|
* detect" in `source/HotReload/README.md`) for the full list of hazards
|
||||||
|
* this library cannot detect for you.
|
||||||
|
*
|
||||||
|
* @section hotreload_requirements Requirements on a hot-reloadable module
|
||||||
*
|
*
|
||||||
* REQUIREMENTS ON A HOT-RELOADABLE MODULE
|
|
||||||
* ------------------------------------------
|
|
||||||
* Matching an old build's function up with the new build's requires a
|
* Matching an old build's function up with the new build's requires a
|
||||||
* name that is stable across recompiles. extern "C" linkage -- see
|
* name that is stable across recompiles. `extern "C"` linkage -- see
|
||||||
* cpp-style-guide.md section 11 -- is the simplest way to get one: no
|
* `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
|
* mangling at all, so the name never depends on the compiler, its
|
||||||
* version, or the function's signature.
|
* version, or the function's signature.
|
||||||
*
|
*
|
||||||
* A real, unmodified C++ mangled name works too, and needs no special
|
* A real, unmodified C++ mangled name works too, and needs no special
|
||||||
* handling by this library -- classification only ever looks at a
|
* handling by this library -- classification only ever looks at a
|
||||||
* symbol's type and section, never at what its name looks like, so an
|
* symbol's type and section, never at what its name looks like, so an
|
||||||
* ordinary out-of-line method, a virtual method (redirected wherever
|
* ordinary out-of-line method, a virtual method (redirected wherever it's
|
||||||
* it's reached from -- directly or through a vtable slot, since both
|
* reached from -- directly or through a vtable slot, since both hold the
|
||||||
* hold the same address), and an operator overload all patch exactly
|
* same address), and an operator overload all patch exactly like any
|
||||||
* like any extern "C" function. What extern "C" actually buys you is
|
* `extern "C"` function. What `extern "C"` actually buys you is a name
|
||||||
* a name you can type by hand into hot_reload_get_symbol() and a
|
* you can type by hand into hot_reload_get_symbol() and a guarantee that
|
||||||
* guarantee that it won't change if you rebuild with a different
|
* it won't change if you rebuild with a different compiler; a mangled
|
||||||
* compiler; a mangled name gives up both of those, only stays stable
|
* name gives up both of those, only stays stable within one ABI family
|
||||||
* within one ABI family (GCC and Clang share the Itanium C++ ABI;
|
* (GCC and Clang share the Itanium C++ ABI; MSVC does not), and -- the
|
||||||
* MSVC does not), and -- the one hazard that has nothing to do with
|
* one hazard that has nothing to do with this library and everything to
|
||||||
* this library and everything to do with what "hot-reloadable" can
|
* do with what "hot-reloadable" can even mean -- only exists to patch at
|
||||||
* even mean -- only exists to patch at all if the compiler actually
|
* all if the compiler actually emitted an out-of-line body for it, which
|
||||||
* emitted an out-of-line body for it, which an implicitly-inline
|
* an implicitly-inline method defined in a class body, or a template
|
||||||
* method defined in a class body, or a template instantiated at the
|
* instantiated at the call site, generally will not. See the "Hot
|
||||||
* call site, generally will not. See the top-level README's "Hot
|
* reloading C++ symbols" section of `source/HotReload/README.md` for the
|
||||||
* reloading C++ symbols" section for the full picture, including how
|
* full picture, including how to get a readable name for logging with
|
||||||
* to get a readable name for logging with hot_reload_demangle().
|
* hot_reload_demangle().
|
||||||
|
*
|
||||||
|
* Every function the module exports needs room at its entry point for the
|
||||||
|
* jump this library writes over it. Compile the module with
|
||||||
|
* `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to
|
||||||
|
* guarantee that; without it, a function too small to hold the jump is
|
||||||
|
* skipped and reported as ::HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than
|
||||||
|
* patched. Also avoid identical-code-folding linker options (MSVC
|
||||||
|
* `/OPT:ICF`, `--icf` in gold/lld) for the module: two different exported
|
||||||
|
* functions folded into one address can only be patched to one target.
|
||||||
|
*
|
||||||
|
* @section hotreload_lifetime What carries over a reload -- and what does not
|
||||||
|
*
|
||||||
|
* Only *code* is redirected. Every build of the module is a separate
|
||||||
|
* mapping with its own copy of everything else, so:
|
||||||
|
* - Global and `static` variables are not carried over. Code that runs
|
||||||
|
* after a reload uses the *new* build's copies, freshly initialised;
|
||||||
|
* keep state you need to survive in memory the host owns and pass it in.
|
||||||
|
* - Static constructors of the new build run when it is loaded (on every
|
||||||
|
* reload); static destructors of every version run when the context is
|
||||||
|
* destroyed.
|
||||||
|
* - Every version stays loaded until hot_reload_context_destroy(). Old
|
||||||
|
* entry points keep working (they are now jumps), one extra hop per
|
||||||
|
* reload; destroying the context unloads them all, so no pointer into
|
||||||
|
* any of the modules may be used afterwards.
|
||||||
|
*
|
||||||
|
* @section hotreload_threads Threading
|
||||||
|
*
|
||||||
|
* A context is not thread-safe: serialize all calls that use one context.
|
||||||
|
* Call hot_reload_reload() from a quiescent point -- one where no thread is
|
||||||
|
* executing inside the module's functions -- since a function that is being
|
||||||
|
* executed while its entry bytes are rewritten cannot be protected.
|
||||||
*/
|
*/
|
||||||
#ifndef HOT_RELOAD_H
|
#ifndef HOT_RELOAD_H
|
||||||
#define HOT_RELOAD_H
|
#define HOT_RELOAD_H
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def HOT_RELOAD_API
|
||||||
|
* @brief Marks every function of this library's own C API.
|
||||||
|
*
|
||||||
|
* Expands to nothing in the default configuration, where the library is linked statically.
|
||||||
|
* Define `HOT_RELOAD_BUILD_SHARED` while building the library as a shared library, and
|
||||||
|
* `HOT_RELOAD_USE_SHARED` while compiling code that uses such a build, to get
|
||||||
|
* `__declspec(dllexport)`/`__declspec(dllimport)` on Windows or default symbol visibility
|
||||||
|
* elsewhere.
|
||||||
|
*
|
||||||
|
* Not to be confused with ::HOT_RELOAD_EXPORT, which marks symbols in a hot-reloadable
|
||||||
|
* *module* rather than in this library.
|
||||||
|
*/
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#ifdef HOT_RELOAD_BUILD_SHARED
|
#ifdef HOT_RELOAD_BUILD_SHARED
|
||||||
#define HOT_RELOAD_API __declspec(dllexport)
|
#define HOT_RELOAD_API __declspec(dllexport)
|
||||||
@@ -90,23 +137,29 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Marks a single declaration for export under
|
/**
|
||||||
* hot_reload_restrict_exports() (cmake/HotReload.cmake) without
|
* @def HOT_RELOAD_EXPORT
|
||||||
* otherwise changing its linkage: pair with extern "C" yourself for a
|
* @brief Marks a single declaration in a hot-reloadable *module* for export
|
||||||
* free function you want a stable, human-typable name for (see this
|
* under `hot_reload_restrict_exports()` (`cmake/HotReload.cmake`).
|
||||||
* file's own comment on when you'd want that) -- extern "C" first,
|
|
||||||
* then this macro, then the return type, e.g.
|
|
||||||
* `extern "C" HOT_RELOAD_EXPORT int my_function(...)`, which is the
|
|
||||||
* one ordering GCC/Clang accept without a warning here. Leave the
|
|
||||||
* extern "C" off to keep a C++ class or method's real mangled name,
|
|
||||||
* e.g. a virtual method a host only ever reaches through a vtable
|
|
||||||
* slot (see the top-level README's "Hot reloading C++ symbols"
|
|
||||||
* section).
|
|
||||||
*
|
*
|
||||||
* hot_reload_restrict_exports() is a narrower export surface, not a
|
* This is for the plugins you hot-reload, not for this library's own API
|
||||||
* quieter one: see the README's "Why do std::string/std::vector
|
* above (::HOT_RELOAD_API). It does not otherwise change a declaration's
|
||||||
* symbols show up as PATCHED?" section for why it does not, and
|
* linkage: pair it with `extern "C"` yourself for a free function you want
|
||||||
* structurally cannot, suppress those specifically. */
|
* a stable, human-typable name for (see @ref hotreload_requirements for
|
||||||
|
* when you'd want that) -- `extern "C"` first, then this macro, then the
|
||||||
|
* return type, e.g. `extern "C" HOT_RELOAD_EXPORT int my_function(...)`,
|
||||||
|
* which is the one ordering GCC/Clang accept without a warning here. Leave
|
||||||
|
* the `extern "C"` off to keep a C++ class or method's real mangled name,
|
||||||
|
* e.g. a virtual method a host only ever reaches through a vtable slot
|
||||||
|
* (see the "Hot reloading C++ symbols" section of
|
||||||
|
* `source/HotReload/README.md`).
|
||||||
|
*
|
||||||
|
* @note `hot_reload_restrict_exports()` is a *narrower* export surface,
|
||||||
|
* not a *quieter* one: see the "Why do std::string/std::vector
|
||||||
|
* symbols show up as PATCHED?" section of `source/HotReload/README.md`
|
||||||
|
* for why it does not, and structurally cannot, suppress those
|
||||||
|
* specifically.
|
||||||
|
*/
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define HOT_RELOAD_EXPORT __declspec(dllexport)
|
#define HOT_RELOAD_EXPORT __declspec(dllexport)
|
||||||
#else
|
#else
|
||||||
@@ -118,196 +171,394 @@ extern "C"
|
|||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Opaque -- see cpp-style-guide.md section 16 ("Do: use opaque
|
/**
|
||||||
* pointers ... to hide implementation details"). The real definition
|
* @defgroup hotreload_types Types and callbacks
|
||||||
* is the C++ class hot_reload::HotReloadContext (HotReloadContext.hpp),
|
* @brief The status codes, event/option structs, and callback signatures
|
||||||
* never visible from this header. */
|
* used throughout the rest of this API.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opaque handle to a hot-reload context.
|
||||||
|
*
|
||||||
|
* See `cpp-style-guide.md` section 16 ("Do: use opaque pointers ... to
|
||||||
|
* hide implementation details"). The real definition is the C++ class
|
||||||
|
* `hot_reload::HotReloadContext` (`HotReloadContext.hpp`), never visible
|
||||||
|
* from this header.
|
||||||
|
*/
|
||||||
typedef struct HotReloadContext HotReloadContext;
|
typedef struct HotReloadContext HotReloadContext;
|
||||||
|
|
||||||
/* Every fallible function returns one of these -- see section 16
|
/**
|
||||||
* ("Do: return an integer error code / status enum from every
|
* @brief Result of any fallible call in this API.
|
||||||
* fallible function"). */
|
*
|
||||||
|
* See `cpp-style-guide.md` section 16 ("Do: return an integer error
|
||||||
|
* code / status enum from every fallible function"). Render one of these
|
||||||
|
* as text with hot_reload_status_string(); for a fuller, platform-specific
|
||||||
|
* explanation of a ::HOT_RELOAD_ERROR_LOAD_FAILED, see
|
||||||
|
* hot_reload_last_error().
|
||||||
|
*/
|
||||||
typedef enum HotReloadStatus
|
typedef enum HotReloadStatus
|
||||||
{
|
{
|
||||||
|
/** Success. */
|
||||||
HOT_RELOAD_OK = 0,
|
HOT_RELOAD_OK = 0,
|
||||||
|
|
||||||
/* ctx, module_name, or path/new_path was NULL, or module_name
|
/**
|
||||||
* was not found for hot_reload_reload / hot_reload_get_symbol. */
|
* A required argument (@c ctx, @c module_name, or @c path / @c new_path)
|
||||||
|
* was `NULL`, or hot_reload_load() was called for a @c module_name that is
|
||||||
|
* already loaded -- call hot_reload_reload() for that name instead.
|
||||||
|
*/
|
||||||
HOT_RELOAD_ERROR_INVALID_ARGUMENT,
|
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(),
|
* `dlopen()`/`LoadLibrary()` (or the equivalent parse of the file on
|
||||||
* this is non-destructive: the previously loaded version is left
|
* disk) failed for the given path.
|
||||||
* running untouched, exactly as if reload had not been called --
|
*
|
||||||
* see the file comment above HotReloadContext::reload(). Call
|
* On hot_reload_reload(), this is non-destructive: the previously
|
||||||
* hot_reload_last_error() for the platform's own description of
|
* loaded version is left running untouched, exactly as if reload had
|
||||||
* why -- this one status code covers everything from a missing
|
* not been called. Call hot_reload_last_error() for the
|
||||||
* file to a genuinely corrupt image to (a real case, found while
|
* platform's own description of why -- this one status code covers
|
||||||
* building this feature) a shared library with an unresolved
|
* everything from a missing file to a genuinely corrupt image to (a
|
||||||
* symbol reference, and those need very different fixes. */
|
* real case, found while building this feature) a shared library
|
||||||
|
* with an unresolved symbol reference, and those need very different
|
||||||
|
* fixes.
|
||||||
|
*
|
||||||
|
* It is also reported when the file could not be copied to its staging
|
||||||
|
* name (see hot_reload_load()): the source is unreadable, or the directory
|
||||||
|
* it lives in is not writable.
|
||||||
|
*/
|
||||||
HOT_RELOAD_ERROR_LOAD_FAILED,
|
HOT_RELOAD_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
|
* hot_reload_reload() was called for a @c module_name that was never
|
||||||
* diff against. Call hot_reload_load() once, up front. */
|
* 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,
|
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).
|
* This process's CPU architecture has no patch backend compiled in
|
||||||
* Nothing was patched; hot_reload_load() still succeeded, you
|
* (see `Architecture.hpp` -- currently x86, x86-64, and ARM64).
|
||||||
* just cannot hot_reload_reload() on this target. */
|
* Nothing was patched; hot_reload_load() still succeeded, you just
|
||||||
|
* cannot hot_reload_reload() on this target.
|
||||||
|
*/
|
||||||
HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE
|
HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE
|
||||||
} HotReloadStatus;
|
} HotReloadStatus;
|
||||||
|
|
||||||
/* Reported once per matched/unmatched symbol during hot_reload_reload()
|
/**
|
||||||
* via HotReloadOptions::on_symbol_event, so the host application can
|
* @brief What happened to one symbol during a hot_reload_reload() call.
|
||||||
* log (or assert on, in a test) exactly what happened -- nothing here
|
*
|
||||||
* is silent. */
|
* Reported once per matched/unmatched symbol via
|
||||||
|
* HotReloadOptions::on_symbol_event, so the host application can log (or
|
||||||
|
* assert on, in a test) exactly what happened -- nothing here is silent.
|
||||||
|
*/
|
||||||
typedef enum HotReloadSymbolEvent
|
typedef enum HotReloadSymbolEvent
|
||||||
{
|
{
|
||||||
/* Existed in both builds and was classified as code in both --
|
/**
|
||||||
* the old function's entry now jumps to the new one. */
|
* Existed in both builds and was classified as code in both -- the
|
||||||
|
* old function's entry now jumps to the new one.
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_PATCHED,
|
HOT_RELOAD_SYMBOL_PATCHED,
|
||||||
|
|
||||||
/* Exists in the new build only. Nothing to patch -- it becomes
|
/**
|
||||||
* reachable the normal way, e.g. via hot_reload_get_symbol(). */
|
* Exists in the new build only. Nothing to patch -- it becomes
|
||||||
|
* reachable the normal way, e.g. via hot_reload_get_symbol().
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_ADDED,
|
HOT_RELOAD_SYMBOL_ADDED,
|
||||||
|
|
||||||
/* Existed in the old build, missing from the new one. Per
|
/**
|
||||||
* cpp-style-guide.md's spirit of never leaving a caller with an
|
* Existed in the old build, missing from the new one.
|
||||||
|
*
|
||||||
|
* Per `cpp-style-guide.md`'s spirit of never leaving a caller with an
|
||||||
* ambiguous, half-updated state: the old implementation is left
|
* ambiguous, half-updated state: the old implementation is left
|
||||||
* running as-is unless HotReloadOptions::on_deleted_symbol
|
* running as-is unless HotReloadOptions::on_deleted_symbol supplies a
|
||||||
* supplies a stub to redirect to instead -- see that field. */
|
* stub to redirect to instead -- see that field.
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_REMOVED,
|
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 --
|
* Present in both builds by name, but the symbol table classifies it
|
||||||
* e.g. an accidentally-exported global. Never touched, in either
|
* as data (or something else non-function) in either build -- e.g.
|
||||||
* direction. This is the mechanical half of the "only executable
|
* an accidentally-exported global.
|
||||||
* code changes" guarantee described at the top of this file. */
|
*
|
||||||
|
* Never touched, in either direction. This is the mechanical half of
|
||||||
|
* the "only executable code changes" guarantee described in
|
||||||
|
* @ref hotreload_what_it_wont.
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE,
|
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
|
* Classified as a function in both builds, but the old function's
|
||||||
* platform's jump sequence needs to write -- patching it would
|
* compiled size is smaller than the number of bytes this platform's
|
||||||
* overwrite whatever comes after it in memory. Skipped rather
|
* jump sequence needs to write -- patching it would overwrite
|
||||||
* than guessed; see hot_reload_enable_patchable_functions() in
|
* whatever comes after it in memory.
|
||||||
* cmake/HotReload.cmake for how to stop this from happening. */
|
*
|
||||||
|
* Skipped rather than guessed. Compile the module with
|
||||||
|
* `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to
|
||||||
|
* stop this from happening; `hot_reload_enable_patchable_functions()` in
|
||||||
|
* `cmake/HotReload.cmake` applies whichever one your compiler needs.
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL,
|
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 /
|
* Classified as a function in both builds, correctly sized, but the
|
||||||
* VirtualProtect failed -- e.g. a hardened environment that denies
|
* OS refused to make its page temporarily writable (`mprotect` /
|
||||||
* W^X transitions). Rare in a normal developer setup; reported
|
* `VirtualProtect` failed -- e.g. a hardened environment that denies
|
||||||
* rather than silently ignored either way. */
|
* W^X transitions).
|
||||||
|
*
|
||||||
|
* Rare in a normal developer setup; reported rather than silently
|
||||||
|
* ignored either way.
|
||||||
|
*/
|
||||||
HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE
|
HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE
|
||||||
} HotReloadSymbolEvent;
|
} HotReloadSymbolEvent;
|
||||||
|
|
||||||
/* One event report, passed to HotReloadOptions::on_symbol_event.
|
/**
|
||||||
* old_address/new_address are NULL when not applicable (e.g.
|
* @brief One symbol's outcome, passed to HotReloadOptions::on_symbol_event.
|
||||||
* old_address is NULL for HOT_RELOAD_SYMBOL_ADDED). */
|
*/
|
||||||
typedef struct HotReloadSymbolEventInfo
|
typedef struct HotReloadSymbolEventInfo
|
||||||
{
|
{
|
||||||
const char* name;
|
const char* name; /**< The symbol's real name (mangled, if it's a C++ symbol -- see hot_reload_demangle()). */
|
||||||
HotReloadSymbolEvent kind;
|
HotReloadSymbolEvent kind; /**< What happened to it -- see ::HotReloadSymbolEvent. */
|
||||||
void* old_address;
|
void* old_address; /**< The symbol's address in the previous build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_ADDED). */
|
||||||
void* new_address;
|
void* new_address; /**< The symbol's address in the new build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_REMOVED). */
|
||||||
} HotReloadSymbolEventInfo;
|
} HotReloadSymbolEventInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Callback invoked once per symbol on every hot_reload_reload() call.
|
||||||
|
*
|
||||||
|
* Called synchronously, on the thread that called hot_reload_reload(), while the reload is in
|
||||||
|
* progress.
|
||||||
|
*
|
||||||
|
* @param event Describes one symbol's outcome; valid only for the duration of the call.
|
||||||
|
* @param user_data Whatever was passed as HotReloadOptions::user_data, untouched.
|
||||||
|
*
|
||||||
|
* @warning Do not call hot_reload_load() or hot_reload_reload() on the same context from inside
|
||||||
|
* this callback.
|
||||||
|
*/
|
||||||
typedef void (*HotReloadSymbolEventFn)(const HotReloadSymbolEventInfo* event, void* user_data);
|
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
|
* @brief Callback invoked instead of patching, once per ::HOT_RELOAD_SYMBOL_REMOVED event.
|
||||||
* instead of them silently continuing to run old code forever. Return
|
*
|
||||||
* the address of a replacement to redirect the old function to (its
|
* Lets the host catch stale references during development instead of them
|
||||||
* only safe use is a fixed-signature stub -- e.g. `void stub(void)`--
|
* silently continuing to run old code forever.
|
||||||
* that logs/asserts; this library has no way to know the deleted
|
*
|
||||||
* function's real signature, so redirecting to anything that reads
|
* Called synchronously, on the thread that called hot_reload_reload(), while the reload is in
|
||||||
* arguments the caller didn't pass is undefined behavior, exactly
|
* progress; like HotReloadSymbolEventFn, it must not call hot_reload_load() or
|
||||||
* like calling any function through the wrong function-pointer type).
|
* hot_reload_reload() on the same context.
|
||||||
* Return NULL (or pass a NULL callback in HotReloadOptions to begin
|
*
|
||||||
* with) to leave the old implementation running untouched -- the
|
* @param symbol_name The removed function's real (possibly mangled) name.
|
||||||
* default, and the safe choice unless you specifically want deleted
|
* @param user_data Whatever was passed as HotReloadOptions::user_data, untouched.
|
||||||
* functions to be loud. */
|
* @return The address of a replacement to redirect the old function to, or
|
||||||
|
* `NULL` to leave the old implementation running untouched (the
|
||||||
|
* default, and the safe choice unless you specifically want
|
||||||
|
* deleted functions to be loud).
|
||||||
|
*
|
||||||
|
* @warning A non-`NULL` return's only safe use is a fixed-signature stub
|
||||||
|
* (e.g. `void stub(void)`) that logs/asserts; this library has
|
||||||
|
* no way to know the deleted function's real signature, so
|
||||||
|
* redirecting to anything that reads arguments the caller didn't
|
||||||
|
* pass is undefined behavior, exactly like calling any function
|
||||||
|
* through the wrong function-pointer type.
|
||||||
|
*/
|
||||||
typedef void* (*HotReloadDeletedSymbolFn)(const char* symbol_name, void* user_data);
|
typedef void* (*HotReloadDeletedSymbolFn)(const char* symbol_name, void* user_data);
|
||||||
|
|
||||||
/* Passed to hot_reload_context_create(); NULL is a valid options
|
/**
|
||||||
* pointer meaning "no callbacks, default policy for everything". */
|
* @brief Options passed to hot_reload_context_create().
|
||||||
|
*
|
||||||
|
* A `NULL` options pointer is valid and means "no callbacks, default
|
||||||
|
* policy for everything."
|
||||||
|
*/
|
||||||
typedef struct HotReloadOptions
|
typedef struct HotReloadOptions
|
||||||
{
|
{
|
||||||
HotReloadSymbolEventFn on_symbol_event; /* nullable */
|
HotReloadSymbolEventFn on_symbol_event; /**< Nullable. See ::HotReloadSymbolEventFn. */
|
||||||
HotReloadDeletedSymbolFn on_deleted_symbol; /* nullable */
|
HotReloadDeletedSymbolFn on_deleted_symbol; /**< Nullable. See ::HotReloadDeletedSymbolFn. */
|
||||||
void* user_data; /* passed back to both callbacks, untouched */
|
void* user_data; /**< Passed back to both callbacks, untouched. */
|
||||||
} HotReloadOptions;
|
} HotReloadOptions;
|
||||||
|
|
||||||
/* Caller owns the returned pointer and must call
|
/** @} */ /* end of hotreload_types */
|
||||||
* hot_reload_context_destroy() -- see cpp-style-guide.md section 16
|
|
||||||
* ("Document ownership transfer explicitly"). options may be NULL. */
|
/**
|
||||||
|
* @defgroup hotreload_lifecycle Context lifecycle
|
||||||
|
* @brief Creating and destroying a hot-reload context.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a new hot-reload context.
|
||||||
|
* @param options Nullable; see ::HotReloadOptions. Its contents are copied, so it need not
|
||||||
|
* outlive this call.
|
||||||
|
* @return A new context, or `NULL` if allocation failed. The caller owns it and must call
|
||||||
|
* hot_reload_context_destroy() (see `cpp-style-guide.md` section 16,
|
||||||
|
* "Document ownership transfer explicitly").
|
||||||
|
*/
|
||||||
HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options);
|
HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destroys a context created by hot_reload_context_create().
|
||||||
|
*
|
||||||
|
* Unloads every version of every module the context loaded (running their static destructors).
|
||||||
|
* From then on, every pointer obtained through this context -- functions, data, and the old
|
||||||
|
* entry points that had been patched to jump into newer versions -- is dangling and must not be
|
||||||
|
* used. Destroy a context only when nothing can still call into its modules.
|
||||||
|
*
|
||||||
|
* @param ctx The context to destroy. Safe to pass `NULL`.
|
||||||
|
*/
|
||||||
HOT_RELOAD_API void hot_reload_context_destroy(HotReloadContext* ctx);
|
HOT_RELOAD_API void hot_reload_context_destroy(HotReloadContext* ctx);
|
||||||
|
|
||||||
/* Loads path for the first time under module_name -- an arbitrary
|
/** @} */ /* end of hotreload_lifecycle */
|
||||||
* 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
|
* @defgroup hotreload_loading Loading and reloading
|
||||||
* if module_name was already loaded -- call hot_reload_reload()
|
* @brief The core operations: register a module, hot-reload it, and
|
||||||
* instead of hot_reload_load() again for the same name. */
|
* resolve its symbols.
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loads a module for the first time under a logical name.
|
||||||
|
*
|
||||||
|
* The file is not loaded in place: it is first copied to a uniquely named sibling
|
||||||
|
* (`<path>.hotreload.<module_name>.<n>`) and that copy is loaded, which keeps a rebuild that
|
||||||
|
* overwrites @c path in place from being mistaken for the already-loaded version. The copy is
|
||||||
|
* deleted again right after loading on POSIX; on Windows it stays until the module is unloaded
|
||||||
|
* (see the top-level README). This means the directory containing @c path must be writable.
|
||||||
|
*
|
||||||
|
* The module's static constructors run as part of loading it.
|
||||||
|
*
|
||||||
|
* @note If the module's symbol table cannot be read (for instance a `.so` whose section headers
|
||||||
|
* were stripped), loading still succeeds but the module exports nothing as far as this
|
||||||
|
* library is concerned: hot_reload_get_symbol() finds nothing and a later
|
||||||
|
* hot_reload_reload() has nothing to patch.
|
||||||
|
*
|
||||||
|
* @param ctx The context to load into.
|
||||||
|
* @param module_name An arbitrary caller-chosen key used to look the
|
||||||
|
* module back up in later hot_reload_reload()/
|
||||||
|
* hot_reload_get_symbol() calls; it does not need to match
|
||||||
|
* anything on disk.
|
||||||
|
* @param path Path to the shared library (`.so`/`.dll`) to load. Always treated as a file path:
|
||||||
|
* a bare file name such as `plugin.so` means the file of that name in the current
|
||||||
|
* working directory (it is never looked up through the library search path).
|
||||||
|
* @return ::HOT_RELOAD_OK, or ::HOT_RELOAD_ERROR_INVALID_ARGUMENT if
|
||||||
|
* an argument was `NULL` or @c module_name was already loaded (call
|
||||||
|
* hot_reload_reload() instead of hot_reload_load() again for the same
|
||||||
|
* name), or ::HOT_RELOAD_ERROR_LOAD_FAILED (see hot_reload_last_error()
|
||||||
|
* for why).
|
||||||
|
*/
|
||||||
HOT_RELOAD_API HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path);
|
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
|
* @brief Hot-reloads a module previously registered with hot_reload_load().
|
||||||
* version, and patches every one that is safe to patch in place --
|
*
|
||||||
* see HotReloadSymbolEvent above for exactly what happens to each
|
* Loads a new build of the module, diffs its exported functions against
|
||||||
* kind of change. On any HOT_RELOAD_ERROR_* return, nothing was
|
* the previous version, and patches every one that is safe to patch in
|
||||||
* changed: the previous version keeps running exactly as it was. */
|
* place -- see ::HotReloadSymbolEvent for exactly what happens to each
|
||||||
|
* kind of change. The previous version stays loaded (its entry points now
|
||||||
|
* jump into the new one), and the new build's static constructors run.
|
||||||
|
* If the new build exports no functions at all -- including when its symbol
|
||||||
|
* table could not be read -- every previously exported function is reported
|
||||||
|
* as ::HOT_RELOAD_SYMBOL_REMOVED.
|
||||||
|
*
|
||||||
|
* Call this from a quiescent point (see @ref hotreload_threads), and see
|
||||||
|
* @ref hotreload_lifetime for what does not carry over. @c new_path follows
|
||||||
|
* the same rules as @c path in hot_reload_load().
|
||||||
|
*
|
||||||
|
* @param ctx The context @c module_name was hot_reload_load()'ed into.
|
||||||
|
* @param module_name The same logical name passed to hot_reload_load().
|
||||||
|
* @param new_path Path to the new build of the shared library.
|
||||||
|
* @return ::HOT_RELOAD_OK, or an `HOT_RELOAD_ERROR_*` code. On any error,
|
||||||
|
* nothing was changed: the previous version keeps running exactly
|
||||||
|
* as it was.
|
||||||
|
*/
|
||||||
HOT_RELOAD_API HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path);
|
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
|
* @brief Resolves a symbol in a module's current build.
|
||||||
* that did not exist yet when the host first started, such as one
|
*
|
||||||
* reported via HOT_RELOAD_SYMBOL_ADDED. Returns NULL if module_name
|
* Always resolves against the freshest build loaded for @c module_name,
|
||||||
* is unknown or symbol_name is not an exported function in it. */
|
* patched or not -- useful for calling a function that did not exist yet
|
||||||
|
* when the host first started, such as one reported via
|
||||||
|
* ::HOT_RELOAD_SYMBOL_ADDED. Any exported symbol resolves, data included
|
||||||
|
* (a global variable, say); only functions are ever patched.
|
||||||
|
*
|
||||||
|
* @param ctx The context @c module_name was hot_reload_load()'ed into.
|
||||||
|
* @param module_name The module to resolve @c symbol_name in.
|
||||||
|
* @param symbol_name The symbol's real (possibly mangled) name.
|
||||||
|
* @return The symbol's current address, or `NULL` if an argument is `NULL`,
|
||||||
|
* @c module_name is unknown, or @c symbol_name is not exported by
|
||||||
|
* the module's newest build.
|
||||||
|
*/
|
||||||
HOT_RELOAD_API void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name);
|
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
|
/** @} */ /* end of hotreload_loading */
|
||||||
* 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
|
* @defgroup hotreload_diagnostics Diagnostics
|
||||||
* Windows -- e.g. "undefined symbol: _ZN6Widget4tickEi" for a shared
|
* @brief Turning a status code or a mangled name into something readable.
|
||||||
* 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,
|
* @brief The platform's own explanation for the most recent failure on this context.
|
||||||
* right here, once something ODR-uses it and the loader tries to
|
*
|
||||||
* resolve it for real). Overwritten by the next
|
* Only meaningful immediately after hot_reload_load() or
|
||||||
* hot_reload_load()/hot_reload_reload() call on this ctx; copy it out
|
* hot_reload_reload() on this *same* @c ctx returned something other than
|
||||||
* immediately if you need to keep it past that point (e.g. for a log
|
* ::HOT_RELOAD_OK: the underlying platform's own description of why --
|
||||||
* line). Empty if the platform gave no further detail, or if the
|
* `dlerror()` on POSIX, the formatted message for `GetLastError()` on
|
||||||
* failure never reached the platform loader at all (e.g. a NULL
|
* Windows -- e.g. `"undefined symbol: _ZN6Widget4tickEi"` for a shared
|
||||||
* argument, which is HOT_RELOAD_ERROR_INVALID_ARGUMENT before
|
* library with an unresolved reference (a real, confusing-until-you-see-
|
||||||
* anything platform-specific happens). A static string internally --
|
* this-message case: a non-inline static class member that is declared
|
||||||
* never free() this. */
|
* but never given an out-of-class definition compiles fine and links fine
|
||||||
|
* into a shared library -- the linker allows a shared library to keep
|
||||||
|
* undefined symbols by default -- and only fails, right here, once
|
||||||
|
* something ODR-uses it and the loader tries to resolve it for real).
|
||||||
|
*
|
||||||
|
* @param ctx The context to query.
|
||||||
|
* @return The platform's error message. Empty if it gave no further
|
||||||
|
* detail, or if the failure never reached the platform loader at
|
||||||
|
* all (e.g. a `NULL` argument, which is
|
||||||
|
* ::HOT_RELOAD_ERROR_INVALID_ARGUMENT before anything
|
||||||
|
* platform-specific happens). Owned by @c ctx -- never `free()`
|
||||||
|
* this. It stays valid only until the next hot_reload_load()/
|
||||||
|
* hot_reload_reload() call on this @c ctx, or until the context is
|
||||||
|
* destroyed; copy it out immediately if you need to keep it past
|
||||||
|
* that point (e.g. for a log line).
|
||||||
|
*/
|
||||||
HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx);
|
HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx);
|
||||||
|
|
||||||
/* A static string literal -- never free() this. */
|
/**
|
||||||
|
* @brief Renders a ::HotReloadStatus as text.
|
||||||
|
* @param status The status to render.
|
||||||
|
* @return A static string literal -- never `free()` this.
|
||||||
|
*/
|
||||||
HOT_RELOAD_API const char* hot_reload_status_string(HotReloadStatus status);
|
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
|
* @brief Renders a symbol name for a human to read.
|
||||||
* library itself was built with) becomes something like
|
*
|
||||||
* "Widget::tick(int)"; anything else, including an ordinary extern "C"
|
* A real C++ mangled name (Itanium on GCC/Clang/MinGW, MSVC's own scheme
|
||||||
* name, comes back unchanged. Safe to call unconditionally on any
|
* under MSVC -- whichever this library itself was built with) becomes
|
||||||
* HotReloadSymbolEventInfo::name or hot_reload_get_symbol() key --
|
* something like `"Widget::tick(int)"`; anything else, including an
|
||||||
* this never changes what name means for lookup purposes, it is only
|
* ordinary `extern "C"` name, comes back unchanged. Safe to call
|
||||||
* ever applied when you ask for it, purely for display. Returns NULL
|
* unconditionally on any HotReloadSymbolEventInfo::name or
|
||||||
* only if name itself is NULL or allocation fails; caller must free
|
* hot_reload_get_symbol() key -- this never changes what a name means for
|
||||||
* a non-NULL result with hot_reload_free_string(). */
|
* lookup purposes, it is only ever applied when you ask for it, purely
|
||||||
|
* for display.
|
||||||
|
*
|
||||||
|
* @param name The name to render.
|
||||||
|
* @return The rendered name, or `NULL` if @c name itself is `NULL` or
|
||||||
|
* allocation fails. Caller must free a non-`NULL` result with
|
||||||
|
* hot_reload_free_string().
|
||||||
|
*/
|
||||||
HOT_RELOAD_API char* hot_reload_demangle(const char* name);
|
HOT_RELOAD_API char* hot_reload_demangle(const char* name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Frees a string returned by hot_reload_demangle().
|
||||||
|
* @param str The string to free. Safe to pass `NULL`.
|
||||||
|
*/
|
||||||
HOT_RELOAD_API void hot_reload_free_string(char* str);
|
HOT_RELOAD_API void hot_reload_free_string(char* str);
|
||||||
|
|
||||||
|
/** @} */ /* end of hotreload_diagnostics */
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file Architecture.cpp
|
||||||
|
* @brief Per-architecture jump-patch backends for x86, x86-64, and ARM64.
|
||||||
|
*/
|
||||||
#include "Architecture.hpp"
|
#include "Architecture.hpp"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -51,7 +55,8 @@
|
|||||||
* executing through the exact bytes being patched -- doing that in
|
* executing through the exact bytes being patched -- doing that in
|
||||||
* general on a variable-length ISA needs a signal-handler-based retry
|
* general on a variable-length ISA needs a signal-handler-based retry
|
||||||
* protocol (a much bigger mechanism, well beyond a dev-loop tool's
|
* protocol (a much bigger mechanism, well beyond a dev-loop tool's
|
||||||
* requirements). See the README for this precondition stated plainly.
|
* requirements). See the "Limitations" section of the top-level README
|
||||||
|
* for this precondition stated plainly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace hot_reload::detail
|
namespace hot_reload::detail
|
||||||
@@ -60,20 +65,30 @@ namespace hot_reload::detail
|
|||||||
{
|
{
|
||||||
#if defined(__x86_64__) || defined(_M_X64)
|
#if defined(__x86_64__) || defined(_M_X64)
|
||||||
|
|
||||||
|
/// Size in bytes of the CET landing pad (`endbr64`) a function may start with.
|
||||||
constexpr std::size_t kLandingPadBytes = 4;
|
constexpr std::size_t kLandingPadBytes = 4;
|
||||||
|
|
||||||
|
/// Encoding of `endbr64`, the x86-64 CET indirect-branch landing pad.
|
||||||
constexpr unsigned char kEndbr64[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFA};
|
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
|
/// Size in bytes of the jump written over a function's entry:
|
||||||
// new modules were loaded, unlike a 5-byte relative jump
|
/// `jmp qword ptr [rip+0]` followed by an 8-byte absolute address. That reaches any
|
||||||
// (E9 rel32) which only reaches +/-2GB. Verified byte-for-byte
|
/// 64-bit address regardless of how far apart the old and new modules were loaded,
|
||||||
// against `as`/objdump output while building this library.
|
/// 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;
|
constexpr std::size_t kJumpBytes = 14;
|
||||||
|
|
||||||
|
/// Whether the function at @p entry starts with an `endbr64` landing pad.
|
||||||
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
||||||
|
/// @return True if the first bytes are `endbr64`.
|
||||||
bool hasLandingPad(const unsigned char* entry)
|
bool hasLandingPad(const unsigned char* entry)
|
||||||
{
|
{
|
||||||
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
|
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the x86-64 absolute jump at @p site, payload first and the opcode last.
|
||||||
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
||||||
|
/// @param target Address the jump transfers control to.
|
||||||
void writeJumpAt(unsigned char* site, const void* target)
|
void writeJumpAt(unsigned char* site, const void* target)
|
||||||
{
|
{
|
||||||
std::uint64_t targetValue = 0;
|
std::uint64_t targetValue = 0;
|
||||||
@@ -99,20 +114,29 @@ namespace hot_reload::detail
|
|||||||
|
|
||||||
#elif defined(__i386__) || defined(_M_IX86)
|
#elif defined(__i386__) || defined(_M_IX86)
|
||||||
|
|
||||||
|
/// Size in bytes of the CET landing pad (`endbr32`) a function may start with.
|
||||||
constexpr std::size_t kLandingPadBytes = 4;
|
constexpr std::size_t kLandingPadBytes = 4;
|
||||||
|
|
||||||
|
/// Encoding of `endbr32`, the 32-bit x86 CET indirect-branch landing pad.
|
||||||
constexpr unsigned char kEndbr32[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFB};
|
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
|
/// Size in bytes of the jump written over a function's entry: `e9 rel32`, a plain
|
||||||
// (mod 2^32 wraparound gives the right answer regardless of
|
/// relative near jump. On 32-bit x86 the entire address space fits in the signed 32-bit
|
||||||
// "distance"), so unlike x86-64 there is no need for an
|
/// displacement (mod 2^32 wraparound gives the right answer regardless of "distance"),
|
||||||
// absolute/indirect form here.
|
/// so unlike x86-64 there is no need for an absolute/indirect form here.
|
||||||
constexpr std::size_t kJumpBytes = 5;
|
constexpr std::size_t kJumpBytes = 5;
|
||||||
|
|
||||||
|
/// Whether the function at @p entry starts with an `endbr32` landing pad.
|
||||||
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
||||||
|
/// @return True if the first bytes are `endbr32`.
|
||||||
bool hasLandingPad(const unsigned char* entry)
|
bool hasLandingPad(const unsigned char* entry)
|
||||||
{
|
{
|
||||||
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
|
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the x86 relative jump at @p site, displacement first and the opcode last.
|
||||||
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
||||||
|
/// @param target Address the jump transfers control to.
|
||||||
void writeJumpAt(unsigned char* site, const void* target)
|
void writeJumpAt(unsigned char* site, const void* target)
|
||||||
{
|
{
|
||||||
std::int32_t rel32 = static_cast<std::int32_t>(
|
std::int32_t rel32 = static_cast<std::int32_t>(
|
||||||
@@ -128,35 +152,47 @@ namespace hot_reload::detail
|
|||||||
|
|
||||||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||||||
|
|
||||||
|
/// Size in bytes of the BTI landing pad a function may start with (one instruction word).
|
||||||
constexpr std::size_t kLandingPadBytes = 4;
|
constexpr std::size_t kLandingPadBytes = 4;
|
||||||
|
|
||||||
// Matches any BTI variant (plain/C/J/JC): bits [31:6] are fixed
|
/// Whether the instruction word at @p entry is any BTI variant (plain/C/J/JC): bits
|
||||||
// at 0b010101000000001001001, differing only in bits [6:5]
|
/// [31:6] are fixed at 0b010101000000001001001, differing only in bits [6:5] (the
|
||||||
// (the qualifier) -- equivalently, the low byte is one of
|
/// qualifier) -- equivalently, the low byte is one of 0x1F/0x5F/0x9F/0xDF and the next 3
|
||||||
// 0x1F/0x5F/0x9F/0xDF and the next 3 bytes are always
|
/// bytes are always 24 03 D5. Verified with `as`/objdump for all 4 variants while
|
||||||
// 24 03 D5. Verified with `as`/objdump for all 4 variants
|
/// building this library.
|
||||||
// while building this library.
|
/// @param entry The function's entry address (at least 4 readable bytes).
|
||||||
|
/// @return True if the first instruction is a BTI landing pad.
|
||||||
bool isBti(const unsigned char* entry)
|
bool isBti(const unsigned char* entry)
|
||||||
{
|
{
|
||||||
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
|
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the function at @p entry starts with a BTI landing pad.
|
||||||
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
||||||
|
/// @return True if the first instruction is a BTI landing pad.
|
||||||
bool hasLandingPad(const unsigned char* entry)
|
bool hasLandingPad(const unsigned char* entry)
|
||||||
{
|
{
|
||||||
return isBti(entry);
|
return isBti(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ldr x17, #8 ; br x17 ; <8-byte absolute address>. x16/x17
|
/// Size in bytes of the jump written over a function's entry:
|
||||||
// are AArch64's designated "intra-procedure-call" scratch
|
/// `ldr x17, #8 ; br x17 ; <8-byte absolute address>`. x16/x17 are AArch64's designated
|
||||||
// registers (IP0/IP1) -- the AAPCS64 explicitly reserves them
|
/// "intra-procedure-call" scratch registers (IP0/IP1) -- the AAPCS64 explicitly reserves
|
||||||
// for exactly this kind of linker/veneer-style long branch, so
|
/// them for exactly this kind of linker/veneer-style long branch, so clobbering them at
|
||||||
// clobbering them at a function's entry point, before any of
|
/// a function's entry point, before any of its real prologue has executed, is always
|
||||||
// its real prologue has executed, is always safe. Byte values
|
/// safe. Byte values verified with `as`/objdump while building this library.
|
||||||
// verified with `as`/objdump while building this library.
|
|
||||||
constexpr std::size_t kJumpBytes = 16;
|
constexpr std::size_t kJumpBytes = 16;
|
||||||
|
|
||||||
|
/// Encoding of `ldr x17, #8` (loads the literal address stored 8 bytes further on).
|
||||||
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
|
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
|
||||||
|
|
||||||
|
/// Encoding of `br x17` (branches to the address `ldr x17, #8` loaded).
|
||||||
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
|
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
|
||||||
|
|
||||||
|
/// Writes the ARM64 `ldr`/`br` pair plus literal address at @p site, payload first and
|
||||||
|
/// the leading `ldr` last.
|
||||||
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
||||||
|
/// @param target Address the jump transfers control to.
|
||||||
void writeJumpAt(unsigned char* site, const void* target)
|
void writeJumpAt(unsigned char* site, const void* target)
|
||||||
{
|
{
|
||||||
std::uint64_t targetValue = 0;
|
std::uint64_t targetValue = 0;
|
||||||
|
|||||||
@@ -2,56 +2,91 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @namespace hot_reload::detail
|
||||||
|
* @brief Internal building blocks of the hot-reload runtime -- not part of the public API.
|
||||||
|
*
|
||||||
|
* The jump-patch backends (`Architecture.hpp`), the page-protection helper
|
||||||
|
* (`MemoryProtection.hpp`), the loader wrapper (`ModuleHandle.hpp`) and the symbol-table readers
|
||||||
|
* (`SymbolTable.hpp`) all live here. They are implementation details of
|
||||||
|
* hot_reload::HotReloadContext and may change freely; the supported surface is the C API in
|
||||||
|
* `hot_reload.h`.
|
||||||
|
*/
|
||||||
namespace hot_reload::detail
|
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,
|
* @brief What planPatch() found at one specific function's entry point.
|
||||||
/// and how many of those (if any) are a landing pad that must be
|
*
|
||||||
/// preserved rather than overwritten. See Architecture.cpp's file
|
* How many bytes from the entry address must be safe to overwrite, and
|
||||||
/// comment for why a landing pad can be present at all.
|
* 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
|
struct PatchLayout
|
||||||
{
|
{
|
||||||
/// Total bytes from the function's entry address that this
|
/**
|
||||||
/// patch touches, landing pad included -- compare this against
|
* Total bytes from the function's entry address that this patch
|
||||||
/// the symbol's compiled size before writing anything.
|
* touches, landing pad included -- compare this against the
|
||||||
|
* symbol's compiled size before writing anything.
|
||||||
|
*/
|
||||||
std::size_t totalBytes = 0;
|
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
|
* How many leading bytes are a landing pad and must be left
|
||||||
/// entryAddress + patchOffset.
|
* untouched; the jump itself is written starting at
|
||||||
|
* `entryAddress + patchOffset`.
|
||||||
|
*/
|
||||||
std::size_t patchOffset = 0;
|
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 --
|
* @brief Whether this build has a jump-patch backend for the current CPU architecture.
|
||||||
/// see the top of Architecture.cpp). x86, x86-64, and ARM64 are
|
*
|
||||||
/// supported; anything else (including 32-bit ARM, whose ARM/Thumb
|
* Decided at compile time -- see the top of `Architecture.cpp`. x86,
|
||||||
/// interworking this library does not attempt -- see the README)
|
* x86-64, and ARM64 are supported; anything else (including 32-bit
|
||||||
/// returns false here, and HotReloadContext surfaces that as
|
* ARM, whose ARM/Thumb interworking this library does not attempt --
|
||||||
/// HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
* see the "Scope" section of `source/HotReload/README.md`) returns false here, and
|
||||||
|
* `HotReloadContext` surfaces that as
|
||||||
|
* ::HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
||||||
|
*
|
||||||
|
* @return True if writeJump() can succeed on this architecture.
|
||||||
|
*/
|
||||||
bool isArchitectureSupported();
|
bool isArchitectureSupported();
|
||||||
|
|
||||||
/// Inspects the bytes already sitting at entryAddress to determine
|
/**
|
||||||
/// the real patch layout for THIS function, accounting for a
|
* @brief Determines the real patch layout for one specific function.
|
||||||
/// possible leading landing pad. Safe to call even when
|
*
|
||||||
/// isArchitectureSupported() is false (returns a zeroed PatchLayout).
|
* Inspects the bytes already sitting at @p entryAddress, accounting
|
||||||
|
* for a possible leading landing pad. Safe to call even when
|
||||||
|
* isArchitectureSupported() is false (returns a zeroed PatchLayout).
|
||||||
|
*
|
||||||
|
* @param entryAddress The function's entry address.
|
||||||
|
* @return The layout to check against the symbol's compiled size
|
||||||
|
* before writing anything, and to pass to writeJump().
|
||||||
|
*/
|
||||||
PatchLayout planPatch(const void* entryAddress);
|
PatchLayout planPatch(const void* entryAddress);
|
||||||
|
|
||||||
/// Overwrites entryAddress so that control transfers unconditionally
|
/**
|
||||||
/// to targetAddress, following the layout planPatch() would return
|
* @brief Overwrites a function's entry so it jumps to another address.
|
||||||
/// for this same entryAddress. The caller must already have made
|
*
|
||||||
/// [entryAddress, entryAddress + planPatch(entryAddress).totalBytes)
|
* Follows the layout planPatch() would return for this same
|
||||||
/// writable (see MemoryProtection.hpp) and must flush the
|
* @p entryAddress.
|
||||||
/// instruction cache over that same range afterwards -- this
|
*
|
||||||
/// function does neither.
|
* @param entryAddress The function entry to overwrite.
|
||||||
///
|
* @param targetAddress Where control should transfer to instead.
|
||||||
/// Only meaningful precondition: the caller has already checked the
|
* @return False only if isArchitectureSupported() is false.
|
||||||
/// target symbol's compiled size against planPatch(entryAddress)
|
*
|
||||||
/// .totalBytes; this function does not re-check it and will happily
|
* @pre The caller has already made
|
||||||
/// overwrite whatever comes next in memory if asked to.
|
* `[entryAddress, entryAddress + planPatch(entryAddress).totalBytes)`
|
||||||
///
|
* writable (see `MemoryProtection.hpp`) and will flush the
|
||||||
/// Returns false only if isArchitectureSupported() is false.
|
* instruction cache over that same range afterwards -- this
|
||||||
|
* function does neither.
|
||||||
|
* @pre The caller has already checked the target symbol's compiled
|
||||||
|
* size against `planPatch(entryAddress).totalBytes`; this
|
||||||
|
* function does not re-check it and will happily overwrite
|
||||||
|
* whatever comes next in memory if asked to.
|
||||||
|
*/
|
||||||
bool writeJump(void* entryAddress, const void* targetAddress);
|
bool writeJump(void* entryAddress, const void* targetAddress);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file HotReloadContext.cpp
|
||||||
|
* @brief The diff-and-patch orchestration logic: load, reload, and per-symbol policy.
|
||||||
|
*/
|
||||||
#include "HotReloadContext.hpp"
|
#include "HotReloadContext.hpp"
|
||||||
|
|
||||||
#include "Architecture.hpp"
|
#include "Architecture.hpp"
|
||||||
@@ -12,6 +16,12 @@ namespace hot_reload
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/// Copies a file byte for byte, overwriting the destination.
|
||||||
|
/// @param from The file to read.
|
||||||
|
/// @param to The file to create (or truncate).
|
||||||
|
/// @return True if the whole copy succeeded. An empty source counts as a failure (inserting
|
||||||
|
/// zero characters into an output stream sets its failbit) -- harmless, since an
|
||||||
|
/// empty file is never a loadable module anyway.
|
||||||
bool copyFile(const std::string& from, const std::string& to)
|
bool copyFile(const std::string& from, const std::string& to)
|
||||||
{
|
{
|
||||||
std::ifstream in(from, std::ios::binary);
|
std::ifstream in(from, std::ios::binary);
|
||||||
@@ -28,32 +38,67 @@ namespace hot_reload
|
|||||||
return static_cast<bool>(out);
|
return static_cast<bool>(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort: deletes any staging copy left over from a
|
/// The module name as it appears inside a staging copy's file name.
|
||||||
// PREVIOUS run of this same host/module -- see
|
///
|
||||||
// stagePathForLoad()'s own comment for why those copies exist
|
/// module_name is an arbitrary caller-chosen key (hot_reload.h promises it need not match
|
||||||
// at all. On POSIX this normally finds nothing (an active
|
/// anything on disk), so it may contain characters that are not legal in a file name --
|
||||||
// run's own copies are already gone by the time it makes its
|
/// a '/' in "engine/renderer", say. Everything outside a conservative set is replaced by
|
||||||
// next one -- see the std::remove() calls after each
|
/// '_'. stagePathForLoad() and cleanupOrphanedStagingFiles() must agree on the result, so
|
||||||
// ModuleHandle::load() below). On Windows, a copy backing a
|
/// both call this. Names made only of letters, digits, '-' and '_' are unchanged.
|
||||||
// still-loaded module cannot be deleted for as long as that
|
///
|
||||||
// module stays mapped -- which, by this library's own design,
|
/// @param moduleName The logical module name.
|
||||||
// is for the rest of the CURRENT process's lifetime (see
|
/// @return @p moduleName with every character outside `[A-Za-z0-9_-]` replaced by '_'.
|
||||||
// HotReloadContext.hpp's LoadedModule comment) -- so within a
|
std::string stagingTag(const std::string& moduleName)
|
||||||
// single long-running session some accumulation is expected
|
{
|
||||||
// and unavoidable; this specifically cleans up the PREVIOUS
|
std::string tag = moduleName;
|
||||||
// session's leftovers once that process has exited and
|
for (char& character : tag)
|
||||||
// released whatever lock Windows held. Called once, here, at
|
{
|
||||||
// the first hot_reload_load() for a given name -- not on every
|
const bool isLower = character >= 'a' && character <= 'z';
|
||||||
// reload() -- since a copy from earlier in the SAME session
|
const bool isUpper = character >= 'A' && character <= 'Z';
|
||||||
// would still be locked for the same reason and retrying
|
const bool isDigit = character >= '0' && character <= '9';
|
||||||
// wouldn't change that.
|
const bool isDashOrUnderscore = character == '-' || character == '_';
|
||||||
|
const bool safe = isLower || isUpper || isDigit || isDashOrUnderscore;
|
||||||
|
if (!safe)
|
||||||
|
{
|
||||||
|
character = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Best-effort: deletes any staging copy left over from a
|
||||||
|
/// PREVIOUS run of this same host/module -- see
|
||||||
|
/// stagePathForLoad()'s own comment for why those copies exist
|
||||||
|
/// at all.
|
||||||
|
///
|
||||||
|
/// On POSIX this normally finds nothing (an active
|
||||||
|
/// run's own copies are already gone by the time it makes its
|
||||||
|
/// next one -- see the std::remove() calls after each
|
||||||
|
/// ModuleHandle::load() below). On Windows, a copy backing a
|
||||||
|
/// still-loaded module cannot be deleted for as long as that
|
||||||
|
/// module stays mapped -- which, by this library's own design,
|
||||||
|
/// is for as long as the owning context lives, usually the rest
|
||||||
|
/// of the CURRENT process's lifetime (see HotReloadContext.hpp's
|
||||||
|
/// LoadedModule::versions) -- so within a
|
||||||
|
/// single long-running session some accumulation is expected
|
||||||
|
/// and unavoidable; this specifically cleans up the PREVIOUS
|
||||||
|
/// session's leftovers once that process has exited and
|
||||||
|
/// released whatever lock Windows held. Called once, at
|
||||||
|
/// the first hot_reload_load() for a given name -- not on every
|
||||||
|
/// reload() -- since a copy from earlier in the SAME session
|
||||||
|
/// would still be locked for the same reason and retrying
|
||||||
|
/// wouldn't change that.
|
||||||
|
///
|
||||||
|
/// @param sourcePath The path the module is being loaded from; the copies live next to it.
|
||||||
|
/// @param moduleName The module's logical name.
|
||||||
void cleanupOrphanedStagingFiles(const std::string& sourcePath, const std::string& moduleName)
|
void cleanupOrphanedStagingFiles(const std::string& sourcePath, const std::string& moduleName)
|
||||||
{
|
{
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
fs::path source(sourcePath);
|
fs::path source(sourcePath);
|
||||||
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
||||||
std::string prefix = source.filename().string() + ".hotreload." + moduleName + ".";
|
const std::string tag = stagingTag(moduleName);
|
||||||
|
std::string prefix = source.filename().string() + ".hotreload." + tag + ".";
|
||||||
|
|
||||||
std::error_code dirError;
|
std::error_code dirError;
|
||||||
fs::directory_iterator entries(dir, dirError);
|
fs::directory_iterator entries(dir, dirError);
|
||||||
@@ -120,7 +165,9 @@ namespace hot_reload
|
|||||||
// name guaranteed never reused sidesteps it regardless of how
|
// name guaranteed never reused sidesteps it regardless of how
|
||||||
// the build produced sourcePath.
|
// the build produced sourcePath.
|
||||||
++module.reloadCounter;
|
++module.reloadCounter;
|
||||||
std::string staged = sourcePath + ".hotreload." + moduleName + "." + std::to_string(module.reloadCounter);
|
const std::string tag = stagingTag(moduleName);
|
||||||
|
const std::string counter = std::to_string(module.reloadCounter);
|
||||||
|
std::string staged = sourcePath + ".hotreload." + tag + "." + counter;
|
||||||
if (!copyFile(sourcePath, staged))
|
if (!copyFile(sourcePath, staged))
|
||||||
{
|
{
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -8,62 +8,160 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @namespace hot_reload
|
||||||
|
* @brief The C++ implementation behind the C API declared in `hot_reload.h`.
|
||||||
|
*
|
||||||
|
* The one type meant to be used from outside is HotReloadContext, and even that only through the
|
||||||
|
* opaque ::HotReloadContext pointer the C API hands out. The building blocks it is made of live in
|
||||||
|
* hot_reload::detail.
|
||||||
|
*/
|
||||||
namespace hot_reload
|
namespace hot_reload
|
||||||
{
|
{
|
||||||
|
|
||||||
/// The real type behind the opaque ::HotReloadContext the public C
|
/**
|
||||||
/// API hands out -- see HotReloadApi.cpp for the extern "C" shim
|
* @brief The real type behind the opaque ::HotReloadContext the public C API hands out.
|
||||||
/// that casts between the two, following the same opaque-pointer
|
*
|
||||||
/// PIMPL pattern as cpp-style-guide.md's own NetManager example.
|
* See `HotReloadApi.cpp` for the `extern "C"` shim that casts between
|
||||||
|
* the two, following the same opaque-pointer PIMPL pattern as
|
||||||
|
* `cpp-style-guide.md`'s own `NetManager` example.
|
||||||
|
*
|
||||||
|
* A context owns every version of every module loaded through it. Destroying the context
|
||||||
|
* closes all of them, after which any function or data pointer obtained from any of those
|
||||||
|
* modules -- including old, patched entry points that now only jump into a newer version --
|
||||||
|
* is dangling.
|
||||||
|
*
|
||||||
|
* @note Not thread-safe: the class holds no locks, so all calls on one context (and any
|
||||||
|
* reload it performs) must be serialized by the caller.
|
||||||
|
*/
|
||||||
class HotReloadContext
|
class HotReloadContext
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Creates an empty context.
|
||||||
|
* @param options Callbacks and user data to use, copied out immediately; may be null,
|
||||||
|
* meaning "no callbacks".
|
||||||
|
*/
|
||||||
explicit HotReloadContext(const HotReloadOptions* options);
|
explicit HotReloadContext(const HotReloadOptions* options);
|
||||||
|
|
||||||
HotReloadContext(const HotReloadContext&) = delete;
|
HotReloadContext(const HotReloadContext&) = delete;
|
||||||
HotReloadContext& operator=(const HotReloadContext&) = delete;
|
HotReloadContext& operator=(const HotReloadContext&) = delete;
|
||||||
|
|
||||||
/// See hot_reload_load()'s doc comment in hot_reload.h.
|
/**
|
||||||
|
* @brief Implements hot_reload_load().
|
||||||
|
* @param moduleName The logical name to register the module under.
|
||||||
|
* @param path Path to the shared library to load.
|
||||||
|
* @return The status hot_reload_load() reports; see its doc comment in `hot_reload.h`.
|
||||||
|
*/
|
||||||
HotReloadStatus load(const std::string& moduleName, const std::string& path);
|
HotReloadStatus load(const std::string& moduleName, const std::string& path);
|
||||||
|
|
||||||
/// See hot_reload_reload()'s doc comment in hot_reload.h.
|
/**
|
||||||
|
* @brief Implements hot_reload_reload().
|
||||||
|
* @param moduleName The logical name the module was loaded under.
|
||||||
|
* @param newPath Path to the new build of the shared library.
|
||||||
|
* @return The status hot_reload_reload() reports; see its doc comment in `hot_reload.h`.
|
||||||
|
*/
|
||||||
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
|
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
|
||||||
|
|
||||||
/// See hot_reload_get_symbol()'s doc comment in hot_reload.h.
|
/**
|
||||||
|
* @brief Implements hot_reload_get_symbol().
|
||||||
|
* @param moduleName The logical name the module was loaded under.
|
||||||
|
* @param symbolName The symbol's real (possibly mangled) name.
|
||||||
|
* @return The symbol's address in the module's newest build, or null if the module or
|
||||||
|
* symbol is unknown; see hot_reload_get_symbol()'s doc comment in `hot_reload.h`.
|
||||||
|
*/
|
||||||
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
|
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
|
||||||
|
|
||||||
/// See hot_reload_last_error()'s doc comment in hot_reload.h.
|
/**
|
||||||
|
* @brief Implements hot_reload_last_error().
|
||||||
|
* @return The platform's message for the most recent load()/reload() failure on this
|
||||||
|
* context; empty if there was none. Valid until the next load()/reload().
|
||||||
|
*/
|
||||||
const std::string& lastError() const
|
const std::string& lastError() const
|
||||||
{
|
{
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Everything tracked for one logical module name. Has no
|
/// Everything tracked for one logical module name. Has no meaning
|
||||||
/// meaning outside HotReloadContext, so it lives here rather
|
/// outside HotReloadContext, so it lives here rather than in its
|
||||||
/// than in its own header -- see cpp-style-guide.md's carve-out
|
/// own header -- see cpp-style-guide.md's carve-out for small
|
||||||
/// for small types with no use outside their owning type.
|
/// types with no use outside their owning type.
|
||||||
struct LoadedModule
|
struct LoadedModule
|
||||||
{
|
{
|
||||||
// Every version ever loaded for this name, kept alive for
|
/// Every version ever loaded for this name, kept alive for as long as the owning
|
||||||
// the rest of the process's lifetime: once a function is
|
/// context lives (in practice usually the rest of the process): once a function is
|
||||||
// patched, anything that already holds its old address
|
/// patched, anything that already holds its old address must stay valid memory,
|
||||||
// must stay valid memory forever, even though it now holds
|
/// even though it now holds nothing but a jump. versions.back() is the active one.
|
||||||
// nothing but a jump. versions.back() is the active one.
|
|
||||||
std::vector<detail::ModuleHandle> versions;
|
std::vector<detail::ModuleHandle> versions;
|
||||||
|
|
||||||
|
/// The exported symbols of the newest build (versions.back()), as reported by
|
||||||
|
/// detail::readExportedSymbols(); what the next reload() diffs against.
|
||||||
std::vector<detail::ExportedSymbol> currentSymbols;
|
std::vector<detail::ExportedSymbol> currentSymbols;
|
||||||
|
|
||||||
|
/// How many staging copies have been made for this name so far; makes each copy's
|
||||||
|
/// file name unique (see stagePathForLoad()).
|
||||||
int reloadCounter = 0;
|
int reloadCounter = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stages a source file through a fresh, uniquely-named copy.
|
||||||
|
*
|
||||||
|
* Called before a path is ever passed to detail::ModuleHandle::load() -- see the .cpp
|
||||||
|
* definition's comment for why (a dlopen caching gotcha on POSIX; a Windows file-locking
|
||||||
|
* side effect that turns out to be handled by the same fix).
|
||||||
|
*
|
||||||
|
* @param module The module the copy is for; its reloadCounter is advanced.
|
||||||
|
* @param moduleName The module's logical name (used, sanitized, in the copy's file name).
|
||||||
|
* @param sourcePath The file to copy.
|
||||||
|
* @return The copy's path, or an empty string if copying failed.
|
||||||
|
*/
|
||||||
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
|
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Redirects one matched function pair, or emits a SKIPPED_* event instead if that
|
||||||
|
* isn't safe.
|
||||||
|
*
|
||||||
|
* Patches @p oldSymbol's entry to jump to @p newSymbol's address; emits exactly one event
|
||||||
|
* describing the outcome either way.
|
||||||
|
*
|
||||||
|
* @param oldSymbol The function in the previous build.
|
||||||
|
* @param newSymbol The same-named function in the new build.
|
||||||
|
*/
|
||||||
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
|
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles a symbol present in the old build but missing from the new one.
|
||||||
|
*
|
||||||
|
* Emits REMOVED, and first redirects to a caller-supplied stub if
|
||||||
|
* HotReloadOptions::on_deleted_symbol provides one and it's safe to do so.
|
||||||
|
*
|
||||||
|
* @param oldSymbol The symbol the new build no longer exports.
|
||||||
|
*/
|
||||||
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
|
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Invokes HotReloadOptions::on_symbol_event, if one was supplied.
|
||||||
|
* @param kind What happened to the symbol.
|
||||||
|
* @param name The symbol's real (possibly mangled) name.
|
||||||
|
* @param oldAddress The symbol's address in the previous build, or null.
|
||||||
|
* @param newAddress The symbol's address in the new build, or null.
|
||||||
|
*/
|
||||||
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
|
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
|
||||||
|
|
||||||
|
/// Every module loaded so far, by logical name.
|
||||||
std::unordered_map<std::string, LoadedModule> m_modules;
|
std::unordered_map<std::string, LoadedModule> m_modules;
|
||||||
|
|
||||||
|
/// HotReloadOptions::on_symbol_event, or null.
|
||||||
HotReloadSymbolEventFn m_onSymbolEvent;
|
HotReloadSymbolEventFn m_onSymbolEvent;
|
||||||
|
|
||||||
|
/// HotReloadOptions::on_deleted_symbol, or null.
|
||||||
HotReloadDeletedSymbolFn m_onDeletedSymbol;
|
HotReloadDeletedSymbolFn m_onDeletedSymbol;
|
||||||
|
|
||||||
|
/// HotReloadOptions::user_data, handed back to both callbacks untouched.
|
||||||
void* m_userData;
|
void* m_userData;
|
||||||
|
|
||||||
|
/// The message lastError() returns.
|
||||||
std::string m_lastError;
|
std::string m_lastError;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file MemoryProtection.cpp
|
||||||
|
* @brief Code-page write-access RAII guard implementation (mprotect/VirtualProtect).
|
||||||
|
*/
|
||||||
#include "MemoryProtection.hpp"
|
#include "MemoryProtection.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -20,6 +24,8 @@ namespace hot_reload::detail
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/// The operating system's virtual-memory page size.
|
||||||
|
/// @return The page size in bytes (4096 if the platform could not report one).
|
||||||
std::size_t systemPageSize()
|
std::size_t systemPageSize()
|
||||||
{
|
{
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
|||||||
@@ -5,50 +5,89 @@
|
|||||||
namespace hot_reload::detail
|
namespace hot_reload::detail
|
||||||
{
|
{
|
||||||
|
|
||||||
/// RAII guard: makes [address, address + length) writable for its
|
/**
|
||||||
/// lifetime (the OS only grants permissions in whole pages, so the
|
* @brief RAII guard that temporarily makes a code range writable.
|
||||||
/// guard rounds out to the enclosing page range internally) and
|
*
|
||||||
/// restores the original protection when it goes out of scope.
|
* Makes `[address, address + length)` writable for its lifetime (the
|
||||||
///
|
* OS only grants permissions in whole pages, so the guard rounds out
|
||||||
/// Assumes the range started out READ+EXECUTE, as every platform's
|
* to the enclosing page range internally) and restores the original
|
||||||
/// loader maps a .text/code section -- restoring to exactly that,
|
* protection when it goes out of scope.
|
||||||
/// rather than querying and restoring whatever the previous
|
*
|
||||||
/// protection actually was, since POSIX mprotect() has no portable
|
* @note Assumes the range started out READ+EXECUTE, as every
|
||||||
/// way to ask for the current protection of a range and code pages
|
* platform's loader maps a `.text`/code section -- restoring to
|
||||||
/// are never anything else in practice.
|
* exactly that, rather than querying and restoring whatever the
|
||||||
|
* previous protection actually was, since POSIX `mprotect()`
|
||||||
|
* has no portable way to ask for the current protection of a
|
||||||
|
* range and code pages are never anything else in practice.
|
||||||
|
*
|
||||||
|
* @note The change applies to whole pages and is process-wide: every thread sees the
|
||||||
|
* enclosing pages as writable (and still executable) while the guard is alive.
|
||||||
|
*/
|
||||||
class ScopedCodeWriteAccess
|
class ScopedCodeWriteAccess
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Attempts to make a range writable immediately.
|
||||||
|
*
|
||||||
|
* Check isValid() afterwards: the platform call can be refused (for example by a
|
||||||
|
* hardened environment that denies writable+executable transitions).
|
||||||
|
*
|
||||||
|
* @param address Start of the range.
|
||||||
|
* @param length Length of the range in bytes.
|
||||||
|
*/
|
||||||
ScopedCodeWriteAccess(void* address, std::size_t length);
|
ScopedCodeWriteAccess(void* address, std::size_t length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Restores the range's original protection (does nothing if isValid() is false).
|
||||||
|
*/
|
||||||
~ScopedCodeWriteAccess();
|
~ScopedCodeWriteAccess();
|
||||||
|
|
||||||
ScopedCodeWriteAccess(const ScopedCodeWriteAccess&) = delete;
|
ScopedCodeWriteAccess(const ScopedCodeWriteAccess&) = delete;
|
||||||
ScopedCodeWriteAccess& operator=(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
|
* @brief Whether the platform call to relax protection succeeded.
|
||||||
/// written to (the destructor is still safe to run either way).
|
*
|
||||||
|
* When false, nothing was changed and the range must not be
|
||||||
|
* written to (the destructor is still safe to run either way).
|
||||||
|
*
|
||||||
|
* @return True if the range is currently writable.
|
||||||
|
*/
|
||||||
bool isValid() const
|
bool isValid() const
|
||||||
{
|
{
|
||||||
return m_valid;
|
return m_valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call exactly once, after writing the new bytes and before
|
/**
|
||||||
/// this guard is destroyed: flushes the instruction cache over
|
* @brief Flushes the instruction cache over the patched range.
|
||||||
/// the original [address, address + length) range so every
|
*
|
||||||
/// core sees the new instructions rather than a stale fetch
|
* Call exactly once, after writing the new bytes and before this
|
||||||
/// (required on ARM64, where instruction and data caches are
|
* guard is destroyed, so every core sees the new instructions
|
||||||
/// not automatically coherent; effectively a formality on
|
* rather than a stale fetch (required on ARM64, where instruction
|
||||||
/// x86/x86-64, called anyway for correctness on that
|
* and data caches are not automatically coherent; effectively a
|
||||||
/// architecture too).
|
* formality on x86/x86-64, called anyway for correctness on that
|
||||||
|
* architecture too).
|
||||||
|
*/
|
||||||
void finishWrite();
|
void finishWrite();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// Start of the page-aligned region whose protection was changed.
|
||||||
void* m_pageAddress;
|
void* m_pageAddress;
|
||||||
|
|
||||||
|
/// Length in bytes of that page-aligned region (a whole number of pages).
|
||||||
std::size_t m_pageLength;
|
std::size_t m_pageLength;
|
||||||
|
|
||||||
|
/// Start of the range the caller actually asked to write to.
|
||||||
void* m_rangeAddress;
|
void* m_rangeAddress;
|
||||||
|
|
||||||
|
/// Length in bytes of the range the caller actually asked to write to.
|
||||||
std::size_t m_rangeLength;
|
std::size_t m_rangeLength;
|
||||||
|
|
||||||
|
/// The protection to restore on destruction, in the platform's own encoding
|
||||||
|
/// (`PROT_*` flags on POSIX, a `PAGE_*` constant on Windows).
|
||||||
unsigned long m_previousProtection;
|
unsigned long m_previousProtection;
|
||||||
|
|
||||||
|
/// Whether the constructor's protection change succeeded.
|
||||||
bool m_valid;
|
bool m_valid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @file ModuleHandle.cpp
|
||||||
|
* @brief RAII dlopen()/LoadLibrary() wrapper implementation.
|
||||||
|
*/
|
||||||
#include "ModuleHandle.hpp"
|
#include "ModuleHandle.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
// NOMINMAX: see MemoryProtection.cpp's identical comment -- same
|
// NOMINMAX: see MemoryProtection.cpp's identical comment -- same
|
||||||
@@ -23,9 +28,12 @@ namespace hot_reload::detail
|
|||||||
{
|
{
|
||||||
/// GetLastError() alone is just a number; this renders it the
|
/// GetLastError() alone is just a number; this renders it the
|
||||||
/// way a person would want to read it, the same information
|
/// way a person would want to read it, the same information
|
||||||
/// dlerror() gives directly on POSIX. Written against
|
/// dlerror() gives directly on POSIX. Exercised under Wine (MinGW cross-build) -- see
|
||||||
/// documented Win32 behavior; not exercised on real Windows --
|
/// "What's actually been tested" in the top-level README; not verified against a real
|
||||||
/// see source/HotReload/README.md's verification notes.
|
/// Windows/MSVC build.
|
||||||
|
/// @return The system's message for the calling thread's last Win32 error, without
|
||||||
|
/// trailing line breaks, or a generic message naming the error code if the
|
||||||
|
/// system has none.
|
||||||
std::string formatLastWindowsError()
|
std::string formatLastWindowsError()
|
||||||
{
|
{
|
||||||
DWORD errorCode = GetLastError();
|
DWORD errorCode = GetLastError();
|
||||||
@@ -71,6 +79,7 @@ namespace hot_reload::detail
|
|||||||
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
|
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
|
||||||
: m_native(other.m_native)
|
: m_native(other.m_native)
|
||||||
, m_loadedBase(other.m_loadedBase)
|
, m_loadedBase(other.m_loadedBase)
|
||||||
|
, m_errorMessage(std::move(other.m_errorMessage))
|
||||||
{
|
{
|
||||||
other.m_native = nullptr;
|
other.m_native = nullptr;
|
||||||
other.m_loadedBase = nullptr;
|
other.m_loadedBase = nullptr;
|
||||||
@@ -90,6 +99,7 @@ namespace hot_reload::detail
|
|||||||
}
|
}
|
||||||
m_native = other.m_native;
|
m_native = other.m_native;
|
||||||
m_loadedBase = other.m_loadedBase;
|
m_loadedBase = other.m_loadedBase;
|
||||||
|
m_errorMessage = std::move(other.m_errorMessage);
|
||||||
other.m_native = nullptr;
|
other.m_native = nullptr;
|
||||||
other.m_loadedBase = nullptr;
|
other.m_loadedBase = nullptr;
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,18 @@ namespace hot_reload::detail
|
|||||||
// first runs under RTLD_LAZY's deferred binding. For a
|
// first runs under RTLD_LAZY's deferred binding. For a
|
||||||
// long-running host, a clean, catchable load failure is far
|
// long-running host, a clean, catchable load failure is far
|
||||||
// preferable to a delayed, uncatchable one.
|
// preferable to a delayed, uncatchable one.
|
||||||
void* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
//
|
||||||
|
// The "./" prefix: dlopen() only treats its argument as a file
|
||||||
|
// path if it contains a '/'. A bare name (e.g. "plugin.so") is
|
||||||
|
// instead searched for like a library name -- LD_LIBRARY_PATH,
|
||||||
|
// ld.so.cache, the default directories -- and is never looked
|
||||||
|
// for in the current directory, so a plain relative file name
|
||||||
|
// would fail with a baffling "cannot open shared object file"
|
||||||
|
// for a file that is sitting right there. Everything this
|
||||||
|
// library loads is a real file on disk (the staged copy
|
||||||
|
// HotReloadContext makes), so always force path semantics.
|
||||||
|
const std::string loadPath = path.find('/') == std::string::npos ? "./" + path : path;
|
||||||
|
void* module = dlopen(loadPath.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (module == nullptr)
|
if (module == nullptr)
|
||||||
{
|
{
|
||||||
const char* error = dlerror();
|
const char* error = dlerror();
|
||||||
|
|||||||
@@ -5,53 +5,103 @@
|
|||||||
namespace hot_reload::detail
|
namespace hot_reload::detail
|
||||||
{
|
{
|
||||||
|
|
||||||
/// RAII wrapper around dlopen()/LoadLibrary(). Move-only, closes
|
/**
|
||||||
/// the underlying module on destruction (or on being replaced by a
|
* @brief RAII wrapper around `dlopen()`/`LoadLibrary()`.
|
||||||
/// move-assignment).
|
*
|
||||||
|
* Move-only, closes the underlying module on destruction (or on
|
||||||
|
* being replaced by a move-assignment).
|
||||||
|
*
|
||||||
|
* @note Closing a module unmaps its code (unless the platform keeps it resident, e.g. a
|
||||||
|
* library that defines C++ unique symbols). Any function pointer into a closed module
|
||||||
|
* -- including old, patched entry points that jump into it -- dangles from then on.
|
||||||
|
*/
|
||||||
class ModuleHandle
|
class ModuleHandle
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/** @brief Creates an empty handle that owns no module (isValid() is false). */
|
||||||
ModuleHandle() = default;
|
ModuleHandle() = default;
|
||||||
|
|
||||||
|
/** @brief Closes the module, if this handle owns one. */
|
||||||
~ModuleHandle();
|
~ModuleHandle();
|
||||||
|
|
||||||
ModuleHandle(const ModuleHandle&) = delete;
|
ModuleHandle(const ModuleHandle&) = delete;
|
||||||
ModuleHandle& operator=(const ModuleHandle&) = delete;
|
ModuleHandle& operator=(const ModuleHandle&) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Takes ownership of @p other's module (and its error message), leaving @p other
|
||||||
|
* empty.
|
||||||
|
* @param other The handle to move from.
|
||||||
|
*/
|
||||||
ModuleHandle(ModuleHandle&& other) noexcept;
|
ModuleHandle(ModuleHandle&& other) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Closes this handle's current module, then takes ownership of @p other's module
|
||||||
|
* (and its error message), leaving @p other empty.
|
||||||
|
* @param other The handle to move from.
|
||||||
|
* @return This handle.
|
||||||
|
*/
|
||||||
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
|
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
|
||||||
|
|
||||||
/// Loads path. On failure the returned handle's isValid() is
|
/**
|
||||||
/// false and loadedBase() is null.
|
* @brief Loads a shared library.
|
||||||
|
*
|
||||||
|
* @param path Path to the shared library to load. Always treated as a filesystem path: a
|
||||||
|
* bare file name such as `plugin.so` means the file of that name in the current
|
||||||
|
* working directory (on POSIX it is loaded as `./plugin.so`, because `dlopen()`
|
||||||
|
* would otherwise search the library path for it instead).
|
||||||
|
* @return The loaded handle. On failure, isValid() is false and
|
||||||
|
* loadedBase() is null -- check errorMessage() for why.
|
||||||
|
*/
|
||||||
static ModuleHandle load(const std::string& path);
|
static ModuleHandle load(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether load() succeeded.
|
||||||
|
* @return True if this handle owns a loaded module.
|
||||||
|
*/
|
||||||
bool isValid() const
|
bool isValid() const
|
||||||
{
|
{
|
||||||
return m_native != nullptr;
|
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
|
* @brief The address to pass as SymbolTable's `loadedBase`.
|
||||||
/// bias (struct link_map::l_addr) that turns an ELF symbol's
|
*
|
||||||
/// file-relative st_value into a real address.
|
* On Windows this is simply the `HMODULE`; on POSIX it is the
|
||||||
|
* runtime load bias (`struct link_map::l_addr`) that turns an
|
||||||
|
* ELF symbol's file-relative `st_value` into a real address.
|
||||||
|
*
|
||||||
|
* @return The value described above, or null if isValid() is false.
|
||||||
|
*/
|
||||||
void* loadedBase() const
|
void* loadedBase() const
|
||||||
{
|
{
|
||||||
return m_loadedBase;
|
return m_loadedBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only meaningful when isValid() is false: the underlying
|
/**
|
||||||
/// platform's own description of why loading failed --
|
* @brief Why load() failed, if isValid() is false.
|
||||||
/// dlerror() on POSIX, the formatted message for
|
*
|
||||||
/// GetLastError() on Windows. Captured immediately inside
|
* The underlying platform's own description of why loading
|
||||||
/// load()'s failing branch, before any other dl*/Win32 call
|
* failed -- `dlerror()` on POSIX, the formatted message for
|
||||||
/// that could otherwise overwrite it first. Empty if load()
|
* `GetLastError()` on Windows. Captured immediately inside
|
||||||
/// was never attempted, or if the platform gave no message.
|
* load()'s failing branch, before any other POSIX dl-family or
|
||||||
|
* Win32 call that could otherwise overwrite it first.
|
||||||
|
*
|
||||||
|
* @return The platform's message, or empty if load() was never
|
||||||
|
* attempted or the platform gave no message.
|
||||||
|
*/
|
||||||
const std::string& errorMessage() const
|
const std::string& errorMessage() const
|
||||||
{
|
{
|
||||||
return m_errorMessage;
|
return m_errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// The platform's own handle (`HMODULE` or the `dlopen()` handle); null if none.
|
||||||
void* m_native = nullptr;
|
void* m_native = nullptr;
|
||||||
|
|
||||||
|
/// See loadedBase().
|
||||||
void* m_loadedBase = nullptr;
|
void* m_loadedBase = nullptr;
|
||||||
|
|
||||||
|
/// See errorMessage().
|
||||||
std::string m_errorMessage;
|
std::string m_errorMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file SymbolTable.cpp
|
||||||
|
* @brief Reads and classifies a module's exported symbols (ELF and PE).
|
||||||
|
*/
|
||||||
#include "SymbolTable.hpp"
|
#include "SymbolTable.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -5,6 +9,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
|
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
|
||||||
@@ -49,13 +54,18 @@ namespace hot_reload::detail
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/// One PE section, reduced to what the function/data classification needs.
|
||||||
struct SectionInfo
|
struct SectionInfo
|
||||||
{
|
{
|
||||||
std::uint32_t rvaBegin;
|
std::uint32_t rvaBegin; /**< First RVA covered by the section. */
|
||||||
std::uint32_t rvaEnd;
|
std::uint32_t rvaEnd; /**< One past the last RVA covered by the section. */
|
||||||
bool executable;
|
bool executable; /**< Whether the section is marked as containing code. */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Turns a relative virtual address inside the mapped image into a string pointer.
|
||||||
|
/// @param base The image's base address.
|
||||||
|
/// @param rva A relative virtual address inside the image.
|
||||||
|
/// @return `base + rva`, viewed as a C string.
|
||||||
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
|
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
|
||||||
{
|
{
|
||||||
return reinterpret_cast<const char*>(base + rva);
|
return reinterpret_cast<const char*>(base + rva);
|
||||||
@@ -170,9 +180,11 @@ namespace hot_reload::detail
|
|||||||
// large real function) -- it only ever gives a hard "not
|
// large real function) -- it only ever gives a hard "not
|
||||||
// enough room" answer when the gap itself is already small.
|
// enough room" answer when the gap itself is already small.
|
||||||
// PE has no per-export size field the way ELF does (see
|
// PE has no per-export size field the way ELF does (see
|
||||||
// below), so /hotpatch (applied automatically by
|
// below), so /hotpatch (applied by calling
|
||||||
// cmake/HotReload.cmake) is the primary safety mechanism on
|
// hot_reload_enable_patchable_functions() from
|
||||||
// Windows; this is a secondary backstop, not a substitute.
|
// cmake/HotReload.cmake on the plugin target) is the primary
|
||||||
|
// safety mechanism on Windows; this is a secondary backstop,
|
||||||
|
// not a substitute.
|
||||||
std::vector<std::uint32_t> functionRvasSorted;
|
std::vector<std::uint32_t> functionRvasSorted;
|
||||||
for (const Candidate& candidate : candidates)
|
for (const Candidate& candidate : candidates)
|
||||||
{
|
{
|
||||||
@@ -236,6 +248,10 @@ namespace hot_reload::detail
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/// Reads a whole file into memory.
|
||||||
|
/// @param path The file to read.
|
||||||
|
/// @return The file's bytes, or an empty vector if it could not be opened, is empty, or
|
||||||
|
/// could not be read completely.
|
||||||
std::vector<char> readWholeFile(const std::string& path)
|
std::vector<char> readWholeFile(const std::string& path)
|
||||||
{
|
{
|
||||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
@@ -257,83 +273,191 @@ namespace hot_reload::detail
|
|||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a range read out of an ELF file lies entirely inside that file.
|
||||||
|
///
|
||||||
|
/// Every offset and length in an ELF header is untrusted input until proven otherwise:
|
||||||
|
/// the dynamic loader validates the program headers it maps, but nothing validates the
|
||||||
|
/// section header table this reader walks. Written so the sum can never overflow.
|
||||||
|
///
|
||||||
|
/// @param offset Start of the range, as read from the file.
|
||||||
|
/// @param length Length of the range, as read from the file.
|
||||||
|
/// @param fileSize Size of the whole file in bytes.
|
||||||
|
/// @return True if `[offset, offset + length)` is inside `[0, fileSize)`.
|
||||||
|
bool rangeInFile(std::uint64_t offset, std::uint64_t length, std::size_t fileSize)
|
||||||
|
{
|
||||||
|
return offset <= fileSize && length <= fileSize - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies one `T` out of the file, if it lies entirely inside it.
|
||||||
|
///
|
||||||
|
/// A `memcpy` rather than a `reinterpret_cast`, so an offset read from a corrupt file
|
||||||
|
/// can never produce a misaligned access.
|
||||||
|
///
|
||||||
|
/// @tparam T A trivially copyable ELF structure (`Elf64_Ehdr`, `Elf32_Sym`, ...).
|
||||||
|
/// @param file The whole file.
|
||||||
|
/// @param offset Where the structure starts.
|
||||||
|
/// @param[out] out Receives the structure.
|
||||||
|
/// @return False (leaving @p out untouched) if the structure would extend past the end.
|
||||||
|
template <typename T>
|
||||||
|
bool readStruct(const std::vector<char>& file, std::uint64_t offset, T& out)
|
||||||
|
{
|
||||||
|
if (!rangeInFile(offset, sizeof(T), file.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(&out, file.data() + offset, sizeof(T));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a NUL-terminated string in an ELF string table, without ever reading past it.
|
||||||
|
/// @param table Start of the string table.
|
||||||
|
/// @param tableSize Size of the string table in bytes.
|
||||||
|
/// @param offset The string's offset within the table, as read from the file.
|
||||||
|
/// @return The string, or an empty view if @p offset is outside the table or the string
|
||||||
|
/// is not NUL-terminated inside it.
|
||||||
|
std::string_view stringAt(const char* table, std::uint64_t tableSize, std::uint64_t offset)
|
||||||
|
{
|
||||||
|
if (offset >= tableSize)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const char* begin = table + offset;
|
||||||
|
const auto remaining = static_cast<std::size_t>(tableSize - offset);
|
||||||
|
const void* end = std::memchr(begin, '\0', remaining);
|
||||||
|
if (end == nullptr)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto* endPtr = static_cast<const char*>(end);
|
||||||
|
return std::string_view(begin, static_cast<std::size_t>(endPtr - begin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
|
||||||
|
/// @return The symbol's type (`STT_*`).
|
||||||
|
unsigned char elfSymType(unsigned char info)
|
||||||
|
{
|
||||||
|
return static_cast<unsigned char>(ELF32_ST_TYPE(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
|
||||||
|
/// @return The symbol's binding (`STB_*`).
|
||||||
|
unsigned char elfSymBind(unsigned char info)
|
||||||
|
{
|
||||||
|
return static_cast<unsigned char>(ELF32_ST_BIND(info));
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses one ELF class (32 or 64-bit); Ehdr/Shdr/Sym is
|
/// Parses one ELF class (32 or 64-bit); Ehdr/Shdr/Sym is
|
||||||
/// Elf32_* or Elf64_* depending on the caller, matched against
|
/// Elf32_* or Elf64_* depending on the caller, matched against
|
||||||
/// e_ident[EI_CLASS] before this is ever instantiated.
|
/// e_ident[EI_CLASS] before this is ever instantiated.
|
||||||
template <typename Ehdr, typename Shdr, typename Sym, typename SymType, typename SymBind>
|
///
|
||||||
std::vector<ExportedSymbol> parseElf(
|
/// Every offset, length and index read from the file is bounds-checked first (see
|
||||||
const std::vector<char>& file, void* loadedBase, SymType symType, SymBind symBind)
|
/// rangeInFile()), so a damaged image produces a shorter result rather than an
|
||||||
|
/// out-of-bounds read.
|
||||||
|
///
|
||||||
|
/// @tparam Ehdr The ELF file header type (`Elf32_Ehdr` or `Elf64_Ehdr`).
|
||||||
|
/// @tparam Shdr The ELF section header type (`Elf32_Shdr` or `Elf64_Shdr`).
|
||||||
|
/// @tparam Sym The ELF symbol type (`Elf32_Sym` or `Elf64_Sym`).
|
||||||
|
/// @param file The whole shared object, as read from disk.
|
||||||
|
/// @param loadedBase The runtime load bias to add to every symbol's `st_value`.
|
||||||
|
/// @return Every defined, global or weak symbol in `.dynsym`, classified; empty if the
|
||||||
|
/// file's section headers or symbol tables are missing or inconsistent.
|
||||||
|
template <typename Ehdr, typename Shdr, typename Sym>
|
||||||
|
std::vector<ExportedSymbol> parseElf(const std::vector<char>& file, void* loadedBase)
|
||||||
{
|
{
|
||||||
std::vector<ExportedSymbol> result;
|
std::vector<ExportedSymbol> result;
|
||||||
if (file.size() < sizeof(Ehdr))
|
|
||||||
|
Ehdr ehdr{};
|
||||||
|
if (!readStruct(file, 0, ehdr))
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
if (ehdr.e_shoff == 0 || ehdr.e_shnum == 0)
|
||||||
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
|
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) >
|
const auto sectionTableBytes = static_cast<std::uint64_t>(ehdr.e_shnum) * sizeof(Shdr);
|
||||||
file.size())
|
const bool sectionsFit = rangeInFile(ehdr.e_shoff, sectionTableBytes, file.size());
|
||||||
|
if (ehdr.e_shentsize != sizeof(Shdr) || !sectionsFit)
|
||||||
{
|
{
|
||||||
return result; // truncated/corrupt file
|
return result; // truncated/corrupt file
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* sections = reinterpret_cast<const Shdr*>(file.data() + ehdr->e_shoff);
|
std::vector<Shdr> sections(ehdr.e_shnum);
|
||||||
|
const auto sectionBytes = sections.size() * sizeof(Shdr);
|
||||||
|
std::memcpy(sections.data(), file.data() + ehdr.e_shoff, sectionBytes);
|
||||||
|
|
||||||
// Section header string table, to find sections by name.
|
// Section header string table, to find sections by name.
|
||||||
if (ehdr->e_shstrndx >= ehdr->e_shnum)
|
if (ehdr.e_shstrndx >= ehdr.e_shnum)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const char* shstrtab = file.data() + sections[ehdr->e_shstrndx].sh_offset;
|
const Shdr& shstrSection = sections[ehdr.e_shstrndx];
|
||||||
|
if (!rangeInFile(shstrSection.sh_offset, shstrSection.sh_size, file.size()))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const char* shstrtab = file.data() + shstrSection.sh_offset;
|
||||||
|
|
||||||
const Shdr* dynsymSection = nullptr;
|
const Shdr* dynsymSection = nullptr;
|
||||||
const Shdr* dynstrSection = nullptr;
|
const Shdr* dynstrSection = nullptr;
|
||||||
for (std::size_t i = 0; i < ehdr->e_shnum; ++i)
|
for (const Shdr& section : sections)
|
||||||
{
|
{
|
||||||
const char* name = shstrtab + sections[i].sh_name;
|
const auto nameOffset = section.sh_name;
|
||||||
if (std::strcmp(name, ".dynsym") == 0)
|
const std::string_view name = stringAt(shstrtab, shstrSection.sh_size, nameOffset);
|
||||||
|
if (name == ".dynsym")
|
||||||
{
|
{
|
||||||
dynsymSection = §ions[i];
|
dynsymSection = §ion;
|
||||||
}
|
}
|
||||||
else if (std::strcmp(name, ".dynstr") == 0)
|
else if (name == ".dynstr")
|
||||||
{
|
{
|
||||||
dynstrSection = §ions[i];
|
dynstrSection = §ion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dynsymSection == nullptr || dynstrSection == nullptr)
|
if (dynsymSection == nullptr || dynstrSection == nullptr)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
if (!rangeInFile(dynsymSection->sh_offset, dynsymSection->sh_size, file.size()) ||
|
||||||
|
!rangeInFile(dynstrSection->sh_offset, dynstrSection->sh_size, file.size()))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const char* dynstr = file.data() + dynstrSection->sh_offset;
|
const char* dynstr = file.data() + dynstrSection->sh_offset;
|
||||||
std::size_t symbolCount = dynsymSection->sh_size / sizeof(Sym);
|
const auto symBytes = static_cast<std::size_t>(dynsymSection->sh_size);
|
||||||
const auto* symbols = reinterpret_cast<const Sym*>(file.data() + dynsymSection->sh_offset);
|
const std::size_t symbolCount = symBytes / sizeof(Sym);
|
||||||
|
|
||||||
for (std::size_t i = 0; i < symbolCount; ++i)
|
for (std::size_t i = 0; i < symbolCount; ++i)
|
||||||
{
|
{
|
||||||
const Sym& sym = symbols[i];
|
Sym sym{};
|
||||||
|
const char* symPtr = file.data() + dynsymSection->sh_offset + i * sizeof(Sym);
|
||||||
|
std::memcpy(&sym, symPtr, sizeof(Sym));
|
||||||
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
|
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
|
||||||
{
|
{
|
||||||
continue; // imported/unresolved, not defined here
|
continue; // imported/unresolved, not defined here
|
||||||
}
|
}
|
||||||
unsigned char bind = symBind(sym.st_info);
|
unsigned char bind = elfSymBind(sym.st_info);
|
||||||
if (bind != STB_GLOBAL && bind != STB_WEAK)
|
if (bind != STB_GLOBAL && bind != STB_WEAK)
|
||||||
{
|
{
|
||||||
continue; // not part of this module's visible surface
|
continue; // not part of this module's visible surface
|
||||||
}
|
}
|
||||||
|
const auto symNameOffset = sym.st_name;
|
||||||
|
const auto dynstrSize = dynstrSection->sh_size;
|
||||||
|
const std::string_view name = stringAt(dynstr, dynstrSize, symNameOffset);
|
||||||
|
if (name.empty())
|
||||||
|
{
|
||||||
|
continue; // name offset outside .dynstr (corrupt file)
|
||||||
|
}
|
||||||
|
|
||||||
ExportedSymbol exported;
|
ExportedSymbol exported;
|
||||||
exported.name = dynstr + sym.st_name;
|
exported.name = std::string(name);
|
||||||
exported.address = static_cast<char*>(loadedBase) + sym.st_value;
|
exported.address = static_cast<char*>(loadedBase) + sym.st_value;
|
||||||
exported.size = static_cast<std::size_t>(sym.st_size);
|
exported.size = static_cast<std::size_t>(sym.st_size);
|
||||||
|
|
||||||
bool isFuncType = symType(sym.st_info) == STT_FUNC;
|
bool isFuncType = elfSymType(sym.st_info) == STT_FUNC;
|
||||||
bool sectionExecutable = false;
|
bool sectionExecutable = false;
|
||||||
if (sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON && sym.st_shndx < ehdr->e_shnum)
|
const bool realSection = sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON;
|
||||||
|
if (realSection && sym.st_shndx < ehdr.e_shnum)
|
||||||
{
|
{
|
||||||
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
|
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
|
||||||
}
|
}
|
||||||
@@ -354,18 +478,17 @@ namespace hot_reload::detail
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ELF32_ST_TYPE/BIND and ELF64_ST_TYPE/BIND decode the same bit layout (the System V ABI
|
||||||
|
// defines st_info identically for both classes) -- elfSymType()/elfSymBind() above use the
|
||||||
|
// ELF32 macros for both, so one pair of extractors serves both instantiations below.
|
||||||
unsigned char elfClass = static_cast<unsigned char>(file[EI_CLASS]);
|
unsigned char elfClass = static_cast<unsigned char>(file[EI_CLASS]);
|
||||||
if (elfClass == ELFCLASS64)
|
if (elfClass == ELFCLASS64)
|
||||||
{
|
{
|
||||||
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(
|
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(file, loadedBase);
|
||||||
file, loadedBase, [](unsigned char info) { return ELF64_ST_TYPE(info); },
|
|
||||||
[](unsigned char info) { return ELF64_ST_BIND(info); });
|
|
||||||
}
|
}
|
||||||
if (elfClass == ELFCLASS32)
|
if (elfClass == ELFCLASS32)
|
||||||
{
|
{
|
||||||
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(
|
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(file, loadedBase);
|
||||||
file, loadedBase, [](unsigned char info) { return ELF32_ST_TYPE(info); },
|
|
||||||
[](unsigned char info) { return ELF32_ST_BIND(info); });
|
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,45 +7,60 @@
|
|||||||
namespace hot_reload::detail
|
namespace hot_reload::detail
|
||||||
{
|
{
|
||||||
|
|
||||||
/// One exported symbol found in a loaded module, already classified.
|
/**
|
||||||
|
* @brief One exported symbol found in a loaded module, already classified.
|
||||||
|
*/
|
||||||
struct ExportedSymbol
|
struct ExportedSymbol
|
||||||
{
|
{
|
||||||
std::string name;
|
std::string name; /**< The symbol's real (possibly mangled) name. */
|
||||||
void* address = nullptr;
|
void* address = nullptr; /**< The symbol's runtime address. */
|
||||||
|
|
||||||
/// The single load-bearing safety gate behind the "only
|
/**
|
||||||
/// executable code changes" guarantee in hot_reload.h: false
|
* The single load-bearing safety gate behind the "only
|
||||||
/// for anything not mechanically identified as code (a data
|
* executable code changes" guarantee in `hot_reload.h`: false
|
||||||
/// object, a forwarder export, or a symbol kind this reader
|
* for anything not mechanically identified as code (a data
|
||||||
/// does not recognize at all). HotReloadContext never patches
|
* object, a forwarder export, or a symbol kind this reader does
|
||||||
/// a symbol where this is false, on either side of a reload.
|
* not recognize at all). `HotReloadContext` never patches a
|
||||||
|
* symbol where this is false, on either side of a reload.
|
||||||
|
*/
|
||||||
bool isFunction = false;
|
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
|
* Compiled size in bytes if known, 0 if not. ELF gives this
|
||||||
/// such field, so this holds a conservative estimate derived
|
* exactly (the symbol table's `st_size` field); PE gives no such
|
||||||
/// from the distance to the next exported function in the
|
* field, so this holds a conservative estimate derived from the
|
||||||
/// same section -- see the file comment in SymbolTable.cpp
|
* distance to the next exported function in the same section --
|
||||||
/// for exactly what that heuristic can and cannot catch.
|
* see the file comment in `SymbolTable.cpp` for exactly what
|
||||||
|
* that heuristic can and cannot catch.
|
||||||
|
*/
|
||||||
std::size_t size = 0;
|
std::size_t size = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Reads every exported, defined symbol belonging to the module
|
/**
|
||||||
/// mapped at loadedBase (the base address dlopen()/LoadLibrary()
|
* @brief Reads and classifies every exported, defined symbol in a loaded module.
|
||||||
/// returned for it), classifying each one as code or data.
|
*
|
||||||
///
|
* On POSIX this parses @p imagePath on disk (its ELF `.dynsym` /
|
||||||
/// On POSIX this parses imagePath on disk (its ELF .dynsym /
|
* `.dynstr` / section-header table) and adds @p loadedBase as the
|
||||||
/// .dynstr / section-header table) and adds loadedBase as the
|
* runtime load bias. On Windows this ignores @p imagePath and reads
|
||||||
/// runtime load bias. On Windows this ignores imagePath and reads
|
* directly from the already-mapped image at @p loadedBase (its PE
|
||||||
/// directly from the already-mapped image at loadedBase (its PE
|
* export directory and section table) -- see the per-platform
|
||||||
/// export directory and section table) -- see the per-platform
|
* implementation blocks in `SymbolTable.cpp` for why each platform
|
||||||
/// implementation blocks in SymbolTable.cpp for why each platform
|
* reads from a different place.
|
||||||
/// reads from a different place.
|
*
|
||||||
///
|
* A file that is truncated or otherwise corrupt in the parts this reader relies on (the
|
||||||
/// Returns an empty vector, never throws, if the image could not
|
* section header table and the tables it points at) is treated like any other unparseable
|
||||||
/// be parsed as this platform's native format (including the rare
|
* image: every offset and length read from the file is range-checked against the file's size
|
||||||
/// case of a .so with its section header table deliberately
|
* before it is used, so a damaged image yields fewer (or no) symbols rather than an
|
||||||
/// stripped -- see the file comment in SymbolTable.cpp).
|
* out-of-bounds read.
|
||||||
|
*
|
||||||
|
* @param imagePath Path to the module on disk (POSIX only -- see above).
|
||||||
|
* @param loadedBase The base address `dlopen()`/`LoadLibrary()` returned for it.
|
||||||
|
* @return Every exported symbol, classified via ExportedSymbol::isFunction.
|
||||||
|
* Empty (never throws) if the image could not be parsed as
|
||||||
|
* this platform's native format (including the rare case of
|
||||||
|
* a `.so` with its section header table deliberately
|
||||||
|
* stripped -- see the file comment in `SymbolTable.cpp`).
|
||||||
|
*/
|
||||||
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase);
|
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
/* Demangle.cpp -- hot_reload_demangle() / hot_reload_free_string().
|
/**
|
||||||
|
* @file Demangle.cpp
|
||||||
|
* @brief hot_reload_demangle() / hot_reload_free_string() implementation.
|
||||||
*
|
*
|
||||||
* Pure string utility, no HotReloadContext involved, so it gets its
|
* Pure string utility, no HotReloadContext involved, so it gets its
|
||||||
* own file alongside HotReloadApi.cpp rather than crowding that one's
|
* own file alongside HotReloadApi.cpp rather than crowding that one's
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#include "hot_reload/hot_reload.h"
|
#include "hot_reload/hot_reload.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -24,9 +27,9 @@
|
|||||||
// COMPILER implements (Itanium here), not the operating system --
|
// COMPILER implements (Itanium here), not the operating system --
|
||||||
// MinGW is still GCC/Clang generating Itanium-mangled names even
|
// MinGW is still GCC/Clang generating Itanium-mangled names even
|
||||||
// though the OS is Windows. MSVC, on the same OS, uses its own,
|
// though the OS is Windows. MSVC, on the same OS, uses its own,
|
||||||
// unrelated scheme (the #elif below). See README.md's "Hot
|
// unrelated scheme (the #elif below). See the "Hot reloading C++
|
||||||
// reloading C++ symbols" section for why this distinction matters
|
// symbols" section of source/HotReload/README.md for why this
|
||||||
// beyond just this function.
|
// distinction matters beyond just this function.
|
||||||
#include <cxxabi.h>
|
#include <cxxabi.h>
|
||||||
#elif defined(_MSC_VER)
|
#elif defined(_MSC_VER)
|
||||||
// windows.h must come first: dbghelp.h uses types (HANDLE, DWORD,
|
// windows.h must come first: dbghelp.h uses types (HANDLE, DWORD,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file HotReloadApi.cpp
|
||||||
|
* @brief The extern "C" shim implementing hot_reload.h by delegating to hot_reload::HotReloadContext.
|
||||||
|
*/
|
||||||
#include "hot_reload/hot_reload.h"
|
#include "hot_reload/hot_reload.h"
|
||||||
|
|
||||||
#include "../HotReloadContext.hpp"
|
#include "../HotReloadContext.hpp"
|
||||||
@@ -9,6 +13,8 @@ namespace
|
|||||||
/// is a hot_reload::HotReloadContext, following the same
|
/// is a hot_reload::HotReloadContext, following the same
|
||||||
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
|
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
|
||||||
/// example uses. This is the one place that cast happens.
|
/// example uses. This is the one place that cast happens.
|
||||||
|
/// @param ctx A context previously returned by hot_reload_context_create(), or null.
|
||||||
|
/// @return The same object viewed as the C++ type it really is (null if @p ctx is null).
|
||||||
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
|
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
|
||||||
{
|
{
|
||||||
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
|
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* Main.cpp -- loads PluginV1, calls into it, reloads to PluginV2, and
|
/**
|
||||||
|
* @file Main.cpp
|
||||||
|
* @brief Scripted, deterministic demo: loads PluginV1, calls into it, reloads to PluginV2, and
|
||||||
* calls the SAME already-resolved function pointers again to show
|
* calls the SAME already-resolved function pointers again to show
|
||||||
* they now run the new code -- without the Host re-resolving anything.
|
* they now run the new code -- without the Host re-resolving anything.
|
||||||
* Along the way this exercises every HotReloadSymbolEvent kind: watch
|
* Along the way this exercises every HotReloadSymbolEvent kind: watch
|
||||||
@@ -17,33 +19,40 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// The signature every plain-C export in PluginApi.h shares.
|
||||||
using PluginFunction = int (*)(int);
|
using PluginFunction = int (*)(int);
|
||||||
|
|
||||||
// CppWidget::tick's real ABI: an implicit this-pointer followed by
|
/// CppWidget::tick's real ABI: an implicit this-pointer followed by
|
||||||
// its one declared parameter, same calling convention the
|
/// its one declared parameter, same calling convention the
|
||||||
// compiler itself would generate for widget->tick(x) -- this is
|
/// compiler itself would generate for widget->tick(x) -- this is
|
||||||
// just spelling that out explicitly so it can be called through a
|
/// just spelling that out explicitly so it can be called through a
|
||||||
// manually-resolved address instead.
|
/// manually-resolved address instead.
|
||||||
using CppWidgetTickFunction = int (*)(CppWidget*, int);
|
using CppWidgetTickFunction = int (*)(CppWidget*, int);
|
||||||
|
|
||||||
|
/// Stands in for @c plugin_legacy_feature once V2 removes it.
|
||||||
|
/// @return Always -1.
|
||||||
int hostSideDeletedFunctionStub(int)
|
int hostSideDeletedFunctionStub(int)
|
||||||
{
|
{
|
||||||
std::printf(" !! called a deleted plugin function -- this is the Host's stub, not a crash !!\n");
|
std::printf(" !! called a deleted plugin function -- this is the Host's stub, not a crash !!\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Runs once per HOT_RELOAD_SYMBOL_REMOVED, before onSymbolEvent
|
/// Runs once per HOT_RELOAD_SYMBOL_REMOVED, before onSymbolEvent
|
||||||
* below sees the same event -- see hot_reload.h's doc comment on
|
/// below sees the same event -- see hot_reload.h's doc comment on
|
||||||
* HotReloadDeletedSymbolFn. Redirecting a removed function to a
|
/// HotReloadDeletedSymbolFn. Redirecting a removed function to a
|
||||||
* stub living in the HOST executable (not in either plugin build)
|
/// stub living in the HOST executable (not in either plugin build)
|
||||||
* demonstrates that the target of a patch can be anywhere at all,
|
/// demonstrates that the target of a patch can be anywhere at all,
|
||||||
* not just inside the module being reloaded. */
|
/// not just inside the module being reloaded.
|
||||||
|
/// @param symbolName The removed function's real (possibly mangled) name.
|
||||||
|
/// @return The host stub to redirect old callers to.
|
||||||
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
||||||
{
|
{
|
||||||
std::printf(" [event] REMOVED %-22s -> redirecting old callers to the Host's stub\n", symbolName);
|
std::printf(" [event] REMOVED %-22s -> redirecting old callers to the Host's stub\n", symbolName);
|
||||||
return reinterpret_cast<void*>(&hostSideDeletedFunctionStub);
|
return reinterpret_cast<void*>(&hostSideDeletedFunctionStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @param kind The event kind to name.
|
||||||
|
/// @return A short, human-readable label for @p kind.
|
||||||
const char* eventKindName(HotReloadSymbolEvent kind)
|
const char* eventKindName(HotReloadSymbolEvent kind)
|
||||||
{
|
{
|
||||||
switch (kind)
|
switch (kind)
|
||||||
@@ -64,6 +73,8 @@ namespace
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logs every symbol event except REMOVED, which onDeletedSymbol() already logs.
|
||||||
|
/// @param event Describes one symbol's outcome.
|
||||||
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
||||||
{
|
{
|
||||||
// REMOVED is already reported, with more context, by
|
// REMOVED is already reported, with more context, by
|
||||||
@@ -85,6 +96,10 @@ namespace
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the scripted demo described in this file's @ref Main.cpp "file comment".
|
||||||
|
/// @param argc Argument count.
|
||||||
|
/// @param argv `argv[1]`/`argv[2]`, if present, override the compiled-in plugin paths.
|
||||||
|
/// @return 0 on success, 1 if the context or either load/reload step failed.
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
std::string pluginV1Path = argc > 1 ? argv[1] : PLUGIN_V1_PATH;
|
std::string pluginV1Path = argc > 1 ? argv[1] : PLUGIN_V1_PATH;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/* CppWidget V1 -- "before your edit". See PluginV2/src/CppWidget.cpp
|
/**
|
||||||
|
* @file CppWidget.cpp
|
||||||
|
* @brief CppWidget V1 -- "before your edit". See PluginV2/src/CppWidget.cpp
|
||||||
* for "after": same three exports change behavior together, exactly
|
* for "after": same three exports change behavior together, exactly
|
||||||
* like PluginV1/src/Plugin.cpp's plain-C functions do.
|
* like PluginV1/src/Plugin.cpp's plain-C functions do. Every member
|
||||||
|
* defined here is documented in ../Shared/CppWidgetApi.h.
|
||||||
*/
|
*/
|
||||||
#include "CppWidgetApi.h"
|
#include "CppWidgetApi.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
/* PluginV1 -- "before your edit". See PluginV2/src/Plugin.cpp for
|
/**
|
||||||
|
* @file Plugin.cpp
|
||||||
|
* @brief PluginV1 -- "before your edit". See PluginV2/src/Plugin.cpp for
|
||||||
* "after"; the two deliberately export the identical set of names
|
* "after"; the two deliberately export the identical set of names
|
||||||
* (minus plugin_legacy_feature, removed in V2) so HotReloadContext can
|
* (minus plugin_legacy_feature, removed in V2) so HotReloadContext can
|
||||||
* line them up. See ../Shared/PluginApi.h for what each export is
|
* line them up. See ../Shared/PluginApi.h for what each export is
|
||||||
* there to demonstrate.
|
* there to demonstrate (and for the documentation of every function
|
||||||
|
* defined here).
|
||||||
*/
|
*/
|
||||||
#include "PluginApi.h"
|
#include "PluginApi.h"
|
||||||
|
|
||||||
@@ -22,9 +25,8 @@ int plugin_legacy_feature(int input)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
int plugin_tiny_function(int input)
|
void plugin_tiny_function(void)
|
||||||
{
|
{
|
||||||
return input + 1;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
/* CppWidget V2 -- "after your edit". See PluginV1/src/CppWidget.cpp
|
/**
|
||||||
* for "before".
|
* @file CppWidget.cpp
|
||||||
|
* @brief CppWidget V2 -- "after your edit". See PluginV1/src/CppWidget.cpp
|
||||||
|
* for "before". Every member defined here is documented in
|
||||||
|
* ../Shared/CppWidgetApi.h.
|
||||||
*/
|
*/
|
||||||
#include "CppWidgetApi.h"
|
#include "CppWidgetApi.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/* PluginV2 -- "after your edit". See PluginV1/src/Plugin.cpp for
|
/**
|
||||||
|
* @file Plugin.cpp
|
||||||
|
* @brief PluginV2 -- "after your edit". See PluginV1/src/Plugin.cpp for
|
||||||
* "before". plugin_tick's behavior changed, plugin_legacy_feature was
|
* "before". plugin_tick's behavior changed, plugin_legacy_feature was
|
||||||
* deleted, and plugin_new_feature was added -- exactly the three kinds
|
* deleted, and plugin_new_feature was added -- exactly the three kinds
|
||||||
* of change hot_reload.h's HotReloadSymbolEvent enum distinguishes.
|
* of change hot_reload.h's HotReloadSymbolEvent enum distinguishes.
|
||||||
|
* Every function defined here is documented in ../Shared/PluginApi.h.
|
||||||
*/
|
*/
|
||||||
#include "PluginApi.h"
|
#include "PluginApi.h"
|
||||||
|
|
||||||
@@ -24,9 +27,8 @@ int plugin_new_feature(int input)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
int plugin_tiny_function(int input)
|
void plugin_tiny_function(void)
|
||||||
{
|
{
|
||||||
return input + 2;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,88 @@
|
|||||||
/* CppWidgetApi.h -- a C++ (not extern "C") surface living alongside
|
/**
|
||||||
|
* @file CppWidgetApi.h
|
||||||
|
* @brief A C++ (not extern "C") surface living alongside
|
||||||
* PluginApi.h's plain C one, in the SAME plugin build. Every member
|
* PluginApi.h's plain C one, in the SAME plugin build. Every member
|
||||||
* function below keeps its real, compiler-mangled name; nothing here
|
* function below keeps its real, compiler-mangled name; nothing here
|
||||||
* needs special handling by source/HotReload, which classifies
|
* needs special handling by source/HotReload, which classifies
|
||||||
* symbols by their type and section, never by what their name looks
|
* symbols by their type and section, never by what their name looks
|
||||||
* like -- see hot_reload.h's "REQUIREMENTS ON A HOT-RELOADABLE
|
* like -- see the "Requirements on a hot-reloadable module" section of
|
||||||
* MODULE" and README.md's "Hot reloading C++ symbols" for the full
|
* hot_reload.h and the "Hot reloading C++ symbols" section of
|
||||||
* picture.
|
* source/HotReload/README.md for the full picture.
|
||||||
*
|
*
|
||||||
* The two methods below are reached two different ways from
|
* The two methods below are reached two different ways from
|
||||||
* Host/Main.cpp, deliberately: see that file's comments for why.
|
* Host/Main.cpp, deliberately: see that file's comments for why.
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A small C++ class whose members are defined in the plugin, not the host.
|
||||||
|
*
|
||||||
|
* Each member below keeps its real, compiler-mangled name and is reached a different way from
|
||||||
|
* Host/Main.cpp -- through the vtable, through a manually resolved mangled name, and (for the
|
||||||
|
* operator) not at all -- to show that all of them patch like any plain C function.
|
||||||
|
*/
|
||||||
class CppWidget
|
class CppWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Virtual, so that destroying a widget through a base pointer works across the
|
||||||
|
* boundary.
|
||||||
|
*/
|
||||||
virtual ~CppWidget() = default;
|
virtual ~CppWidget() = default;
|
||||||
|
|
||||||
/* Virtual: Host/Main.cpp calls this through ordinary C++
|
/**
|
||||||
|
* @brief Scales its input, through a virtual call.
|
||||||
|
*
|
||||||
|
* Virtual: Host/Main.cpp calls this through ordinary C++
|
||||||
* polymorphic dispatch (widget->virtualTick(x)) and needs no
|
* polymorphic dispatch (widget->virtualTick(x)) and needs no
|
||||||
* manual symbol lookup at all -- the call compiles down to an
|
* manual symbol lookup at all -- the call compiles down to an
|
||||||
* indirect call through the object's vtable slot, not a direct
|
* indirect call through the object's vtable slot, not a direct
|
||||||
* reference to this specific symbol, so it needs no link-time
|
* reference to this specific symbol, so it needs no link-time
|
||||||
* dependency on whichever plugin build actually defines it. */
|
* dependency on whichever plugin build actually defines it.
|
||||||
|
*
|
||||||
|
* @param input The value to scale.
|
||||||
|
* @return V1: `input * 2`. V2: `input * 3`.
|
||||||
|
*/
|
||||||
virtual int virtualTick(int input);
|
virtual int virtualTick(int input);
|
||||||
|
|
||||||
/* An ordinary out-of-line method. Calling this as widget->tick(x)
|
/**
|
||||||
|
* @brief Scales its input, through an ordinary out-of-line method.
|
||||||
|
*
|
||||||
|
* Calling this as widget->tick(x)
|
||||||
* would compile to a direct call to CppWidget::tick's own mangled
|
* would compile to a direct call to CppWidget::tick's own mangled
|
||||||
* symbol, which would need the Host to link against a plugin
|
* symbol, which would need the Host to link against a plugin
|
||||||
* build at build time -- exactly what hot-reloading a plugin
|
* build at build time -- exactly what hot-reloading a plugin
|
||||||
* means avoiding. So Host/Main.cpp instead resolves it manually
|
* means avoiding. So Host/Main.cpp instead resolves it manually
|
||||||
* by its mangled name via hot_reload_get_symbol(), the same way
|
* by its mangled name via hot_reload_get_symbol(), the same way
|
||||||
* it resolves PluginApi.h's plain C functions. */
|
* it resolves PluginApi.h's plain C functions.
|
||||||
|
*
|
||||||
|
* @param input The value to scale.
|
||||||
|
* @return V1: `input * 2`. V2: `input * 3`.
|
||||||
|
*/
|
||||||
int tick(int input);
|
int tick(int input);
|
||||||
|
|
||||||
/* An overloaded operator: mechanically just a function with an
|
/**
|
||||||
|
* @brief An overloaded operator.
|
||||||
|
*
|
||||||
|
* Mechanically just a function with an
|
||||||
* unusual mangled name, reached the same manual way as tick()
|
* unusual mangled name, reached the same manual way as tick()
|
||||||
* above. */
|
* above.
|
||||||
|
*
|
||||||
|
* @param other Ignored; the operator only prints which build it is.
|
||||||
|
* @return A new, empty widget.
|
||||||
|
*/
|
||||||
CppWidget operator+(const CppWidget& other) const;
|
CppWidget operator+(const CppWidget& other) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* The one entry point Host/Main.cpp looks up by a plain, human-typed
|
/**
|
||||||
|
* @brief Creates a widget.
|
||||||
|
*
|
||||||
|
* The one entry point Host/Main.cpp looks up by a plain, human-typed
|
||||||
* name -- extern "C" here for exactly the reason hot_reload.h
|
* name -- extern "C" here for exactly the reason hot_reload.h
|
||||||
* recommends it: something to hand a human-readable name to, without
|
* recommends it: something to hand a human-readable name to, without
|
||||||
* needing to know or hardcode a mangled string. */
|
* needing to know or hardcode a mangled string.
|
||||||
|
*
|
||||||
|
* @return A new widget. The caller owns it and must `delete` it (safe across the plugin
|
||||||
|
* boundary here because host and plugin share one C++ runtime and allocator).
|
||||||
|
*/
|
||||||
extern "C" CppWidget* make_cpp_widget();
|
extern "C" CppWidget* make_cpp_widget();
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
/* PluginApi.h -- the contract PluginV1 and PluginV2 both implement,
|
/**
|
||||||
|
* @file PluginApi.h
|
||||||
|
* @brief The contract PluginV1 and PluginV2 both implement,
|
||||||
* and the Host loads by name. Every function here follows
|
* and the Host loads by name. Every function here follows
|
||||||
* cpp-style-guide.md section 11's rule for anything loaded at runtime:
|
* cpp-style-guide.md section 11's rule for anything loaded at runtime:
|
||||||
* plain C linkage, no classes or exceptions crossing the boundary.
|
* plain C linkage, no classes or exceptions crossing the boundary.
|
||||||
@@ -15,31 +17,64 @@ extern "C"
|
|||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Changes behavior between V1 and V2 -- watch this one get
|
/**
|
||||||
* HOT_RELOAD_SYMBOL_PATCHED. */
|
* @brief Scales its input, and prints which build it is.
|
||||||
|
*
|
||||||
|
* Changes behavior between V1 and V2 -- watch this one get
|
||||||
|
* HOT_RELOAD_SYMBOL_PATCHED.
|
||||||
|
*
|
||||||
|
* @param input The value to scale.
|
||||||
|
* @return V1: `input * 2`. V2: `input * 3`.
|
||||||
|
*/
|
||||||
int plugin_tick(int input);
|
int plugin_tick(int input);
|
||||||
|
|
||||||
/* Only in V1, gone in V2 -- watch this one get
|
/**
|
||||||
* HOT_RELOAD_SYMBOL_REMOVED, and the Host's stub take over for any
|
* @brief A feature that exists only in V1.
|
||||||
* call still aimed at it afterwards. */
|
*
|
||||||
|
* Gone in V2 -- watch this one get HOT_RELOAD_SYMBOL_REMOVED, and the
|
||||||
|
* Host's stub take over for any call still aimed at it afterwards.
|
||||||
|
*
|
||||||
|
* @param input The value to echo.
|
||||||
|
* @return @p input, unchanged.
|
||||||
|
*/
|
||||||
int plugin_legacy_feature(int input);
|
int plugin_legacy_feature(int input);
|
||||||
|
|
||||||
/* Only in V2 -- watch this one get HOT_RELOAD_SYMBOL_ADDED, then
|
/**
|
||||||
* get called for the first time right after. */
|
* @brief A feature that exists only in V2.
|
||||||
|
*
|
||||||
|
* Watch this one get HOT_RELOAD_SYMBOL_ADDED, then get called for the
|
||||||
|
* first time right after.
|
||||||
|
*
|
||||||
|
* @param input The value to scale.
|
||||||
|
* @return `input * 10`.
|
||||||
|
*/
|
||||||
int plugin_new_feature(int input);
|
int plugin_new_feature(int input);
|
||||||
|
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
/* Deliberately too small to safely hold a jump on any of this
|
/**
|
||||||
* library's backends (its patchable-function-entry padding is
|
* @brief An empty function, too small to hold the jump this library writes.
|
||||||
* overridden down to zero) -- watch this one get
|
*
|
||||||
* HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than corrupt whatever
|
* Its patchable-function-entry padding is overridden down to zero, so
|
||||||
* comes after it. MSVC has no equivalent per-function override
|
* all that is left is the compiled empty body -- a handful of bytes,
|
||||||
* this demo could apply, so this one only exists on GCC/Clang.
|
* fewer than the jump any of this library's backends writes on x86-64
|
||||||
|
* and ARM64 at any optimization level. Watch this one get
|
||||||
|
* HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than corrupt whatever comes
|
||||||
|
* after it.
|
||||||
|
*
|
||||||
|
* Two configurations cannot promise "too small": unoptimized 32-bit x86
|
||||||
|
* (its jump is only 5 bytes, which even an empty unoptimized function
|
||||||
|
* reaches), and Windows, where a PE image carries no per-function size
|
||||||
|
* and the library can only estimate one from the distance to the next
|
||||||
|
* export. MSVC has no equivalent per-function override this demo could
|
||||||
|
* apply, so this one only exists on GCC/Clang.
|
||||||
*/
|
*/
|
||||||
__attribute__((patchable_function_entry(0))) int plugin_tiny_function(int input);
|
__attribute__((patchable_function_entry(0))) void plugin_tiny_function(void);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* An exported VARIABLE, not a function, sitting right next to
|
/**
|
||||||
|
* @brief The build's version number: 1 in V1, 2 in V2.
|
||||||
|
*
|
||||||
|
* An exported VARIABLE, not a function, sitting right next to
|
||||||
* real functions in the same library and changing value between
|
* real functions in the same library and changing value between
|
||||||
* V1 and V2 just like plugin_tick's behavior does -- watch this
|
* V1 and V2 just like plugin_tick's behavior does -- watch this
|
||||||
* one get HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE. This is the
|
* one get HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE. This is the
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* Main.cpp -- a persistent process, unlike source/examples/
|
/**
|
||||||
|
* @file Main.cpp
|
||||||
|
* @brief Interactive demo: a persistent process, unlike source/examples/
|
||||||
* HotReloadDemo/Host (which runs a fixed script and exits). This one
|
* HotReloadDemo/Host (which runs a fixed script and exits). This one
|
||||||
* loads Plugin/src/LivePlugin.cpp's compiled output once, then polls
|
* loads Plugin/src/LivePlugin.cpp's compiled output once, then polls
|
||||||
* that file's last-write time every second: whenever it changes (i.e.
|
* that file's last-write time every second: whenever it changes (i.e.
|
||||||
@@ -18,13 +20,18 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// Cleared by handleSigint(); the main loop polls this to exit cleanly on Ctrl+C.
|
||||||
std::atomic<bool> g_running{true};
|
std::atomic<bool> g_running{true};
|
||||||
|
|
||||||
|
/// SIGINT handler: requests a clean shutdown instead of the default terminate.
|
||||||
|
/// The signal number itself is unused (this is only ever registered for SIGINT).
|
||||||
void handleSigint(int /*signal*/)
|
void handleSigint(int /*signal*/)
|
||||||
{
|
{
|
||||||
g_running = false;
|
g_running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @param kind The event kind to name.
|
||||||
|
/// @return A short, human-readable label for @p kind.
|
||||||
const char* eventKindName(HotReloadSymbolEvent kind)
|
const char* eventKindName(HotReloadSymbolEvent kind)
|
||||||
{
|
{
|
||||||
switch (kind)
|
switch (kind)
|
||||||
@@ -45,6 +52,8 @@ namespace
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logs every symbol event except REMOVED, which onDeletedSymbol() already logs.
|
||||||
|
/// @param event Describes one symbol's outcome.
|
||||||
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
||||||
{
|
{
|
||||||
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
|
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
|
||||||
@@ -54,6 +63,9 @@ namespace
|
|||||||
std::printf(" [reload] %-24s %s\n", eventKindName(event->kind), event->name);
|
std::printf(" [reload] %-24s %s\n", eventKindName(event->kind), event->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stands in for @c live_tick once a rebuild removes it.
|
||||||
|
/// @param frame The current frame number.
|
||||||
|
/// @return Always -1.
|
||||||
int fallbackForDeletedTick(int frame)
|
int fallbackForDeletedTick(int frame)
|
||||||
{
|
{
|
||||||
std::printf(
|
std::printf(
|
||||||
@@ -63,6 +75,8 @@ namespace
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @param symbolName The removed function's real (possibly mangled) name.
|
||||||
|
/// @return The host fallback to redirect old callers to.
|
||||||
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
|
||||||
{
|
{
|
||||||
std::printf(" [reload] REMOVED %s -> falling back to the Host's stub\n", symbolName);
|
std::printf(" [reload] REMOVED %s -> falling back to the Host's stub\n", symbolName);
|
||||||
@@ -71,6 +85,11 @@ namespace
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the interactive demo described in this file's @ref Main.cpp "file comment".
|
||||||
|
/// @param argc Argument count.
|
||||||
|
/// @param argv `argv[1]`, if present, overrides the compiled-in plugin path.
|
||||||
|
/// @return 0 on clean shutdown (Ctrl+C), 1 if the context could not be created or the initial
|
||||||
|
/// load failed.
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
// Force line buffering regardless of whether stdout is a real
|
// Force line buffering regardless of whether stdout is a real
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* LivePlugin.cpp -- THE FILE TO EDIT.
|
/**
|
||||||
|
* @file LivePlugin.cpp
|
||||||
|
* @brief THE FILE TO EDIT.
|
||||||
*
|
*
|
||||||
* With source/examples/HotReloadLiveDemo/Host running in one
|
* With source/examples/HotReloadLiveDemo/Host running in one
|
||||||
* terminal, rebuild just this file from another with:
|
* terminal, rebuild just this file from another with:
|
||||||
@@ -42,13 +44,19 @@
|
|||||||
#include "hot_reload/hot_reload.h"
|
#include "hot_reload/hot_reload.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The one function this demo starts with; edit this file and rebuild to see it change live.
|
||||||
|
*
|
||||||
|
* See this file's own comment for what to try.
|
||||||
|
*
|
||||||
|
* @param frame The Host's running frame counter, incremented once per second.
|
||||||
|
* @return @p frame times the multiplier below.
|
||||||
|
*/
|
||||||
extern "C" HOT_RELOAD_EXPORT int live_tick(int frame)
|
extern "C" HOT_RELOAD_EXPORT int live_tick(int frame)
|
||||||
{
|
{
|
||||||
int multiplier = 2; // <- try changing this
|
int multiplier = 2; // <- try changing this
|
||||||
int value = frame * multiplier;
|
int value = frame * multiplier;
|
||||||
std::printf(" live_tick (%d) -> %d\n", frame, value);
|
std::printf(" live_tick(%d) -> %d\n", frame, value);
|
||||||
std::cout.flush();
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
/* Exercises source/HotReload end to end against the real PluginV1/
|
/**
|
||||||
|
* @file Main.cpp
|
||||||
|
* @brief Automated test suite: exercises source/HotReload end to end against the real PluginV1/
|
||||||
* PluginV2 builds from source/examples/HotReloadDemo -- see
|
* PluginV2 builds from source/examples/HotReloadDemo -- see
|
||||||
* ../../examples/HotReloadDemo/Shared/PluginApi.h and CppWidgetApi.h
|
* ../../examples/HotReloadDemo/Shared/PluginApi.h and CppWidgetApi.h
|
||||||
* for what each exported name is there to prove. Deliberately does
|
* for what each exported name is there to prove. Deliberately does
|
||||||
@@ -22,8 +24,14 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// Running count of failed CHECK() calls; main() returns non-zero when this is non-zero.
|
||||||
int g_failures = 0;
|
int g_failures = 0;
|
||||||
|
|
||||||
|
/// Records a failed check to stderr and increments g_failures. Called only through CHECK().
|
||||||
|
/// @param condition The check's result.
|
||||||
|
/// @param expression The checked expression, stringified.
|
||||||
|
/// @param file The source file the check appears in.
|
||||||
|
/// @param line The line the check appears on.
|
||||||
void checkImpl(bool condition, const char* expression, const char* file, int line)
|
void checkImpl(bool condition, const char* expression, const char* file, int line)
|
||||||
{
|
{
|
||||||
if (!condition)
|
if (!condition)
|
||||||
@@ -33,12 +41,18 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks @p expr and records a failure (with its source location) if it is false, without
|
||||||
|
/// relying on assert(), which NDEBUG (this project's default Release build) compiles out.
|
||||||
#define CHECK(expr) checkImpl((expr), #expr, __FILE__, __LINE__)
|
#define CHECK(expr) checkImpl((expr), #expr, __FILE__, __LINE__)
|
||||||
|
|
||||||
|
/// The signature every plain-C export in PluginApi.h shares.
|
||||||
using PluginFunction = int (*)(int);
|
using PluginFunction = int (*)(int);
|
||||||
|
|
||||||
|
/// Every symbol event seen so far in the current reload, keyed by symbol name.
|
||||||
std::map<std::string, HotReloadSymbolEvent> g_events;
|
std::map<std::string, HotReloadSymbolEvent> g_events;
|
||||||
|
|
||||||
|
/// Records each event into g_events for later CHECK()s to inspect.
|
||||||
|
/// @param event Describes one symbol's outcome.
|
||||||
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
|
||||||
{
|
{
|
||||||
g_events[event->name] = event->kind;
|
g_events[event->name] = event->kind;
|
||||||
@@ -46,6 +60,8 @@ namespace
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs every check described in this file's @ref Main.cpp "file comment".
|
||||||
|
/// @return 0 if every CHECK() passed, 1 otherwise.
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
HotReloadOptions options{};
|
HotReloadOptions options{};
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
/* A genuine ODR violation, kept deliberately: a non-inline static
|
/**
|
||||||
|
* @file Fixture.cpp
|
||||||
|
* @brief A genuine ODR violation, kept deliberately: a non-inline static
|
||||||
* class member that is declared but never given an out-of-class
|
* class member that is declared but never given an out-of-class
|
||||||
* definition. This compiles cleanly and links cleanly into a shared
|
* definition. This compiles cleanly and links cleanly into a shared
|
||||||
* library -- a shared library is allowed to keep undefined symbols by
|
* library -- a shared library is allowed to keep undefined symbols by
|
||||||
* default -- and only fails once something actually calls
|
* default -- and only fails once something actually calls
|
||||||
* trigger_undefined_symbol() below, ODR-using counter for the first
|
* trigger_undefined_symbol() below, ODR-using counter for the first
|
||||||
* time. See source/HotReload/README.md's "hot_reload_last_error()"
|
* time. See the "Diagnosing a failed load" section of
|
||||||
* section for the full story; HotReloadTests/Main.cpp loads this
|
* source/HotReload/README.md for the full story; HotReloadTests/Main.cpp
|
||||||
* fixture specifically to confirm hot_reload_last_error() reports
|
* loads this fixture specifically to confirm hot_reload_last_error()
|
||||||
* something useful for exactly this case, rather than leaving it a
|
* reports something useful for exactly this case, rather than leaving it
|
||||||
* silent "load failed" black box.
|
* a silent "load failed" black box.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/// A class with one static member that is deliberately never defined -- see this file's comment.
|
||||||
class HasUndefinedStaticMember
|
class HasUndefinedStaticMember
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/// ODR-uses counter, which is what actually triggers the undefined-symbol link error -- not
|
||||||
|
/// merely declaring or taking its address, and not merely loading this file's compiled output.
|
||||||
|
/// @return Never returns: linking the reference this creates is what fails.
|
||||||
static int useIt()
|
static int useIt()
|
||||||
{
|
{
|
||||||
return counter;
|
return counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static int counter; // declared, deliberately never defined anywhere
|
/// Declared, deliberately never defined anywhere -- see this file's comment.
|
||||||
|
static int counter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The one exported entry point: calls into the class above to force the undefined-symbol error.
|
||||||
|
/// @return Never returns a value in practice; see this file's comment.
|
||||||
extern "C" int trigger_undefined_symbol()
|
extern "C" int trigger_undefined_symbol()
|
||||||
{
|
{
|
||||||
return HasUndefinedStaticMember::useIt();
|
return HasUndefinedStaticMember::useIt();
|
||||||
|
|||||||
Reference in new issue
Block a user