CMake template clenaups and script adaptations

This commit is contained in:
alejo committed 2026-09-12 15:29:06 -03:00
1 parent 3087fcea8a
commit 9248925b77
19 files changed
+361 -170

No files matched your search

+5 -6
View File
@@ -2,16 +2,15 @@
cmake_minimum_required(VERSION 3.25)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
project(
DemoApp
HotReload
VERSION 0.1.0
DESCRIPTION "Demo app exercising the generator-friendly CMake module system"
DESCRIPTION "Cross-platform, cross-compiler C/C++ hot-reload library, plus its examples and tests"
LANGUAGES CXX
)
# Machine-friendly identifier baked into every target as PROJECT_NAME_CODE
# (used by BuildHelpers.cmake's _finalize_module_target). Edit this
# alongside the project() call above when retargeting this template.
set(PROJECT_CODE_NAME "demo")
# (used by BuildHelpers.cmake's _finalize_module_target).
set(PROJECT_CODE_NAME "hotreload")
# All output-path configuration (out/, install/, dist/, docs/, and the
# bin/lib subdirectory names) lives in Configuration.cmake -- included
@@ -206,7 +205,7 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/BuildHelpers.cmake")
# Third-party dependencies -- see cmake/Dependencies.cmake for
# add_vendor_header_only / add_vendor_subdirectory / add_dependency.
# A module declares its own dependencies in its own CMakeLists.txt (see
# source/Core/CMakeLists.txt for an example); nothing to add here.
# source/HotReload/CMakeLists.txt for an example); nothing to add here.
# -------------------------------------------------------
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Dependencies.cmake")
+170 -128
View File
@@ -1,13 +1,35 @@
# CMake module-system demo
# Hot Reload
A minimal C++20 project showing a per-module CMake setup: each module owns
a tiny, self-contained `CMakeLists.txt`, the root `CMakeLists.txt`
discovers modules automatically, three ways to bring in third-party code
(vendored, installed, or fetched) stay out of your warnings and your
compile database, every module gets its own browsable documentation via a
pinned Doxygen that's entirely opt-in (never required just to build),
Ninja itself is pinned and fetched the same way with a graceful fallback
if it's unavailable, and every output path is configured in one place.
A cross-platform, cross-compiler C/C++ library that patches a running
process's own code in place, so a new build of a function takes effect
immediately, without restarting -- see
[`source/HotReload/README.md`](source/HotReload/README.md) for what it
does, what it guarantees, and its honest limitations. This file covers
the project around it: the CMake module system, its tooling, and how
the examples and tests fit together.
```sh
scripts/setup.sh # optional, one-time -- see "Setup" below
scripts/build.sh Release -DBUILD_EXAMPLES=ON
./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
@@ -21,15 +43,12 @@ cmake/DoxygenPin.cmake shared version/URL/hash parameters for the pinne
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() -- see source/HotReload/README.md
source/Core/CMakeLists.txt library module "core_lib" (SHARED), depends on fmt
source/HelloApp/CMakeLists.txt executable module "hello_exe", depends on vendored tiny_ansi
source/FarewellApp/CMakeLists.txt executable module "farewell_exe", depends on vendored tiny_case
source/HotReload/CMakeLists.txt library module "hotreload_lib" (STATIC) -- see source/HotReload/README.md
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
vendor/tiny_ansi/ header-only vendored dependency (no build of its own)
vendor/tiny_case/ vendored dependency with its own CMakeLists.txt
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
@@ -39,8 +58,8 @@ scripts/refactor.sh, refactor.bat auto-fix formatting/naming across this projec
.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 -- now also applied for
real at source/HotReload/src/c-api/ and source/HotReload/include/hot_reload/
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
```
@@ -49,21 +68,20 @@ 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) a couple of lines:
Each module's `CMakeLists.txt` is (conventionally) one line:
```cmake
# source/Core/CMakeLists.txt
add_dependency(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 11.0.2
FIND_PACKAGE_ARGS NAMES fmt
)
add_lib_module(core_lib TYPE SHARED OUTPUT_NAME "demo_core" DEPENDS fmt::fmt)
# 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.
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`)
@@ -134,11 +152,16 @@ normal reconfigure doesn't spuriously touch its contents or mtime.
## Three ways to bring in a dependency (`cmake/Dependencies.cmake`)
| Helper | For | Example |
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` | `vendor/tiny_ansi/` |
| `add_vendor_subdirectory(SUBDIR)` | A vendored dependency with its own `CMakeLists.txt`, under `vendor/<SUBDIR>` | `vendor/tiny_case/` |
| `add_dependency(NAME <FetchContent_Declare args>)` | An installed package (`FIND_PACKAGE_ARGS ...`) with a fetch-from-source fallback | `fmt` in `source/Core/` |
| `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
@@ -153,10 +176,9 @@ code you don't control. Their targets are also dropped from
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 (`fmt::fmt`,
`tiny_case`, `tiny_ansi`, ...) 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.
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)
@@ -191,11 +213,11 @@ Once set up:
```bash
scripts/docs.sh # build docs for every module, prints where to open it
scripts/docs.sh core_lib # build docs for just one module (docs_<target>)
scripts/docs.sh hotreload_lib # build docs for just one module (docs_<target>)
```
```bat
scripts\docs.bat
scripts\docs.bat core_lib
scripts\docs.bat hotreload_lib
```
Output goes to `out/docs/<module>/html/index.html` (a sibling of
@@ -205,15 +227,8 @@ 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/Core/include/core/Greeting.hpp`:
```cpp
/// Builds a greeting for @p name.
///
/// @param name the person (or thing) being greeted.
/// @return a formatted greeting string, e.g. "Hello, Ale! (from demo_core)".
std::string makeGreeting(const std::string& name);
```
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/)
@@ -222,7 +237,7 @@ 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.
page -- `source/HotReload/README.md` already does this.
**Configuring it** -- nothing is required, but everything is overridable:
@@ -266,19 +281,24 @@ cmake -G Ninja -S . -B out/build -DCMAKE_CXX_COMPILER=clang++ -DDIST_TARGET=linu
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 -- see the file itself for
the full rules and rationale), enforced by the tooling described in
[`TOOLING.md`](TOOLING.md):
`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 template naming override (`snake_case`) for a C API subtree, if/when this project has one -- see below |
| `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 |
@@ -286,34 +306,48 @@ the full rules and rationale), enforced by the tooling described in
**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/Core # just one file or directory
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\Core
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. This is exactly how this project's own code
(`source/`) was brought into compliance when the toolkit was integrated --
see the renames noted further down.
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/`, `vendor/`, and `.git/` are never walked, so this can't
accidentally spend minutes reformatting/analyzing a fetched dependency's
entire source tree (verified against `out/build/_deps/fmt-src/`
specifically, since that's what triggered this exact problem before the
fix). Passing an explicit scope takes it as given: `refactor.sh` still
excludes `vendor`/`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\Core`).
`.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
@@ -330,42 +364,18 @@ 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.
Two adjustments made when integrating the toolkit into this specific
project (not part of the toolkit as provided -- see `TOOLING.md` in this
repo for the toolkit's own documentation of everything else):
- **`vendor/` is excluded** from the pre-commit hook's staged-file check
(`git diff ... ':!vendor/*'`). Vendored code (`cmake/Dependencies.cmake`)
isn't this project's code, so it isn't held to this style guide -- same
reasoning as its `compile_commands.json` and warning-flag exclusions
elsewhere in this project.
- **`.clang-tidy` gained one option**, `LocalConstantCase: camelBack`. The
style guide's own text says only *global/static* `const`/`constexpr`
values are "constants" for naming purposes -- everything else stays
under the `camelCase` Local Variables rule. Without this option, a plain
local `const` (e.g. `const std::string name = ...;` inside a function)
silently fell through to the general `PascalCase` constant rule instead,
which would have flagged completely ordinary code. Verified against a
small isolated test file: with the fix, a local `const` is accepted in
`camelCase` while a `static const`/`constexpr` still correctly requires
`PascalCase`.
`c-api/.clang-tidy` is included as a template, positioned exactly as the
toolkit ships it (`c-api/.clang-tidy`) -- this project doesn't currently
have a C API (Part II) surface. If you add one, move this file to that
subtree's root (`c-api/`, `public-include/`, wherever your `extern "C"`
headers live); `clang-tidy` always uses the closest `.clang-tidy` file up
the directory tree, so files under that directory automatically pick up
the C API's `snake_case` naming instead of this project's `camelCase`.
This project's own code was reformatted and renamed to comply when the
toolkit was integrated -- e.g. `Greeting.hpp`/`.cpp` (was `greeting.h`/
`.cpp`) and `makeGreeting`/`makeFarewell` (was `make_greeting`/
`make_farewell`) in `source/Core/`. `main.cpp` in each app module became
`Main.cpp` for the same reason (`PascalCase` file names) -- the style
guide doesn't call out an entry-point exception, so none was assumed;
rename back to lowercase if you'd rather treat that specific convention
as an exception.
**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
@@ -388,19 +398,32 @@ Any immediate child directory that contains its own `CMakeLists.txt` gets
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; order doesn't affect linking (CMake
resolves target names across the whole project at generate time --
verified, not assumed).
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), `git` (for
FetchContent's `GIT_REPOSITORY`-based dependencies like `fmt`), and
network access the first time you configure (for `fmt`, cached afterward
under `out/build/_deps`). Notably **not** in this list: Ninja and Doxygen
are both pinned and fetched automatically (see "Setup" below), not system
dependencies, and `scripts/build.sh` warns rather than fails if Ninja
isn't available at all (see "Build" below).
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
@@ -409,6 +432,11 @@ 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
@@ -443,14 +471,24 @@ Safe to re-run any time -- each step is a no-op if there's nothing to do
## Build
```bash
scripts/build.sh # Release (default)
scripts/build.sh Debug # or Debug / RelWithDebInfo / MinSizeRel
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 Debug
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.
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 --
@@ -474,19 +512,21 @@ a Visual Studio generator on Windows) instead.
## Run
```bash
scripts/run.sh hello_exe
scripts/run.sh farewell_exe -- Ale
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 hello_exe
scripts\run.bat farewell_exe -- Ale
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.
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
@@ -497,14 +537,16 @@ scripts/install.sh
scripts\install.bat
```
Always use the install script (or pass `--component demo` yourself if
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
demo` and always filtering on it is what keeps `cmake --install` scoped to
just this project's own artifacts.
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
@@ -517,7 +559,7 @@ just this project's own artifacts.
```cmake
# source/MyTool/CMakeLists.txt
add_exe_module(my_tool_exe OUTPUT_NAME "my-tool" DEPENDS core_lib)
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,
+2 -2
View File
@@ -217,7 +217,7 @@ endfunction()
# add_lib_module(NAME [TYPE STATIC|SHARED] [OUTPUT_NAME name] [DEPENDS ...]
# [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from a library module's own CMakeLists.txt (e.g. source/Core/CMakeLists.txt).
# Called from a library module's own CMakeLists.txt (e.g. source/HotReload/CMakeLists.txt).
# Sources/headers are globbed relative to that module's own directory:
# src/*.cpp and include/*.h(pp) underneath it. TYPE picks STATIC or SHARED
# for this module only; omit it to use the project-wide default
@@ -283,7 +283,7 @@ endmacro()
# =============================================================================
# add_exe_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from an app module's own CMakeLists.txt (e.g. HelloApp/CMakeLists.txt).
# Called from an app module's own CMakeLists.txt (e.g. HotReloadDemo/Host/CMakeLists.txt).
# Sources/headers are globbed relative to that module's own directory:
# *.cpp and include/*.h(pp) underneath it.
# =============================================================================
+2 -2
View File
@@ -26,7 +26,7 @@
# -DDOXYGEN_<ANY_DOXYFILE_TAG>=... any Doxyfile tag can be set this way --
# see https://www.doxygen.nl/manual/config.html
# A module can opt out with NO_DOCS, e.g.:
# add_lib_module(core_lib TYPE SHARED NO_DOCS ...)
# add_lib_module(hotreload_lib TYPE STATIC NO_DOCS ...)
# =============================================================================
# =============================================================================
@@ -65,7 +65,7 @@
# -DDOXYGEN_<ANY_DOXYFILE_TAG>=... any Doxyfile tag can be set this way --
# see https://www.doxygen.nl/manual/config.html
# A module can opt out with NO_DOCS, e.g.:
# add_lib_module(core_lib TYPE SHARED NO_DOCS ...)
# add_lib_module(hotreload_lib TYPE STATIC NO_DOCS ...)
# =============================================================================
include_guard(GLOBAL)
+28
View File
@@ -52,3 +52,31 @@ function(hot_reload_enable_patchable_functions TARGET_NAME)
"see HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL in hot_reload.h.")
endif()
endfunction()
# Restricts TARGET's export surface to only what's explicitly marked
# with HOT_RELOAD_EXPORT (hot_reload.h) -- the opposite of both
# GCC/Clang's default (export every symbol with external linkage) and
# WINDOWS_EXPORT_ALL_SYMBOLS. Narrower, not quieter: see the top-level
# README's "Why do std::string/std::vector symbols show up as
# PATCHED?" section for the one specific kind of noise this
# deliberately does NOT remove, and why removing it would need
# excluding all weak-linkage symbols by default -- which would just as
# happily hide a hot-reloadable template of your OWN.
#
# What this DOES remove: your own internal helpers you never meant to
# expose, and (on GCC/Clang, if the target statically links its C++
# runtime) incidental runtime-library internals -- both real,
# observed cases from building this library and its examples.
function(hot_reload_restrict_exports TARGET_NAME)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(${TARGET_NAME} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
elseif(MSVC)
# MSVC already exports nothing by default -- WINDOWS_EXPORT_ALL_SYMBOLS
# is what opts OUT of that; simply not setting it (and not adding
# individual __declspec(dllexport)/HOT_RELOAD_EXPORT other than where
# you want them) is this function's Windows equivalent. Nothing to
# apply here, but calling this on every platform your target builds
# for, unconditionally, is still the point -- see hot_reload_enable_
# patchable_functions() immediately above for the same pattern.
endif()
endfunction()
+11 -2
View File
@@ -2,10 +2,18 @@
REM Configure (if needed) and build the project, preferably with Ninja.
REM
REM Usage (from the project root):
REM scripts\build.bat [BUILD_TYPE]
REM scripts\build.bat [BUILD_TYPE] [extra cmake -D args...]
REM
REM BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel (default: Release)
REM
REM Anything after BUILD_TYPE is passed straight through to cmake's
REM configure step (up to 8 extra arguments -- %2 through %9, batch's own
REM positional-parameter limit), e.g.:
REM scripts\build.bat Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
REM CMake caches -D options across reconfigures, so passing them once and
REM then calling scripts\build.bat plain afterward for incremental builds
REM keeps them set -- no need to repeat them on every call.
REM
REM Set BUILD_DIR to build somewhere other than out\build (see Configuration.cmake).
setlocal enabledelayedexpansion
@@ -15,6 +23,7 @@ if "%BUILD_DIR%"=="" set "BUILD_DIR=out\build"
set "BUILD_TYPE=%~1"
if "%BUILD_TYPE%"=="" set "BUILD_TYPE=Release"
if "%TOOLCACHE_DIR%"=="" set "TOOLCACHE_DIR=.cache\tools"
set "EXTRA_CMAKE_ARGS=%2 %3 %4 %5 %6 %7 %8 %9"
where cmake >nul 2>nul
if errorlevel 1 (
@@ -60,7 +69,7 @@ set "GENERATOR_ARGS="
:configure
echo ==^> Configuring (%BUILD_TYPE%) into %BUILD_DIR%\
cmake %GENERATOR_ARGS% -S . -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=%BUILD_TYPE%
cmake %GENERATOR_ARGS% -S . -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=%BUILD_TYPE% %EXTRA_CMAKE_ARGS%
if errorlevel 1 exit /b 1
echo ==^> Building
+13 -2
View File
@@ -2,10 +2,17 @@
# Configure (if needed) and build the project, preferably with Ninja.
#
# Usage (from the project root):
# scripts/build.sh [BUILD_TYPE]
# scripts/build.sh [BUILD_TYPE] [extra cmake -D args...]
#
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel (default: Release)
#
# Anything after BUILD_TYPE is passed straight through to cmake's
# configure step, e.g.:
# scripts/build.sh Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
# CMake caches -D options across reconfigures, so passing them once and
# then calling `scripts/build.sh` plain afterward for incremental builds
# keeps them set -- no need to repeat them on every call.
#
# Set BUILD_DIR to build somewhere other than out/build (see Configuration.cmake).
set -euo pipefail
shopt -s nullglob
@@ -15,6 +22,10 @@ cd "$SCRIPT_DIR/.."
BUILD_DIR="${BUILD_DIR:-out/build}"
BUILD_TYPE="${1:-Release}"
if [ "$#" -gt 0 ]; then
shift
fi
EXTRA_CMAKE_ARGS=("$@")
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
@@ -51,7 +62,7 @@ else
fi
echo "==> Configuring (${BUILD_TYPE}) into ${BUILD_DIR}/"
cmake "${GENERATOR_ARGS[@]}" -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}"
cmake "${GENERATOR_ARGS[@]}" -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" "${EXTRA_CMAKE_ARGS[@]}"
echo "==> Building"
cmake --build "${BUILD_DIR}"
+1 -1
View File
@@ -11,7 +11,7 @@ REM place either way -- this only affects the message below).
REM
REM Usage (from the project root):
REM scripts\docs.bat REM every module
REM scripts\docs.bat core_lib REM just one module's docs (docs_<target>)
REM scripts\docs.bat hotreload_lib REM just one module's docs (docs_<target>)
setlocal
cd /d "%~dp0.."
+1 -1
View File
@@ -11,7 +11,7 @@
#
# Usage (from the project root):
# scripts/docs.sh # every module
# scripts/docs.sh core_lib # just one module's docs (docs_<target>)
# scripts/docs.sh hotreload_lib # just one module's docs (docs_<target>)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+1 -1
View File
@@ -18,7 +18,7 @@ cd /d "%~dp0.."
if "%BUILD_DIR%"=="" set "BUILD_DIR=out\build"
set "BUILD_TYPE=%~1"
if "%BUILD_TYPE%"=="" set "BUILD_TYPE=Release"
set "COMPONENT=demo"
set "COMPONENT=hotreload"
if not exist "%BUILD_DIR%" (
echo error: "%BUILD_DIR%" not found. Run scripts\build.bat first.
+1 -1
View File
@@ -21,7 +21,7 @@ cd "$SCRIPT_DIR/.."
BUILD_DIR="${BUILD_DIR:-out/build}"
BUILD_TYPE="${1:-Release}"
COMPONENT="demo" # must match PROJECT_CODE_NAME in CMakeLists.txt
COMPONENT="hotreload" # must match PROJECT_CODE_NAME in CMakeLists.txt
if [ ! -d "$BUILD_DIR" ]; then
echo "error: '${BUILD_DIR}' not found. Run scripts/build.sh first." >&2
+4 -4
View File
@@ -11,10 +11,10 @@ REM entire source tree the way scanning the whole repo from "." would.
REM
REM Usage, from the project root: scripts\refactor.bat [optional path]
REM scripts\refactor.bat every file under source\ and c-api\
REM scripts\refactor.bat source\Core just one file or directory -- taken
REM as given, with no exclusion filter,
REM so pick a path that doesn't overlap
REM with out\/.cache\/vendor\/.git\
REM scripts\refactor.bat source\HotReload just one file or directory --
REM taken as given, with no exclusion
REM filter, so pick a path that doesn't
REM overlap with out\/.cache\/.git\
setlocal enabledelayedexpansion
cd /d "%~dp0.."
+1 -1
View File
@@ -14,7 +14,7 @@
#
# Usage (from the project root):
# scripts/refactor.sh # every file under source/ and c-api/
# scripts/refactor.sh source/Core # just one file or directory
# scripts/refactor.sh source/HotReload # just one file or directory
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+2 -2
View File
@@ -8,8 +8,8 @@ REM Usage (from the project root):
REM scripts\run.bat <target-name> [-- program-args...]
REM
REM Examples:
REM scripts\run.bat hello_exe
REM scripts\run.bat farewell_exe -- Ale
REM scripts\run.bat hotreload_demo_host
REM scripts\run.bat hotreload_live_demo_host
setlocal enabledelayedexpansion
cd /d "%~dp0.."
+2 -2
View File
@@ -8,8 +8,8 @@
# scripts/run.sh <target-name> [-- program-args...]
#
# Examples:
# scripts/run.sh hello_exe
# scripts/run.sh farewell_exe -- Ale
# scripts/run.sh hotreload_demo_host
# scripts/run.sh hotreload_live_demo_host
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+80 -13
View File
@@ -195,6 +195,57 @@ holds on Linux/macOS as tested here; it's worth double-checking on
Windows if you link the CRT in an unusual way (e.g. mismatched
static/dynamic CRT between the host and a plugin).
## Why do std::string/std::vector symbols show up as PATCHED?
Because they're real, addressable functions too, and classification
doesn't -- can't -- know that a symbol "belongs to the standard library"
as opposed to your own code. Confirmed directly: any plugin that uses
`std::string`/`std::vector<T>` internally will show template
instantiations like `std::vector<int>::_M_realloc_insert<...>` getting
`HOT_RELOAD_SYMBOL_PATCHED` on every single reload, even one that
changed nothing about how you use them -- because these are genuinely
present, genuinely exported (with default visibility, by default, on
every build tested here), and genuinely at a new address every time,
since that's just true of everything in a freshly-loaded `.so`/`.dll`.
**This is safe.** It's the exact same template instantiation in both
builds -- same mangled name, same object layout, same ABI, and (using
the same compiler and standard library on both sides, which you already
need for mangled names to match at all -- see above) functionally
equivalent code. Redirecting old calls to the new copy changes where the
logic runs, never what it does.
**It is not fixable by hiding symbols, and that's not just an
unimplemented feature.** The obvious fix -- compile with
`-fvisibility=hidden -fvisibility-inlines-hidden` -- does not work here,
confirmed directly: `libstdc++`'s headers mark these template
instantiations `default` visibility explicitly, in the header itself,
specifically so multiple `.so`s can share type identity for RTTI and
exception handling across shared-library boundaries. That annotation
wins over your translation unit's own `-fvisibility=hidden` default,
on purpose, and this library has no way to override it that wouldn't
also risk breaking that cross-DSO guarantee for anyone who needs it.
**A blanket "don't patch weak-linkage symbols" rule would be the wrong
fix even if it worked**, which is why this library doesn't do that
either: weak/COMDAT linkage is also exactly how a *hot-reloadable
template of your own* would be classified. A rule that reliably filters
out the standard library's internals would just as reliably filter out
your own reloadable generic code.
**What actually helps, if the log's noise level bothers you, is
narrowing your plugin's export surface** with
`hot_reload_restrict_exports()` (`cmake/HotReload.cmake`) and
`HOT_RELOAD_EXPORT` (`hot_reload.h`) -- see "Using it in your own
module" below. This doesn't remove the standard-library instantiations
specifically (see above), but it does stop your own internal helpers,
and (on GCC/Clang, if a target statically links its C++ runtime)
incidental runtime-library internals, from becoming part of the diff at
all -- both real cases observed while building this library's own
examples. Beyond that, filtering the event log for *display* is a
one-line addition in your own `on_symbol_event` callback -- e.g. skip
anything whose `hot_reload_demangle()`'d name starts with `"std::"`.
## Scope: what's actually implemented and checked
| | x86 | x86-64 | ARM64 | ARM32 |
@@ -364,9 +415,11 @@ fix this in any shared library, hot-reloaded or not.
## Using it in your own module
1. Every function you want reloadable needs C linkage --
`extern "C"` -- for the same reason any `dlopen()`'d interface
needs it (see `hot_reload.h`'s file comment and style guide §11).
1. `extern "C"` isn't required -- a real C++ mangled name works too
(see "Hot reloading C++ symbols" above) -- but it's still the
simplest choice for anything you want to look up by a plain,
human-typed name, and the only way to get a name stable across
compilers.
2. Build it as its own `SHARED` library target and call
`hot_reload_enable_patchable_functions(<target>)`
(`cmake/HotReload.cmake`) on it, so every exported function has
@@ -374,16 +427,30 @@ fix this in any shared library, hot-reloaded or not.
any function whose compiled body happens to be large enough on its
own -- functions that aren't get skipped
(`HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL`), not corrupted.
3. On Windows, also set `WINDOWS_EXPORT_ALL_SYMBOLS ON` on that
target, or add explicit `__declspec(dllexport)` to each function
yourself -- MSVC exports nothing by default. (Prefer explicit
per-symbol export in a real project, even though the demo uses the
blanket property to match this repo's existing `add_lib_module()`
convention: exporting everything means anything your build happens
to pull in -- including, as this project's own Windows testing
turned up, incidental runtime-library internals -- becomes part of
your reloadable surface too, redirected right along with the
functions you actually meant.)
3. Decide how much of the target's export surface you want to be
deliberate about:
- **Simplest**: do nothing else. On GCC/Clang/MinGW this exports
every symbol with external linkage by default; on MSVC, also set
`WINDOWS_EXPORT_ALL_SYMBOLS ON` to match (MSVC exports nothing by
default otherwise). `source/examples/HotReloadDemo`'s plugins use
this style.
- **Recommended for a real project**: call
`hot_reload_restrict_exports(<target>)` (`cmake/HotReload.cmake`)
instead, and mark each intended entry point with `HOT_RELOAD_EXPORT`
(`hot_reload.h`) -- `extern "C" HOT_RELOAD_EXPORT int my_function(...)`
for a plain-name free function (that exact order -- GCC/Clang want
the attribute after `extern "C"`), or just `HOT_RELOAD_EXPORT` alone
on a C++ class/method you want exported with its real mangled name
intact. This keeps anything you didn't explicitly mark -- your own
internal helpers, or (on GCC/Clang, if the target statically links
its C++ runtime) incidental runtime-library internals -- out of the
diff entirely. It does *not* narrow out `std::string`/`std::vector`
template instantiations specifically; see "Why do std::string/
std::vector symbols show up as PATCHED?" above for why not, and why
that's fine either way. `source/examples/HotReloadLiveDemo`'s
plugin uses this style -- compare the two examples' exported symbol
lists (`nm -D --defined-only <path> | c++filt`) to see the
difference directly.
4. Have your host `hot_reload_load()` it once at startup and
`hot_reload_reload()` it after every rebuild, from a point where
nothing else is calling into it (see "What this cannot detect"
@@ -90,6 +90,29 @@
#endif
#endif
/* Marks a single declaration for export under
* hot_reload_restrict_exports() (cmake/HotReload.cmake) without
* otherwise changing its linkage: pair with extern "C" yourself for a
* free function you want a stable, human-typable name for (see this
* file's own comment on when you'd want that) -- extern "C" first,
* then this macro, then the return type, e.g.
* `extern "C" HOT_RELOAD_EXPORT int my_function(...)`, which is the
* one ordering GCC/Clang accept without a warning here. Leave the
* extern "C" off to keep a C++ class or method's real mangled name,
* e.g. a virtual method a host only ever reaches through a vtable
* slot (see the top-level README's "Hot reloading C++ symbols"
* section).
*
* hot_reload_restrict_exports() is a narrower export surface, not a
* quieter one: see the README's "Why do std::string/std::vector
* symbols show up as PATCHED?" section for why it does not, and
* structurally cannot, suppress those specifically. */
#ifdef _WIN32
#define HOT_RELOAD_EXPORT __declspec(dllexport)
#else
#define HOT_RELOAD_EXPORT __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C"
{
@@ -17,12 +17,22 @@ endif()
set_target_properties(hotreload_live_plugin PROPERTIES
OUTPUT_NAME "hotreload_live_plugin"
WINDOWS_EXPORT_ALL_SYMBOLS ON)
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
target_include_directories(hotreload_live_plugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../../HotReload/include)
_set_output_dirs(hotreload_live_plugin
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
_apply_compile_options(hotreload_live_plugin)
hot_reload_enable_patchable_functions(hotreload_live_plugin)
# Demonstrates the narrower-export pattern (see hot_reload.h's
# HOT_RELOAD_EXPORT and this function's own comment in
# cmake/HotReload.cmake) -- contrast with HotReloadDemo's PluginV1/
# PluginV2, which deliberately leave the GCC/Clang/MinGW default
# (export everything) in place to show that style too. Both are
# correct; this is the one this project would actually recommend.
hot_reload_restrict_exports(hotreload_live_plugin)
_register_module_target(EXAMPLE hotreload_live_plugin)
add_dependencies(all_examples hotreload_live_plugin)
@@ -39,9 +39,11 @@
* never patched, exactly like plugin_build_marker in
* source/examples/HotReloadDemo.
*/
#include "hot_reload/hot_reload.h"
#include <cstdio>
extern "C" int live_tick(int frame)
extern "C" HOT_RELOAD_EXPORT int live_tick(int frame)
{
int multiplier = 2; // <- try changing this
int value = frame * multiplier;