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,11 @@
# _discover_module_subdirectories() (cmake/BuildHelpers.cmake) only
# add_subdirectory()'s the IMMEDIATE children of source/examples/ --
# this file exists purely so that mechanism finds this folder at all,
# then hands off to its three real targets: one host executable and
# two builds of the same plugin (see PluginV1/CMakeLists.txt for why
# two separate builds rather than one). Shared/ has no CMakeLists.txt
# of its own -- it is just PluginApi.h, pulled in by PluginV1/PluginV2
# via target_include_directories(), not add_subdirectory().
add_subdirectory(PluginV1)
add_subdirectory(PluginV2)
add_subdirectory(Host)
@@ -0,0 +1,15 @@
add_example_module(hotreload_demo_host OUTPUT_NAME "hotreload_demo_host" DEPENDS hotreload_lib)
target_include_directories(hotreload_demo_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
# The Host loads PluginV1/PluginV2 itself, at runtime, through
# source/HotReload -- it must NOT link against them directly (that
# would defeat the entire point of loading them dynamically via
# dlopen()/LoadLibrary()), so this is a build-order-only dependency
# plus each one's exact build output path baked in as a default, never
# a target_link_libraries().
add_dependencies(hotreload_demo_host hotreload_demo_plugin_v1 hotreload_demo_plugin_v2)
target_compile_definitions(hotreload_demo_host PRIVATE
"PLUGIN_V1_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v1>\""
"PLUGIN_V2_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v2>\""
)
+202
View File
@@ -0,0 +1,202 @@
/* Main.cpp -- loads PluginV1, calls into it, reloads to PluginV2, and
* calls the SAME already-resolved function pointers again to show
* they now run the new code -- without the Host re-resolving anything.
* Along the way this exercises every HotReloadSymbolEvent kind: watch
* the printed event log against PluginV1/src/Plugin.cpp and
* PluginV2/src/Plugin.cpp side by side. The second half does the same
* thing for CppWidgetApi.h's real C++ (non-extern-"C") surface -- see
* that header's comments for why its two methods are each reached a
* different way below.
*/
#include "CppWidgetApi.h"
#include "hot_reload/hot_reload.h"
#include <cstdio>
#include <string>
namespace
{
using PluginFunction = int (*)(int);
// CppWidget::tick's real ABI: an implicit this-pointer followed by
// its one declared parameter, same calling convention the
// compiler itself would generate for widget->tick(x) -- this is
// just spelling that out explicitly so it can be called through a
// manually-resolved address instead.
using CppWidgetTickFunction = int (*)(CppWidget*, int);
int hostSideDeletedFunctionStub(int)
{
std::printf(" !! called a deleted plugin function -- this is the Host's stub, not a crash !!\n");
return -1;
}
/* Runs once per HOT_RELOAD_SYMBOL_REMOVED, before onSymbolEvent
* below sees the same event -- see hot_reload.h's doc comment on
* HotReloadDeletedSymbolFn. Redirecting a removed function to a
* stub living in the HOST executable (not in either plugin build)
* demonstrates that the target of a patch can be anywhere at all,
* not just inside the module being reloaded. */
void* onDeletedSymbol(const char* symbolName, void* /*userData*/)
{
std::printf(" [event] REMOVED %-22s -> redirecting old callers to the Host's stub\n", symbolName);
return reinterpret_cast<void*>(&hostSideDeletedFunctionStub);
}
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*/)
{
// REMOVED is already reported, with more context, by
// onDeletedSymbol() above -- it fires exactly once per removed
// symbol, same as this callback would.
if (event->kind == HOT_RELOAD_SYMBOL_REMOVED)
{
return;
}
// event->name is always the real symbol-table name (the exact
// string hot_reload_get_symbol() would need) -- demangling it
// is purely a display choice, made here and only here; a
// plain extern "C" name like "plugin_tick" comes back
// unchanged, so this is safe to apply unconditionally.
char* readableName = hot_reload_demangle(event->name);
std::printf(" [event] %-30s %s\n", eventKindName(event->kind), readableName != nullptr ? readableName : event->name);
hot_reload_free_string(readableName);
}
}
int main(int argc, char** argv)
{
std::string pluginV1Path = argc > 1 ? argv[1] : PLUGIN_V1_PATH;
std::string pluginV2Path = argc > 2 ? argv[2] : PLUGIN_V2_PATH;
HotReloadOptions options{};
options.on_symbol_event = &onSymbolEvent;
options.on_deleted_symbol = &onDeletedSymbol;
options.user_data = nullptr;
HotReloadContext* ctx = hot_reload_context_create(&options);
if (ctx == nullptr)
{
std::fprintf(stderr, "hot_reload_context_create failed\n");
return 1;
}
std::printf("== loading plugin V1 (%s) ==\n", pluginV1Path.c_str());
HotReloadStatus status = hot_reload_load(ctx, "plugin", pluginV1Path.c_str());
if (status != HOT_RELOAD_OK)
{
std::fprintf(stderr, "hot_reload_load failed: %s (%s)\n", hot_reload_status_string(status),
hot_reload_last_error(ctx));
hot_reload_context_destroy(ctx);
return 1;
}
// Resolved ONCE, before the reload below -- and never re-resolved.
// The point of this demo is that these exact pointers keep working
// correctly after the plugin underneath them changes.
auto tick = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_tick"));
auto legacy = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_legacy_feature"));
auto markerBefore = static_cast<int*>(hot_reload_get_symbol(ctx, "plugin", "plugin_build_marker"));
std::printf("\n-- calling V1 through freshly-resolved pointers --\n");
tick(21);
legacy(7);
std::printf(" plugin_build_marker = %d\n", markerBefore != nullptr ? *markerBefore : -1);
// ---------------------------------------------------------------
// The same plugin build ALSO exports a real C++ class
// (CppWidgetApi.h) with no extern "C" anywhere -- a virtual
// method and an ordinary out-of-line method, both with real
// compiler-mangled names. Same "plugin" module name as above,
// since it is physically the same shared library on disk.
// ---------------------------------------------------------------
std::printf("\n== the same plugin build's C++ surface (CppWidgetApi.h, no extern \"C\") ==\n");
auto makeCppWidget = reinterpret_cast<CppWidget* (*) ()>(hot_reload_get_symbol(ctx, "plugin", "make_cpp_widget"));
CppWidget* widget = makeCppWidget != nullptr ? makeCppWidget() : nullptr;
// CppWidget::tick(int)'s real, compiler-mangled name -- see
// CppWidgetApi.h's comment on why this one is resolved manually
// rather than called as widget->tick(x). Re-derive this yourself
// with `nm -D --defined-only <plugin path> | c++filt` if you
// change CppWidget's signature; there is no portable way to ask
// the compiler for another declaration's mangled name at compile
// time, which is exactly the inconvenience extern "C" exists to
// avoid for anything you need to look up this way.
auto tickByMangledName =
reinterpret_cast<CppWidgetTickFunction>(hot_reload_get_symbol(ctx, "plugin", "_ZN9CppWidget4tickEi"));
std::printf("-- V1: virtual method through REAL polymorphic dispatch (widget->virtualTick) --\n");
if (widget != nullptr)
{
widget->virtualTick(21);
}
std::printf("-- V1: ordinary method through a manually-resolved mangled-name pointer --\n");
if (tickByMangledName != nullptr && widget != nullptr)
{
tickByMangledName(widget, 21);
}
std::printf("\n== reloading \"plugin\" to V2 (%s) ==\n", pluginV2Path.c_str());
status = hot_reload_reload(ctx, "plugin", pluginV2Path.c_str());
if (status != HOT_RELOAD_OK)
{
std::fprintf(stderr, "hot_reload_reload failed: %s (%s)\n", hot_reload_status_string(status),
hot_reload_last_error(ctx));
delete widget;
hot_reload_context_destroy(ctx);
return 1;
}
std::printf("\n-- calling the exact same pointers from before, un-re-resolved --\n");
tick(21); // same function pointer as above -- now runs V2's tripling logic
legacy(7); // same function pointer as above -- now runs the Host's stub instead of V1's body
std::printf("\n-- plugin_new_feature only exists in V2, resolved fresh --\n");
auto newFeature = reinterpret_cast<PluginFunction>(hot_reload_get_symbol(ctx, "plugin", "plugin_new_feature"));
if (newFeature != nullptr)
{
newFeature(21);
}
auto markerAfter = static_cast<int*>(hot_reload_get_symbol(ctx, "plugin", "plugin_build_marker"));
std::printf("\n-- plugin_build_marker: proof that data is never patched --\n");
std::printf(" resolved fresh from V2 right now = %d\n", markerAfter != nullptr ? *markerAfter : -1);
std::printf(" V1's ORIGINAL address, read again = %d (still V1's value -- never touched)\n",
markerBefore != nullptr ? *markerBefore : -1);
std::printf("\n-- the SAME widget object, same vtable slot, same manually-resolved pointer --\n");
if (widget != nullptr)
{
widget->virtualTick(21); // same object, same vtable slot -- now V2's tripling logic
}
if (tickByMangledName != nullptr && widget != nullptr)
{
tickByMangledName(widget, 21); // same resolved pointer as before -- also now V2's logic
}
delete widget;
hot_reload_context_destroy(ctx);
return 0;
}
@@ -0,0 +1,39 @@
# Not built through add_lib_module(): a hot-reloadable plugin is a
# different shape from an ordinary library module. PluginV1 and its
# sibling PluginV2 are two SEPARATE binaries that deliberately export
# the SAME extern "C" symbol names (see ../Shared/PluginApi.h) --
# that's exactly what lets HotReloadContext line them up and patch one
# into the other at runtime. add_lib_module() assumes one canonical
# build per module name and would not fit two builds of the same name
# needing to coexist, so this is plain add_library(), reusing the
# project's usual output-directory/warnings helpers directly. Gated on
# BUILD_EXAMPLES exactly like every other target under source/examples/.
if(BUILD_EXAMPLES)
add_library(hotreload_demo_plugin_v1 SHARED src/Plugin.cpp src/CppWidget.cpp)
else()
add_library(hotreload_demo_plugin_v1 SHARED EXCLUDE_FROM_ALL src/Plugin.cpp src/CppWidget.cpp)
endif()
target_include_directories(hotreload_demo_plugin_v1 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
set_target_properties(hotreload_demo_plugin_v1 PROPERTIES
OUTPUT_NAME "hotreload_demo_plugin_v1"
# Matches add_lib_module()'s own SHARED-library branch
# (cmake/BuildHelpers.cmake) exactly, for exactly the same reason:
# MinGW's linker exports every global symbol from a DLL by
# default, but MSVC exports NONE unless told to -- without this,
# PluginApi.h's functions would not be visible in the export table
# at all when built with MSVC, silently breaking the whole demo on
# that one toolchain.
WINDOWS_EXPORT_ALL_SYMBOLS ON)
_set_output_dirs(hotreload_demo_plugin_v1
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
_apply_compile_options(hotreload_demo_plugin_v1)
# The whole point of this target: give source/HotReload's jump-patch
# backends guaranteed room to write into. See cmake/HotReload.cmake.
hot_reload_enable_patchable_functions(hotreload_demo_plugin_v1)
_register_module_target(EXAMPLE hotreload_demo_plugin_v1)
add_dependencies(all_examples hotreload_demo_plugin_v1)
@@ -0,0 +1,32 @@
/* CppWidget V1 -- "before your edit". See PluginV2/src/CppWidget.cpp
* for "after": same three exports change behavior together, exactly
* like PluginV1/src/Plugin.cpp's plain-C functions do.
*/
#include "CppWidgetApi.h"
#include <cstdio>
int CppWidget::virtualTick(int input)
{
int result = input * 2;
std::printf(" CppWidget::virtualTick(%d) -> %d [V1: input * 2, via vtable]\n", input, result);
return result;
}
int CppWidget::tick(int input)
{
int result = input * 2;
std::printf(" CppWidget::tick(%d) -> %d [V1: input * 2, via mangled-name lookup]\n", input, result);
return result;
}
CppWidget CppWidget::operator+(const CppWidget& /*other*/) const
{
std::printf(" CppWidget::operator+ [V1]\n");
return CppWidget();
}
extern "C" CppWidget* make_cpp_widget()
{
return new CppWidget();
}
@@ -0,0 +1,31 @@
/* PluginV1 -- "before your edit". See PluginV2/src/Plugin.cpp for
* "after"; the two deliberately export the identical set of names
* (minus plugin_legacy_feature, removed in V2) so HotReloadContext can
* line them up. See ../Shared/PluginApi.h for what each export is
* there to demonstrate.
*/
#include "PluginApi.h"
#include <cstdio>
int plugin_tick(int input)
{
int result = input * 2;
std::printf(" plugin_tick(%d) -> %d [V1: input * 2]\n", input, result);
return result;
}
int plugin_legacy_feature(int input)
{
std::printf(" plugin_legacy_feature(%d) -> %d [V1, about to be removed in V2]\n", input, input);
return input;
}
#if defined(__GNUC__) || defined(__clang__)
int plugin_tiny_function(int input)
{
return input + 1;
}
#endif
int plugin_build_marker = 1;
@@ -0,0 +1,24 @@
# See ../PluginV1/CMakeLists.txt for why this is hand-written instead
# of add_lib_module() -- this is that same explanation's other half:
# PluginV2 exports the identical extern "C" names as PluginV1 (see
# ../Shared/PluginApi.h) with a different implementation standing in
# for "the same file, after your edit" in this demo's story.
if(BUILD_EXAMPLES)
add_library(hotreload_demo_plugin_v2 SHARED src/Plugin.cpp src/CppWidget.cpp)
else()
add_library(hotreload_demo_plugin_v2 SHARED EXCLUDE_FROM_ALL src/Plugin.cpp src/CppWidget.cpp)
endif()
target_include_directories(hotreload_demo_plugin_v2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Shared)
set_target_properties(hotreload_demo_plugin_v2 PROPERTIES
OUTPUT_NAME "hotreload_demo_plugin_v2"
WINDOWS_EXPORT_ALL_SYMBOLS ON)
_set_output_dirs(hotreload_demo_plugin_v2
"${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples"
"${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}/examples")
_apply_compile_options(hotreload_demo_plugin_v2)
hot_reload_enable_patchable_functions(hotreload_demo_plugin_v2)
_register_module_target(EXAMPLE hotreload_demo_plugin_v2)
add_dependencies(all_examples hotreload_demo_plugin_v2)
@@ -0,0 +1,31 @@
/* CppWidget V2 -- "after your edit". See PluginV1/src/CppWidget.cpp
* for "before".
*/
#include "CppWidgetApi.h"
#include <cstdio>
int CppWidget::virtualTick(int input)
{
int result = input * 3;
std::printf(" CppWidget::virtualTick(%d) -> %d [V2: input * 3, via vtable]\n", input, result);
return result;
}
int CppWidget::tick(int input)
{
int result = input * 3;
std::printf(" CppWidget::tick(%d) -> %d [V2: input * 3, via mangled-name lookup]\n", input, result);
return result;
}
CppWidget CppWidget::operator+(const CppWidget& /*other*/) const
{
std::printf(" CppWidget::operator+ [V2]\n");
return CppWidget();
}
extern "C" CppWidget* make_cpp_widget()
{
return new CppWidget();
}
@@ -0,0 +1,33 @@
/* PluginV2 -- "after your edit". See PluginV1/src/Plugin.cpp for
* "before". plugin_tick's behavior changed, plugin_legacy_feature was
* deleted, and plugin_new_feature was added -- exactly the three kinds
* of change hot_reload.h's HotReloadSymbolEvent enum distinguishes.
*/
#include "PluginApi.h"
#include <cstdio>
int plugin_tick(int input)
{
int result = input * 3;
std::printf(" plugin_tick(%d) -> %d [V2: input * 3]\n", input, result);
return result;
}
/* plugin_legacy_feature deliberately does not exist in V2 anymore. */
int plugin_new_feature(int input)
{
int result = input * 10;
std::printf(" plugin_new_feature(%d) -> %d [V2: brand new]\n", input, result);
return result;
}
#if defined(__GNUC__) || defined(__clang__)
int plugin_tiny_function(int input)
{
return input + 2;
}
#endif
int plugin_build_marker = 2;
@@ -0,0 +1,47 @@
/* CppWidgetApi.h -- a C++ (not extern "C") surface living alongside
* PluginApi.h's plain C one, in the SAME plugin build. Every member
* function below keeps its real, compiler-mangled name; nothing here
* needs special handling by source/HotReload, which classifies
* symbols by their type and section, never by what their name looks
* like -- see hot_reload.h's "REQUIREMENTS ON A HOT-RELOADABLE
* MODULE" and README.md's "Hot reloading C++ symbols" for the full
* picture.
*
* The two methods below are reached two different ways from
* Host/Main.cpp, deliberately: see that file's comments for why.
*/
#pragma once
class CppWidget
{
public:
virtual ~CppWidget() = default;
/* Virtual: Host/Main.cpp calls this through ordinary C++
* polymorphic dispatch (widget->virtualTick(x)) and needs no
* manual symbol lookup at all -- the call compiles down to an
* indirect call through the object's vtable slot, not a direct
* reference to this specific symbol, so it needs no link-time
* dependency on whichever plugin build actually defines it. */
virtual int virtualTick(int input);
/* An ordinary out-of-line method. Calling this as widget->tick(x)
* would compile to a direct call to CppWidget::tick's own mangled
* symbol, which would need the Host to link against a plugin
* build at build time -- exactly what hot-reloading a plugin
* means avoiding. So Host/Main.cpp instead resolves it manually
* by its mangled name via hot_reload_get_symbol(), the same way
* it resolves PluginApi.h's plain C functions. */
int tick(int input);
/* An overloaded operator: mechanically just a function with an
* unusual mangled name, reached the same manual way as tick()
* above. */
CppWidget operator+(const CppWidget& other) const;
};
/* 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
* recommends it: something to hand a human-readable name to, without
* needing to know or hardcode a mangled string. */
extern "C" CppWidget* make_cpp_widget();
@@ -0,0 +1,55 @@
/* PluginApi.h -- the contract PluginV1 and PluginV2 both implement,
* and the Host loads by name. Every function here follows
* cpp-style-guide.md section 11's rule for anything loaded at runtime:
* plain C linkage, no classes or exceptions crossing the boundary.
*
* This header is deliberately identical for both plugin versions --
* in a real project this is the file that stays stable while
* PluginV1/src/Plugin.cpp and PluginV2/src/Plugin.cpp (standing in for
* "the same file, before and after your edit") diverge.
*/
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
/* Changes behavior between V1 and V2 -- watch this one get
* HOT_RELOAD_SYMBOL_PATCHED. */
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
* call still aimed at it afterwards. */
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. */
int plugin_new_feature(int input);
#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
* overridden down to zero) -- watch this one get
* HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than corrupt whatever
* comes after it. 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);
#endif
/* An exported VARIABLE, not a function, sitting right next to
* real functions in the same library and changing value between
* V1 and V2 just like plugin_tick's behavior does -- watch this
* one get HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE. This is the
* mechanical proof behind hot_reload.h's "only executable code
* changes" guarantee: nothing here tells the library "this one is
* data, don't touch it" -- it discovers that itself from the
* symbol table, the same way it discovers plugin_tick is code.
*/
extern int plugin_build_marker;
#ifdef __cplusplus
}
#endif
@@ -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