Creating and using shared libraries with different compilers on different operating systems
If you want to make your C/C++ project portable between operating systems and compilers, there are some things to consider. In this article I focus on shared libraries and give a practical overview on how to create and use shared libraries with various compilers on various operating systems, with the goal of portable source code and a unified build process.
“A shared library or shared object is a file that is shared by executable files and further shared objects files.” A shared library on Linux is called “dynamically linked shared object”, and has the file extension .so. The windows equivalent is the “dynamic link library” usually with file extension .dll.1
This article is structured as follows:
At first we create and use a minimal shared library on Linux with GCC. We take a look at the necessary gcc options for compiling and linking and check at the exported symbols of the .so file with the tool “nm”.
Then we build the same code on Windows with MinGW. At first by directly linking the dynamic link library. Then, we also build and use a so-called import library - we have to consider some platform/compiler specifics at this point. I give a brief explanation what an import library is and when it ’s needed.
We go on by building our minimal demo application with other compilers, like the Microsoft Visual C++ Compiler (MSVC) and clang/LLVM. To make the build process more convenient and more flexible, we use CMake at this point.
Last but not least, a few words about the interoperability of libraries created with different compilers (ABI compatibility) follow.
The final version of the minimalistic portable code demo, that is gradually developed in this post, is available on github.
Shared libraries with Linux/GCC
Let’s start building our demo on Linux with GCC. The header of our shared library is shared.h and its implementation is in shared.cpp. Our application is implemented in main.cpp, where we want to use our shared library:
shared.h:
#ifndef SHARED_H__
#define SHARED_H__
void f();
class X {
public:
X();
void mX();
};
#endif
shared.cpp:
#include "shared.h"
#include <iostream>
void f() {
std::cout << "f()\n";
}
X::X() {
std::cout << "X::X()\n";
}
void X::mX() {
std::cout << "X::mX()\n";
}
Now let’s compile our library with GCC:
g++ -fPIC -shared shared.cpp -o libshared.so
The command above compiles shared.cpp to a shared library (-shared) with position independent code (-fPIC). The compiler output (-o) is written to libshared.so. Let’s take a quick look external symbols of our library. We can use “nm” for this purpose:
nm -gC libshared.so
The option -g tells nm that we are just interested in external symbols. With -C the low-level symbol names are translated to human readable symbols and we can directly see the names of our methods/functions the output (on Linux/GCC, all symbols are exported by default - this is not necessarily the case with oder compilers):
...
00000000000008e4 T f()
00000000000008c0 T X::X()
...
Fine, our library exports the symbols f()
and X::X()
- ready to be used by some application -> main.cpp:
#include "shared.h"
int main() {
f();
X x;
x.mX();
}
We compile main.cpp and link our shared library with:
g++ -L. -lshared -o main main.cpp
The option -L.
adds the current directory “.” to the linker search path – without this the linker will not find our library. The generated executable is “main”.
Before we execute our program, let’s check what libraries it is linked with. We can do this with the command ldd
:
ldd main
Like expected, libshared.so is among the linked libraries, but it cannot be found:
...
linux-vdso.so.1 (0x00007fff635fe000)
libshared.so => not found
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f334b44d000)
...
We will also get an error when trying to execute our program:
$> ./main
./main: error while loading shared libraries: libshared.so: cannot open shared object file: No such file or directory
To solve this problem, we must somehow specify where the library can be found. On Linux, the environment variable LD_LIBRARY_PATH can be used to specify directories where libraries are searched for first (before the standard set of directories)((GNU Dynamic Loader search directories)) => prepending our commands above with LD_LIBRARY_PATH=./ solves the issue of the not found library and the program will execute like expected:
$> LD_LIBRARY_PATH=. ./main
f()
X::X()
X::mX()
Shared libraries with Windows/MinGW
To be able to compile/link our code with different compilers, we have to consider some compiler specifics.
- MSVC cannot directly link to a dll -> we need a so-called “import library”.
- Symbol Visibility: GCC/MinGW exports all symbols by default, MSVC exports no symbol by default.
Linking the DLL directly
If you are on Windows and compile everything with MinGW (the DLL and the application), linking directly to the dll is fine. You can take the source code from above without any changes and build it very similar like on Linux:
g++ -fPIC --shared -o shared.dll shared.cpp
g++ -L. -lshared -o main main.cpp
The above lines produce shared.dll and main.exe that links directly against shared.dll.
Creating and linking the import library
Like shown above, it is perfectly fine to directly link the DLL if the DLL was also created by MinGW. But this is not the common way to work with shared libraries on windows because just a few compilers support this - the Microsoft Visual Studio Compiler is not among them.
On Windows, a DLL usually has a matching so called “import library”. If an executable wants to use functions of a DLL it does not link the DLL directly but it’s import library.
Import libraries are similar to static libraries and usually also have a .lib file ending. In contrast to static libraries, they do not contain the complete function definitions, but just “stubs” of the exported symbols of the DLL to satisfy the linker and to load the DLL at runtime.
We can create the DLL and its corresponding import library with MinGW((Creating import libraries with MinGW)):
g++ -c shared.cpp -o shared.o
g++ -shared -o shared.dll shared.o -Wl,--out-implib,libshared_dll.lib
The option -Wl,--out-implib,libshared_dll.lib
tells the compiler to also create the corresponding import library _libshareddll.lib. Then we compile our application and link the import library instead of the dll:
g++ -o main.exe main.o -L. -llibshared_dll
Symbol visibility
The default behavior of MinGW is to export ALL symbols of a DLL. But if one or more functions are declared with __declspec(dllexport)
, only those functions are exported.
The behavior of the Microsoft Visual Studio Compiler is different: Per default NO symbols are exported, just symbols explicitly declared with __declspec(dllexport).
So let’s make the code “MSVC compatible”, without loosing the compatibility to the other compilers:
We add an additional file shared_EXPORTS.h
:
#ifndef _SHARED_EXPORTS_H__
#define _SHARED_EXPORTS_H__
#ifdef _WIN32
#ifdef shared_EXPORTS
#define SHARED_EXPORT __declspec(dllexport)
#else
#define SHARED_EXPORT __declspec(dllimport)
#endif
#else
#define SHARED_EXPORT
#endif
#endif /* _SHARED_EXPORTS_H__ */
and modify our shared.h as follows:
#ifndef SHARED_H__
#define SHARED_H__
#include "shared_EXPORTS.h"
void SHARED_EXPORT f();
class SHARED_EXPORT X {
public:
X();
void mX();
};
#endif
We compile the code now similar like before but we define shared_EXPORTS
by passing -Dshared_EXPORTS
to gcc when compiling shared.cpp
:
g++ -Dshared_EXPORTS -c shared.cpp
g++ -shared -o shared.dll shared.o -Wl,--out-implib,libshared_dll.lib
g++ -o main.exe main.o -L. -llibshared_dll
When using the library (i.e. linking it to main.exe), shared_EXPORTS
is not defined, and SHARED_EXPORT
is set to __declspec(dllimport)
. This means when we build the library, our functions are exported and when we want to use our library, the functions are imported from the dll.
Creating shared libraries with CMake
Up to now, we can build the same source code on Linux with GCC and on Windows with MinGW. We also prepared the code to be compilable with the Microsoft Visual Studio Compiler (MSVC).
Now we also want to build and use a DLL with other compilers like MSVC. Therefore we don’t create the in Visual Studio solution by hand, instead we choose a more flexible method and use CMake.
CMake is a cross-platform build tool that tremendously simplifies building C/C++ code with different compilers on different platforms. With just a few steps we can build our code without having to worry about the exact compilation/linking steps. If you don’t know CMake, I’ll highly recommend you to check it out. This is no CMake tutorial, I’ll just show the general procedure:
- At first we create generic project description in a more or less “declarative”, compiler and platform independent way. Therefore we create a file “CMakeLists.txt” in the source root folder with a just few lines (see below).
- Then, we run “cmake” on the platform where we want to build. This generates a platform and compiler specific, “native” build description (e.g. Makefiles, a Visual Studio Solution or others) from the generic CMakeLists.txt.
- To actually build the code, we have to invoke the “native build command”, e.g. “make” if we use Makefiles.
ad 1.) The “generic project description” in CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(sharedLibsDemo) # create a project with the given name
add_library(shared SHARED shared.cpp) # compile "shared.cpp" and create a SHARED library called "shared"
add_executable(main main.cpp) # compile "main.cpp" the the executable called "main"
include (GenerateExportHeader)
GENERATE_EXPORT_HEADER(shared # generates the export header shared_EXPORTS.h automatically
BASE_NAME shared
EXPORT_MACRO_NAME shared_EXPORTS
EXPORT_FILE_NAME shared_EXPORTS.h
STATIC_DEFINE SHARED_EXPORTS_BUILT_AS_STATIC)
target_link_libraries(main shared) # link our previously created shared library "shared" to the exectable "main"
By using the GENERATE_EXPORT_HEADER
CMake macro like above, you don’t need to create the shared_EXPORTS.h
on your own - it’s created automatically by CMake.
ad 2.+3.) Let CMake generate the platform and compiler specific project description with CMake:
on Linux/GCC, generate Unix Makefiles and build with make:
cmake .
make
on Windows/MinGW, to generate MinGW Makefiles and build with mingw32-make.exe:
cmake -G "MinGW Makefiles" .
mingw32-make.exe
on Windows/MSVC to generate Visual Studio 12 Solution and build the ALL_BUILD target:
cmake -G "Visual Studio 12 2013" .
cmake --build . --target ALL_BUILD --config Release
CMake makes it quite easy to build with any other of the supported compilers. E.g., building with clang and Ninja can be done as simple as:
export CXX=/usr/bin/clang++
export CC=/usr/bin/clang
cmake -G "Ninja"
ninja
Interoperability of Libraries (ABI compatibility)
Libraries written in C++, compiled with different compilers, or even just with different releases of the same compiler, often cannot be linked together2. Sometimes there exists basic ABI (Application Binary Interface) compatibility (e.g. GCC and clang) but it’s likely that you have to recompile your library and your application from source with the same compiler to be able to link them.
DLLs are slightly different: If the DLL is written in C it can be linked even with applications written in C++. DLLs written in C++ work too, as long as you expose its symbols through a C interface declared with extern "C"
. Extern “C” makes a function-name in C++ have ‘C’ linkage and no compiler specific name mangling is applied. For C++ classes this means you have to make some kind of “C wrapper” and expose those functions declared with extern "C"
. If you try to use the C++ interface of a library compiled with a different compiler you will likely get linker errors because different compilers often use incompatible name mangling schemes of the exported C++ symbols (the exported symbols are named differently).
For our demo shared library from above this means: We cannot build our demo with MinGW and link it to our application “main” built with MSVC. But we can easily declare our C function f()
with extern "C"
. This would allow us to use function f() of the DLL even in applications build with different compilers:
...
extern "C" void SHARED_EXPORT f();
...
To also make the functionality of class X available to be used by other compilers (e.g. you want to distribute the library in binary form), the most robust way would be to create a C wrapper: wrap the methods into C functions, and declare them as extern "C"
. There are also other techniques to create a binary compatible C++ interface (at least to a certain degree) - if you are interested in how this can be done, I suggest you to read this post3.
Of course there is no ABI compatibility between libraries/applications compiled on different operating systems unless you cross compile - but that’s another topic.
The final version of the minimalistic portable code demo, that is gradually developed in this post, is available on github.
If you have questions or any kind of feedback please leave a comment below.