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

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):

  1. #include <iostream>
  2. #ifdef HAVE_MPI
  3. #include <mpi.h>
  4. #endif
  5. int main()
  6. {
  7. #ifdef HAVE_MPI
  8. // initialize MPI
  9. MPI_Init(NULL, NULL);
  10. // query and print the rank
  11. int rank;
  12. MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  13. std::cout << "hello from rank " << rank << std::endl;
  14. // initialize MPI
  15. MPI_Finalize();
  16. #else
  17. std::cout << "hello from a sequential binary" << std::endl;
  18. #endif /* HAVE_MPI */
  19. }

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

具体实施

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

  1. 声明一个C++11项目:

    1. cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
    2. project(recipe-09 LANGUAGES CXX)
    3. set(CMAKE_CXX_STANDARD 11)
    4. set(CMAKE_CXX_EXTENSIONS OFF)
    5. set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. 然后,我们引入一个选项USE_MPI来选择MPI并行化,并将其设置为默认值ON。如果为ON,我们使用find_package来定位MPI环境:

    1. option(USE_MPI "Use MPI parallelization" ON)
    2. if(USE_MPI)
    3. find_package(MPI REQUIRED)
    4. endif()
  3. 然后定义可执行目标,并有条件地设置相应的库依赖项(MPI::MPI_CXX)和预处理器定义(HAVE_MPI),稍后将对此进行解释:

    1. add_executable(example example.cpp)
    2. target_link_libraries(example
    3. PUBLIC
    4. $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
    5. )
    6. target_compile_definitions(example
    7. PRIVATE
    8. $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
    9. )
  4. 如果找到MPI,还将打印由FindMPI.cmake导出的INTERFACE_LINK_LIBRARIES,为了方便演示,使用了cmake_print_properties()函数:

    1. if(MPI_FOUND)
    2. include(CMakePrintHelpers)
    3. cmake_print_properties(
    4. TARGETS MPI::MPI_CXX
    5. PROPERTIES INTERFACE_LINK_LIBRARIES
    6. )
    7. endif()
  5. 首先使用默认MPI配置。观察cmake_print_properties()的输出:

    1. $ mkdir -p build_mpi
    2. $ cd build_mpi
    3. $ cmake ..
    4. -- ...
    5. --
    6. Properties for TARGET MPI::MPI_CXX:
    7. 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. 编译并运行并行例子:

    1. $ cmake --build .
    2. $ mpirun -np 2 ./example
    3. hello from rank 0
    4. hello from rank 1
  7. 现在,创建一个新的构建目录,这次构建串行版本:

    1. $ mkdir -p build_seq
    2. $ cd build_seq
    3. $ cmake -D USE_MPI=OFF ..
    4. $ cmake --build .
    5. $ ./example
    6. hello from a sequential binary

工作原理

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

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

  1. target_link_libraries(example
  2. PUBLIC
  3. $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
  4. )
  5. target_compile_definitions(example
  6. PRIVATE
  7. $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
  8. )

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

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

  1. if(MPI_FOUND)
  2. target_link_libraries(example
  3. PUBLIC
  4. MPI::MPI_CXX
  5. )
  6. target_compile_definitions(example
  7. PRIVATE
  8. HAVE_MPI
  9. )
  10. 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$<JOIN:$<TARGET_PROPERTY:INCLUDE_DIRECTORIES>, -I>将生成一个字符串,其中包含正在处理的目标的包含目录,每个目录的前缀由-I表示。

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