CMake 官方教程 Step2

245 阅读7分钟

Tutorial Step 2: Adding a Library

在上一篇教程中我们了解了如何创建一个基本的 CMake 项目,现在,我们来了解一下如何在我们的项目中使用一个库,以及如何让我们选择是否使用我们自定的库。

Exercise 1 - Creating a Library

要在 CMake 中添加库,要使用 add_library() 命令并指定应由哪些源文件组成库。

建议使用一个或多个子目录来组织项目,而不是将所有源文件放在一个目录中。在本篇教程中,我们将专门为库创建一个子目录。在顶级 CMakeLists.txt 文件中,我们将使用 add_subdirectory() 命令来一起构建子文件夹。

一旦库被创建,它通过 target_include_directories() 命令和 target_link_libraries() 命令来连接到我们的可执行目标文件。

这个 Exercise 的目标是添加并使用一个库。

Relative Resources

add_library()

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [<source>...])

用指定的源文件为项目添加一个库。

  • STATIC 表示静态库(默认)。
  • SHARED 表示共享库。
  • MODULE 表示用于插件的模块库。
  • [<source>...] 是库的源文件列表。

add_subdirectory()

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])

add_subdirectory() 是 CMake 中用于向构建系统添加子目录的命令。通过使用这个命令,你可以将子目录中的 CMakeLists.txt 文件添加到主项目中,使得这些子目录中的内容可以被构建。

  • source_dir 指定源 CMakeLists.txt 和代码文件所在的目录。
  • binary_dir 是用于存放构建系统生成文件的目录,如果不提供,默认为 ${CMAKE_CURRENT_BINARY_DIR}/source_dir
  • EXCLUDE_FROM_ALL 可选参数,表示这个子目录的构建规则不会被默认构建。

target_link_libraries()

target_link_libraries() 是 CMake 中用于指定一个目标(例如可执行文件、静态库、共享库等)所依赖的库的命令。通过这个命令,你可以告诉 CMake 构建系统在链接目标时应该链接哪些库。

语法如下:

target_link_libraries(target_name
                      <PRIVATE|PUBLIC|INTERFACE> item1 item2 ...
                      ...)

其中:

  • target_name 是目标的名称,可以是可执行文件、静态库或共享库的名称。
  • <PRIVATE|PUBLIC|INTERFACE> 是用于指定链接属性的关键字。
    • PRIVATE:只对当前目标有效。
    • PUBLIC:对当前目标和依赖于当前目标的目标都有效。
    • INTERFACE:只对依赖于当前目标的目标有效。
  • item1 item2 ... 是要链接的库或目标的名称。

以下是一些示例:

# 添加一个库
add_library(my_library STATIC my_source.cpp)

# 添加一个可执行文件,并链接到 my_library
add_executable(my_executable main.cpp)
target_link_libraries(my_executable PRIVATE my_library)

# 添加另一个库,用于演示 PUBLIC 和 INTERFACE
add_library(another_library STATIC another_source.cpp)

# 添加一个目标,并链接到两个库
add_executable(another_executable another_main.cpp)
target_link_libraries(another_executable PUBLIC my_library INTERFACE another_library)

在这个例子中:

  • my_executable 只能访问 my_library 的链接信息,因为它是 PRIVATE 链接属性。
  • another_executable 可以访问 my_libraryanother_library 的链接信息,因为它是 PUBLIC 链接属性,而 another_libraryINTERFACE 链接属性。

Solution

需要完成 TODO 1TODO 6,在这个练习中,我们将向我们的项目添加一个库,其中包含我们自己的计算数字平方根的实现。然后,可执行文件可以使用这个库,而不是使用编译器提供的标准平方根函数。

对于本教程,我们将把这个库放在一个名为 MathFunctions 的子目录中。这个目录已经包含了头文件 MathFunctions.hmysqrt.h,以及它们各自的源文件 MathFunctions.cxxmysqrt.cxxmysqrt.cxx 有一个名为 mysqrt 的函数,它提供与编译器的 sqrt 函数类似的功能。MathFunctions.cxx 包含一个 sqrt 函数来调用 mysqrt 函数。

首先修改 MathFunctions/CMakeLists.txt,添加库:

# TODO 14: Remove mysqrt.cxx from the list of sources

# TODO 1: Add a library called MathFunctions with sources MathFunctions.cxx
# and mysqrt.cxx
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)
# TODO 7: Create a variable USE_MYMATH using option and set default to ON

# TODO 8: If USE_MYMATH is ON, use target_compile_definitions to pass
# USE_MYMATH as a precompiled definition to our source files

# TODO 12: When USE_MYMATH is ON, add a library for SqrtLibrary with
# source mysqrt.cxx

# TODO 13: When USE_MYMATH is ON, link SqrtLibrary to the MathFunctions Library

接着修改顶层的 CMakeLists.txt 文件,需要向构建系统添加子目录,指定可执行文件所依赖的库,再修改可执行文件的头文件搜索路径:

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

# TODO 2: Use add_subdirectory() to add MathFunctions to this project
add_subdirectory(MathFunctions)
# add the executable
add_executable(Tutorial tutorial.cxx)

# TODO 3: Use target_link_libraries to link the library to our executable
target_link_libraries(Tutorial PUBLIC MathFunctions)
# TODO 4: Add MathFunctions to Tutorial's target_include_directories()
# Hint: ${PROJECT_SOURCE_DIR} is a path to the project source. AKA This folder!

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           "${PROJECT_SOURCE_DIR}/MathFunctions"
                           )

最后再修改 tutorial.cxx,使用 mysqrt() 函数代替原来的:

#include <cmath>
#include <iostream>
#include <string>

// TODO 5: Include MathFunctions.h
#include "TutorialConfig.h"
#include "MathFunctions.h"
int main(int argc, char* argv[])
{
  if (argc < 2) {
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

  // convert input to double
  const double inputValue = std::stod(argv[1]);

  // TODO 6: Replace sqrt with mathfunctions::sqrt

  // calculate square root
  const double outputValue = mathfunctions::sqrt(inputValue);
  std::cout << "The square root of " << inputValue << " is " << outputValue
            << std::endl;
  return 0;
}

Build and Run

与之前的教程类似,由于是第一次构建,使用:

mkdir Step2_build
cd Step2_build
cmake ../Step2
cmake --build .

Exercise 2 - Adding an Option

现在,让我们在 Mathfunction 库中添加一个选项,以允许开发者选择是用我们自定义的库构建项目还是使用编译器内置的函数构建项目。虽然对于本教程来说,确实没有任何这样做的必要,但是对于大型项目来说,这种情况很常见。

CMake 可以使用 option() 命令执行此操作。这为用户提供了一个变量,他们可以在配置 cmake 构建时更改这个变量。这个设置会存储在缓存中,因此开发者在构建时不需要每次都设置该值。

这个 Exercise 的目标是添加一个 option 来使用内置函数构建项目。

Relative Resources

option()

option(<variable> "<help_text>" [value])

option() 是 CMake 中用于定义用户选项的命令。通过使用 option(),可以在 CMakeLists.txt 文件中为用户提供一些选项,这些选项可以影响项目的配置。

  • <variable> 是用于存储选项状态的变量名。
  • "<help_text>" 是一个帮助字符串,用于描述选项的作用。
  • [value] 是一个可选的初始值,可以是 ONOFF,如果没有提供则默认为 OFF
# 定义一个选项,用于决定是否编译示例程序
option(BUILD_EXAMPLES "Build examples" ON)

# 如果 BUILD_EXAMPLES 为 ON,则添加一个示例可执行文件
if (BUILD_EXAMPLES)
    add_executable(example main.cpp)
endif()

target_compile_definitions()

target_compile_definitions(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

target_compile_definitions() 是 CMake 中用于为目标(例如可执行文件、静态库或共享库)设置预处理器定义的命令。通过这个命令,你可以向源代码中添加预定义的宏,以影响代码的编译过程。

  • <target> 是目标的名称,可以是可执行文件、静态库或共享库的名称。
  • PUBLIC|PRIVATE|INTERFACE 是用于指定定义的范围的关键字。
    • PUBLIC:定义对目标和依赖于目标的目标都可见。
    • PRIVATE:定义仅对当前目标可见。
    • INTERFACE:定义对依赖于目标的目标可见。
  • [items1...] 是预处理器定义的列表。
example

这个命令有点难懂,下面解释一下:

预处理器定义(Preprocessor Definitions)是源代码中使用预处理器指令 #define 创建的符号或宏。这些定义在编译过程中被预处理器替换为指定的文本。在 CMake 中,通过 target_compile_definitions() 命令,你可以在编译时向目标添加预处理器定义。

比如下面这个 C++ 文件:

#include <iostream>

#ifdef DEBUG_MODE
#define LOG(message) std::cout << "[DEBUG] " << message << std::endl
#else
#define LOG(message) std::cout << message << std::endl
#endif

int main() {
    LOG("Hello, World!");
    return 0;
}

在这个例子中,通过宏 DEBUG_MODE 的定义与否,可以控制是否启用调试模式下的日志输出。你可以使用 CMake 在构建时添加预处理器定义,例如:

add_executable(my_executable example.cpp)

# 添加一个预处理器定义 DEBUG_MODE
target_compile_definitions(my_executable PRIVATE DEBUG_MODE)

在该文件中,target_compile_definitions()DEBUG_MODE 定义添加到了 my_executable 目标的编译选项中,使得在构建 my_executable 时,预处理器会将 DEBUG_MODE 视为已定义,从而选择对应的代码。

Solution

需要完成 TODO 7 到 TODO 14。首先修改 MathFunctions/CMakeLists.txt,设置变量 USE_MYMATH 以及选择是否在编译时添加预处理器定义。当 USE_MYMATHON 时,将设置预处理器定义 USE_MYMATH 。然后,我们可以使用这个预处理器定义来启用或禁用源代码的某些部分。

# TODO 1: Add a library called MathFunctions with sources MathFunctions.cxx
# and mysqrt.cxx
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)
# TODO 7: Create a variable USE_MYMATH using option and set default to ON
option(USE_MYMATH "Use tutorial provided math implementation" ON)
# TODO 8: If USE_MYMATH is ON, use target_compile_definitions to pass
# USE_MYMATH as a precompiled definition to our source files
if (USE_MYMATH)
    target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

接着在 MathFunctions.cxx 中设置不同预处理器定义的结果:

#include "MathFunctions.h"

// TODO 11: include cmath
#include <cmath>
// TODO 10: Wrap the mysqrt include in a precompiled ifdef based on USE_MYMATH
#ifdef USE_MYMATH
#include "mysqrt.h"
#endif
namespace mathfunctions {
double sqrt(double x)
{
  // TODO 9: If USE_MYMATH is defined, use detail::mysqrt.
  // Otherwise, use std::sqrt.
  #ifdef USE_MYMATH
      return detail::mysqrt(x);
  #else
      return std::sqrt(x);
  #endif
}
}

此时,如果 USE_MYMATHOFF,将不会使用 mysqrt.cxx,但它仍然会被编译,因为 Mathfunction 目标的源码列表中列出了 mysqrt.cxx

有几种方法可以解决这个问题。第一个选项是使用 target_sources() 从 USE_MYMATH 块中添加 mysqrt.cxx。另一种选择是在 USE_MYMATH 块中创建一个额外的库,它负责编译 mysqrt.cxx。在本教程中,我们将创建一个附加库。

修改 MathFunctions/CMakeLists.txt

# TODO 14: Remove mysqrt.cxx from the list of sources

# TODO 1: Add a library called MathFunctions with sources MathFunctions.cxx
# and mysqrt.cxx
add_library(MathFunctions MathFunctions.cxx)
# TODO 7: Create a variable USE_MYMATH using option and set default to ON
option(USE_MYMATH "Use tutorial provided math implementation" ON)
# TODO 8: If USE_MYMATH is ON, use target_compile_definitions to pass
# USE_MYMATH as a precompiled definition to our source files
if (USE_MYMATH)
    target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
    add_library(SqrtLibrary STATIC mysqrt.cxx)
    target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# TODO 12: When USE_MYMATH is ON, add a library for SqrtLibrary with
# source mysqrt.cxx

# TODO 13: When USE_MYMATH is ON, link SqrtLibrary to the MathFunctions Library

Build and Run

默认状态下 USE_MYMATHON,直接编译只需要使用:

cd ../Step2_build
cmake --build .

这时候使用的是 mysqrt() 函数。如果要改变 USE_MYMATH 变量的值,需要重新创建构建环境并修改 option,在 CMake 中,使用 -D 选项来定义一个 CMake 变量:

cmake ../Step2 -DUSE_MYMATH=OFF

然后再次编译:

cmake --build .

这时在运行就是使用编译器内置的 sqrt() 函数了。