How do libraries work? Part 3.

In previous two parts we’ve dealt with creating dynamic libraries on MacOS. Today we’re moving on to different system – Linux. I was planning to go with Windows, but running a web browser while using Virtualbox on my poor laptop is a nightmare and I think I will need to run my old laptop for this.

Nonetheless, here we are. After some struggle with manual pages, g++ tricks 2-week idleness finishes. As always you can find code on gitlab. I tried to reproduce closely what I was trying to achieve in previous blog posts so that I will be able to write a summary comparing different approaches.

Library code and build process

Header file included 4 functions – constructor, destructor, add and sub:

extern "C" void init() __attribute__((constructor));
extern "C" void destroy() __attribute__((destructor));
extern "C" int add(int, int);
extern "C" int sub(int, int);

Source file defined those functions:

#include <iostream>
#include "mymath.h"

void init() {
    printf("Initializing mymath\n");
}

void destroy(){
    printf("Destroying mymath\n");
}

int add(int x, int y){
    std::cout << "Adding\n";
    return x + y;
}

int sub(int x, int y){
    std::cout << "Subtracting\n";
    return x - y;
}

Now, something new is in Makefile:

all:
    g++ -Wall -fPIC -shared -Wl,-soname,libmymath -Wl,-init,init mymath.cpp -o libmymath.so.1.0 -lc

A lot of switches come here, so let’s break it down.

  • -Wall – includes warnings
  • -fPIC – informs compiler to generate “position independent code“, which is required by dynamic libraries. Quoting the manual:  “such code accesses all constant addresses through a global offset table ( GOT ). The dynamic loader resolves the GOT entries when the program starts”.
  • -shared – informs compiler to produce linkable object.
  • -Wl – linker options giving my library a name by which i will be able to refer to it later (using -lmymath while compiling target program) and defining entry point (I did not define any “exit” points with -fini to see what happens).

The guide I was using suggested splitting the creation of an object file and an ELF file, which I skipped.

Program with linked library

First, I wanted to try out how a library linked at compilation time works. Well… This was interesting. Let’s look at the Makefile:

all:
    g++ -Wall -Iinclude -Llib src/main.cpp -lmymath -o bin/test

I do not know how much time I wasted before I learned that I need to place -lmymath after source files. Funny enough, in previous parts this was the default way Simple C++ plugin for VS Code generated a Makefile, but this time writing it from scratch I got stuck for ever. As they say: hours of debugging can save minutes of reading documentation. Everything else was pretty straightforward: I copied header file to include/ and library to lib/ folder and wrote a source file:

#include <cstdio>
#include "mymath.h"

int main(){
    printf("Mymath adding. 3+4=%d\n", add(3, 4));
    printf("Mymath subtracting 3-4=%d\n", sub(3, 4));
    return 0;
}

And finally the output:

Initializing mymath
Initializing mymath
Adding
Mymath adding. 3+4=7
Subtracting
Mymath subtracting 3-4=-1
Destroying mymath

Note that “Initializing mymath” appears twice and where the results of add and sub functions are evaluated. I am curious whether this double call to constructor is caused by passing -Wl,-init,init to compiler as well as setting constructor attribute in source code. I am keen to play around with it more and search something in documentation. If you know about it, drop me a line in comments.

With regards to evaluating my library functions before calling printf – this is something I expected, but at first I was a little confused when I used cout below.

Program with dynamically loaded library

Loading additional code on runtime turned out to be very similar as in linux: get handle to library, load functions, use them, unload. Both use the same POSIX functions.

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

int main() {
    void* lib_handle;
    int (*add)(int, int);
    int (*sub)(int, int);

    lib_handle = dlopen("/usr/local/lib/libmymath.so", RTLD_LAZY);
    if(!lib_handle) {
        std::cout << "Did not find libmymath\n";
        return 1;
    }
    add = (int(*)(int, int))dlsym(lib_handle, "add");
    sub = (int(*)(int, int))dlsym(lib_handle, "sub");
    if(!add || !sub){
        std::cerr << "Could not load functions\n";
        return 2;
    }
    std::cout << "adding 3+4=" << (*add)(3, 4) << std::endl;
    std::cout << "subtracting 3-4=" << (*sub)(3, 4) << std::endl;
    dlclose(lib_handle);
    return 0;
}

The output was different due to the fact that I used cout instead of printf and initializers seem to not have been called. I have no idea why.

adding 3+4=Adding
7
subtracting 3-4=Subtracting
-1

Summary

That’s it. Same task, two different operating systems. Next week, I will try to deliver similar example for Windows and then, one week later a comparison of all the approaches and summary of what I’ve learned.

Check this yolinux post on libraries, since it contains more extensive description of what I was doing here.