Hot-Reloading game code in C
Problem
When doing graphics programming for games or “recreational” programming, one wants to have a fast feedback loop.
Additionally, when testing / debugging a game, we often need to reload the logic of the game and test it with a given game state. Often enough we can’t fully serialise and reload game state from/to disk.
Usually this is done by using a scripting language, which is often embedded in games for speed reasons.
Using shared libraries and POSIX dlopen(3)
and dlsym(3)
we can archive the same effect without using a scripting language in C. I believe, that in fact any language which compiles down to binary code and supports the C calling convention could be used1.
The Goal
Create a minimal example of hot-code-reloading using raylib.
The goal is to be able to hot-reload the game code shared object while the game is running, whenever we hit the
r
key.Document the solution.
The Plan
I want to create some graphics window on the screen, and I want to hot-reload changes in the graphics code.
To do this, I’ll use raylib which has a very simple API and is easy to use, and has plenty of examples.
I’ll create a main executable which will:
Load a shared library containing the game code and find and bind symbols form that shared library to global function pointes
Initialise a graphics window
Initialise global game state using a function from the game code
Start a game loop, doing:
Check if the user hits the
r
key and if so, reload and rebind the game code shared libraryUpdate the game state
Update (draw) the next frame
Implementation
This is what it looks like in the end – nothing spectacular:
Initialization
Initialization is pretty straight forward:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(void) {
const int screenWidth = 800;
const int screenHeight = 450;
if (do_reload() != 0) {
return 1;
};
state = init_gamestate();
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(screenWidth, screenHeight, "Hot Reloading example.");
SetTargetFPS(60);
// Game Loop Here
CloseWindow();
free_gamestate(state);
state = NULL;
return 0;
}
The
init_gamestate()
is a function of the game code shared library which allocates and initializes global game state. Thefree_gamestate()
frees that memory again.The
do_reload()
function handles the loading of the shared library and the binding of the function pointers, more on that later.The rest of the code is how a window of a given size with flags and title is opened and closed in raylib.
The game loop
The is pretty easy, too:
1
2
3
4
5
6
7
8
9
10
while (!WindowShouldClose())
{
if (IsKeyPressed(KEY_R))
do_reload();
update_gamestate(state);
update_frame(state);
}
The game loop executes until we get a signal that we should close the window
for every frame we:
We check if key
r
is hit and call ourdo_reload()
to reload all game codeWe update the game state by calling the
update_gamestate()
functionWe call the
update_frame()
function to redraw the frame.
The update_gametate()
, update_frame()
, init_gamestate()
and free_gamestate()
functions are globally defined likes so:
1
2
3
4
5
6
7
8
9
10
11
12
#incude "game.h"
...
init_gamestate_t init_gamestate = NULL;
free_gamestate_t free_gamestate = NULL;
update_gamestate_t update_gamestate = NULL;
update_frame_t update_frame = NULL;
...
These type aliases and the game state are defined in game.h
as function pointers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef _GAME_H
#define _GAME_H 1
#include "raylib.h"
typedef struct {
unsigned long long tick;
Camera3D camera;
Vector3 cubePosition;
} gamestate_t;
typedef gamestate_t * (*init_gamestate_t)();
typedef int (*free_gamestate_t)(gamestate_t *state);
typedef int (*update_gamestate_t)(gamestate_t *state);
typedef int (*update_frame_t)(gamestate_t *state);
#endif
Loading and Reloading
To load a shared library into the current process memory, the dlopen(3)
call is used. That call takes a path to a shared object and returns a handle to the shared object. We than can use dlsym(3)
to search that library for symbols to obtain their address.
We use this to find the game code function addresses and then set the global function pointers to these addresses.
All this is done in reload.c
.
To have it easier for the caller, a symtab_t
type alias is defined in reload.h
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef RELOAD_H_
#define RELOAD_H_
typedef struct {
char *symbol; // name of the symbol
void **ptr; // address to be updated
} symtab_t;
/**
* Load given dll and rebind symbols found in supplied
* symbol table.
*/
int reload(char *path, symtab_t *symtab);
#endif // RELOAD_H_
Using this, the do_reload()
function of the main executable calls the reload()
function with a “symbol table” containing the global function pointers and the name of the shared object to load:
1
2
3
4
5
6
7
8
9
10
int do_reload() {
static symtab_t symtab[] = {
{"init_gamestate", (void **)&init_gamestate},
{"free_gamestate", (void **)&free_gamestate},
{"update_gamestate", (void **)&update_gamestate},
{"update_frame", (void **)&update_frame},
{0}
};
return reload("libgamecode.so", symtab);
}
The reload()
function is implemented like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int reload(char *dll_name, symtab_t *symtab) {
static void *game_dll = NULL;
fprintf(stderr, "DLL_NAME: %s\n", dll_name);
if (game_dll) {
fprintf(stderr, "CLOSING DLL Handle %p\n", game_dll);
dlclose(game_dll);
game_dll = NULL;
}
game_dll = dlopen(dll_name, RTLD_LAZY | RTLD_LOCAL);
if (!game_dll) {
fprintf(stderr, "ERROR: Could not open shared library %s: %s\n",
dll_name, dlerror());
return 1;
}
fprintf(stderr, "NEW DLL Handle: %p\n", game_dll);
while (symtab->symbol) {
void *sym = dlsym(game_dll, symtab->symbol);
if (!sym) {
fprintf(stderr, "ERROR: Caould not open symbol: %s: %s\n",
symtab->symbol, dlerror());
return 2;
}
fprintf(stderr, "RELOAD: found symbol %s -> %p\n", symtab->symbol, symtab->ptr);
*symtab->ptr = sym;
symtab++;
}
return 0;
}
Graphics and Game Code
The update_gamestate()
and update_game()
functions are defined in a separate source file which will be built into a shared object. It’s basically the core_3d_camera.c
example from raylib:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "debug.h"
#include "raylib.h"
#include "game.h"
gamestate_t * init_gamestate() {
gamestate_t *state = malloc(sizeof(gamestate_t));
state->tick = 0LL;
state->camera.position = (Vector3){ 0.0f, 10.0f, 10.0f }; // Camera position
state->camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Camera looking at point
state->camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target)
state->camera.fovy = 45.0f; // Camera field-of-view Y
state->camera.projection = CAMERA_PERSPECTIVE; // Camera mode type
state->cubePosition = (Vector3){ 0.0f, 0.0f, 0.0f };
return state;
}
int free_gamestate(gamestate_t *state) {
assert(state);
free(state);
return 0;
}
int update_gamestate(gamestate_t *state) {
assert(state);
state->tick += 1;
return 0;
}
int update_frame(gamestate_t *state) {
assert(state);
BeginDrawing();
ClearBackground(RAYWHITE);
BeginMode3D(state->camera);
DrawCube(state->cubePosition, 2.0f, 2.0f, 2.0f, GREEN);
DrawCubeWires(state->cubePosition, 2.0f, 2.0f, 2.0f, MAROON);
DrawGrid(100, 1.0f);
EndMode3D();
DrawText("Welcome to the third dimension!", 10, 40, 20, DARKGRAY);
char buf[80];
snprintf(buf, 80, "TICK %lld", state->tick);
DrawText(buf, 10, 60, 20, DARKGRAY);
DrawFPS(10, 10);
EndDrawing();
return 0;
}
Conclusion
I was very surprised to see how easy and straight-forward hot-reloading can be. Of course, I’m aware that this implementation is not thread-safe. I have been using python and other programming languages, where hot code reloading is supported. I see that C has an advantage here, because we need to build our own data structures, and have no support for OOP, polymorphism etc, which complicates things greatly (how is reloading a class going to affect objects of that class?).
I’m also very pleased how easy it is to get things done in raylib.
Links
The code is available on GitHub.
I got inspired to try this by this blog post, which does something similar but for Windows.
I also found this video by tsoding helpful.