How Script Hot Reloading with cr.h Works in the Equilibrium Engine
The Equilibrium Engine implements script hot reloading with cr.h by compiling game logic into separate shared libraries that the host application loads, monitors, and swaps at runtime while preserving static state via the CR_STATE macro and managing ECS module lifecycle through the HotReloadableModule component.
The Equilibrium Engine (clibequilibrium/equilibriumengine) leverages the single-header cr.h library to enable zero-downtime iteration on C code. This article examines the practical mechanics of script hot reloading with cr.h, from host initialization to state persistence and ECS integration.
The Architecture of cr.h Hot Reloading
The cr.h library operates on a host-plugin model. The host executable manages the lifecycle of shared libraries (plugins), while each plugin implements a standardized entry point that cr.h invokes during load, update, and unload operations.
Defining the Host Application
The host program must define CR_HOST before including cr.h to activate the host-side API. This macro enables functions like cr_plugin_open, cr_plugin_update, and cr_plugin_close, and activates the CR_STATE persistence mechanism.
In launcher/launcher.cc, the host initialization follows this pattern:
#define CR_HOST
#include <cr.h>
#include <engine.h>
int main() {
cr_plugin sandbox;
world_t *world = engine_init(...).world;
sandbox.userdata = world;
cr_plugin_open(sandbox, CR_PLUGIN("sandbox"));
while (engine_running) {
cr_plugin_update(sandbox); // Checks for .so changes
}
cr_plugin_close(sandbox);
}
Creating Reloadable Plugins
Each script module compiles to a separate shared library (e.g., libsandbox.so) that exports a single entry point: cr_main. The plugin implements CR_EXPORT int cr_main(struct cr_plugin *ctx, enum cr_op operation) to handle lifecycle events.
In launcher/sandbox/main.c, the plugin structure appears as:
#include <cr.h>
#include <equilibrium.h>
static bool CR_STATE initialized = false;
CR_EXPORT int cr_main(struct cr_plugin *ctx, enum cr_op op) {
if (op == CR_CLOSE) return 0;
world_t *world = ctx->userdata;
if (!initialized) {
ECS_IMPORT(world, TransformSystem);
initialized = true;
} else {
CHECK_IF_PLUGIN_RELOADED(ctx, op);
ECS_IMPORT_HOT_RELOADABLE(ctx, MyHotSystem);
}
return 0;
}
Preserving State Across Reloads
Static variables declared with the CR_STATE macro automatically persist across plugin reloads. When cr_plugin_update detects a changed shared library, it unloads the old binary, loads the new version, and copies the previous memory contents back into the new static variables.
In launcher/sandbox/main.c, line 5 demonstrates this pattern:
static bool CR_STATE initialized = false;
This ensures that initialization logic runs only once during the first load, not after every reload.
Integrating with the ECS World
The Equilibrium Engine extends cr.h with ECS-specific cleanup logic through the HotReloadableModule component defined in equilibrium/components/hot_reloadable_module.h.
Tracking Reloadable Modules
When a plugin uses ECS_IMPORT_HOT_RELOADABLE(ctx, ModuleName), the macro creates an entity with a HotReloadableModule component that stores the plugin's cr_plugin pointer and module name. This registration allows the engine to identify which ECS entities belong to which shared library.
Automatic Cleanup on Reload
The reload_modules function iterates over all HotReloadableModule entities associated with a reloading plugin. It deletes the previous entity (and its resources) via ecs_delete before the new plugin version re-registers its components. This prevents stale pointers from persisting in the ECS world.
From equilibrium/components/hot_reloadable_module.h (lines 21-57), the cleanup logic ensures that the ECS world remains consistent even as code modules are swapped at runtime.
Practical Implementation: Code Examples
Minimal Hot-Reloadable Plugin
This example from launcher/sandbox/main.c demonstrates the complete lifecycle of a reloadable script:
/* sandbox/main.c */
#include <equilibrium.h>
#include <cr.h>
static bool CR_STATE initialized = false;
CR_EXPORT int cr_main(struct cr_plugin *ctx, enum cr_op op) {
if (op == CR_CLOSE) return 0;
world_t *world = ctx->userdata;
if (!initialized) {
ECS_IMPORT(world, TransformSystem);
ECS_IMPORT(world, SdlSystem);
initialized = true;
} else {
CHECK_IF_PLUGIN_RELOADED(ctx, op);
ECS_IMPORT_HOT_RELOADABLE(ctx, MyHotSystem);
}
return 0;
}
Declaring a Hot-Reloadable ECS System
Module authors use the ECS_IMPORT_HOT_RELOADABLE macro to register systems that require cleanup on reload:
/* my_system.c */
#include <cr.h>
#include "hot_reloadable_module.h"
CR_EXPORT void cr_main(struct cr_plugin *ctx, enum cr_op op) {
if (op == CR_LOAD) {
ECS_IMPORT_HOT_RELOADABLE(ctx, MySystem);
}
}
The macro automatically creates the tracking entity necessary for the reload_modules cleanup routine.
Host-Side Plugin Management
The host application in launcher/launcher.cc orchestrates the reload cycle:
/* launcher/launcher.cc */
#define CR_HOST
#include <cr.h>
#include <engine.h>
int main() {
cr_plugin sandbox;
world_t *world = engine_init(...).world;
sandbox.userdata = world;
cr_plugin_open(sandbox, CR_PLUGIN("sandbox"));
while (engine_running) {
cr_plugin_update(sandbox);
engine_tick();
}
cr_plugin_close(sandbox);
return 0;
}
Key Source Files
-
launcher/launcher.cc– Host program that definesCR_HOST, initializescr_pluginobjects, and drives the update loop viacr_plugin_update. -
launcher/sandbox/main.c– Reference implementation of a reloadable plugin demonstratingCR_STATE,CHECK_IF_PLUGIN_RELOADED, andECS_IMPORT_HOT_RELOADABLE. -
equilibrium/components/hot_reloadable_module.h– Defines theHotReloadableModulecomponent and thereload_modulesfunction that manages ECS cleanup during script reloads. -
3rdparty/cr/cr.h– The single-header hot-reload runtime providingcr_plugin_open,cr_plugin_update,CR_STATE, and the plugin/host API.
Summary
-
Script hot reloading with cr.h requires compiling game logic as separate shared libraries that export a
cr_mainentry point. -
The host application defines
CR_HOSTbefore includingcr.hand manages plugin lifecycle throughcr_plugin_open,cr_plugin_update, andcr_plugin_close. -
State persistence across reloads is achieved via the
CR_STATEmacro, which automatically preserves static variable values when the shared library is swapped. -
The ECS integration uses the
HotReloadableModulecomponent to track which entities belong to which plugin, ensuringreload_modulescan delete stale resources before the new code re-registers its systems. -
Developers trigger reloads by overwriting the shared library file;
cr_plugin_updatedetects the change and performs the swap without restarting the host process.
Frequently Asked Questions
How does cr.h detect when a shared library has changed?
The cr_plugin_update function checks the file modification time of the plugin's shared library on disk. If the timestamp differs from the currently loaded version, cr.h unloads the old library, loads the new binary, restores CR_STATE variables, and invokes cr_main with the CR_LOAD operation.
What happens to static variables that are not marked with CR_STATE?
Static variables without the CR_STATE macro are reset to their initial values when the plugin reloads. Only variables explicitly declared with CR_STATE (e.g., static int CR_STATE counter = 0;) have their memory preserved across the unload/load cycle.
Can multiple plugins be hot-reloaded simultaneously?
Yes. The host application can create multiple cr_plugin instances (e.g., for sandbox, editor, and engine-simulation modules). Each plugin is updated independently via cr_plugin_update, allowing developers to reload specific systems without affecting others.
How does the ECS world prevent stale pointers after a reload?
The HotReloadableModule component tracks which ECS entities belong to which plugin. When cr_plugin_update triggers a reload, the reload_modules function iterates over these entities and calls ecs_delete to remove the old module's resources before the new plugin version re-registers its components.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →