Dynamically Linked Libraries & Shared Object Files
TITLE
Dynamically Linked Libraries & Shared Object Files
DESCRIPTION
Prior to reading this, a common working knowledge of the C programming language is very helpful.
This demo will go over the semantics of dynamically linked libraries as used in C. Look for parallels between this and opening/reading/closing a file/directory/etc in this post.
A dynamically linked library is a compiled binary similar to an object file (think foo.o) that has been compiled such that other executing programs can use the functions therein for their own purposes.
Let's say, for instance, that we have some trivial file called my_dl.c whose contents are as follows:
int add(int a, int b) {
return a + b;
}
That's it, no headers, no extra fluff, it is what it is: a file that contains a simple declaration and definition of a function that adds two numbers. Note the types of parameters (int and int) and the return type (also int) as these will be important later.
To compile this into a shared object file, we must call gcc as follows:
$ gcc -shared -fPIC -o my_dl.so my_dl.c
The -fPIC option creates a position-independent shared library. This means that the code is not dependent on being offset relative to some other bit of code. This makes it suitable for inclusion in a shared library. For trivial examples like this, we do not need it. However, for more complex libraries it can be important.
After running the previous command, we have created a shared object file called my_dl.so.
Let us now start to build a driver program to test that our shared object library was built properly. First, let us create a C file, and we will call it dl_practice.c. At the top of dl_practice.c let us include the following libraries:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
Next, let's create a main(), and inside it declare a couple variables we will need later:
int main() {
void *handle;
int (*add)(int, int);
The void *handle will be a handle to our shared library once we open it. The function pointer add will be how we will access the function in our shared library. Note that the name add is not important, but rather the return type and the parameter types are what are important.
Now, let us open our shared library:
handle = dlopen("./my_dl.so", RTLD_NOW);
if (!handle) {
fprintf(stderr, "Error: Failed to create handle: %s.\n", dlerror());
return EXIT_FAILURE;
}
There are a few things to note here. If the call to dlopen fails, handle will contain the value NULL. The flag RTLD_NOW is one of two values that must be passed there, the other being RTLD_LAZY. This just has to do with how and when the library is loaded. We won't concern ourselves with this for now and just use RTLD_NOW. Note also the use of dlerror to get the "reason" for failure. This is analogous to the use of strerror(errno) in our past programs, but is specifically for the dlfcn library.
So, we have a handle to our library. However, we cannot actually use the function yet. This is because we need to actually load that function. To do so, we will make the following call:
*(int**)(&add) = dlsym(handle, "add");
Now, our function pointer add can call the function we declared in our shared library. Therefore, if we were to call printf("%d\n", add(2, 3));, the number 5 would be printed to standard out.
Now, when we are done using our shared library we must make sure we close it. To do so, make the following call:
dlclose(handle);
return 0;
}
Do you notice any similarity between this and opening/closing a file?
To compile this driver program, we do not need to change the #included libraries at the top. However, we do need to pass one additional flag to gcc so that we can use dlfcn:
$ gcc -o dl_practice dl_practice.c -Wall -Werror -pedantic-errors -ldl
The -ldl at the end tells gcc to find the dlfcn library and include it.
My whole very simple makefile for this little demo looks as follows:
all:
gcc -shared -fPIC -o my_dl.so my_dl.c -Wall -Werror -pedantic-errors
gcc -o dl_practice dl_practice.c -Wall -Werror -pedantic-errors -ldl
clean:
rm *.so dl_practice
Once you get it working, try making some more dynamically linkable shared libraries. Try new functions and function signatures. Try creating two functions with the same name but different behavior in two separate libraries. How does this work? What happens? What did you expect to happen?
Elliot Wasem, <elliotbielwasem@gmail.com>
SEE ALSO