How do libraries work? Part 2

In the previous part we’ve taken a quick look on how to create a dynamic library in MacOS system that would be used as dependent library. While this approach is really straightforward, and has all the advantages of the dynamic loading (smaller binary file size, ability to patch library without the need to recompile entire project etc.), it lacks one very cool feature: being able to manage load code when it’s actually needed, providing better memory management options. Let’s turn it into runtime loaded dynamic library.

Changes in the library…

Unfortunately the usage of namespaces was troublesome and I was discouraged to do so and use C-compatible code. It won’t be a big deal, but we have to add the extern "C" part to make it work (more about what it does here):

#include <iostream>
define EXPORT __attribute__((visibility("default")))
__attribute__((constructor))
static void _constructor() {
    std::cout << "Initializing mymath\n";
}
EXPORT
extern "C" int add(int a, int b) {
    return a + b;
}
EXPORT
extern "C" int sub(int a, int b) {
    return a - b;
}
__attribute__((destructor))
static void _destructor() {
    std::cout << "Destroying mymath\n";
}

Apart from adding the extern, nothing special happens here. Let’s move to our test program.

…and a new code

#include <iostream>
#include <dlfcn.h>

int main() {
    const char* libName = "bin/mymath.A.dylib";
    void *libHandle;
    std::cout << "# main() starts" << std::endl;
    libHandle = dlopen(libName, RTLD_NOW);
    if(libHandle == NULL) {
        std::cerr << "Error opening mymath:\n" << dlerror() << std::endl;;
        return 1;
    }
    int (*mymath_add)(int, int) = (int(*)(int, int))dlsym(libHandle, "add");
    if(mymath_add == NULL) {
        std::cerr << "Error opening while getting address of add:\n" << dlerror() << std::endl;;
        return 2;
    }
    int (*mymath_sub)(int, int) = (int(*)(int, int))dlsym(libHandle, "sub");
    if(mymath_sub == NULL) {
        std::cerr << "Error opening while getting address of add:\n" << dlerror() << std::endl;;
        return 2;
    }
    std::cout << "# testing add: 55+66=" << mymath_add(55, 66) << std::endl;
    std::cout << "# testing subtract: 55-66=" << mymath_sub(55, 66) << std::endl;
    if(dlclose(libHandle) != 0) {
        std::cerr << "Error closing library:\n" << dlerror() << std::endl;;
        return 2;
    }
    std::cout << "# main() exits" << std::endl;
    if(argc == 2 && strncmp(argv[1], "crash\n", strlen("crash\n"))){
        std::cout << "# testing subtract: 55-66=" << mymath_sub(55, 66) << std::endl;
    }
    return 0;
}

First of all, note that I do not include mymath.h header file. Everything will be included during runtime. In order to do so, I need to pass path to the library to dyld_open() function. The path can be relative, and the function will search for the library in following order according to its man page:

man 3 dlopen

dlopen() searches for a compatible Mach-O file in the directories speci- fied by a set of environment variables and the process’s current working directory. When set, the environment variables contain a colon-separated list of directory paths, which can be absolute or relative to the current working directory.
When path does not contain a slash character (i.e. it is just a leaf name), dlopen() searches the following until it finds a compatible Mach-O file: $LD_LIBRARY_PATH, $DYLD_LIBRARY_PATH, current working directory, $DYLD_FALLBACK_LIBRARY_PATH.

Having obtained handle to the library I am able to search in its memory for functions I want to get – I search for symbols using dlsym() function. That’s where magic with casting happens that is so strange I still have problems with understanding it. Since dlsym() returns void* pointing to function I need to cast it to pointer to function returning an int and taking 2 integers as argument. The beautiful result is here:

int (*mymath_add)(int, int) = (int(*)(int, int))dlsym(libHandle, "add");

Same applies to function responsible for subtracting.

Having checked if I obtained correct function handles I can use them. The last step is freeing resources. If I try to call any of the functions I will get a segmentation fault which you can check running code with “crash” argument.

Additional resources: