Hot Reloading C Code

Shared libraries have long been promoted as method of updating a program's library dependencies without needing to relink the whole program. For people who ship long lasting serious software, shared libraries have been largely found to be a mistake. Ultimately any working software should be linked statically before shipping unless you want to experience dll-hell. However, during program development, shared libraries provide an excellent way of boosting iteration time, although not in the manner they are typically employed.

Runtime Dynamic Linking

While the above assertion is true in practice, what about runtime library loading à la dlopen(3) or LoadLibraryA()? When writing software targeting Win32 this is a common method of making sure that your application can still run with reduced functionality when the system is missing optional libraries. This is not the use case proposed here. Instead I am referring to a method which is strictly limited to program development.

One complaint that some people have about compiled programming languages is that the turn around time when making changes can be quite long. This is one of the reasons that you may prefer to use a scripting language for some applications. However, any serious high performance program cannot be written in such a language and instead must be compiled to machine code before execution. For a large portion of programmers this means that any time you make a change you must close the program, recompile, restart, and do the necessary actions to restore your program to the state prior to closing.

What if it is difficult or requires a large sequence of actions to restore the programs state? This wastes precious development time and provides an opportunity for a loss of focus.

This does not need to be the case! By putting a little thought into your program structure and understanding how global variables work, it is possible to avoid this time sink altogether and reload 99% of your compiled application entirely at runtime.

Example

A nontrivial program will typically have a run loop similar to the one below:

 1int
 2main(void)
 3{
 4	ProgramCtx ctx = {0};
 5	/* ... */
 6	while (!ctx.program_should_exit) {
 7		do_debug_checks();
 8
 9		do_program_things(&ctx);
10	}
11	/* do not waste user's time doing unnecessary cleanup */
12	return 0;
13}

Where do_debug_checks() is some function that should only exist when doing a debug build. To not pollute the main program code with unreadable #ifdefs I usually include something like this at the top of the file:

 1#ifdef _DEBUG
 2
 3static void do_debug_checks(void);
 4
 5static char *libname = "./program.so";
 6static void *libhandle;
 7
 8typedef do_program_things_fn(ProgramCtx *);
 9static do_program_things_fn *do_program_things;
10
11#else
12
13static void do_debug_checks(void) { }
14#include "program.c"
15
16#endif

The important part to notice here is that do_program_things() can be accessed via a function pointer during debugging and through a normal static function address when built in release mode. do_debug_checks() will be completely optimized out and incur no runtime cost when it is not needed. I have #included program.c in release mode since unity builds are faster and produce more optimal code (C compilers are really bad at optimizing across translation units). Then to actually build the program I use two steps:

1cc $libcflags program.c -o program.so -shared -fPIC $ldflags
2cc $cflags -o program main.c $ldflags

Usually I make sure the first line still works even if _DEBUG is not #defined though its not strictly necessary. Finally do_debug_checks() is something like:

 1static void
 2do_debug_checks(void)
 3{
 4	static struct timespec updated_time;
 5	struct timespec test_time = get_filetime(libname);
 6	if (filetime_is_newer(test_time, updated_time) {
 7		updated_time = test_time;
 8		if (libhandle)
 9			dlclose(libhandle);
10		libhandle = dlopen(libname);
11		if (!libhandle)
12			printf("dlopen: %s\n", dlerror());
13		do_program_things = dlsym(libhandle, "do_program_things");
14		if (!do_program_things)
15			printf("dlsym: %s\n", dlerror());
16	}
17}

First check if program.so has been updated since the last time the program has passed through the loop. The first time through updated_time will be 0 and the check will pass and on other passes the check will only pass if program.so has been updated while the program was running. Then close the old instance of the handle, with an included NULL check since glibc is broken garbage and will crash if you pass something invalid to dlclose(3), and open the new version. Here you may want to include some sort of stall (eg. nanosleep(3) for 100ms) because the file modification time can be updated before the new version of the library has been written to the disk. I include a printf so that if the library failed to load I will get a hint as to why program crashes later on when this procedure returns. This is behaviour is perfectly acceptable since this is a debug only code path and it is fine to fail loudly. Finally the do_program_things() function pointer is updated to the new version so that the main loop can make use of it for actually doing the thing, whatever that may be.

Gotchas

With the above code snippets you are ready to modify compiled parts of your program without restarting but there are still a few things to watch out for.

Static Variables

Variables declared with the static keyword or in the global scope within "program.c" will lose their values when the library is reloaded. If it is important that these values are not reset they must be stored in ProgramCtx variable held by the main process.

Function Pointers

If the ProgramCtx contains pointers to functions declared inside of "program.c" they will be invalid after reloading. For program optimization purposes it is best to avoid using function pointers altogether but if they are absolutely necessary you must include a method of fixing them up after reloading. The best method I have seen is to replace the function pointers in ProgramCtx with indices into a global function pointer table. This is essentially the same thing as a vtable but with one less level of indirection. At runtime your program should call a procedure that updates the table with the new pointers when the library is reloaded.

Compiler Inlining

When using a unity build and every function in your program is declared static the compiler tends to inline everything into main at higher optimization levels. This is good for performance because the CPU does not have to waste time saving and restoring registers or pushing function parameters onto the stack. However you cannot declare do_program_things() as static if you need to export it as a library symbol. To get around this you can use a simple macro:

1#ifdef _DEBUG
2#define DEBUG_EXPORT
3#else
4#define DEBUG_EXPORT static
5#endif

Of course if you are targeting Win32 you should use __declspec(dllexport) instead when doing a debug build.

ProgramCtx Modification

If you rearrange anything in the ProgramCtx struct while the program is running the most likely outcome is that your program will crash. Personally I find this to be an acceptable trade-off since spending too much effort on debug only code is pointless. There are multiple workarounds for this though. If you are OK with simply appending new members after the old members, and they don't need initialization to anything other than 0, you can include a big char array at the end of the struct. When you want to add a new member subtract its size from the char array and add it right above. More complex methods exist if you want to be able to rearrange the struct at runtime but for my use cases writing the code to do so is a waste of time.

For a practical example of code designed around this principal see my Colour Picker here or on github.

Published: Jun. 17, 2024
Updated: Jul. 3, 2024