Code Hot-Reloading in C

In some large applications it is desirable to be able to update code on the fly, without having to restart the program, for example a text editor with plugin system, where you want plugins to automatically reload when they are recompiled. This is trivial to do with a scripted language such as Lua or Python, but it is often more difficult to do in a native language like C or Rust.

This tutorial outlines the use of libdl to create a scripting system for some kind of game engine that allows game logic code to be hot-reloaded without restarting the game. The method demonstrated here will work for C on most Unix-like systems; The code examples shown were written for some form of Linux. This method will also work on Windows, but you will have to replace dlopen with LoadLibrary, dlsym with GetProcAddress and dlclose with FreeLibrary. You will also have to replace the dlfcn.h include with windows.h. Other than that the process is identical.

A complete project demonstrating this tutorial can be found on GitHub.

How It Works

To do any kind of code hot-reloading, dynamic linking using shared libraries is required. The project is compiled into three parts:

When the program starts up, the first thing that happens is a copy of the scripts dynamic library is made. This is so that the compiler and our program aren't working on the same file, which means it is less likely that all hell will break loose when we try to recompile the scripts. The original shared object is also statted to find out its time of last modification. This is so that changes to it can be detected (inotify could also be used).

The copy of the file is then opened using dlopen. The necessary functions are found using dlsym and cast to function pointers to be called later. Note that casting void pointers (supplied by libdl) to function pointers is not a standard C99 feature. However, it is widely supported by most major compilers, including Clang and MSVC. There's no other way to do it, unfortunately, without writing your own library for loading dynamic libraries.

The Core

The core project will be extremely simple; It will only have a single function, for demonstration purposes, which will print the current date and time. It will be split across a header and a source file, which will look something like this:


#include <stdio.h>
#include <time.h>

#define buf_size 32

void core_print_time() {
	time_t timer;
	char buffer[buf_size];
	struct tm* tm_info;

	timer = time(NULL);
	tm_info = localtime(&timer);

	strftime(buffer, buf_size, "%Y-%m-%d %H:%M:%S", tm_info);
	puts(buffer);
}

In any real program, you would want to put anything that you people to be able to call from the plugin system in the core project.

A Sample "Script"

The "script" project will contain the code that will be able to hot-reload. For hot-reloading to work properly and be useful, there needs to be some persistent data that stays the same through different reloads, but can also have it's size changed between reloads. The way I implemented this was to have a "storage" structure that gets allocated by the bootstrapper and then a pointer is to the instance is passed through an on_reload function. The script also has to return out the size of the storage structure so that the bootstrapper knows how much to allocate.

The script looks like this:


struct storage {
	int i_val;
	float f_val;
	double d_val;
};

struct storage* instance;

size_t get_instance_size() {
	return sizeof(struct storage);
}

/* Update the pointer, in case the bootstrapper
 * reallocated the storage. */
void on_reload(void* ptr) {
	instance = ptr;
}

void on_update() {
	core_print_time();
}

The Bootstrapper

Now here's the real simplistic magic of this system.

First we need to define function pointer types for all of the functions that need calling from the script shared library.


typedef size_t (*get_instance_size_func)();
typedef void (*on_reload_func)(void*);
typedef void (*on_update_func)();

Now to setup some data that will be used by the bootstrapper. For this example, I am storing it in global data, but you probably want to encapsulate it in a structure so that it is easier to manage in a larger program.

This data will include a handle to the shared library, a pointer to the "instance", which will be allocated with the size information provided by the script. The current instance size and the last modification time must also be stored so that changes in both of those can be detected and acted upon. On top of that, function pointers to the functions that will be loaded need to be stored so that they can be called later.


const char* working_lib_path = "./workinglib";

void* handle = 0;
const char* lib_path = "./bin/libscripts.so";
uint64_t lib_mod_time = 0;
size_t instance_size = 0;
void* instance = 0;

get_instance_size_func get_instance_size = 0;
on_reload_func on_reload = 0;
on_update_func on_update = 0;

Next, a function will be setup to initialise the function pointers. This is the first instance that libdl is used:


static void load_funcs() {
	handle = dlopen(working_lib_path, RTLD_NOW);
	if (!handle) {
		fprintf(stderr, "Failed to load library: %s\n", working_lib_path);
		return;
	}

	get_instance_size = (get_instance_size_func)dlsym(handle, "get_instance_size");
	on_reload = (on_reload_func)dlsym(handle, "on_reload");
	on_update = (on_update_func)dlsym(handle, "on_update");
}

We also need a way to copy the script library to create a "working" copy of it, so that the compiler can work on the original without causing problems. This can be setup in a rather minimal way (error checking has been omitted):


static void copy_script_lib() {
	FILE* src, * dst;
	size_t size, read, i;
	char buffer[2048];

	src = fopen(lib_path, "rb");
	dst = fopen(working_lib_path, "wb");

	fseek(src, 0, SEEK_END);
	size = ftell(src);
	fseek(src, 0, SEEK_SET);

	for (i = 0; i < size; i += 2048) {
		read = fread(buffer, 1, 2048, src);
		fwrite(buffer, read, 1, dst);
	}

	fclose(src);
	fclose(dst);
}

For this example, I'm going to use an infinite loop that simply sleeps the thread for a second every iteration. In a game this would be your main loop, or in a GUI application you might want this to be in your event or redraw loop. This loop will call the on_update function. After the loop has finished, the instance will need to be freed and the handle closed.


while (1) {
	on_update();

	sleep(1);
}

free(instance);

dlclose(handle);

Now for the actual hot-reload. This will stat the original file and check if it has changed since the last iteration. If it has changed, the handle to the scripts library is closed (if it was already open). We then check if the size of the storage has been updated, and if it has, then we reallocate the instance. Finally, the on_reload function is called.


if (stat(lib_path, &lib_stat) == 0) {
	if (lib_stat.st_mtime > lib_mod_time) {
		/* The file has been updated, do the reload. */

		if (handle) {
			dlclose(handle);
		}

		/* Update the copy and reload the functions. */
		copy_script_lib();
		load_funcs();

		/* Check for a reallocation. */
		new_size = get_instance_size();
		if (new_size != instance_size) {
			instance = realloc(instance, new_size);

			/* Set any new memory that was allocated to zero. */
			if (new_size > instance_size) {
				memset((char*)instance + instance_size, 0, new_size - instance_size);
			}

			instance_size = new_size;
		}

		lib_mod_time = lib_stat.st_mtime;

		on_reload(instance);
	}
}

Again, you can find the code for the complete project on GitHub.