laitimes

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

In the world of software development, the build system plays a crucial role, not only determining the efficiency of the project, but also directly affecting the flow of team collaboration. For many C++ developers, CMake is the tool of choice for building automated processes due to its powerful features and wide compatibility.

Original link: https://journal.hexmos.com/cmake-survial-guide/

Disclaimer: Reproduction without permission is prohibited.

作者 | Shrijith Venkatramana翻译 | 郑丽媛出品 | CSDN(ID:CSDNnews)

I've been working on some programming challenges in C++ lately, and one of the important aspects of managing a C++ project is dependency management.

Today, we enjoy the convenience of instant package managers in many programming ecosystems:

● Use npm in Node.js/JavaScript

● Use cargo in Rust

● Use pip in Python

In C++, despite the availability of package managers like Conan, you'll often find CMake to be the unavoidable choice when working on real projects. So if you want to work in the C++ ecosystem, learning how to use CMake isn't optional, it's mandatory.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"
"5 Minute CMake How-To Fix My C++ Packaging Problem!"

What exactly is CMake and why should you learn it?

CMake is a cross-platform build system builder. Cross-platform is important because CMake is able to abstract away the differences between platforms to some extent.

For example, on a Unix-like system, CMake generates makefile files, which are then used to build the project. On Windows, CMake generates Visual Studio project files, which are then used to build the project.

It's important to note that different platforms often have their own compilation and debugging toolchains: gcc for Unix, clang for macOS, and so on.

Another important aspect of the C++ ecosystem is the ability to work with both executables and libraries.

Executables can be based on the following different factors:

● Target CPU architecture

● Target operating system

● Other factors

There are also different options for how libraries are linked (linking refers to using the functionality of another codebase in your code without having to know its implementation):

● Static links

● Dynamic linking

I've had internal prototyping projects where I needed to call the underlying operating system APIs to perform certain tasks, and the only efficient way to do that was to build on top of some C++ library.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

How CMake Works: Three Phases

1. Configuration phase

CMake reads all CMakeLists.txt files and creates an intermediate structure to determine the next steps (e.g., listing source files, collecting libraries to link, etc.).

2. Build phase

Based on the intermediate output of the configuration phase, CMake generates platform-specific build files (e.g., makefiles on Unix systems, etc.).

3. Build phase

Use platform-specific tools such as make or ninja to build executables or libraries.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

A simple example of a CMake project (Hello World!)

Let's say you have a C++ source file that calculates the square root of numbers.

tutorial.cxx

// A simple program that computes the square root of a number              #include <cmath>              #include <cstdlib> // TODO 5: Remove this line              #include <iostream>              #include <string>                  // TODO 11: Include TutorialConfig.h                  int main(int argc, char* argv[])              {              if (argc < 2) {              // TODO 12: Create a print statement using Tutorial_VERSION_MAJOR              // and Tutorial_VERSION_MINOR              std::cout << "Usage: " << argv[0] << " number" << std::endl;              return 1;              }                  // convert input to double              // TODO 4: Replace atof(argv[1]) with std::stod(argv[1])              const double inputValue = atof(argv[1]);                  // calculate square root              const double outputValue = sqrt(inputValue);              std::cout << "The square root of " << inputValue << " is " << outputValue              << std::endl;              return 0;              }           

CMakeLists.txt

project(Tutorial)              add_executable(tutorial tutorial.cxx)           

The above two lines are the minimum instructions required to generate an executable. Theoretically, we should also specify the minimum version number for CMake, and omitting CMake will default to a version (skip this part for now).

Strictly speaking, the project directive isn't required, but we'll keep it anyway. So the most important lines of code are:

add_executable(tutorial tutorial.cxx)           

This line of code specifies the target binary tutorial and the source file tutorial.cxx.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

How to build

Here's a set of commands for building your project and testing your binaries, which you'll explain in more detail later:

mkdir build              cd build/              cmake ..              ls -l # inspect generated build files              cmake --build .              ./tutorial 10 # test the binary           

As you can see from the steps above, the whole build process involves about 5-6 steps.

First of all, in CMake, we should separate build-related content from the source code, so create a build directory first:

mkdir build           

Then we can do all the build-related operations in the build directory:

cd build           

From this step onwards, we'll perform several build-related tasks.

First, generate a configuration file:

cmake ..           

In this step, CMake generates a platform-specific profile. On my Ubuntu system, I see the generated makefiles, which are quite verbose, but I don't need to worry about them at the moment.

Next, I trigger the build based on the newly generated file:

cmake --build .           

This step uses the generated build file to generate the target binary tutorial.

Finally, I can verify that the binaries are working as expected with the following command:

./tutorial 16           

I got the expected answer, which means that the build process is working fine!

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

Inject variables into a C++ project

CMake provides a mechanism through Config.h.in that allows you to specify variables in CMakeLists.txt that can be used in your .cpp files.

Here's an example where we define the version number of a project in CMakeLists.txt and use it in a program.

Config.h.in

In this file, the variables from CMakeLists.txt will appear in the form of @VAR_NAME@.

#pragma once                  #define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@              #define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@              #define AUTHOR_NAME "@AUTHOR_NAME@"           

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)              project(Tutorial)                  # Define configuration variables              set(PROJECT_VERSION_MAJOR 1)              set(PROJECT_VERSION_MINOR 0)              set(AUTHOR_NAME "Jith")                  # Configure the header file              configure_file(Config.h.in Config.h)                  # Add the executable              add_executable(tutorial tutorial.cxx)                  # Include the directory where the generated header file is located              target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           

Note that we've added cmake_minimum_required to specify the minimum CMake version required, which is a good practice when writing CMakeLists.txt files.

We then use multiple set() statements to define the desired variable name. Next, specify the configuration file Config.h.in from which the variables set above can be used.

Finally, CMake generates header files after the variable placeholders are populated, and these dynamically generated header files need to be included in the project.

In our example, the Config.h file will be placed in the ${CMAKE_BINARY_DIR} directory, so we just need to specify that path.

You might be curious about the PRIVATE tab in the following line:

target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           
"5 Minute CMake How-To Fix My C++ Packaging Problem!"

Understand the two key concepts of CMake: visibility modifiers and goals

在 CMake 中,有三个可见性修饰符:PRIVATE、PUBLIC、INTERFACE。

These modifiers can be used in commands such as: target_include_directories and target_link_libraries, among others.

These modifiers are specified in the context of Targets. The goal is an abstraction in CMake that represents a certain type of output:

● Executable target (via add_executable) generates binaries

● The library target (via add_library) generates the library file

● Custom targets (via add_custom_target) generate arbitrary files via scripts, etc

All of the above goals will result in a specific file or artifact as output. A special case of library targets is the Interface Target. The interface target is defined as follows:

add_library(my_interface_lib INTERFACE)              target_include_directories(my_interface_lib INTERFACE include/)           

Here, my_interface_lib doesn't immediately generate any files. However, in subsequent stages, some specific goals may depend on my_interface_lib. This means that the include directory specified in the interface target will also be dependent. THEREFORE, THE INTERFACE LIBRARY CAN BE SEEN AS A CONVENIENT MECHANISM FOR BUILDING DEPENDENCY TREES.

With the concept of goals and dependencies understood, let's go back to the concept of visibility modifiers.

PRIVATE VISIBILITY

1target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           

PRIVATE indicates that the target tutorial will use the specified include directory. However, if other targets link to the tutorial in a later phase, the containing directory will not be passed to those dependencies.

PUBLIC VISIBILITY

1target_include_directories(tutorial PUBLIC "${CMAKE_BINARY_DIR}")           

Using the PUBLIC modifier means that the target tutorial needs to use that containing directory, and any other targets that depend on the tutorial will inherit that containing directory as well.

INTERFACE 可见性

1target_include_directories(tutorial INTERFACE "${CMAKE_BINARY_DIR}")           

The INTERFACE modifier indicates that the tutorial itself does not need the containing directory, but any other targets that depend on the tutorial will inherit the containing directory.

To summarize briefly, the visibility modifier works as follows:

● PRIVATE: The source files and dependencies are only passed to the current target;

● PUBLIC: The source files and dependencies are passed to the current target and the target on which it depends;

INTERFACE: THE SOURCE FILE AND DEPENDENCIES ARE NOT PASSED TO THE CURRENT TARGET, BUT ARE PASSED TO THE TARGET THAT DEPENDS ON IT.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

Divide your project build into libraries and directories

As projects continue to grow, modularity is often required to organize projects and manage complexity.

In CMake, you can use subdirectories to specify individual modules and their custom build processes. We can have a master CMake configuration that triggers the build of multiple libraries (subdirectories) and finally links all the modules together.

Here's a simplified example. We'll create a module/library called MathFunctions, which will be built as a static library (generating MathFunctions.a on Unix systems) and then link it to our main program.

First, the source file (the code is relatively simple):

MathFunctions.h

#pragma once                  namespace mathfunctions {              double sqrt(double x);              }           

MathFunctions.cxx

#include "MathFunctions.h"              #include "mysqrt.h"                  namespace mathfunctions {              double sqrt(double x)              {              return detail::mysqrt(x);              }              }           

mysqrt.h

#pragma once                  namespace mathfunctions {              namespace detail {              double mysqrt(double x);              }              }           

mysqrt.cxx

#include "mysqrt.h"                  #include <iostream>                  namespace mathfunctions {              namespace detail {              // a hack square root calculation using simple operations              double mysqrt(double x)              {              if (x <= 0) {              return 0;              }                  double result = x;                  // do ten iterations              for (int i = 0; i < 10; ++i) {              if (result <= 0) {              result = 0.1;              }              double delta = x - (result * result);              result = result + 0.5 * delta / result;              std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;              }              return result;              }              }              }           

These snippets introduce a namespace called mathfunctions, which contains a custom implementation of the sqrt function. This allows us to define our own square root function in our project without conflicting with other versions of sqrt.

Next, how do you build that folder as a Unix binary? We need to create a custom CMake sub-config for that module/library:

MathFunctions/CMakeLists.txt

add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)           

With this simple add_library directive, we specify the .cxx file that needs to be compiled to generate the library file.

But that's not enough, the core of the solution is how to link this subdirectory or library to our main project:

tutorial.cxx(使用库/模块版本)

#include "Config.h"              #include "MathFunctions.h"              #include <cmath>              #include <cstdlib>               #include <iostream>              #include <string>                  int main(int argc, char* argv[])              {              std::cout << "Project Version: " << PROJECT_VERSION_MAJOR << "." << PROJECT_VERSION_MINOR << std::endl;              std::cout << "Author: " << AUTHOR_NAME << std::endl;                  if (argc < 2) {              std::cout << "Usage: " << argv[0] << " number" << std::endl;              return 1;              }                  const double inputValue = atof(argv[1]);                  // use library function              const double outputValue = mathfunctions::sqrt(inputValue);              std::cout << "The square root of " << inputValue << " is " << outputValue              << std::endl;              return 0;              }           

In this file, we import MathFunctions.h and use the namespace mathfunctions to call the custom sqrt function. We all know that MathFunctions.h is in a subdirectory, but you can reference it directly as if it were in the root directory, how does that work? The answer lies in the revised main CMake configuration file:

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)              project(Tutorial)                  # Define configuration variables              set(PROJECT_VERSION_MAJOR 1)              set(PROJECT_VERSION_MINOR 0)              set(AUTHOR_NAME "Jith")                  # Configure the header file              configure_file(Config.h.in Config.h)                  add_subdirectory(MathFunctions)                  add_executable(tutorial tutorial.cxx)                      target_include_directories(tutorial PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")                  target_link_libraries(tutorial PUBLIC MathFunctions)           

Here are a few new commands:

● add_subdirectory Specify a subdirectory build, and CMake will take care of the build tasks in that subdirectory.

● target_include_directories 告诉 CMake MathFunctions 文件夹的路径,这样我们可以在 tutorial.cxx 中直接引用 MathFunctions.h。

● target_link_libraries 将 MathFunctions 库链接到主程序 tutorial 中。

When I built the project on Linux, I saw that the libMathFunctions.a file was generated in the build/MathFunctions directory, which is a statically linked library file that has become part of the main program.

Now, we can move the generated tutorial executable around as we like, and it will continue to function normally because libMathFunctions.a is already statically linked into the main program.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

What's next?

It's really interesting to learn how CMake works and how to use it to accomplish some basic tasks.

CMake solves most of the problems I'm having with C++ packaging right now. At the same time, it's interesting to explore Conan and vcpkg to simplify dependency management in C++. I should learn more about these tools and try them out in the future if I have the chance.

"5 Minute CMake How-To Fix My C++ Packaging Problem!"

Read on