Loading libraries in C++ on Linux

This is that kind of code that we don't write all the time, so let's remember the different ways to load and create libraries on Linux

There are 3 ways a library can be used by an application

  1. Importing a static library
  2. Static linking a shared library
  3. Dynamic loading a shared library

We are going to show all the forms in this article

Creating the library

We are going to create a very simple library (some code is omitted to improve the reading).

This library simply adds an increasing value to an integer or print a string with an increasing prefix


struct stateful_writer {
    int _counter {};
    int increment(int how_much) noexcept;
    void write(const std::string &what) noexcept;

And the implementation


#include "stateful_writer.h"

int stateful_writer::increment(int how_much) noexcept {
    return how_much + _counter;

void stateful_writer::write(const std::string &what) noexcept {
    std::cout << _counter << " " << what << std::endl;

Creating the application with static linking

The application uses the library to perform some very important tasks

#include <stateful_writer.h>

int main(int argc, char **argv) {
    stateful_writer w;
    w.write("We're all stories");
    w.write("in the end");

    std::cout << "how much: " << w.increment(10) << std::endl;
    std::cout << "how much: " << w.increment(10) << std::endl;
    std::cout << "how much: " << w.increment(10) << std::endl;

The expected output is:

1 We're all stories
2 in the end
how much: 13
how much: 14
how much: 15

Compiling the static library

First, what is that ?

Every time we compile a C++ file, a object file is generated (Ex: stateful_writer.o). A static library is an archive of those object files that can be included in the executable binary during the link stage, just like the source was part of the project.

As C++ still doesn't have a common ABI defined, the only way to ensure a static library is compatible is using the same compiler.

We are now going to compile the static library

# Compiles the source code and generates the object file stateful_writer.o 
g++ -std=c++17 -fPIC -c stateful_writer.cpp
# Generates the static library libstateful_writer.a including the object stateful_writer.o
ar r libstateful_writer.a stateful_writer.o

Explanation of the parameters:

  1. -fPIC: This tells the compiler we are using position independent code. This is used by the shared library, but it causes no harm here.
  2. ar r: inserts or replaces an object in a library

Compiling the application with the static library

# Compiles the source code and generates the object file main.o
# ../lib points to the location of the file stateful_writer.h
g++ -c main.cpp -I../lib

# Generates the executable binary app_static including the static library libstateful_writer.a
# Adding this .a file is the same as adding all .o files contained in this library
g++ -o app_static main.o ../lib/libstateful_writer.a

Explanation of the parameters:

To run this binary, simply call:


Compiling the application with the shared library

Let's first generate the shared library, using the previously generated stateful_writer.o

# Generates a shared library libstateful_writer.so
g++ -std=c++17 -shared -o libstateful_writer.so stateful_writer.o

# Generates a executable binary app using the shared library libstateful_writer.so
g++ -o app main.o -L../lib -lstateful_writer

Explanation of the parameters:

  1. -shared: Tells the compiler to generate a shared library, not an executable binary
  2. -L: Adds a directory to the library resolution path
  3. -l: Links a library to the executable binary. Please note -l automatically inserts the lib prefix on the name of the given static library

To run this binary, it's slightly more complicated. As libstateful_writer.so is shared, the operating system needs to know its location.

LD_LIBRARY_PATH=../lib ./app

Creating the application with dynamic loading

Different from the static linking, this method is more flexible as the library is loaded in runtime. As I am writing this, C++ doesn't have an ABI defined, so we need to use a C interface to ensure it will work regardless of the compiler used to compile of this shared library

Changing the library to export functions to the dynamic loader

We have to add some methods in our implementation to export our class using C free functions

// This is important. extern "C" tells the C++ compiler that this will be exported in the .so file using C ABI format
extern "C" {

// C language does not have classes, so we need to return something opaque.
void *new_writer() {
    stateful_writer *w = new stateful_writer();
    return (void *)w;

void delete_writer(void *writer_handle) {
    stateful_writer *w = (stateful_writer *)writer_handle;
    delete w;

int stateful_increment(void *writer_handle, int val) {
    stateful_writer *w = (stateful_writer *)writer_handle;
    return w->increment(val);

void stateful_write(void *writer_handle, const char *c) {
    stateful_writer *w = (stateful_writer *)writer_handle;
    return w->write(c);

Now with our C functions defined, let's create a header with function pointers types to simplify the life of the application using this library

using new_writer_f = void *();
using delete_writer_f = void(void *);
using stateful_increment_f = int(void *, int );
using stateful_write_f = void(void *, const char *);

Creating a dynamic load version of our application

There are 3 main functions for dynamic loading a library

Now, we will create some helper functions to load a library

// Loads a library and returns a pointer to the .so instance in memory
void *load_lib(const char *lib_name) {
    void *lib = dlopen(lib_name, RTLD_NOW);
    if(lib == nullptr) {
        _print_error(lib, lib_name);
    return lib;

And print an error if something fails

// If any error happens, get a description of the error
void _print_error(void *lib, const char *name) {
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        std::cerr << "Cannot load '" << name << "': " << dlsym_error << std::endl;

And returns a void* to a certain function

// Returns a pointer to an exported function from the library
void *load_function(void *lib_handle, const char *name) {
    void *p = dlsym(lib_handle, name);
    if(p == nullptr) {
        _print_error(p, name);
    return p;

Now, let's load the library and pointers to the functions

// Header of the dynamic loading library
#include <dlfcn.h>

#include <stateful_writer_imports.h>

// ... ... ...

// Getting the library filename as 1st parameter
const char *lib_filename = argv[1];

void *lib = load_lib(lib_filename);
if(lib == nullptr) { return 1; }

new_writer_f *new_writer = (new_writer_f *)load_function(lib, "new_writer");
if(lib == nullptr) { return 2; }

delete_writer_f *delete_writer = (delete_writer_f *)load_function(lib, "delete_writer");
if(lib == nullptr) { return 2; }

stateful_increment_f *stateful_increment = (stateful_increment_f *)load_function(lib, "stateful_increment");
if(lib == nullptr) { return 2; }

stateful_write_f *stateful_write = (stateful_write_f *)load_function(lib, "stateful_write");
if(lib == nullptr) { return 2; }

Now with everything loaded, let's use this incredible library

void *writer = new_writer();
stateful_write(writer, "Exterminate! Exterminate!");
stateful_write(writer, "Delete! Delete!");

std::cout << "How much: " << stateful_increment(writer, 100) << std::endl;

// We cannot forget to destroy the object

Also, let's not forget to close the library at the end


Compiling the application

To generate the application , we need to link libdl.so with our executable binary to have the function dlopen, dlclose and dlsym working.

# Compiles the source code and generates the object file main_dyn.o
# ../lib points to the location of the file stateful_writer_imports.h
g++ -I../lib -Wall -c main_dyn.cpp

# Generates a executable binary app without linking any library
g++ -Wall main_dyn.o -ldl -o app_dyn

And to run, we just execute the binary with the library name

./app_dyn ../lib/libstateful_writer.so 
1 Exterminate! Exterminate!
2 Delete! Delete!
How much: 103

That is all.

Challenge: Discover where the references in the application are from.

Complete source code