Building programs with dynamic runtime libraries

Photo by Ryunosuke Kikuno / Unsplash

In my last post, I wrote about the multiple stages during the compilation of a program. The last stage in this process was the linking stage wherein the compiler resolves all the functional calls to various modules. The linker resolves these references from static or dynamic libraries that are linked to the program. This post will delve into creating and using dynamic libraries on your system.


During compilation when we pass in multiple *.c files or *.o files to the compiler,  the linker combines all these files into one static executable. Optionally we can configure the program to make use of dynamic shared libraries which can be loaded during runtime, instead of being compiled within the executable. Dynamic libraries have multiple advantages over static executables. For one, they allow the user to extract the common codebase out of a single executable into an independent object. This object file can then be shared among multiple programs. An option like this reduces the overall size of the static executable and also provides a common library that can be shared.

Static executable

To create a dynamic library we can pass in the --shared flag to the compiler. The compiler will then output a shared library file without an entry point. The output file is a dynamic library that can be linked to any static executable during runtime.

Let's create a shared library in C using the Clang compiler. Here we define a function called charCount within charcount.c which provides the count of every alphabet in a given string.

#include <stdio.h>
#include <stdlib.h>


int* charCount(char *string){
    /*
    This function gives the count of each character in a string.
    */
    int *count = calloc(256, sizeof(int));

    for(int i=0; string[i]!='\n'; i++){
        count[string[i]]++;
    }
    return count;
}
charcount.c

Let's compile charcount.c into a shared library using --shared flag.

clang --shared charcount.c -o charcount.so

Note that we have prepended the output file with a lib prefix. This is necessary as the compiler identifies libraries based on this prefix standard. The .so extension is a UNIX standard for shared library files.

Our next step would be to use this library within another program. Let's create this program within the main.c file as shown below. The main  function basically takes in an input string from the user and passes it to charCount which returns an array containing counts for every utf-8 character.

#include <stdio.h>
#include <stdlib.h>

int* charCount(char *string);

int main() {
   char *value = malloc(256 * sizeof(char));
   printf("Enter a string: ");
   fgets(value, 256, stdin);
   int *result = charCount(value);
   for (int i = 0; i < 256; i++) {
      if(*result > 0) {
         printf("%c: %d\n", i, *result);
      }
      result++;
   }

   return 0;
}
main.c

Note that the main.c file does not import the charcount.c file. All we have in the main.c file is the declaration of charCount which is defined within charcount.c.

We can now compile the main executable in the following way.

clang -L. -lcharcount main.c -o main

The -L. flag indicates the compiler should look for dynamic libraries in the current directory, so make sure the charcount.so is in the same directory as main.c. The -lcharcount flag represents the name of the library and directs the compiler to link libcharcount.so. The l stands for lib.

You can now call the executable to run the program. If the shared library is present within the same directory, the program will run. If the shared object is located in another directory, it will have to be specified within the LD_LIBRARY_PATH variable.

LD_LIBRARY_PATH=other/dir ./main

Note that the code from charcount.c will be loaded during runtime and is not present in main. You can verify this by calling objdump -t main.

➜ objdump -t main                         

main:   file format mach-o 64-bit x86-64

SYMBOL TABLE:
0000000100008020 l     O __DATA,__data __dyld_private
0000000100000000 g     F __TEXT,__text __mh_execute_header
0000000100003e90 g     F __TEXT,__text _main
0000000000000000         *UND* ___stdinp
0000000000000000         *UND* _charCount
0000000000000000         *UND* _fgets
0000000000000000         *UND* _malloc
0000000000000000         *UND* _printf
0000000000000000         *UND* dyld_stub_binder

As you can notice the mapped address for _charCount is 0000000000000000 which means it is not assigned whereas a static executable would have an address for _charCount.

When we run the program, the linker resolves the charCount function during runtime by finding the dynamic library in the directory and loading the code within the memory.

Dynamic loading of libraries

Common examples of shared libraries include libc.so which is the C library containing stdio and stdlib function. You will usually find it in the /usr/local/lib or /usr/lib/ directory, depending on your computer OS. The compiler by default searches these directories for dynamic references during compile-time and runtime. If the library is present in any other directory, it needs to be added to the LD_LIBRARY_PATH ( DYLD_LIBRARY_PATH on macOS) environment variable.

Shared libraries are named differently on different operating systems. On Windows, they are known as .dll files. DLL stands for Dynamic-link library. On Linux, they are known as .so files which stand for Shared Objects. In macOS, they are commonly referred to as Dynamic Libraries with the .dylib extension.

A major advantage of using dynamic libraries is that the code can be separately updated and compiled for the library as well as the static programs. If a library is shared among 100 programs, only the library needs to be updated once. The programs can then refer to the updated version without any change. It also decreases compile time as the library code does not need to be compiled on every small change made to the executable. I would suggest you use dynamic or static libraries within your program, based on your requirements. If the executable needs to be loaded quickly and does not need to share code with another program, it is better to go for a static executable.

If you like such content please do consider following me on Twitter. I frequently delve into the internals of software and journal my learnings on this blog, so make sure you also subscribe to it.


References

  1. Baraban, B. D. (2018, December 12). C Dynamic Libraries — What, Why, and How? - Brennan D Baraban. Medium. https://medium.com/@bdov_/https-medium-com-bdov-c-dynamic-libraries-what-why-and-how-66cf777019a7
  2. Baraban, B. D. (2018a, October 7). C Static Libraries — What, Why, and How? - Brennan D Baraban. Medium. https://medium.com/@bdov_/https-medium-com-bdov-c-static-libraries-what-why-and-how-b6b442b054d3
  3. Chaparro, L. (2020, January 26). Static and Dynamic Libraries in C Language - Luis Chaparro. Medium. https://medium.com/@luischaparroc/https-medium-com-luischaparroc-dynamic-libraries-in-c-96a989848476
  4. How to Load Libraries at Runtime. (2019, February 19). YouTube. https://www.youtube.com/watch?v=_kIa4D7kQ8I
Lezwon Castelino

Lezwon Castelino

Freelancer | Open Source Contributor | Ex- @PyTorchLightnin Core ⚡ | Solutions Hacker | 20+ Hackathons