In this article, we will look at how we can use CMake to compile and install a library we have developed, be it a static, dynamic, or header-only library. We look at all the steps we need to take to make the installation successful, which will contain steps to automatically generate files that other projects need to discover and find in our library. We will end this article by testing the library installation by creating a simple project that consumes our developed library.
By the end of this article, you have all the tools at your disposal to develop a library that can be easily used by other projects, regardless of the operating system. So, let’s get straight into it and see how you can become a cross-platform library developer kingpin.
Download Resources
All developed code and resources in this article are available for download. If you are encountering issues running any of the scripts, please refer to the instructions for running scripts downloaded from this website.
- Download: Build-Systems-Part-4
In this series
- Part 1: How to use Makefiles to automate CFD solver compilations
- Part 2: Introduction to CMake for CFD practitioners
- Part 3: Advanced CMake features to power up your CFD development
- Part 4: How to compile, install, and use custom libraries with CMake
- Part 5: How to add external libraries into your CFD solver using CMake
- Part 6: How to automate testing with CMake, CTest, and gtest
In this article
Introduction
In this article, we look at the more complex aspect of CMake: dependency management. Working with dependencies/libraries in a compiled language is hard, and we have explored this topic numerous times on this website. First, we need to ensure that we have a matching interface, that is, class definitions that expose the functionality of our library to the end-user. Once we make a change, even just changing the name of one function, compilation errors are produced. In other words, breaking the application programming interface (API) will break the compilation.
But even if we keep the API consistent and are able to compile both the library and the code that depends on it, we may still get errors at runtime due to incompatible application binary interfaces (ABI). The ABI sets out how the compiled binaries (executable, library, operating system, etc.) communicate with each other. Different platforms have different communication (calling) patterns, and so we need to compile our executable or library for each operating system, build mode (debug/release), and architecture (32-bit/64-bit).
You may not be familiar with the term ABI but I’m sure you have come across software download pages where you had to specify your operating system, or architecture (32-bit/64-bit), and this is to ensure that the software you are downloading can correctly communicate with its dependencies, in this case, the operating system.
If we pick an interpreted language, for example, we have far fewer restrictions, as we only need to ensure a consistent API. We check at runtime if if the required functions are available, and then we simply execute them. Since all of these libraries use the same runtime (e.g. a Python/Matlab/Javascript/etc. interpreter), they automatically satisfy the ABI requirements, as it is the job of the interpreter to communicate with the operating system (and other potential dependencies). Of course, this means that we have to compile the interpreter for different platforms.
With CMake, working with dependencies can be rather simple and straightforward, but there are a few things we need to do as library developers to make it painless and simple. If we only provide instructions for CMake to build our project, as we did when we first looked at CMake, then the end-user will have a difficult time working with our library, and in this article, I want to look at the additional steps we need to take to make working with libraries a joyful experience.
We will return to our complex number library, as it is so simple that we don’t have to concentrate on the source code but rather look at the build script and how to provide a clean installation. In order to do that, we will need some of the advanced CMake features we looked at in the previous article. We will explore how to compile and install static, dynamic, and header-only libraries, as well as how to use these libraries in an external project, so as to show how we can consume the library as an end-user.
Once you have gone through this article, you will add to your CMake expertise and be able to work with additional dependencies in your project. This requires, of course, that these dependencies are already available. You can, of course, download, compile, and install your external dependencies manually, such as the CGNS library or GTest, but since these projects (and pretty much any other C++ library) already provide a CMake file for their build stage, there are better ways to integrate them directly with our own projects.
Furthermore, we can also use external package managers, such as Conan (yes, please) and vckpg (no, thank you), to manage our dependencies. They will download, compile (if necessary) and install all our dependencies and expose them to CMake automatically. All we have to do is include some files generated by Conan (I’m not even considering vckpg anymore) within our CMake file. We magically have access to all required dependencies. Life couldn’t be easier. In the next article, we will look at both Conan and CMake’s internal options for working with libraries.
Compiling and installing static, dynamic, and header-only libraries with CMake
Let us turn our attention to the complex number library again that we saw in the opening article on CMake and see how we can compile, install, and use it using either a static, dynamic, or header-only library. For the header-only library, we will need to make some changes to the source files, so we will treat it separately (as its own project). We will see later, that it doesn’t matter how our library is provided to the end user, they will include our library in the same way regardless of the way it was provided (i.e. as either a static, dynamic, or header-only library).
Static and dynamic libraries
Let us quickly refresh our memory on how the static and dynamic library was structured. We only looked at how to compile a static library with CMake when we first looked at it in conjunction with the complex number example, but getting a dynamic library to work is just as simple and requires only changing one keyword. If you sat through the pain of getting a dynamic library to work on Windows, you will feel relieved and realise that things can be simple, and CMake is working hard for us in the background so that we don’t have to.
Project structure
The project structure has slightly changed now, where we retain our build/
folder for all compiled source files, but we have gained a new folder called cmake/
. It is a convention to use a folder called cmake/
is you want to include additional CMake files in your project (e.g. functions or configuration files). In our case, we use a file called <library-name>Config.cmake.in
, where <library-name>
is the name of our library (here complexNumber
).
We need to provide this file, which will be used by CMake to generate a <library-name>Config.cmake
file. This file is used by other projects that want to use our library. If these projects also use CMake, then this file will provide instructions to CMake for these other projects on where to find our library exactly. You may have also noticed the file ending *.in
, which indicates that this is a configuration file and will be populated with information later by CMake. If you are new to configuration files, I have written about configuration files in the previous article.
root
├── build/
├── cmake/
│ └── complexNumbersConfig.cmake.in
├── complexNumberLib/
│ ├── CMakeLists.txt
│ ├── complexNumbers.hpp
│ └── complexNumbers.cpp
├── CMakeLists.txt
└── CMakeOptions.txt
Then, we have replaced the name of the src/
folder to complexNumberLib
. This is, perhaps, a somewhat Pythonic way of doing things, where the source folder is typically named after the library itself (single name principle), which offers here the advantage that when we later install our header files, which are located in complexNumberLib/*.hpp
, we will retain this structure in our installation directory, i.e. header files will be installed to <cmake-install-prefix>/include/complexNumberLib/*.hpp
.
Sure, we could leave the folder name to src/
in our project, which would then result in an installation path of <cmake-install-prefix>/include/src/*.hpp
, which isn’t ideal. We could then do some CMake magic to replace the folder name src/
but only for the installation path. But why force something in CMake if we can just provide a clean folder structure? Just because src/
is conventional, it doesn’t always mean it is the best choice. Let’s be brave and break conventions when they suck!
Finally, we have our CMakeLists.txt
and CMakeOptions.txt
files, which will be used to steer the compilation and install the library on our system. First, I want to have a quick look at the source file and changes that we have to do here compared to previous articles, and then we will go through the specific CMake files in the subsequent section.
Source files
Within the complexNumberLib/
folder, we have both a header and a source file. These are similar to what we have looked at before, but I want to reproduce here the header file to show what difference we have to make, relating to dealing with dynamic libraries on Windows. If you are a long-time reader of this website, you’ll know what I am about to show you, yes, it is time for preprocessor-hell!
If you have no idea what I am talking about, I already vented my frustration with Windows in a previous article, looking at this issue in great depth, only to realise later that this is a volatile (but required) hack. You won’t need these articles to understand the rest of the discussion but feel free to go through them if cross-compilation of dynamic libraries is important to you (I’d argue it should be, but I seem to be one of the only people in the CFD community who sees it that way …). Don’t worry; you will still be able to follow the rest of this article, even if you haven’t read the linked articles.
The header file in all its glory is reproduced below:
#pragma once
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
#if defined(COMPILEDLL)
#define EXPORT __declspec(dllexport)
#elif defined(COMPILELIB)
#define EXPORT
#else
#define EXPORT __declspec(dllimport)
#endif
#else
#define EXPORT
#endif
#include <iostream>
#include <cmath>
#include <stdexcept>
#include <limits>
class ComplexNumber {
public:
EXPORT ComplexNumber(double real, double imaginary);
EXPORT ~ComplexNumber();
EXPORT double Re() const;
EXPORT double Im() const;
EXPORT void setRe(double real);
EXPORT void setIm(double imaginary);
EXPORT void conjugate();
EXPORT double magnitude() const;
EXPORT ComplexNumber operator+(const ComplexNumber &other);
EXPORT ComplexNumber operator-(const ComplexNumber &other);
EXPORT ComplexNumber operator*(const ComplexNumber &other);
EXPORT ComplexNumber operator/(const ComplexNumber &other);
EXPORT friend std::ostream &operator<<(std::ostream &os, const ComplexNumber &c);
private:
void isNan(double real, double imaginary) const;
private:
double _real;
double _imaginary;
};
Now look at the beauty that is lines 3-13. Truly magnificent, windows-only, gobbledygook. In a nutshell, if this is new to you, what we have to do is add __declspec(dllexport)
in-front of function calls to mark a function for export (this will then go into your dynamic library, i.e. into your *.dll
(dynamic linked library, dll). If you forget to do this and you are trying to compile a dynamic library on Windows, you won’t get the *.dll
file, which is a good indicator that you forgot to export function calls.
Later, when you want to use your *.dll
library, then you have to use the lines __declspec(dllimport)
instead. Since we now have a situation of having two competing function signatures, we could either write two header files where we only change the __declspec
definition (but this includes copy and pasting, and we want to follow the DRY principle, so this isn’t a great idea), or hack our logic into the header file using pre-processor directives and then let the compiler insert the correct __declspec
definition. We have opted for the latter, and that is common practice (unfortunately).
So, when we want to build a dynamic library on Windows, we see that we need to define the pre-processor variable COMPILEDLL
. This requires us to also define the pre-processor directive COMPILELIB
(which doesn’t set anything for the EXPORT
variable) if we just want to use a static library. Otherwise, the default is to use the __declspec(dllimport)
, which is what we want when using this library later.
Incidentally, if you are on UNIX and enjoy the magic of Linux or macOS for developing code, you don’t have to worry about exporting function names. It is done automatically for you. We are in the 21st century, and Microsoft still hasn’t figured out how to do this automatically. But I remain hopeful that this is on the agenda for the 22nd century!
CMake build scripts
The CMakeLists.txt
file in the root folder is given below. Look through it and then come back for a discussion underneath the file.
# Select the minimum cmake version required. If in doubt, use the same version you are using
# To find out your cmake version, run 'cmake --version' in the console
cmake_minimum_required(VERSION 3.23)
# Set the project name/description and project settings like the C++ standard required
project(
complexNumbers
LANGUAGES CXX
VERSION 1.0.0
DESCRIPTION "A simple C++ library for complex numbers"
)
# include custom options
include(CMakeOptions.txt)
# Add a postfix for debug library builds to avoid name clashes with release builds
if(WIN32)
set(CMAKE_DEBUG_POSTFIX d)
endif()
if(${ENABLE_SHARED})
# Create a shared (dynamic) library for the complex number class
add_library(${CMAKE_PROJECT_NAME} SHARED)
# add correct compiler flags so that we can correctly import/export symbols
if (MSVC)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
endif()
else()
# Create a static library for the complex number class
add_library(${CMAKE_PROJECT_NAME} STATIC)
# add correct compiler flags so that we can correctly import/export symbols
if (MSVC)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILELIB)
endif()
endif()
target_include_directories(${CMAKE_PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# go through the complexNumberLib/ subdirectory and add any files to the library
add_subdirectory(complexNumberLib)
### INSTALLING ###
# install the headers that we specified with FILE_SET (header files for the user to include in their projects)
install(TARGETS ${CMAKE_PROJECT_NAME}
EXPORT complexNumbersTargets
FILE_SET HEADERS
)
# provide some basic meta data about the library to generate the library's config file
install(
EXPORT complexNumbersTargets
FILE complexNumbersTargets.cmake
DESTINATION "lib/cmake/complexNumbers"
NAMESPACE complexNumbers::
)
# generate the library's config file
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/complexNumbersConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
INSTALL_DESTINATION "lib/cmake/complexNumbers"
)
# install the config file
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
DESTINATION "lib/cmake/complexNumbers"
)
The first part of the CMakeLists.txt
file is hopefully not a surprise anymore. We set the minimum CMake version here to version 3.23 as we want to use the FILE_SET
command, which was introduced in 3.23 (and the newest feature we use in this CMake file). We also set some project-specific metadata and, then, on line 14, proceed to include the CMakeOptions.txt
file to make library-specific options available.
The content of the CMakeOptions.txt
file is shown below:
option(ENABLE_SHARED "Build shared (dynamic) library? If set to off, a static library will be built" OFF)
It contains a single option labelled ENABLE_SHARED
, which allows us to set whether we want to build the library as a static or dynamic library. By default, the library will be built as a static library.
On line 17-19, we ask CMake to add the letter d
to the name of our library if we are on Windows and if we are building a debug version of our library. This is a Windows convention and is required to allow for debug and release libraries to sit in the same folder. We need that as Windows strongly discriminates between debug and release builds, and we must pick the correct one, something which is rather relaxed on UNIX systems. We’ll see that later in action where it will become clearer.
Returning to the CMakeLists.txt
file, we see that we use this ENABLE_SHARED
option on lines 21-37 to either create a new library through the add_library()
command, where we either use the STATIC
or SHARED
keyword to indicate whether we want to build a static or dynamic library. The name of the library is derived from the project name we set on line 7, which is available to us through the ${CMAKE_PROJECT_NAME}
variable.
We also set the pre-processor to either COMPILEDLL
or COMPILELIB
, which will pass that information in a platform-independent manner to the compiler, i.e. on Windows using the cl.exe
compiler, we either get the /DCOMPILEDLL
or /DCOMPILELIB
compiler flag added to the compilation, and on UNIX, using either the GNU or clang compiler, we, nothing. The pre-processor directives are only defined for Windows (see line 3 in the header file), but if we wanted something to be added to our UNIX compilers, we would get -D
instead of /D
as a compiler flag.
On lines 39-43, we include the root folder using a generator expression, something that we did discuss in the previous article as well. Let’s explore that in more detail. If we are building the library, then we are including ${CMAKE_CURRENT_SOURCE_DIR}
during the compilation. This is nothing else than specifying during the compilation that we want to include the project’s root folder using the include flag -I.
or /I.
This allows us to make relative imports from the root folder in our source files, such as complexNumberLib/complexNumbers.hpp
. This is what we do in the complexNumberLib/complexNumbers.cpp
file at the top. If we do not include ${CMAKE_CURRENT_SOURCE_DIR}
, then we would have to rewrite this to include as simply complexNumbers.hpp
, i.e. look for the header file in the current directory. For a simple project like this, it would be fine to leave it, but for more complex projects it makes navigating your library a lot easier, if all includes are relative to the root.
During the installation, though, we do not want to have the same include path. The reason is that CMake works with absolute paths in the background, and having an absolute path point to your project folder will make no sense. You want to point to the installation directory instead, and using the generator expression to distinguish between the build and install phase allows us to conditionally set the include path here for our library. The good news is that lines 39-43 are pretty much boilerplate instructions, i.e. you can copy and paste it into your own project, and it will work.
On line 46, we are using the add_subdirectory()
command to instruct CMake to look for an additional CMakeLists.txt
file within the complexNumberLib/
folder. This file has the following content:
target_sources(${CMAKE_PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.cpp
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.hpp
)
We simply add the source file (e.g. complexNumbers.cpp
) to our library target. It is set as PRIVATE
, as only our library will need to know about this file. Then, we are using the PUBLIC FILE_SET HEADERS
instruction to let CMake know that we want to include some header files as well in our target, which will not be used during compilation but rather be made available later when we install the project. All header files marked under the FILE_SET
command will get exported and land in the <cmake-install-prefix>/include
folder.
And now, we get to the juice bit, the part that we did not cover in detail in our first article about CMake, and looking through it, you can understand why (we needed a few more advanced concepts which we covered in the previous article). To make things simple, I have copied lines 50-76 below again, so we don’t have to scroll up and down. I’ll be referring to the line numbers shown below to make things simple but keep in mind that these are different to the line numbers in the CMakeLists.txt
file shown above.
# install the headers that we specified with FILE_SET (header files for the user to include in their projects)
install(TARGETS ${CMAKE_PROJECT_NAME}
EXPORT complexNumbersTargets
FILE_SET HEADERS
)
# provide some basic meta data about the library to generate the library's config file
install(
EXPORT complexNumbersTargets
FILE complexNumbersTargets.cmake
DESTINATION "lib/cmake/complexNumbers"
NAMESPACE complexNumbers::
)
# generate the library's config file
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/complexNumbersConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
INSTALL_DESTINATION "lib/cmake/complexNumbers"
)
# install the config file
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
DESTINATION "lib/cmake/complexNumbers"
)
On lines 2-5, we set some basic information about the library. In particular, we use the EXPORT
command to define an installation target. It is a bit like the add_library()
or add_executable()
command, where we define a name for the library or executable, which we can use to add files and include directories to this target specifically. In a similar manner, the EXPORT
command allows us to associate any installation information with this target name, such as install directories and files that should get installed (copied into the installation folder).
The FILE_SET
command on line 4 makes sure that all header files marked under the FILE_SET
command (i.e. see the complexNumberLib/CMakeLists.txt
file) will get installed into the installation directory. We now bind this FILE_SET
instruction with the exported install target.
Lines 8-13 define some additional meta-information, which, again, are associated with the EXPORT
target. We specify that we want to create a <library-name>Targets.cmake
file, which will be automatically generated by CMake based on the information provided within all the install()
commands as seen above. This file name is conventional, and you should use the same wording.
The DESTINATION
specifies where this <library-name>Targets.cmake
file should be located, and again, by convention, we use lib/cmake/<library-name>
. I have seen permutations of that in the wild, e.g. cmake/lib/<library-name>
, which probably should work just as well, but the former makes sure that all *.cmake
files from all libraries will be located in the same folder.
Then, we have the NAMESPACE complexNumber::
instruction. A namespace is similar to a C++ namespace in that we can provide a dedicated namespace for all of our libraries. Sometimes, you may want to provide a set of libraries under a common library name. Boost may be one such example, which probably most C++ programmers will have at least heard about. Then, you would identify your libraries later as <namespace>::libraryName
. By convention, we use a namespace even for single libraries, where the library and namespace are the same.
Lines 16-21 take the configuration file cmake/complexNumberConfig.cmake.in
and produce the corresponding complexNumbersConfig.cmake
file. It will be located in the ${CMAKE_CURRENT_BINARY_DIR}
, i.e. the build/
folder, but then installed into the same lib/cmake/complexNumbers
folder, just like the complexNumbersTargets.cmake
file. Let’s have a look at the configuration file; the content is given below:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# any additional libraries we depend on should go here, e.g.
# find_dependency(Gtest::Gtest)
# notice that we use find_dependency(), not find_package() here
include("${CMAKE_CURRENT_LIST_DIR}/complexNumbersTargets.cmake")
We can see the @PACKAGE_INIT@
variable at the beginning of the file, which will be replaced by CMake during the installation. I.e. if you later look into the file <cmake-install-prefix>/lib/cmake/complexNumbers/complexNumbersConfig.cmake
, you will see this variable replaced by some CMake content. The include()
directives on lines 3 and 9 are the only other requirement that we have to put here and are mostly boilerplate as well, we would just have to change the library name on line 9 if we wanted to use this file in another project.
Essentially, what this file offers us is the opportunity to look for additional dependencies, i.e., libraries, on which our project depends. In our case, we have none. We can see in the comments that we use the find_dependency()
syntax to look for a library. Notice that in the example, we use Google Test (GTest
), which is provided by its own namespace. So to find GTest
, we would have to write find_dependency(GTest::Gtest)
. Later, we will see that this command simply wraps around another command called find_package()
, that we will use to find libraries.
Returning back to the CMakeLists.txt
excerpt above, on lines 24-27 we simply provide additional instructions to install the generated configuration file, and this is all the information required to get a clean installation. So, with all of our CMake files now ready and prepared, let us go ahead and compile and install the library next.
Command line instructions
The command line instructions for compiling the library are provided next. Make sure that you have a clean build/ folder available, which does not contain any files and that you have changed into the build/ folder on the command line. Then, you can go ahead and build a static library first using the following command (I am using Ninja here as my preferred build generator, but you can omit this and use the system’s default if you prefer or don’t have Ninja
installed):
cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_SHARED=OFF -G Ninja ..
With the build file generated, proceed to generate the library using the following command:
cmake --build . --config Release
And finally, once the library is available and compiled, we want to install it into a specific directory. I am on Windows here and will use C:\temp
as my installation prefix. This is to show that even if we build in a non-standard path (i.e. one that is not in the environmental variables), we can still discover these libraries later, thanks to the C:\temp\lib\cmake\complexNumbers\*.cmake
files that we have generated. The install command becomes:
cmake --install . --prefix "C:\temp" --config Release
These three steps have generated a release build of the static library. If you are on UNIX, hooray, go ahead and use the library. If you are on Windows, hold on; we need to talk. Remember ABI compatibility we mentioned in the introduction? Remember that I said that this includes communication with the operating system? It turns out, on Windows, you can’t mix and match debug and release builds, this breaks the ABI (but not on UNIX, testing it on Ubuntu shows that you can use library compiled in release mode within a project that is compiled in debug mode).
If you tried to use a library on Windows that was compiled in release mode, but then your project is compiled in debug mode, you will likely see an error message like the following:
complexNumbers.lib(complexNumbers.cpp.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' do
esn't match value '2' in main.obj [C:\Users\tom\code\buildSystems\Build-Systems-Part-4-static-shared-Testing\build\
complexNumberTest.vcxproj]
complexNumbers.lib(complexNumbers.cpp.obj) : error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MD_DynamicRe
lease' doesn't match value 'MDd_DynamicDebug' in main.obj [C:\Users\tom\code\buildSystems\Build-Systems-Part-4-stat
ic-shared-Testing\build\complexNumberTest.vcxproj]
Both _ITERATOR_DEBUG_LEVEL
and MD_DynamicRelease
are variables set by the linker, and it checks that all dependencies have the same entry (i.e. either debug or release). So, if you want to use your library on Windows, make sure that you either provide both a debug and release build, or be consistent in the use of either a debug/release compilation.
Since both release and debug libraries have the same name, we can only provide one or the other. Thus, on Windows, the convention is that we provide two versions of a library with the name <lib-name>.lib|dll
for a release build of a static|dynamic library and <lib-name>d.lib|dll
for a debug build. Notice the additional d
after <lib-name>
. In this way, we can have both libraries compiled and sitting next to each other in the same folder.
This is why we included line 17-19 within the CMakeLists.txt
file, i.e. we had:
if(WIN32)
set(CMAKE_DEBUG_POSTFIX d)
endif()
Since this is a common convention (I would label it as a hack or workaround, though!), CMake has support out of the box for this, and we don’t have to write messy CMake generator expressions to check if we have a release or debug build. Instead, CMake does that for us automatically. By the way, if you are wondering why we use WIN32
(aren’t we firmly established in the 64-bit era?), well, CMake has been around for some time, and it is just showing its age. But again, I remain hopeful for changes to be made by the 22nd
century!
If you wanted to build the dynamic library, then you would simply have to change the first command in this section. i.e. we then have
cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_SHARED=ON -G Ninja ..
The rest of the commands remain the same. You will notice, though, that the dynamic library will be installed into the <cmake-install-prefix>/bin
directory for Windows, with the wrapper library still located in the <cmake-install-prefix>/lib
folder (on UNIX, you get everything in the lib/
folder as expected). The location doesn’t matter, though, as CMake will expose the file locations through the <cmake-install-prefix>/lib/cmake/complexNumber/*.cmake
files to any other project that wants to use this complex number library.
However, as you may remember as well from previous articles, if we are working with Windows and dynamic libraries, we’ll need to copy any *.dll
file into the same folder as our executable. This may sound trivial, but now we have potentially different library names (thanks to the d
suffix), as well as different locations of executables depending on the build script used (some will be in the build/
folder, others will be in build/Debug
or build/Release
). There is no uniform handling of this, and we’ll need to hack that into our CMake scripts later when we test the library.
At this point, though, we are done with compiling and installing, and we can use our library within another project. Before we do, though, let us quickly look at the changes we would have to make if we wanted to provide a header-only library.
Header-only libraries
Header-only libraries are a weird niche topic for me in the context of C++. The fact that header-only libraries work at all is somewhat surprising (not from a technical point but from a design philosophy point of view). C++ requires a compiler to translate code into machine instructions, but nothing gets compiled within a header-only library, so you don’t need a compiler (technically) to write your own header-only library.
Of course, you may want to provide some tests to ensure your code is working, and you probably want others to use your code. A header-only library will eventually get compiled, but not by you as the library developer, but rather by the end-user when they include it in their project and then compile it.
Having said that, apart from perhaps compiling test executables, a build script is just as important for a header-only library, as it can keep track of all header files that need to be copied during the installation, as well as generate all required *.cmake
files that other projects can use to find this header-only library in a non-standard path.
Compared to our static and dynamic library example above, there are very few differences between the two projects, but they are important enough to highlight so that you can see what needs to be changed should you wish to do so. I’ll concentrate on the differences, rather than going through all steps again, so we can wrap up this section fairly quickly.
Project structure
The project itself remains largely unchanged in terms of its structure, and the only change here is that we have lost the complexNumberLib/complexNumbers.cpp
source file. All of that content has now migrated into the complexNumberLib/complexNumbers.hpp
header file. All pre-processor directives are gone now, as these are only relevant for dynamic libraries, which we can’t compile now and so we don’t need these directives anymore.
As a consequence, we don’t have any options anymore, and so we can remove the CMakeOptions.txt
file. A real header-only library, though, likely would still have some configuration options, such as enabling testing as part of the build process, so you would likely still require the support of some options to be set during the configuration.
root
├── build/
├── cmake/
│ └── complexNumbersConfig.cmake.in
├── complexNumberLib/
│ ├── CMakeLists.txt
│ └── complexNumbers.hpp
└── CMakeLists.txt
Source files
For completeness, I have decided to reproduce the entire header file here so you can directly see the differences to the previous header file. As alluded to above, the pre-processor directives are gone, and all implementation of the functions is now within the header file itself.
#pragma once
#include <iostream>
#include <cmath>
#include <stdexcept>
#include <limits>
class ComplexNumber {
public:
ComplexNumber(double real, double imaginary) : _real(real), _imaginary(imaginary) {
isNan(real, imaginary);
}
~ComplexNumber() = default;
double Re() const { return _real; }
double Im() const { return _imaginary; }
void setRe(double real) { _real = real; }
void setIm(double imaginary) { _imaginary = imaginary; }
void conjugate() { _imaginary *= -1.0; }
double magnitude() const { return std::sqrt(std::pow(_real, 2) + std::pow(_imaginary, 2)); }
ComplexNumber operator+(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real + other._real;
c._imaginary = _imaginary + other._imaginary;
return c;
}
ComplexNumber operator-(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real - other._real;
c._imaginary = _imaginary - other._imaginary;
return c;
}
ComplexNumber operator*(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real * other._real - _imaginary * other._imaginary;
c._imaginary = _real * other._imaginary + _imaginary * other._real;
return c;
}
ComplexNumber operator/(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
double denominator = other._real * other._real + other._imaginary * other._imaginary;
if (std::abs(denominator) < std::numeric_limits<double>::epsilon())
throw std::runtime_error("Complex number division by zero");
ComplexNumber c(0, 0);
c._real = (_real * other._real + _imaginary * other._imaginary) / denominator;
c._imaginary = (_imaginary * other._real - _real * other._imaginary) / denominator;
return c;
}
friend std::ostream &operator<<(std::ostream &os, const ComplexNumber &c) {
os << "(" << c._real << ", " << c._imaginary << ")";
return os;
}
private:
void isNan(double real, double imaginary) const {
if (std::isnan(real) || std::isnan(imaginary))
throw std::runtime_error("Complex number is NaN");
}
private:
double _real;
double _imaginary;
};
CMake build scripts
The root-level CMakeLists.txt
file also changes slightly. Most of the file remains unchanged, especially the part around the installation, but then we have to change the type of the library from either STATIC
or SHARED
to INTERFACE
. In CMake, if we are working with a header-only library, it is identified as INTERFACE
internally.
The target_include_directories()
call also lost its generator expression, we no longer need to differentiate between the build and install target, as we no longer have a build target. Since we only have the install target left, we can use that to simplify our build instructions.
Both these changes are shown in the file excerpt below:
# Create a header-only library for the complex number class
add_library(${CMAKE_PROJECT_NAME} INTERFACE)
target_include_directories(${CMAKE_PROJECT_NAME}
INTERFACE
${CMAKE_INSTALL_INCLUDEDIR}
)
Notice also that we can remove calls to set(CMAKE_DEBUG_POSTFIX d)
and target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
, as we are no longer building a library. Thus, there is no need for a d
library name suffix or pre-processor directive that is applicable to dynamic libraries only.
The complexNumberLib/CMakeLists.txt
file also has to change since we no longer have a source file to compile. Though, we still want to associate the header file with the library (target), so that it gets installed as part of the installation step. This is shown below as well:
target_sources(${CMAKE_PROJECT_NAME}
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.hpp
)
And these were all changes required to CMake! We simply change the library target, remove the project directory for the build stage (though technically we could have left it in, the generator expression would have always evaluated to the install step, never the build step), and removed the source file from the sources as well. Now, let’s look at the changes on the command line to get this library installed.
Command line instructions
While we do not have to build this library, we still need to perform the configuration step. Since we are not building anything, we do not need a generator like Ninja
, and we don’t have to specify a build type such as debug or release (that is up to decide for the project that uses our library). Since we also removed the CMakeOptions.txt
file, and there are no further variables for us to set for the configuration, we can simply run CMake with the following command:
cmake ..
As already mentioned, we skip the build stage, and this is pretty much the only difference on the command line. The next step is the install step, which still requires a prefix
(i.e. install location) to be specified. I am using C:\temp
in this example again (though I have also tested ~/temp
on UNIX, which is working fine for the header-only and static/dynamic library compilation). The command is:
cmake --install . --prefix "C:\temp"
This will have installed the header-only library on your system. With that done, let us have a look how to use all three types of libraries within another project.
Using the installed libraries in other projects
Up until this point, we haven’t actually achieved anything more than what we did when introducing CMake for the first time, where we looked at the complex number library as well. We did compile and install it (though we didn’t look at the differences between header-only and compiled libraries). What I want to do in this section is to emulate what another project would have to do to use our library. For that, we will develop a very simple project that simply tests the arithmetic overloaded operators within the complex number class.
Project structure
This is the simplest project you can have; it consists of our build/
folder, a main.cpp
source file that tests some of the features of the complex number class, as well as the CMakeLists.txt
file, which is where all the magic is happening. In addition, we retain a CMakeOptions.txt
file, which could have, however, been integrated into the main CMakeLists.txt
file. It keeps the options separated, though, from the main build logic, and this is retained here. The structure is given below as well.
root
├── build/
├── main.cpp
├── CMakeLists.txt
└── CMakeOptions.txt
Source file of a simple test program for complex numbers
Let’s start by looking at the content of the main.cpp
file. This simple test program includes the header file of our library and then defines two complex numbers that are combined through the various arithmetic operators, e.g. addition, subtraction, multiplication, and division. These are stored in different complex numbers, and the result is then printed to the console for verification. There is no assertion going on, this is not a replacement of a unit test, it is just to show that we can fidn the library and work with it. The code is given below:
#include <iostream>
#include "complexNumberLib/complexNumbers.hpp"
int main() {
ComplexNumber a(2, 3);
ComplexNumber b(4, 5);
ComplexNumber c = a + b;
ComplexNumber d = a - b;
ComplexNumber e = a * b;
ComplexNumber f = a / b;
std::cout << c << std::endl;
std::cout << d << std::endl;
std::cout << e << std::endl;
std::cout << f << std::endl;
return 0;
}
CMake build scripts
If you don’t care about dynamic libraries, or you do but only for UNIX systems, you can reduce your CMakeLists.txt
file down to only 5 commands. However, we are good CFD developers, and we take cross-platform development seriously. So, our CMake file will now contain 48 lines instead of 5. The excess in lines required is not just a Windows fault; there are still rather hacky commands we need from CMake, which are cross-platform but just not very precise.
Be that as it may, working with libraries in CMake is actually rather straight forward, and there is excellent support for finding libraries, mainly thanks to the generated *.cmake
files that we generated and installed as part of our complex number library generation step. Let’s look at the CMaekLists.txt
file, and then discuss it below.
cmake_minimum_required(VERSION 3.23)
project(
complexNumberTest
VERSION 1.0
LANGUAGES CXX
)
# include custom options
include(CMakeOptions.txt)
# Find packages will search for a specific library. CMake will fail if the package is REQUIRED but not found
find_package(complexNumbers REQUIRED)
# Add an executable and provide the source file directly.
add_executable(${CMAKE_PROJECT_NAME} main.cpp)
# Make sure you link your executable with your library. Notice the namespace that we defined earlier.
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE complexNumbers::complexNumbers)
if (MSVC)
if(${ENABLE_SHARED})
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
else()
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILELIB)
endif()
endif()
# On Windows, we need the *.dll file in the same folder as the executable to be able to run it
if(WIN32)
if(EXISTS "${CMAKE_PREFIX_PATH}/bin/complexNumbers.dll" OR EXISTS "${CMAKE_PREFIX_PATH}/bin/complexNumbersd.dll")
add_custom_command(
TARGET ${CMAKE_PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
"${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
)
add_custom_command(
TARGET ${CMAKE_PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
"${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
)
endif()
endif()
Lines 1-10 should hopefully now be self-explanatory, as we have looked at this before a few times. A new command for us is the find_package()
command on line 13, which is similar to the find_dependency()
command that I mentioned before when we wrote our <library-name>Config.cmake.in
configuration file.
find_package()
is a bit of magic (not really, but as someone who has spent quite a lot of time manually compiling, installing, and linking libraries on the command line, it feels like cheating!). It will look for a given library and then has two options if it is not found. If we specify that the library is REQUIRED
, CMake will abort the configuration step. If we omit this keyword, CMake will configure without issues and only print a warning to the console that a package was not found.
Some packages (libraries) are essential for your code to work. For example, when we developed our CGNS-based mesh reading library for structured and unstructured grids, we require the CGSN library to be available, otherwise we can’t build the library. In this case, REQUIRED
would need to be specified, e.g. find_package(cgns REQUIRED)
. In other cases, we can proceed with the build process, even if a package is not found. For example, we can still build a library even if the test framework like Google test (gtest) is not found (it won’t affect the build of the library itself).
We then proceeded to line 16 to generate an executable, where I specified the source file here as the second argument. For a larger project, I would omit that source file and add it instead by calling target_sources()
, but let’s keep things simple here.
Then, the real magic happens on line 19; we are now instructing CMake to link our executable from line 16 with the complex number library we found on line 13 (if it wasn’t found, CMake would have never gotten to lines 13 and 16 in the first place). Notice that we now have to use the namespace complexNumber::
that we have specified before in the install()
command within the root level CMakeLists.txt
script of the complex number project.
We link against the library using PRIVATE
. There may be circumstances where you need to link here using PUBLIC
, for example, if you use a dynamic (SHARED
) library and you are developing a library yourself, and you know that whoever uses your library also needs to have access to the library you are linking against. Take the CGNS mesh reader library we worked on as an example; if we were to link the dynamic CGNS library against our mesh reader library, using PUBLIC
here would allow anyone using our library to also access CGNS-specific functions/definitions.
On lines 21-27, we check if we want to compile a static or dynamic version of our library. The ENABLE_SHARED
variable is set similar to the complex number library itself within the CMakeOptions.txt
file and defaults to a static library build if nothing is specified. To overwrite this, we would pass -DENABLE_SHARED=ON|OFF
to CMake during configuration.
And then we have lines 30-47. Any comment starting with # On Windows, ...
should make you nervous and induce anxiety. It does for me, as I have written many comments starting just like this. I know, from personal experience, that every comment starting with # On Windows, ...
or similar, is responsible for a small nervous breakdown on the developer’s side. So let’s go through it.
First, we check if we are on Windows, and if so, if the complex number library exists as a dynamic library within the CMake prefix path. We check for both release (complexNumbers.dll
) and debug (complexNumbersd.dll
) versions of this library. If it does exist, we invoke the add_custom_command()
function in CMake, which allows us to perform some routine tasks (like copying files) in a platform indepdendent manner (i.e. no need to write simple bash or PowerShell scripts). Let’s break this command down in the form that we use it:
add_custom_command(
TARGET ${CMAKE_PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
<path-and-file-to-copy>
<file-destination>
)
We say that whenever we invoke the ${CMAKE_PROJECT_NAME}
target, which we do when we type cmake --build .
into our console (as it will look to build any executables or libraries that were defined with add_executable()
or add_library()
), it will run this command. Which command you ask? This is specified by the COMMAND
keyword on line 4, which can be any command you could type in a terminal. Here, we use ${CMAKE_COMMAND} -E
, which will be replaced by cmake -E
. There are several internal commands that you can provide here as an argument.
We are using the copy_if_different
command, that will look for a file and only copy it if it has changed since the last copy was done. It has two arguments, where the first points to the path and filename that we want to copy, while the second points to the location where we want to store the file.
What we are trying to achieve here is to copy the complexNumbers(d).dll
file into our build/
, build/Debug
, or build/Release
directory, so that we can invoke any executable that will be generated in any of these folders. Thus, we see in our CMakeLists.txt
file two commands to copy the dynamic library. The <path-and-file-to-copy>
for the first command is given as:
${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>
First, we check that within the CMake prefix path (e.g. C:\temp
here, which is the install directory for our library), if we have a file called bin/complexNumbersd.dll
if we are building a debug version of our executable or bin/complexNumbers.dll
if we are building a release version. This is what the convoluted generator expression is checking for. Then, on line 37, we specify the path to copy this file to:
${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>
Here, ${CMAKE_BINARY_DIR}
is the build/
folder, and then we dynamically construct a string based on the build type, the following paths are possible:
build/Debug/complexNumbersd.dll
build/Release/complexNumbers.dll
However, the executable may not necessarily be generated in a build/Debug
or build/Release
folder (in-fact, this is only the case for MSBuild, XCode, and Ninja multi-config). Instead, we may have our executable located just in the build/
folder. In this case, we also need to copy the dynamic library into the build/
folder itself, and this is what lines 39-45 are doing. Having done this, we are now able to consume not just static and header-only libraries with our project, but dynamic libraries as well.
Command line instructions to build and execute the test program
The same is true for the command line. It doesn’t matter which of three types of library you are consuming, nor on which operating system you are. The build instructions will always be the same. Before we start, make sure the build/
folder is present in your project, and if it is, go ahead and configure the project.
The configure step is shown below. The build type is set to release simply because we used a release build for our complex number above. You may have decided to build a debug version instead, in which case you have to change this instruction to debug (that is, if you are in Windows, UNIX users can, again, mix and match build types to their heart’s content).
cmake -DCMAKE_PREFIX_PATH=C:\temp -DCMAKE_BUILD_TYPE=Release -G Ninja ..
I’m using Ninja
as my generator again, but you can safely omit this flag. And then there is -DCMAKE_PREFIX_PATH=C:\temp
. This is the folder where CMake will look for any lib/cmake/*/*.cmake
files that could give CMake an indication of where to find any library in a non-standard search path, including any header files that were installed.
Next, we need to compile and link our executable to our library. This is achieved with the usual build command:
cmake --build . --config Release
We specify again that we want to build in release mode but adjust that as you need based on your previous CMAKE_BUILD_TYPE
settings. Once completed, you should now have a new executable in your build/
folder called either complexNumberTest.exe
on Windows or complexNumberTest
on UNIX, i.e. no file extension.
On Windows, if you are already within the build/
folder, you can execute this file using the following command:
.\complexNumberTest.exe
And on UNIX, use:
./complexNumberTest
This should print the following messages to your console:
(6, 8)
(-2, -2)
(-7, 22)
(0.560976, 0.0487805)
This is just the output from the different arithmetic operations, so seeing that we are getting some complex numbers printed to the screen means the library was successfully linked to our executable. Congratulations, you are now able to develop your own libraries and make them available to other projects in a sensible manner.
Summary
In this article, we explored how to work with static, dynamic, and header-only libraries and showcased how to use a simple, complex number library to compile and install it, as well as how to consume this installed library within another project. We have seen that CMake provides a clean interface to us and that we achieve cross-platform compilation for different types of libraries with just a few lines of build script and a handful of commands on the console.
If you have made it all the way to the end of this article and are reading it, this means that you are serious about developing code that is easy for other people/projects to consume. Please make use of this knowledge. Library development can be hard if you have to start from scratch, but most of what you will need is covered in this article. And anything else you may need will be covered in the next articles. So stay tuned for that, but in the meantime, always think of how you can export your work as a library and move away from executables. It makes using your code that much easier!
Tom-Robin Teschner is a senior lecturer in computational fluid dynamics and course director for the MSc in computational fluid dynamics and the MSc in aerospace computational engineering at Cranfield University.