Add project files.

This commit is contained in:
alejo committed 2026-09-12 00:08:03 -03:00
commit 3087fcea8a
71 files changed
+8325

No files matched your search

@@ -0,0 +1,5 @@
# See source/examples/HotReloadDemo/CMakeLists.txt for why this
# grouping file needs to exist at all (one-level-deep discovery in
# cmake/BuildHelpers.cmake).
add_subdirectory(Plugin)
add_subdirectory(Host)
@@ -0,0 +1,8 @@
add_example_module(hotreload_live_demo_host OUTPUT_NAME "hotreload_live_demo_host" DEPENDS hotreload_lib)
# Build-order-only, exactly like HotReloadDemo/Host -- see that file's
# CMakeLists.txt comment. This Host must NOT link against the plugin
# directly; it loads it dynamically through source/HotReload instead.
add_dependencies(hotreload_live_demo_host hotreload_live_plugin)
target_compile_definitions(hotreload_live_demo_host PRIVATE
"LIVE_PLUGIN_PATH=\"$<TARGET_FILE:hotreload_live_plugin>\"")
@@ -0,0 +1,202 @@
/* Main.cpp -- a persistent process, unlike source/examples/
* HotReloadDemo/Host (which runs a fixed script and exits). This one
* loads Plugin/src/LivePlugin.cpp's compiled output once, then polls
* that file's last-write time every second: whenever it changes (i.e.
* you rebuilt it), it reloads automatically. Run this, then edit and
* rebuild the plugin in another terminal -- see LivePlugin.cpp's own
* comment for exactly what to try. Stop with Ctrl+C.
*/
#include "hot_reload/hot_reload.h"
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <filesystem>
#include <string>
#include <thread>
namespace
{
std::atomic<bool> g_running{true};
void handleSigint(int /*signal*/)
{
g_running = false;
}
const char* eventKindName(HotReloadSymbolEvent kind)
{
switch (kind)
{
case HOT_RELOAD_SYMBOL_PATCHED:
return "PATCHED";
case HOT_RELOAD_SYMBOL_ADDED:
return "ADDED";
case HOT_RELOAD_SYMBOL_REMOVED:
return "REMOVED";
case HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE:
return "SKIPPED (data, not code)";
case HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL:
return "SKIPPED (too small to patch)";
case HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE:
return "SKIPPED (page not writable)";
}
return "?";
}
void onSymbolEvent(const HotReloadSymbolEventInfo* event, void* /*userData*/)
{
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
{
return; // reported, with more context, by onDeletedSymbol below
}
std::printf(" [reload] %-24s %s\n", eventKindName(event->kind), event->name);
}
int fallbackForDeletedTick(int frame)
{
std::printf(
" live_tick was deleted -- this is the Host's fallback (frame %d). Bring it back under the same "
"name and rebuild to try LivePlugin.cpp's suggestion #2 through to the end.\n",
frame);
return -1;
}
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
{
std::printf(" [reload] REMOVED %s -> falling back to the Host's stub\n", symbolName);
return reinterpret_cast<void*>(&fallbackForDeletedTick);
}
}
int main(int argc, char** argv)
{
// Force line buffering regardless of whether stdout is a real
// terminal: this is a "watch it happen live" demo, and glibc (and
// most libc implementations) fully buffer stdout instead of
// line-buffering it the moment it's not connected to a TTY -- a
// redirect to a log file, a `| tee`, or some IDEs' embedded
// terminals all trigger this. Without this call, output can sit
// in the buffer for a long time (observed directly while building
// this: zero output appeared for several seconds of real,
// correctly-running execution until the buffer was force-flushed).
std::setvbuf(stdout, nullptr, _IOLBF, 4096);
std::string pluginPath = argc > 1 ? argv[1] : LIVE_PLUGIN_PATH;
std::printf("================================================================\n");
std::printf(" Hot Reload -- live demo\n");
std::printf("================================================================\n");
std::printf(" Watching: %s\n", pluginPath.c_str());
std::printf(" Edit: source/examples/HotReloadLiveDemo/Plugin/src/LivePlugin.cpp\n");
std::printf(" Rebuild: cmake --build <your build dir> --target hotreload_live_plugin\n");
std::printf(" (or run watch.sh, in this example's own directory, in another\n");
std::printf(" terminal to rebuild automatically every time you save)\n");
std::printf(" Stop: Ctrl+C\n");
std::printf("================================================================\n\n");
std::signal(SIGINT, &handleSigint);
HotReloadOptions options{};
options.on_symbol_event = &onSymbolEvent;
options.on_deleted_symbol = &onDeletedSymbol;
HotReloadContext* ctx = hot_reload_context_create(&options);
if (ctx == nullptr)
{
std::fprintf(stderr, "hot_reload_context_create failed\n");
return 1;
}
if (hot_reload_load(ctx, "live", pluginPath.c_str()) != HOT_RELOAD_OK)
{
std::fprintf(stderr,
"Could not load '%s': %s\n"
"(if the file exists but this still fails, that's usually a genuinely undefined symbol in the "
"plugin -- e.g. a non-inline static class member that's declared but never defined, only caught "
"once something actually calls into the code path that uses it; the message above names it)\n",
pluginPath.c_str(), hot_reload_last_error(ctx));
hot_reload_context_destroy(ctx);
return 1;
}
// Resolved ONCE, here, and deliberately never re-resolved below --
// the whole point of this demo is watching this same pointer keep
// working, correctly, across as many reloads as you make. The one
// case where that stops being true: if you delete live_tick (see
// LivePlugin.cpp suggestion #2) and later re-add it under the same
// name, the re-added one is a NEW, unrelated symbol as far as
// diffing is concerned -- there is no "old" address to patch for
// it, so it comes back as HOT_RELOAD_SYMBOL_ADDED, not PATCHED, and
// this specific pointer (already redirected to the fallback below)
// keeps running that fallback rather than picking the new one back
// up automatically. Restart the Host to resolve it fresh, or call
// hot_reload_get_symbol() again yourself, if you want to see that.
using TickFunction = int (*)(int);
auto tick = reinterpret_cast<TickFunction>(hot_reload_get_symbol(ctx, "live", "live_tick"));
std::error_code timeStatus;
auto lastWriteTime = std::filesystem::last_write_time(pluginPath, timeStatus);
int frame = 0;
while (g_running)
{
std::error_code currentTimeStatus;
auto currentWriteTime = std::filesystem::last_write_time(pluginPath, currentTimeStatus);
if (!currentTimeStatus && currentWriteTime != lastWriteTime)
{
// A short settle delay for a linker that might still be
// mid-write on a slower filesystem -- not load-bearing for
// correctness either way: a reload attempted too early
// just fails cleanly (hot_reload_reload() never disturbs
// the running version on failure) and gets retried next
// iteration, since lastWriteTime below is only updated on
// success.
std::this_thread::sleep_for(std::chrono::milliseconds(150));
std::printf("\n-- change detected in %s, reloading --\n", pluginPath.c_str());
HotReloadStatus status = hot_reload_reload(ctx, "live", pluginPath.c_str());
if (status == HOT_RELOAD_OK)
{
lastWriteTime = currentWriteTime;
}
else
{
std::printf("-- reload failed: %s -- still running the previous version; fix and save again --\n",
hot_reload_last_error(ctx));
}
std::printf("\n");
}
if (tick != nullptr)
{
tick(frame);
}
else
{
std::printf(" (live_tick not found -- build the plugin; see instructions above)\n");
}
// Unlike tick above, deliberately re-resolved every frame:
// live_extra is not expected to exist until you add it (see
// LivePlugin.cpp suggestion #3), so the Host has to keep
// checking rather than assume it already knows the answer.
// This is a cheap lookup over a handful of symbols, not a
// reload -- fine to do every frame.
using ExtraFunction = int (*)(int);
auto extra = reinterpret_cast<ExtraFunction>(hot_reload_get_symbol(ctx, "live", "live_extra"));
if (extra != nullptr)
{
extra(frame);
}
++frame;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
std::printf("\nShutting down.\n");
hot_reload_context_destroy(ctx);
return 0;
}
@@ -0,0 +1,28 @@
# The one target this whole example is about rebuilding, in place,
# while Host is already running and has it loaded. Hand-written rather
# than add_lib_module() for the same reason as HotReloadDemo/PluginV1
# -- see that file's comment -- even though there's only one build of
# this one: what makes this target unusual isn't needing two versions,
# it's needing to be a completely ordinary, repeatedly-overwritable
# build output while another process holds an OLDER build of it open
# via source/HotReload (see that library's HotReloadContext::
# stagePathForLoad() for why the running Host never itself locks this
# exact path).
if(BUILD_EXAMPLES)
add_library(hotreload_live_plugin SHARED src/LivePlugin.cpp)
else()
add_library(hotreload_live_plugin SHARED EXCLUDE_FROM_ALL src/LivePlugin.cpp)
endif()
set_target_properties(hotreload_live_plugin PROPERTIES
OUTPUT_NAME "hotreload_live_plugin"
WINDOWS_EXPORT_ALL_SYMBOLS ON)
_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)
_register_module_target(EXAMPLE hotreload_live_plugin)
add_dependencies(all_examples hotreload_live_plugin)
@@ -0,0 +1,50 @@
/* LivePlugin.cpp -- THE FILE TO EDIT.
*
* With source/examples/HotReloadLiveDemo/Host running in one
* terminal, rebuild just this file from another with:
*
* cmake --build <your build dir> --target hotreload_live_plugin
*
* or run watch.sh (in this same directory) in that other terminal to
* have it rebuild automatically every time you save. Either way, the
* running Host notices within about a second of a successful rebuild
* and hot-reloads -- no restart.
*
* Things worth trying, roughly in order:
*
* 1. Change the message or the multiplier in live_tick() below and
* rebuild -- watch the running Host's output change mid-flight,
* still counting the same frame number it already had.
*
* 2. Delete live_tick() entirely (or rename it) and rebuild -- the
* Host supplies a fallback for exactly this case, so you'll see
* that kick in instead of a crash. Bring the function back under
* the same name and rebuild again... and notice it does NOT
* automatically take back over -- see the Host's own comment on
* why, right where it resolves live_tick.
*
* 3. Add a brand new function:
*
* extern "C" int live_extra(int frame) { ... }
*
* and rebuild -- the Host looks for this one on every frame and
* starts calling it the moment it exists.
*
* 4. Add a new exported variable, e.g.
*
* extern "C" int live_marker = 1;
*
* then change its value on a later edit and rebuild -- watch the
* reload log call it out as data, classified and left alone,
* never patched, exactly like plugin_build_marker in
* source/examples/HotReloadDemo.
*/
#include <cstdio>
extern "C" int live_tick(int frame)
{
int multiplier = 2; // <- try changing this
int value = frame * multiplier;
std::printf(" live_tick(%d) -> %d\n", frame, value);
return value;
}
@@ -0,0 +1,81 @@
# Hot Reload -- live demo
Unlike `source/examples/HotReloadDemo` (a fixed script that loads two
pre-built versions and exits), this one is interactive: a host that
keeps running, and one plugin file you edit and rebuild yourself,
however many times you like, while it's live.
## Run it
```sh
cmake -S . -B out/build -DBUILD_EXAMPLES=ON
cmake --build out/build
./out/build/bin/examples/hotreload_live_demo_host
```
Leave that running. In another terminal, either rebuild by hand after
each edit:
```sh
cmake --build out/build --target hotreload_live_plugin
```
or run the watcher in this directory so saving is all you need to do:
```sh
source/examples/HotReloadLiveDemo/watch.sh out/build
```
Then open `Plugin/src/LivePlugin.cpp` -- it has its own comment with
several concrete things worth trying, in order (change a value, delete
the function, add a new one, add a data symbol). Each one lines up
with something `source/HotReload/README.md`'s "Hot reloading C++
symbols" and "The two guarantees" sections describe -- this is a place
to go watch each of those claims happen, not just read about them.
## What you should see
Editing and saving `live_tick()`'s multiplier and rebuilding should
change the running Host's printed values within about a second, on
the frame count it was already at -- not a restart. This was verified
in exactly this shape while building the feature: host running,
`watch.sh` running, and nothing else done except editing the file and
saving -- the running process picked up the new multiplier on its own
a couple of seconds later.
## If a reload doesn't seem to happen
- Check the target actually rebuilt -- `watch.sh` prints a build
failure clearly rather than silently doing nothing; a plain compile
error leaves the previous `.so`/`.dll` completely untouched, which
is why the Host would keep running old code without any error of
its own in that case.
- The Host polls once a second (see `Host/Main.cpp`) -- give it a
moment.
- Confirm you rebuilt the target this example actually watches,
`hotreload_live_plugin`, not the whole project (rebuilding
everything works too, it's just slower per iteration).
## Why rebuilding a `.so`/`.dll` the Host already has loaded works at all
On Linux/macOS this is unremarkable -- a running process's existing
mapping of a file survives the file being replaced, which is exactly
this project's normal build-then-relink cycle.
On Windows it's normally *not* possible to overwrite or delete a DLL
file a running process has loaded, which would otherwise make this
exact workflow (keep the Host running, rebuild the DLL it already
loaded) fail with the linker unable to write its output. This example
avoids that for a reason that already exists elsewhere in this
library, not anything specific to this demo:
`HotReloadContext::stagePathForLoad()` (`source/HotReload/src/
HotReloadContext.cpp`) copies a plugin to a fresh, uniquely-named file
before ever calling `LoadLibrary`/`dlopen` on it -- built originally to
sidestep a dlopen caching gotcha on POSIX (see that function's
comment) -- so the path this example's build actually writes to is
never the one Windows locks. This reasoning was not verified against
a real Windows rebuild-while-running cycle specifically (unlike the
rest of this library's Windows behavior, which was exercised under
Wine -- see `source/HotReload/README.md`'s verification notes); if a
rebuild on Windows still fails while the Host is running, that
mismatch between reasoning and reality is worth reporting.
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Rebuilds hotreload_live_plugin every time LivePlugin.cpp's mtime
# changes, so you can just edit-and-save instead of switching to a
# terminal to rebuild by hand. Run this in one terminal; run
# hotreload_live_demo_host (built once) in another; edit
# Plugin/src/LivePlugin.cpp in your editor. Matches this project's
# other scripts/*.sh conventions (see scripts/run.sh) even though it
# lives here rather than in scripts/, since it's specific to this one
# example rather than general project tooling.
#
# Usage (from anywhere):
# source/examples/HotReloadLiveDemo/watch.sh [build-dir]
#
# build-dir defaults to out/build (relative to the project root, same
# default as scripts/build.sh); pass your own if you configured
# somewhere else, e.g.:
# source/examples/HotReloadLiveDemo/watch.sh out/build-clang
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/../../.."
BUILD_DIR="${1:-out/build}"
SOURCE_FILE="$SCRIPT_DIR/Plugin/src/LivePlugin.cpp"
TARGET="hotreload_live_plugin"
if [ ! -d "$BUILD_DIR" ]; then
echo "error: build directory '$BUILD_DIR' not found." >&2
echo "Configure it first, e.g.:" >&2
echo " cmake -S . -B $BUILD_DIR -DBUILD_EXAMPLES=ON" >&2
exit 1
fi
file_mtime() {
# GNU stat (Linux) and BSD/macOS stat use incompatible flags --
# try one, fall back to the other.
stat -c %Y "$SOURCE_FILE" 2>/dev/null || stat -f %m "$SOURCE_FILE"
}
echo "Watching: $SOURCE_FILE"
echo "Rebuilds into: $BUILD_DIR (target: $TARGET)"
echo "Edit and save that file -- Ctrl+C here to stop watching (the running Host keeps going either way)."
echo ""
last="$(file_mtime)"
while true; do
current="$(file_mtime)"
if [ "$current" != "$last" ]; then
last="$current"
echo "---- rebuilding ($(date '+%H:%M:%S')) ----"
if cmake --build "$BUILD_DIR" --target "$TARGET"; then
echo "---- done -- the running Host should pick this up within about a second ----"
else
echo "---- build failed -- fix the error above and save again ----"
fi
echo ""
fi
sleep 0.5
done