Defining libraries
To illustrate our example we will define an extremely simple library that exposes just a single function – add, which adds two integers. Let’s assume that we’re very concerned about all the possible integer-operation failure modes and that we’re unable to use exceptions for some reason, so we decide to use simple enum
error codes to report them (this will come into play later). The library consists of three files. If you wish to follow along you can copy-paste, or type-in the listings. Alternatively, a git gist for the complete example is available in the reference section.
calc_status.h:
#pragma once
namespace Add
{
enum class calc_status {
success = 0,
positive_overflow,
negative_overflow,
range_error,
};
} // namespace Add
add.h:
#pragma once
#include "calc_status.h"
namespace Add
{
int add(int l, int r, calc_status& cs) noexcept;
} // namespace Add
add.cpp:
#include "add.h"
#include <limits>
int Add::add(int l, int r, calc_status& cs) noexcept
{
if ((l > 0) && (r > (std::numeric_limits<int>::max() - l)))
{
cs = calc_status::positive_overflow;
return 0;
}
else if ((l < 0) && (r < (std::numeric_limits<int>::min() - l)))
{
cs = calc_status::negative_overflow;
return 0;
}
cs = calc_status::success;
return l + r;
}
The CMakeLists.txt that defines the project looks as follows:
cmake_minimum_required(VERSION 3.19)
project(Fundamentals)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_VERBOSE_MAKEFILE ON)
add_library(Add
calc_status.h # optional
add.h # optional
add.cpp
)
These are the same variables we’ve previously set via command-line. Here we set them in the CMakeLists.txt, so that we don’t have to do it every time we configure the project. We still need them, since we’d like to look at exactly what’s going on.
The set command defines or assigns a value to a variable. The form of the set command presented here operates on what CMake documentation calls a normal variable. In addition to normal variables set can operate on cache and environment variables, but we will not discuss those for now.
CMake variables behave more or less the same way one would expect – they obey scope rules and have a type.
We will gloss over the details, and for now note just that a normal CMake variable is visible from the point of definition onward, and is always of type string. Always. There aren’t really any other data types in CMake. Commands may interpret a variable differently, but the underlying type is always string.
Also, note that variables with the prefix CMAKE_ are either provided by CMake or are otherwise meaningful to CMake when set by the project.
We’ve set the value of the variable to ON. Optionally, we could have quoted the literal, like so: set(CMAKE_EXPORT_COMPILE_COMMANDS “ON”), these calls are equivalent – literals are also of type string. However, CMake interprets some string values as true and some as false, meaning that when evaluated they behave as if they were of type bool. Setting this variable to YES or TRUE would have had the same effect. Setting the variable to OFF, NO or FALSE would have had the opposite outcome.
Next, we introduce our library:
add_library(Add
calc_status.h # optional
add.h # optional
add.cpp
)
Same as add_executable
this command defines a target, which encapsulates all of the details necessary to compile a library. We give the target a name – Add – this will also be the name of the library itself. We also specify the source files. The only required one isadd.cpp, headers are optional, listing them, however, may however help CMake better track changes to these files and generate IDE project files (e.g. for Visual Studio) that are aware of these files – i.e. are able to list them in the project explorer.
This is all that’s required to build a library. Let’s configure and build the project so that we can inspect the output.
Building libraries
We’ll take this opportunity to introduce a different way of configuring and building projects with CMake. From the project root directory run the following:
$ cmake -B build -S .
$ cmake --build build
Instead of manually creating the build directory, cd
‘ing to it, and invoking cmake there, we do everything with a single command. The -B
flag specifies the build directory (created if necessary), and the -S
specifies the source directory. Then, to build the project we run cmake --build build
. Previously we relied on invoking make
directly – here we tell cmake that we’d like to build (rather than configure) the project with the --build
parameter and point it to an already configured build directory. This is a generator-agnostic way of building projects using CMake, which is rather useful for scripting.
So how did cmake go about compiling our library? Let’s take a look at the compile_commands.json
first. We should already have an idea of what to expect.
"command": "/usr/bin/c++ -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/hello_cmake/add.cpp",
Nothing new here – the code is compiled down to an object file which can later be used to build (link) and executable or library. We will need to go through the output of the verbose makefiles to get the full picture. Here are the interesting lines:
/usr/bin/c++ -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/hello_cmake/add.cpp
/usr/bin/ar qc libAdd.a CMakeFiles/Add.dir/add.cpp.o
/usr/bin/ranlib libAdd.a
We have some commands we have not seen before here. After compiling the object file the ar
tool is called, which is the archive-tool – it packs object files into a static library. The flags – qc
– tell the archiver to (q)
do a quick append of the object code to the archive, and (c)
to create the file if necessary. Then ranlib
is executed. This tool creates an index-table of the object code in the archive, which speeds up linking, among other things. You can read more about these tools in the man-pages: ar, ranlib. We could build this exact same library by executing the same commands manually. We’ll skip this step for brevity.
Linking libraries
Great, we have our library! Now let’s see how to use it in an executable. First we introduce a new source file, that trivially uses our add function – main.cpp:
#include <iostream>
#include <limits>
#include "add.h"
#include "calc_status.h"
void add(int a, int b)
{
Add::calc_status cs{};
auto const result = Add::add(a, b, cs);
if (cs == Add::calc_status::success)
{
std::cout << a << " + " << b << " = " << result << "\n";
}
else
{
std::cout << "Error calculating " << a << " + " << b << "\n";
}
}
int main()
{
const int a{42};
const int b{102};
add(a, b);
add(a, std::numeric_limits<int>::max());
}
We already know how to build an executable:
add_executable(fundamentals_part2 main.cpp)
However, if we tried building the project now the linker would complain:
/usr/bin/c++ CMakeFiles/Main.dir/main.cpp.o -o Main/usr/bin/ld: CMakeFiles/Main.dir/main.cpp.o: in function `add(int, int)':main.cpp:(.text+0x30):
undefined reference to `Add::add(int, int, Add::calc_status&)'
It’s telling us, that it was not able to find the definition – object code – for the add function. This makes sense – we’ve included theadd.h header file, which contains only the declaration of the function, its definition is located in add.cpp, we could add this source file to the add_executable
command call, however, we wanted to use the library! Let’s do that instead.
Libraries in CMake are linked using the target_link_libraries
command. This command informs CMake that a given target depends on another target defined with add_library
. Let’s use this command in our CMakeLists:
target_link_libraries(Main
PRIVATE
Add
)
We’ve specified that the target Main depends on the library targetAdd, which here has the implication of linking the static library compiled from the Add target into the executable compiled from target Main. The implications may be broader than that. We also introduced a new keyword here – PRIVATE
. For now, just note that target_link_libraries
should always be called with one of the keywords PRIVATE
, INTERFACE
or PUBLIC
. I’ll expand on both of these concepts later.
Let’s build our project. The build command and relevant output lines are listed below:
$ cmake --build build
/usr/bin/c++ CMakeFiles/Main.dir/main.cpp.o -o Main libAdd.a
$ ./build/Main
42 + 102 = 144
Error calculating 42 + 2147483647
No issues this time. The library libAdd.a
was added to the compiler call, which later forwards it to the linker to finish the job. We’re then able to use the resulting executable.
Let’s now get back to some of the concepts I just glossed over. First I’ll get back to the add_library
command itself. The command actually takes one more important parameter – the type of library.
Library types
The keywords which specify the library type are STATIC
, SHARED
, OBJECT
and INTERFACE
. The first two may seem somewhat obvious, they instruct CMake to build either a static or a shared library, respectively. One might infer that STATIC
– since this is the type CMake chose, when we omitted the specifier, however, that’s not exactly the case. When the library type is not explicitly specified it is STATIC
or SHARED
, depending on the value of CMake flag BUILD_SHARED_LIBS
(note the lack of CMAKE_
prefix, which seems rather inconsistent with other flags/variables). Omitting the library type allows the user to decide what type of library CMake should build. We could use our project to build a shared library without changing a single line of code:
$ cmake -DBUILD_SHARED_LIBS=TRUE build
$ cmake --build build
/usr/bin/c++ -DAdd_EXPORTS -fPIC -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/hello_cmake/add.cpp/usr/bin/c++ -fPIC -shared -Wl,-soname,libAdd.so -o libAdd.so CMakeFiles/Add.dir/add.cpp.o
/usr/bin/c++ CMakeFiles/Main.dir/main.cpp.o -o Main -Wl,-rpath,/home/user/hello_cmake/build libAdd.so
I included the relevant output lines. As we can see the build process is more involved now – there’s more steps to building the shared library and there are multiple new flags passed to each compiler call. This is because building shared libraries, and especially doing so well,
Let’s switch back to building static libraries
$ cmake -DBUILD_SHARED_LIBS=FALSE build
We have two more library types to cover. Let’s discuss OBJECT
first. This library type has a very specific use – it instructs CMake to compile ar
and ranlib
would not be involved in the process at all! Let’s see this in action. Change the add_library
call as follows:
add_library(Add OBJECT add.cpp)
and build the project:
$ cmake --build build
/usr/bin/c++ -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/hello_cmake/add.cpp
/usr/bin/c++ -o CMakeFiles/Main.dir/main.cpp.o -c /home/user/hello_cmake/main.cpp
/usr/bin/c++ CMakeFiles/Main.dir/main.cpp.o CMakeFiles/Add.dir/add.cpp.o -o Main
We see that no library has been built here – two object files have been compiled and then they’re used to link a single executable. This is useful when all we care about is the resulting binary – if the intermediately-built library will not be reused in any way, there’s no reason to waste time packaging the object code – it might as well be used directly. This may provide significant build speed-ups in large codebases.
The remaining library type – INTERFACE
has multiple purposes. The most direct is to represent header-only libraries. How do you build a header-only library? You don’t, of course. So what does the INTERFACE
library do then? It exposes the properties necessary to use the library – most importantly the
To demonstrate and explain this well I would need to introduce another set of concepts, and this post is already getting too long. So I’ll postpone this until a future part.
Summary
We’ve covered a lot of ground in this part of the CMake Fundamentals series. We demonstrated the basic use of add_library
and target_link_libraries
to declare library targets, build libraries, and consume (link) them. We also expanded on the concept of CMake variables, their type, scope, and how to define or assign to them. Further, we discussed the library types that targets defined with add_library
may represent.
In the next part, we will discuss the PRIVATE
, INTERFACE
and PUBLIC
keywords that were just briefly mentioned here. Another concept vital to using CMake well will also be discussed – target properties – giving more insight into how exactly targets work. We will begin working on improving our project directory structure as well.
References
- Git Gist
- add_library command documentation
- target_link_libraries command documentation
- set command documentation
- cmake command documentation
- ar manpage
- ranlib manpage
Well explained, Kudos to you!!!