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 defines CR_HOST, initializes cr_plugin objects, and drives the update loop via cr_plugin_update.

  • launcher/sandbox/main.c – Reference implementation of a reloadable plugin demonstrating CR_STATE, CHECK_IF_PLUGIN_RELOADED, and ECS_IMPORT_HOT_RELOADABLE.

  • equilibrium/components/hot_reloadable_module.h – Defines the HotReloadableModule component and the reload_modules function that manages ECS cleanup during script reloads.

  • 3rdparty/cr/cr.h – The single-header hot-reload runtime providing cr_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_main entry point.

  • The host application defines CR_HOST before including cr.h and manages plugin lifecycle through cr_plugin_open, cr_plugin_update, and cr_plugin_close.

  • State persistence across reloads is achieved via the CR_STATE macro, which automatically preserves static variable values when the shared library is swapped.

  • The ECS integration uses the HotReloadableModule component to track which entities belong to which plugin, ensuring reload_modules can delete stale resources before the new code re-registers its systems.

  • Developers trigger reloads by overwriting the shared library file; cr_plugin_update detects 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →