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.
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.
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 #ifdef
s 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 #include
d
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 #define
d 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.
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.
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.
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.
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.
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.