Hot reloading is a technique used to reload code while the program is running. This is especially useful for anything graphical, as you can see the changes you make in real time. This is usually done more easily in interpreted languages like Python or JavaScript, but it can also be done in C. This repository is a simple example of how to do hot reloading in C, which handles changes in the layout of the state struct.
This is inspired by tsoding's video Hot Code Reloading in C where he implemented hot reloading in his music visualizer. I wanted to take this idea further and handle changes in the layout of the state struct, which is a common problem when doing hot reloading.
The idea is that we offload all the actual logic of the program into a shared library, and then load that library at runtime. We can make a small launcher program that loads the library, and then we can compile the library separately. This way, we can compile the library and the launcher separately, and then we can reload the library without having to recompile the currently running program.
On unix-like systems, we can use dlopen
to load a shared library at runtime. We can then use dlsym
to get the address of the functions in the library, and call it just like any other function. When we want to reload the libary we can use dlclose
to unload the library, and then repeat the process to load the new version of the library. On Windows, we can use LoadLibrary
, FreeLibrary
and GetProcAddress
to do the same thing, but I won't implement this in this repository.
To make hot reloading more useful, we can preserve the state of the program across reloads. This can be easily done by have a struct that holds all the state of the program, and then passing a pointer to that struct across reloads. We can allocate this on the heap, as the heap is shared between the library and the launcher.
However we need to be careful with this, as we can expect the layout of the struct to change between reloads. We need to store extra runtime type information (RTTI) in the struct, so that we can check which parts of the new struct are similar to the old struct. We can then copy the data from the old struct to the new struct, and then free the old struct. This is where X macros come in handy, as we can easily generate the runtime type information. We can use the preprocessor to generate the RTTI, offsetof
and sizeof
and the #
operator to generate the name, type, offset and size of each field in the struct.
However we can't just store the RTTI in a static variable because we will lose it across reloads. Instead we can embed it in state struct itself and copy all the entries during initialization. However we run into a bit of a problem. The RTTI contains string literals, which would need extra heap allocations. This would make our RTTI very fragmented, with lots of small allocations. But we know the size of all of this at compile time, and we can compute it. That means we can just allocate a single block of memory or an arena for the state and the RTTI, and copy everything there. This makes the RTTI very cache friendly, and easy to free when we are done with it.
Putting this all together we can add two lifecycle hooks to the library, plug_pre_reload
and plug_post_reload
. The plug_pre_reload
function is called before the library is reloaded, and the plug_post_reload
function is from the new library after it is loaded. This way we can free the old state and merge the two states together.
During plug_pre_reload
we have to return a pointer to the old state so we can pass it over to the new library during plug_post_reload
and merge the two states together. For simplicity, I chose not to use a hash table or a more complex data structure to store the RTTI. Instead I just used a linear search to find the corresponding field in the new struct. This is fine since the number of fields in the state struct is usually reasonably small and hot reloading is only used during development. While merging the two states, we first allocate a new arena and initialize the new state. Then we loop over each field of the new state, and search for the corresponding field in the old state, with the same name, type and size. If one exists, then we can copy the data to the new state. If we can't find a corresponding field, then we can just leave it as is. This way we can preserve the state across reloads, and only have to reinitialize the fields that have changed. Finally we can free the old arena.
For a production build we can just statically link main.c
and plug.c
and use the preprocessor to conditionally enable the parts that are only used for hotreloading. This way we can have a single standalone binary without any of the hot reloading code.