Files
Volkan/source/examples/HotReloadLiveDemo/Host/Main.cpp
T
2026-09-12 00:08:03 -03:00

203 lines
7.3 KiB
C++

/* 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;
}