Unlock Live Code Reloading in C: A Game-Changer for Development
Share this article
For developers accustomed to dynamic languages like JavaScript or Lisp, the idea of modifying a running program feels natural. But in C? Traditionally, changing logic meant recompiling and restarting—until now. A technique leveraging shared library reloading brings interactive programming to C, revolutionizing development workflows for games, simulations, and long-running applications.
The Shared Library Secret
The core insight is simple yet transformative: build your application's logic as a shared library (.so on Linux, .dll on Windows). A lightweight "wrapper" executable loads this library, monitors it for changes, and hot-swaps it at runtime. As Chris Wellons explains, this approach—pioneered in Casey Muratori's Handmade Hero series—eliminates the restart penalty during development.
Live-updating C code in a Game of Life simulation (Credit: nullprogram.com)
Critical Constraints and Design Patterns
This power comes with constraints:
1. No Global State: Static/global variables vanish on reload. The library must store all state in heap-allocated structs passed by the wrapper.
2. Function Pointer Pitfalls: Pointers to functions within the library become invalid after reload. Solutions require careful architectural design.
3. Standard Library Caution: malloc() and other stdlib functions may introduce hidden state. Minimal external dependencies are ideal.
A well-defined API struct bridges the wrapper and library:
struct game_api {
struct game_state *(*init)();
void (*finalize)(struct game_state *state);
void (*reload)(struct game_state *state);
void (*unload)(struct game_state *state);
bool (*step)(struct game_state *state);
};
The wrapper only interacts via this struct, calling init(), step(), and lifecycle hooks like reload() after a swap.
The Reloading Machinery
On Unix-like systems, dlopen(), dlsym(), and dlclose() handle the heavy lifting. The wrapper:
1. Tracks the library's inode (not modification time) to detect changes.
2. Calls unload() (if defined), then dlclose() the old handle.
3. Loads the new library with dlopen(), fetches the game_api struct via dlsym().
4. Invokes reload() for library-specific reinitialization.
A minimal main loop:
int main(void) {
struct game game = {0};
for (;;) {
game_load(&game); // Reload if library changed
if (game.handle && !game.api.step(game.state)) break;
usleep(100000); // 100ms delay
}
game_unload(&game);
return 0;
}
Why This Changes Everything
While minor race conditions exist (e.g., a library changing mid-reload), the trade-off is acceptable for development. The benefits are profound:
- Instant Feedback: Tweaking game mechanics, UI, or algorithms happens in real-time.
- State Preservation: Game progress or simulation data persists across reloads.
- Architectural Discipline: Enforces decoupled, state-aware design even in C.
Wellons' Game of Life demo provides a tangible starting point. For developers building engines, simulations, or embedded systems, this technique transforms C from a static compiled language into a dynamic prototyping powerhouse—without sacrificing performance or control.