返回介绍

5.9 使用生成器表达式微调配置和编译

发布于 2025-05-06 21:45:56 字数 5431 浏览 0 评论 0 收藏

NOTE : 此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-5/recipe-09 中找到,其中包含一个 C++例子。该示例在 CMake 3.9 版(或更高版本) 中是有效的,并且已经在 GNU/Linux、macOS 和 Windows 上进行过测试。

CMake 提供了一种特定于领域的语言,来描述如何配置和构建项目。自然会引入描述特定条件的变量,并在 CMakeLists.txt 中包含基于此的条件语句。

本示例中,我们将重新讨论生成器表达式。第 4 章中,以简洁地引用显式的测试可执行路径,使用了这些表达式。生成器表达式为逻辑和信息表达式,提供了一个强大而紧凑的模式,这些表达式在生成构建系统时进行评估,并生成特定于每个构建配置的信息。换句话说,生成器表达式用于引用仅在生成时已知,但在配置时未知或难于知晓的信息;对于文件名、文件位置和库文件后缀尤其如此。

本例中,我们将使用生成器表达式,有条件地设置预处理器定义,并有条件地链接到消息传递接口库(Message Passing Interface, MPI),并允许我们串行或使用 MPI 构建相同的源代码。

NOTE : 本例中,我们将使用一个导入的目标来链接到 MPI,该目标仅从 CMake 3.9 开始可用。但是,生成器表达式可以移植到 CMake 3.0 或更高版本。

准备工作

我们将编译以下示例源代码( example.cpp ):

#include <iostream>
​
#ifdef HAVE_MPI
#include <mpi.h>
#endif
int main()
{
#ifdef HAVE_MPI
  // initialize MPI
  MPI_Init(NULL, NULL);
​
  // query and print the rank
  int rank;
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  std::cout << "hello from rank " << rank << std::endl;
​
  // initialize MPI
  MPI_Finalize();
#else
  std::cout << "hello from a sequential binary" << std::endl;
#endif /* HAVE_MPI */
}

代码包含预处理语句( #ifdef HAVE_MPI ... #else ... #endif ),这样我们就可以用相同的源代码编译一个顺序的或并行的可执行文件了。

具体实施

编写 CMakeLists.txt 文件时,我们将重用第 3 章第 6 节的一些构建块:

  1. 声明一个 C++11 项目:
    cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
    project(recipe-09 LANGUAGES CXX)
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. 然后,我们引入一个选项 USE_MPI 来选择 MPI 并行化,并将其设置为默认值 ON 。如果为 ON ,我们使用 find_package 来定位 MPI 环境:
    option(USE_MPI "Use MPI parallelization" ON)
    if(USE_MPI)
        find_package(MPI REQUIRED)
    endif()
  3. 然后定义可执行目标,并有条件地设置相应的库依赖项( MPI::MPI_CXX ) 和预处理器定义( HAVE_MPI ),稍后将对此进行解释:
    add_executable(example example.cpp)
    target_link_libraries(example
      PUBLIC
          $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
      )
    target_compile_definitions(example
      PRIVATE
          $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
      )
  4. 如果找到 MPI,还将打印由 FindMPI.cmake 导出的 INTERFACE_LINK_LIBRARIES ,为了方便演示,使用了 cmake_print_properties() 函数:
    if(MPI_FOUND)
      include(CMakePrintHelpers)
      cmake_print_properties(
        TARGETS MPI::MPI_CXX
        PROPERTIES INTERFACE_LINK_LIBRARIES
        )
    endif()
  5. 首先使用默认 MPI 配置。观察 cmake_print_properties() 的输出:
    $ mkdir -p build_mpi
    $ cd build_mpi
    $ cmake ..
    ​
    -- ...
    --
    Properties for TARGET MPI::MPI_CXX:
    MPI::MPI_CXX.INTERFACE_LINK_LIBRARIES = "-Wl,-rpath -Wl,/usr/lib/openmpi -Wl,--enable-new-dtags -pthread;/usr/lib/openmpi/libmpi_cxx.so;/usr/lib/openmpi/libmpi.so"
  6. 编译并运行并行例子:
    $ cmake --build .
    $ mpirun -np 2 ./example
    ​
    hello from rank 0
    hello from rank 1
  7. 现在,创建一个新的构建目录,这次构建串行版本:
    $ mkdir -p build_seq
    $ cd build_seq
    $ cmake -D USE_MPI=OFF ..
    $ cmake --build .
    $ ./example
    ​
    hello from a sequential binary

工作原理

CMake 分两个阶段生成项目的构建系统:配置阶段(解析 CMakeLists.txt ) 和生成阶段(实际生成构建环境)。生成器表达式在第二阶段进行计算,可以使用仅在生成时才能知道的信息来调整构建系统。生成器表达式在交叉编译时特别有用,一些可用的信息只有解析 CMakeLists.txt 之后,或在多配置项目后获取,构建系统生成的所有项目可以有不同的配置,比如 Debug 和 Release。

本例中,将使用生成器表达式有条件地设置链接依赖项并编译定义。为此,可以关注这两个表达式:

target_link_libraries(example
  PUBLIC
      $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
  )
target_compile_definitions(example
  PRIVATE
      $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
  )

如果 MPI_FOUND 为真,那么 $<BOOL:${MPI_FOUND}> 的值将为 1。本例中, $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX> 将计算 MPI::MPI_CXX ,第二个生成器表达式将计算结果存在 HAVE_MPI 。如果将 USE_MPI 设置为 OFF ,则 MPI_FOUND 为假,两个生成器表达式的值都为空字符串,因此不会引入链接依赖关系,也不会设置预处理定义。

我们可以通过 if 来达到同样的效果:

if(MPI_FOUND)
  target_link_libraries(example
    PUBLIC
        MPI::MPI_CXX
    )
​
  target_compile_definitions(example
    PRIVATE
        HAVE_MPI
    )
endif()

这个解决方案不太优雅,但可读性更好。我们可以使用生成器表达式来重新表达 if 语句,而这个选择取决于个人喜好。但当我们需要访问或操作文件路径时,生成器表达式尤其出色,因为使用变量和 if 构造这些路径可能比较困难。本例中,我们更注重生成器表达式的可读性。第 4 章中,我们使用生成器表达式来解析特定目标的文件路径。第 11 章中,我们会再次来讨论生成器。

更多信息

CMake 提供了三种类型的生成器表达式:

  • 逻辑表达式 ,基本模式为 $<condition:outcome> 。基本条件为 0 表示 false, 1 表示 true,但是只要使用了正确的关键字,任何布尔值都可以作为条件变量。
  • 信息表达式 ,基本模式为 $<information>$<information:input> 。这些表达式对一些构建系统信息求值,例如:包含目录、目标属性等等。这些表达式的输入参数可能是目标的名称,比如表达式 $<TARGET_PROPERTY:tgt,prop> ,将获得的信息是 tgt 目标上的 prop 属性。
  • 输出表达式 ,基本模式为 $<operation>$<operation:input> 。这些表达式可能基于一些输入参数,生成一个输出。它们的输出可以直接在 CMake 命令中使用,也可以与其他生成器表达式组合使用。例如,

    ` -

    I$, -I> 将生成一个字符串,其中包含正在处理的目标的包含目录,每个目录的前缀由 -I`表示。

有关生成器表达式的完整列表,请参考 https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。