Tags:
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
We are going to show all the forms in this article
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
stateful_writer.h
struct stateful_writer {
int _counter {};
int increment(int how_much) noexcept;
void write(const std::string &what) noexcept;
};
And the implementation
stateful_writer.cpp
#include "stateful_writer.h"
int stateful_writer::increment(int how_much) noexcept {
++_counter;
return how_much + _counter;
}
void stateful_writer::write(const std::string &what) noexcept {
++_counter;
std::cout << _counter << " " << what << std::endl;
}
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
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:
-fPIC
: This tells the compiler we are using position independent code. This is used by the shared library, but it causes no harm here.ar r
: inserts or replaces an object in a 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:
-I
: Adds a directory to the include resolution path
-o
: Defines the output name of the generated binary
To run this binary, simply call:
./app_static
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:
-shared
: Tells the compiler to generate a shared library, not an executable binary-L
: Adds a directory to the library resolution path-l
: Links a library to the executable binary. Please note -l
automatically inserts the lib prefix on the name of the given static libraryTo 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
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
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 *);
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;
dlclose(lib);
}
}
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
delete_writer(writer);
Also, let's not forget to close the library at the end
dlclose(lib);
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.