Introduction
While working on a piece of code, you go through this cycle of edit, compile, run. While working on large binaries the compile time may1 be high. Or while running the code, the initial setup may2 take some time before you get to the part which you need to test. How fascinating would it be if your program was already running with its initial state set, and all you need to do is to just edit a small piece of code, compile that part and somehow trigger the reload of that portion of code. This would be highly useful in case you’re developing things iteratively like tuning the UI.
Dynamic Shared Library
Dynamic shared libraries as the name describes are shared binaries that is loaded dynamically by an executing process. The purpose of these libraries is to have
- Shared executable code among multiple executing process loaded once into the memory.
- Ability to update shared libraries without compiling the main executable using these libraries.
Creating a shared library
Let’s say we have following library that provides a few functions.
Compile the shared library with gcc
or clang
.
We can use the above library in the following manner.
Compile & execute the driver file as follows
Using shared libraries
This shared library libmath.so
can be updated and compiled independently without recompiling main.c
. You can try updating the version number of the shared library and run the main again without recompiling it and it would show the newer updated version number.
This however cannot be directly used to update code while the main executable is in the running state. The dynamic library is loaded once and cannot be reloaded the way described above. We need to load the dynamic library manually and add a hook to hot reload this shared library as required.
Dynamically Loaded Library
In this method, we create a shared library in the same way as above but we don’t link it with the main executable while compiling it. The main executable has extra code that loads the shared library at runtime. All POSIX compatible systems provide APIs dlopen
, dlclose
and dlsym
that can be used to achieve what we want here.
dlopen
: Takes the name of the shared library file and returns the handle (pointer) of the loaded library in memory.
dlclose
: Takes the handle of the loaded shared library and unloads in from memory (unmap).
dlsym
: Takes the handle of the loaded shared library, name of symbol and returns the address of the symbol. If the symbol is not present in the loaded .so
file, it returns NULL
.
In the code above, in load_symbols
we’re loading the shared object file with dlopen
passing RTLD_LAZY
(info) flag. If the .so
file is opened, we fetch the symbols add
and version
. Once load_symbols
is successful, the following printf
statements should print the desired values.
The above code example shows how we can execute code from the shared library without linking it while compiling. The above code has two issues, it does not have a trigger to reload code yet and the code looks significantly different than what we wrote earlier. e.g. add
function is now being called by de-referencing the *add
function pointer. What if we want to write the code in such a way such that it’s able to link the shared library when distributing the product but while testing we’re able to hot-reload code. Best of both worlds without making too many changes to either the driver code main.c
or the shared library math.so
. Let’s make the above code better.
Hot Reloading Code from Shared Library
Let do some code reshuffling and move out the load_symbols
and any other functionality related to hot reloading out of main.c
.
In the file above, we can toggle the hot-reload functionality by removing the directive HOTRELOAD
.
When HOTRELOAD
is defined, we load the libmath.so
in the function hot_reload_init
and set the add
& version
symbols. In the next line, we bind SIGINT
to the hot_reload_init
function, so that every time you press Ctrl+C
, it reloads the math.so
file.
If you’ve noticed by now, we’re no longer calling the add
and version
by de-referencing a function pointer. It looks like a normal function call. Here’s the trick defined in the hotreload.h
We’ve just aliased add
to *add_ld
which is the function pointer. add_ld
is provided by an external library hotreload.c
I’ve also typedef
ed the function pointer types and moved them to typeinfo.h
Compilation steps
After compiling these files, main should be able to print the new version number when Ctrl+C
is pressed.
The advantage of this code is that the main.c
remains almost the same if we’re using normal dynamic shared libraries. We can just remove the HOTRELOAD
macro, recompile the main.c
with again linking -lmath
instead of -lhotreload
and it should work just fine.
On top of this, most of the code in the hot-reload library can be auto generated using a script based on the math.h
file.
Caveats
If the shared library has some state associated with it, reloading it will reset the state created in the shared library memory. For example, if we have a int multiplier = 2;
in math.so
that gets set to 5
by the main.c
via a function call, reloading the .so
file will reset this data to 2
again. We can probably write extra code to save and reload this data. The code will need to be designed in such a way that saves this context data and writes it back after reloading the so file if needed.
Resources
- YouTube Video by Tsoding on “Hot Code Reloading in C”
- Dynamically Loaded Library
- [GitHub] Source Code