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