Click the blue textFollow the author
1. Introduction
In slightly more complex projects, we often encounter situations where we need to build multiple executable files, each potentially composed of different source files. More commonly, the project’s source code is distributed across multiple subdirectories rather than being concentrated in a single directory.
ProjectRoot
├── CMakeLists.txt (Top-level CMakeLists.txt)
├── include (Public header files directory)
│ └── mylib.h (Header file for mylib library)
├── src (Root directory for source code)
│ ├── mylib (Source code directory for mylib library)
│ │ ├── mylib.cpp (Implementation of mylib library)
│ │ └── CMakeLists.txt (CMakeLists.txt for mylib library)
│ ├── app (Source code directory for the application)
│ │ ├── main.cpp (Entry point of the application)
│ │ └── CMakeLists.txt (CMakeLists.txt for the application)
│ └── utils (General utility library, optional)
│ ├── utils.h (Header file for utility library)
│ ├── utils.cpp (Implementation of utility library)
│ └── CMakeLists.txt (CMakeLists.txt for utility library)
├── tests (Test code directory, optional)
│ ├── mylib_tests.cpp (Test cases for mylib library)
│ └── CMakeLists.txt (CMakeLists.txt for tests)
└── docs (Project documentation, optional)
Distributing source code across different subdirectories is a good code organization practice that brings many benefits:
- Modularity: Different functional modules can be placed in separate subdirectories, making the code structure clearer and easier to understand and maintain.
- Code Reuse: If multiple executables need to use the same code, that code can be placed in a separate module (directory) and referenced in each executable, avoiding code duplication.
- Separation of Concerns: Each subdirectory can focus on implementing a specific function or module, thereby improving code maintainability.
- Faster Compilation Speed: When a project is large, recompiling all code after every modification can be very time-consuming. By breaking the project into multiple modules, only the modified modules need to be recompiled, significantly improving compilation speed.
To better manage and organize this modular code and avoid redundant builds and code duplication, some code is typically compiled into libraries. Libraries can be linked and used by multiple executables. This article will focus on how to compile project source code into libraries and how to correctly link these libraries to build the final executable files.
2. Preparation
The example from a previous article only involved a single source file, which was simple and straightforward. Now, we will introduce a <span>Message</span>
class and encapsulate it into an independent module to demonstrate a more practical CMake project organization.
The project includes the following files:
<span>helloWorld.cpp</span>
: The main program entry point, using the<span>Message</span>
class to display messages.<span>message.hpp</span>
: The header file for the<span>Message</span>
class, defining the class interface.<span>message.cpp</span>
: The implementation file for the<span>Message</span>
class, containing the specific implementation of the class.
<span>Code for helloWorld.cpp:</span>
#include "Message.hpp" // Include the header file for the Message class
#include <cstdlib>
#include <iostream>
int main() {
Message say_hello("Hello, CMake World!"); // Create Message object
std::cout << say_hello << std::endl; // Use overloaded << operator to output message
Message say_goodbye("Goodbye, CMake World");
std::cout << say_goodbye << std::endl;
return EXIT_SUCCESS;
}
<span>Code for message.hpp:</span>
#pragma once // Prevent header file from being included multiple times
#include <iosfwd> // Provide forward declaration for std::ostream
#include <string> // Include std::string class
class Message {
public:
// Constructor that takes a string as the message content
Message(const std::string &m) : message_(m) {}
// Overloaded << operator to output Message object to ostream
friend std::ostream &operator<<(std::ostream &os, Message &obj) {
return obj.printObject(os);
}
private:
std::string message_; // Private member variable to store message content
// Private member function responsible for the actual output operation
std::ostream &printObject(std::ostream &os);
};
<span>Code for message.cpp:</span>
#include "message.hpp" // Include the header file for the Message class
#include <iostream> // Include std::cout, std::endl
#include <string> // Include std::string class
std::ostream &Message::printObject(std::ostream &os) {
os << "This is my very nice message: " << std::endl;
os << message_;
return os;
}
To build this project, a <span>CMakeLists.txt</span>
file needs to be created to instruct CMake on how to compile and link these source files. The <span>Message</span>
class will be built as a library, and then linked in the <span>helloWorld</span>
main program.
Project Directory Structure:
MyProject/
├── CMakeLists.txt (Top-level CMakeLists.txt)
├── helloWorld.cpp (Main program source file)
├── include/ (Directory for header files)
│ └── message.hpp
└── src/ (Directory for source files)
└── message.cpp
3. CMake Target Management Commands
Before we start writing the <span>CMakeLists.txt</span>
to create the library, let’s understand some CMake target management commands to make the reading easier later.
In CMake, a “target” is a core concept in the build process. A target represents something that needs to be built, such as an executable file, library, module, etc. CMake provides a series of commands to create, configure, and manage these targets.
Function | Syntax |
---|---|
Create executable target | <span>add_executable(<target_name> <source_file_list>)</span> |
Create library target | <span>add_library(<target_name> [STATIC|SHARED|MODULE] <source_file_list>)</span> |
Set target’s header file search path | <span>target_include_directories(<target_name> [SYSTEM] [BEFORE] <INTERFACE|PUBLIC|PRIVATE> <directory_list>)</span> |
Add compile definitions (macros) for target | <span>target_compile_definitions(<target_name> <INTERFACE|PUBLIC|PRIVATE> [definition1] [definition2] ...)</span> |
Add compile options for target | <span>target_compile_options(<target_name> <INTERFACE|PUBLIC|PRIVATE> [option1] [option2] ...)</span> |
Link libraries to target | <span>target_link_libraries(<target_name> <PRIVATE|PUBLIC|INTERFACE> [library1] [library2] ...)</span> |
Set target properties | <span>set_target_properties(<target_name> PROPERTIES <property1> <value1> <property2> <value2> ...)</span> |
Add dependencies between targets | <span>add_dependencies(<target_name> [dependency1] [dependency2] ...)</span> |
Understanding library types:
<span>STATIC</span>
: Creates a static library (<span>.a</span>
or<span>.lib</span>
).<span>SHARED</span>
: Creates a dynamic library (<span>.so</span>
,<span>.dylib</span>
, or<span>.dll</span>
).<span>MODULE</span>
: Creates a module, typically used for plugins or dynamic loading.
Understanding parameters like <span>PUBLIC</span>
, <span>PRIVATE</span>
, and <span>INTERFACE</span>
:
<span>INTERFACE</span>
: The specified header file directories will be passed to any targets linked to this target.<span>PUBLIC</span>
: The specified header file directories will be passed to any targets linked to this target and will also be used to compile this target itself.<span>PRIVATE</span>
: The specified header file directories are only used to compile this target itself and will not be passed to other targets.<span>[SYSTEM]</span>
: Marks the directory as a system directory, which the compiler will handle in a special way.<span>[BEFORE]</span>
: Adds the directory to the beginning of the list, giving it priority in the search.
Using <span>PUBLIC</span>
, <span>PRIVATE</span>
, and <span>INTERFACE</span>
keywords allows for better control over target dependencies, avoiding unnecessary compilation options and linked libraries being passed around.
4. Specific Steps
Use the <span>add_library</span>
command to create a library target, thus achieving code encapsulation.
First, modify the <span>CMakeLists.txt</span>
file to instruct CMake to compile the <span>message.hpp</span>
and <span>message.cpp</span>
files into a static library instead of directly compiling them into an executable. By using the <span>STATIC</span>
keyword, we explicitly specify that we want to create a static library.
# Create a static library
add_library(MessageLib
STATIC
src/message.cpp
include/message.hpp
)
Next, create an executable target responsible for compiling the <span>helloWorld.cpp</span>
file to generate the executable <span>hello-world</span>
.
add_executable(helloWorld helloWorld.cpp)
Finally, we need to link the created static library <span>MessageLib</span>
to the executable <span>helloWorld</span>
. By using the <span>target_link_libraries</span>
command, we can specify which libraries the executable depends on.
target_link_libraries(helloWorld MessageLib)
The complete <span>CMakeLists.txt</span>
is as follows:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(HelloWorld LANGUAGES CXX)
# Set C++ standard (optional but recommended)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Add include directories so the compiler can find Message.hpp
include_directories(${CMAKE_SOURCE_DIR}/include)
# Create a static library (or dynamic library)
add_library(MessageLib
STATIC
src/message.cpp
include/message.hpp
)
# Add executable and link MessageLib library
add_executable(helloWorld helloWorld.cpp)
# Link target library to executable target:
target_link_libraries(helloWorld MessageLib)
After completing the writing of the <span>CMakeLists.txt</span>
, use CMake to configure and build the project.
$ mkdir -p build
$ cd build
$ cmake ..
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fly/workspace/cmakeProj/recipe_05/build
$ cmake --build .
[ 25%] Building CXX object CMakeFiles/MessageLib.dir/src/message.cpp.o
[ 50%] Linking CXX static library libMessageLib.a
[ 50%] Built target MessageLib
[ 75%] Building CXX object CMakeFiles/helloWorld.dir/helloWorld.cpp.o
[100%] Linking CXX executable helloWorld
[100%] Built target helloWorld
After the build is complete, the <span>MessageLib</span>
static library will be compiled (usually a <span>.a</span>
or <span>.lib</span>
file), and it will be linked to the <span>helloWorld</span>
executable file. This way, the <span>helloWorld</span>
executable can directly use the functions and classes defined in the <span>MessageLib</span>
library without relying on the existence of the <span>MessageLib</span>
library at runtime. All necessary code has been embedded into the executable file.
The output when executing the program is as follows:
$ ./helloWorld
This is a message:
Hello, CMake World!
This is a message:
Goodbye, CMake World!
5. Detailed Working Principles
This example’s <span>CMakeLists.txt</span>
file introduces two core commands:<span>add_library(MessageLib STATIC src/message.cpp include/message.hpp</span>
and <span>target_link_libraries(helloWorld MessageLib)</span>
, which together complete the process of compiling source code into a library and linking the library to the executable file.
(1)<span>add_library</span>
command primarily serves to generate the instructions needed to build the library. In simple terms, it tells CMake how to compile the specified source code files (<span>Message.hpp</span>
and <span>Message.cpp</span>
) into a library.
CMake generates the actual library file name based on the following rules:
- Add prefix
<span>lib</span>
. The prefix<span>lib</span>
is added before the target name. So,<span>MessageLib</span>
becomes<span>libMessageLib</span>
. - Add suffix. Depending on the library type (
<span>STATIC</span>
or<span>SHARED</span>
) and the operating system, CMake will add the appropriate suffix.
- On GNU/Linux, the suffix for static libraries is
<span>.a</span>
, and for shared libraries, it is<span>.so</span>
. - On Windows, the suffix for static libraries is
<span>.lib</span>
, and for shared libraries, it is<span>.dll</span>
; note that Windows shared libraries do not have the<span>lib</span>
prefix.
Library types and their characteristics:
-
Static Library (
<span>STATIC</span>
): A static library is a packaged archive of compiled files. The linker copies the code from the static library into the executable file during linking. This means the executable file contains the code from the static library, so there is no runtime dependency on the static library. The advantage of static libraries is the independence of the executable file, while the disadvantage is that the executable file size is larger, and if the static library is updated, the executable file needs to be recompiled. -
Shared Library (
<span>SHARED</span>
): A shared library (also known as a dynamic library) is a library that can be dynamically loaded at runtime. The linker does not copy the code from the shared library into the executable file during linking; it only records the dependency on the shared library. At runtime, the operating system loads the shared library for the executable file to run properly. The advantage of shared libraries is that the executable file size is smaller, and multiple executable files can share a single shared library, saving memory. The disadvantage is that the executable file depends on the shared library; if the shared library is missing or incompatible, the executable file will not run.
(2)<span>target_link_libraries</span>
command is used to link one or more libraries to the specified executable file or other targets.The function of this command:
- Link Library: The
<span>target_link_libraries</span>
command tells the linker to link the<span>MessageLib</span>
library to the<span>helloWorld</span>
executable file. This means the linker will add the functions and variables from the<span>MessageLib</span>
library to the<span>helloWorld</span>
executable file, allowing it to use the functionality provided by the<span>MessageLib</span>
library. - Dependency Management: The
<span>target_link_libraries</span>
command also manages dependencies between targets. It ensures that the<span>MessageLib</span>
library must be built before it is linked to the<span>helloWorld</span>
executable file. CMake will automatically handle the build order, building the<span>MessageLib</span>
library first, then the<span>helloWorld</span>
executable file. If the<span>MessageLib</span>
library changes, CMake will automatically rebuild both the<span>MessageLib</span>
library and the<span>helloWorld</span>
executable file.
After successful compilation, the build directory will contain the following files:
<span>libmessage.a</span>
(on GNU/Linux): Static library file containing the compiled code from<span>message.hpp</span>
and<span>message.cpp</span>
.<span>helloWorld</span>
: Executable file linked to the<span>libMessageLib.a</span>
library.
Besides <span>STATIC</span>
, the <span>add_library</span>
command also accepts other values as library types:
-
<span>SHARED</span>
: Used to create dynamic shared libraries (DSO). Using<span>add_library(message SHARED Message.hpp Message.cpp)</span>
can switch from static libraries to dynamic shared objects (DSO). Unlike static libraries, dynamic shared libraries are loaded at runtime, allowing different programs to share the same library code, thus saving memory and disk space. -
<span>OBJECT</span>
: Used to create object libraries. Object libraries compile the specified source files into object files (e.g.,<span>.o</span>
files) but do not package them into static libraries or link them into shared objects. Object libraries are typically used to create static and dynamic libraries from a single set of source files. They combine multiple object files into static or dynamic libraries. -
<span>MODULE</span>
: Also used to create DSO, but unlike<span>SHARED</span>
libraries,<span>MODULE</span>
libraries are not linked to any targets in the project and are typically used for runtime plugins. Runtime plugins are code modules that can be dynamically loaded and unloaded to extend the functionality of applications.
CMake can also generate special types of libraries that do not produce output files in the build system but are very useful for organizing dependencies and build requirements between targets:
-
<span>IMPORTED</span>
: Used to represent libraries located outside the project. The main purpose of<span>IMPORTED</span>
libraries is to integrate existing external dependencies.<span>IMPORTED</span>
libraries are typically considered immutable because they are outside the project’s control. See: https://cmake.org/cmake/help/latest/manual/cmakebuildsystem.7.html#imported-targets -
<span>INTERFACE</span>
: Similar to<span>IMPORTED</span>
libraries, but<span>INTERFACE</span>
libraries are mutable and do not have location information.<span>INTERFACE</span>
libraries are mainly used to define build requirements for targets outside the project, such as specifying required compiler options or include directories. See: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#interface-libraries -
<span>ALIAS</span>
: Used to define aliases for existing library targets in the project.<span>ALIAS</span>
libraries can simplify references to libraries and improve code readability. Aliases cannot be created for<span>IMPORTED</span>
libraries. See: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#alias-libraries
In this example, we directly specified the source files in the <span>add_library</span>
command. In subsequent articles, we will learn to use the <span>target_sources</span>
command to organize source code. The <span>target_sources</span>
command allows us to add and manage source files more flexibly, such as conditionally adding source files or organizing source files into different groups. Learning the <span>target_sources</span>
command will further enhance our ability to manage CMake projects.
6. The Magic of OBJECT Libraries
<span>OBJECT</span>
libraries are a special type of library provided by CMake that do not directly generate library files but compile source files into object files (<span>.o</span>
or <span>.obj</span>
) for linking by other libraries or executable files. This allows for more flexible construction of static and shared libraries, especially when we want to generate two types of libraries from the same set of source files.
Modify the <span>CMakeLists.txt</span>
to use the <span>OBJECT</span>
library:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(HelloWorld LANGUAGES CXX)
# Set C++ standard (optional but recommended)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Add include directories so the compiler can find Message.hpp
include_directories(${CMAKE_SOURCE_DIR}/include)
# Create an OBJECT target
add_library(message-objs
OBJECT
src/message.cpp
include/message.hpp
)
# Ensure compiled object files are independent of the generation location
# For older compilers or certain platforms, this property may need to be set explicitly
set_target_properties(message-objs PROPERTIES POSITION_INDEPENDENT_CODE ON)
# Create shared library using OBJECT library
add_library(message-shared SHARED $<TARGET_OBJECTS:message-objs>)
# Create static library using OBJECT library
add_library(message-static STATIC $<TARGET_OBJECTS:message-objs>)
# Add executable and link MessageLib library
add_executable(helloWorld helloWorld.cpp)
# Link executable file to the static version of the message library
target_link_libraries(helloWorld message-static)
Generator expressions are special syntax that CMake constructs when generating the build system (i.e., after configuration) to dynamically control the build process based on different build configurations. Generator expressions are wrapped in <span>$<></span>
, allowing for dynamic control of the build process based on conditions such as build type, target platform, etc.
In this example, <span>$<TARGET_OBJECTS:message-objs></span>
generator expression expands all object files in the <span>message-objs</span>
library and passes them as source files to the <span>message-shared</span>
and <span>message-static</span>
libraries. This means that the <span>message-shared</span>
and <span>message-static</span>
libraries will link the object files generated by the <span>message-objs</span>
library, thus achieving the creation of two different types of libraries from the same set of source files.
Compile and run:
$ mkdir -p build
$ cd build/
$ cmake ..
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fly/workspace/cmakeProj/recipe_05_2/build
$ cmake --build .
[ 20%] Building CXX object CMakeFiles/message-objs.dir/src/message.cpp.o
[ 20%] Built target message-objs
[ 40%] Linking CXX shared library libmessage-shared.so
[ 40%] Built target message-shared
[ 60%] Linking CXX static library libmessage-static.a
[ 60%] Built target message-static
[ 80%] Building CXX object CMakeFiles/helloWorld.dir/helloWorld.cpp.o
[100%] Linking CXX executable helloWorld
[100%] Built target helloWorld
$ ./helloWorld
This is a message:
Hello, CMake World!
This is a message:
Goodbye, CMake World!
By default, CMake generates different file names for static and shared libraries (e.g., <span>libmessage-static.a</span>
and <span>libmessage-shared.so</span>
). If you want them both to be named <span>libmessage</span>
(e.g., <span>libmessage.a</span>
and <span>libmessage.so</span>
), you can use the <span>OUTPUT_NAME</span>
property:
add_library(message-shared
SHARED
$<TARGET_OBJECTS:message-objs>
)
set_target_properties(message-shared
PROPERTIES
OUTPUT_NAME "message"
)
add_library(message-static
STATIC
$<TARGET_OBJECTS:message-objs>
)
set_target_properties(message-static
PROPERTIES
OUTPUT_NAME "message"
)
<span>set_target_properties(target PROPERTIES OUTPUT_NAME "name")</span>
: This command is used to set properties for a target (<span>target</span>
). The <span>OUTPUT_NAME</span>
property specifies the output file name of the library (excluding the prefix <span>lib</span>
and suffix). By setting the <span>OUTPUT_NAME</span>
to “message”, we can have CMake generate static and shared libraries named <span>libmessage.a</span>
and <span>libmessage.so</span>
(or other platform-specific file names).
Whether the executable can be dynamically linked to the shared library depends on the operating system and compiler:
-
GNU/Linux and macOS: On these operating systems, it works correctly regardless of the compiler used. This is because the compilers and linkers on GNU/Linux and macOS follow the standard ABI (Application Binary Interface), allowing executable files to dynamically load and link shared libraries at runtime.
-
Windows: On Windows, there are compatibility issues with Visual Studio. While it is possible to compile dynamic link libraries using MinGW or MSYS2, programs compiled with Visual Studio may not load these libraries correctly. This is because Visual Studio uses a different ABI that is incompatible with other Windows compilers.
Why are there compatibility issues on Windows?
The main reason lies in symbol visibility control. Generating usable shared libraries requires programmers to restrict the visibility of symbols. This needs to be implemented with the help of the compiler, but the conventions differ across different operating systems and compilers.
Not all symbols in a shared library need to be externally visible. To reduce the size of the library, improve security, and avoid symbol conflicts, it is often necessary to declare some symbols as private, used only within the library.
On Linux and macOS, compilers provide corresponding mechanisms to control symbol visibility, such as using the <span>__attribute__((visibility("hidden")))</span>
attribute. CMake also has a mechanism to handle this issue, which will be explained in detail in subsequent articles.
7. Conclusion
Understanding the principles of CMake library building and linking is a key step in mastering the CMake build system. What this article covers is just the tip of the iceberg of CMake’s powerful capabilities. It is essential to practice more, write <span>CMakeLists.txt</span>
files, try different configurations and options, and observe the build results. Start with simple projects, gradually increase complexity, and ultimately become proficient in using CMake to build libraries and link targets, thereby simplifying the build process and improving the portability and maintainability of the code.
Lion Welcome to follow my public account Learn technology or contribute