Libraries are very useful when a C++ project becomes large enough, we may want to split the code into multiple library and executable CMake targets in order to make our project more modular and understandable. This can all be achieved with CMake’s add_library(...)
function.
For example, if you are developing a 2D game such as Conway’s Game Of Life, you may want to split all the structures and functions related to graphics into an independent library. Perhaps you can name this library “engine”, which will be linked against your game executable for drawing in 2D.
In this post, we will learn how to create libraries with CMake, the types of libraries you can create and how to link them to other targets.
The Problem – Why Do We Need To Create Libraries?
According to Wikipedia, a library is “a collection of resources by computer programs” in software development, and they include “documentation, subroutines, classes, and values” that aid developers in implementing logic for their software.
Interestingly, if you develop software of any kind, you are probably using various different libraries. For example, do you use the C++ standard library? Do you use C++ vectors? Guess what, you are indeed using libraries in your programming activities. They allow you to share functionality between different bits of code!
Whether they are third-party libraries or libraries you created, they are supposed to be a collection (or module) of functionality that allows the user to perform a specific task. For example, the list below shows some well-known libraries out there.
- GTest (Google Test): This is a library everyone who develops with C/C++ has probably come accross. It helps developers create unit tests, as well as mocking objects.
- OpenGL / Vulkan: Cross-platform libraries for 3D graphics, many popular games use these libraries.
- Numpy: Numerical library for Python. Unsurprisingly, libraries aren’t unique to C/C++!
In summary, you may need to create libraries for a few reasons. Do you have certain bits of code that need to be shared with different programs/executables? Do you want to distribute some code publicly so other people can use it in their projects? Is your codebase getting ridiculously big, and you need to split parts of it into reasonable modules? Then you need to start thinking about libraries.
CMake’s add_library Helps With Portability
Before CMake, there was a time when people wrote massive scripts for building C/C++ projects – Makefiles
. These scripts told computers how to compile projects, which executables to generate, and which libraries to create.
Therefore, the exact compiler flags would need to be hardcoded somewhere when creating a library file. In addition, any other commands needed to combine library files, move them or link them into your executable would also need to be added somewhere in the Makefile
.
For example, the following Makefile
uses the gcc
compiler to create an executable program called cool_program
, created from the source files main.c
and uses the library cool_library.a
.
cool_program: main.o cool_library.a
gcc $^ -o $@
main.o: main.c
gcc -c $< -o $@
cool_library.a: boring_lib1.o boring_lib2.o
ar rcs cool_library.a boring_lib1.o boring_lib2.o
boring_lib1.o: boring_header1.h
gcc -c -o $@ $<
boring_lib2.o: boring_header2.h boring_source2.cpp boring_source3.cpp
gcc -c -o $@ $<
As you can see, the above scripts should work fairly well if you want to compile your project with gcc
on a Unix-like environment. However, what happens if you switch the compiler to clang
or msvc
? What happens if you want to create libraries (.lib
) for Windows, where they aren’t quite ar
chives of object files (.o
)? You would need to write other Makefiles
to work with different environments!
This is where CMake comes to the rescue: it lets us define our libraries at a higher level, and your project will be way more portable as it should work in any supported platform.
Creating Libraries With CMake’s add_library
Without further ado, we finally get to the interesting part. CMake’s function for creating a library is add_library
, and the code block below shows the usage.
add_library(libraryName
[STATIC|SHARED|MODULE]
[EXCLUDE_FROM_ALL]
source1 source2 ....)
Firstly, the first parameter to add_library
is the name of the library. This can be any valid CMake name, and the filename for the compiled library will be that value in your build directory.
In addition, you may optionally add STATIC
, SHARED
, or MODULE
as the second parameter for the library type – if you aren’t aware of the differences between these types, we describe them in the next section.
Following the type, you can also add EXCLUDE_FROM_ALL
as another optional argument to avoid the library being built by default once you invoke cmake --build
. However, this argument is ignored if the library is a dependency for a target that is built by default!
Finally, you need to specify which files will be used to generate the library target. For example, cool_library
in the Makefile
in the previous section is generated from the source files boring_source2.cpp
and boring_source3.cpp
, so we would have something like add_library(cool_library STATIC boring_source1.cpp boring_source2.cpp)
.
If the library has header files to be exported with it, then you should include header directories to the target with CMake.
STATIC, SHARED – What Does The Library Type Mean?
In short words, you can create different types of libraries for different purposes. Interestingly, you may have seen files with the extension “.a” on Linux, or “.lib” on Windows – these files are static libraries. Similarly, “.so” files (Linux) and “.dll” files (Windows), are shared/dynamic libraries.
Ideally, you need to do some research and read online on static vs shared libraries. However, the list below gives a brief explanation of the purposes of each type.
- Static libraries are a collection of object files that executables can directly include in the binary. To be clear, executables link static libraries at compile time. For example,
add_library(mylib STATIC ...)
creates the filelibmylib.a
on Linux, and when executables link that library, the final compiled executable contains copies of the library code in its binary. - Shared libraries are files with functionality that can be referenced at run-time. Executables linking shared libraries do not copy the code of the libraries to its binary, instead they reference the libraries at run-time. You may know about “.so” files (Linux) or “.dll” files (Windows), these are shared objects/libraries that can be created with
add_library(libname SHARED ...)
.
Since static libraries are copied into executables during compilation time, you need to recompile your code if you have made any changes to your library for the changes to take effect. On the other hand, you may replace shared libraries with newer versions without recompiling your executables!
Find out more about static and shared libraries in this Stack Overflow question, as the post has really good comparisons and information on library types.
So What Is A MODULE Library ?
A MODULE
library is very much similar to a shared library. In other words, it’s a library file that executables reference at run-time. However, the difference is that executables don’t have to load the module libraries at the start of execution, they can be lazily loaded at some point in your program.
For example, you can use dlopen(...)
in Linux to open a module library if your program’s logic requires you to do so. If the library isn’t present, your executable still functions.
For this reason, if your platform supports module libraries — Linux, Windows, and Mac all do — then you can use module libraries as a means of adding “plugins” to your software.
Other Library Types
CMake also supports other, less commonly used library types. The list below summarises the types.
- Object libraries are kind of like static libraries, but these aren’t a collection of functionality and are usually created from one source file, along with it’s headers.
- Interface libraries dont compile source code. Instead, they generally provide constraints or information for the linking targets. Could also represent header only libraries.
- Imported libraries also do not compile anything, and are used to represent libraries that can be imported from the system!
For more information, I’d recommend reading the CMake documentation for add_library(...)
, which, unfortunately, can be quite convoluted for new starters.
For easier reading and better explanations of the CMake functions we mentioned in this post, check out Craig Scott’s Professional CMake: A Practical Guide – really worth the money if you work with CMake.
Linking CMake Libraries To Other C++ Targets
Now that you can create library targets with CMake, the next reasonable step is to use them in your programs!
Linking library targets to other targets — whether it’s an executable or another library target — is easy with CMake. The code block below shows the usage for the target_link_libraries(...)
function, which links libraries to other targets in CMake.
target_link_libraries(target_name
[PUBLIC|PRIVATE|INTERFACE]
library1 library2 ....)
Unsurprisingly, the executable/library target_name
links to library1
and library2
with the command above.
The list below summarises the arguments to target_link_libraries
and their purpose.
- Target name is the name of the target you want to add your libraries to.
- Scope is an optional argument that can be either
PUBLIC
,PRIVATE
, orINTERFACE
. - The last parameter(s) is a list of libraries to link, where each item is the name of the library given in
add_library
.
For more information on the scope, check out this section on target_include_directories
in CMake.
Good Practices And Notes On Adding CMake Libraries With add_library
Undoubtedly, as your project grows in size, you will create libraries to modularise your codebase a little bit better.
Firstly, try not to add the prefix “lib” to your library target names. For example, add_library(libcool STATIC ...
) will simply create the file “liblibcool.a” in a Linux environment. So it goes without saying that CMake takes care of the naming conventions and extensions for each platform.
Secondly, according to Craig Scott’s CMake book, omitting the type argument in add_library(...)
is good practice. In essence, this means that the person building your library can specify if the project builds static or shared libraries with the flag BUILD_SHARED_LIBS
.
If the developer sets BUILD_SHARED_LIBS
to True
in the CMake configuration command, running cmake --build ...
generates shared libraries. However, CMake generates static libraries otherwise.
Finally, when building your projects, I recommend using static by default. Unless you have a specific reason to use shared or module libraries, you can avoid compatibility issues when linking outdated shared libraries.
Did I miss anything? Did you spot any errors in the post? Do you have some feedback? Feel free to comment below!
Be First to Comment