2.2 处理与平台相关的源代码

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

理想情况下,应该避免依赖于平台的源代码,但是有时我们没有选择,特别是当要求配置和编译不是自己编写的代码时。本示例中,将演示如何使用CMake根据操作系统编译源代码。

准备工作

修改hello-world.cpp示例代码,将第1章第1节的例子进行修改:

  1. #include <cstdlib>
  2. #include <iostream>
  3. #include <string>
  4. std::string say_hello() {
  5. #ifdef IS_WINDOWS
  6. return std::string("Hello from Windows!");
  7. #elif IS_LINUX
  8. return std::string("Hello from Linux!");
  9. #elif IS_MACOS
  10. return std::string("Hello from macOS!");
  11. #else
  12. return std::string("Hello from an unknown system!");
  13. #endif
  14. }
  15. int main() {
  16. std::cout << say_hello() << std::endl;
  17. return EXIT_SUCCESS;
  18. }

具体实施

完成一个CMakeLists.txt实例,使我们能够基于目标操作系统有条件地编译源代码:

  1. 首先,设置了CMake最低版本、项目名称和支持的语言:

    1. cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
    2. project(recipe-02 LANGUAGES CXX)
  2. 然后,定义可执行文件及其对应的源文件:

    1. add_executable(hello-world hello-world.cpp)
  3. 通过定义以下目标编译定义,让预处理器知道系统名称:

    1. if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    2. target_compile_definitions(hello-world PUBLIC "IS_LINUX")
    3. endif()
    4. if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    5. target_compile_definitions(hello-world PUBLIC "IS_MACOS")
    6. endif()
    7. if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    8. target_compile_definitions(hello-world PUBLIC "IS_WINDOWS")
    9. endif()

    继续之前,先检查前面的表达式,并考虑在不同系统上有哪些行为。

  4. 现在,准备测试它,并配置项目:

    1. $ mkdir -p build
    2. $ cd build
    3. $ cmake ..
    4. $ cmake --build .
    5. $ ./hello-world
    6. Hello from Linux!

Windows系统上,将看到来自Windows的Hello。其他操作系统将产生不同的输出。

工作原理

hello-world.cpp示例中,有趣的部分是基于预处理器定义IS_WINDOWSIS_LINUXIS_MACOS的条件编译:

  1. std::string say_hello() {
  2. #ifdef IS_WINDOWS
  3. return std::string("Hello from Windows!");
  4. #elif IS_LINUX
  5. return std::string("Hello from Linux!");
  6. #elif IS_MACOS
  7. return std::string("Hello from macOS!");
  8. #else
  9. return std::string("Hello from an unknown system!");
  10. #endif
  11. }

这些定义在CMakeLists.txt中配置时定义,通过使用target_compile_definition在预处理阶段使用。可以不重复if-endif语句,以更紧凑的表达式实现,我们将在下一个示例中演示这种重构方式。也可以把if-endif语句加入到一个if-else-else-endif语句中。这个阶段,可以使用add_definitions(-DIS_LINUX)来设置定义(当然,可以根据平台调整定义),而不是使用target_compile_definition。使用add_definitions的缺点是,会修改编译整个项目的定义,而target_compile_definitions给我们机会,将定义限制于一个特定的目标,以及通过PRIVATE|PUBLIC|INTERFACE限定符,限制这些定义可见性。第1章的第8节,对这些限定符有详细的说明:

  • PRIVATE,编译定义将只应用于给定的目标,而不应用于相关的其他目标。
  • INTERFACE,对给定目标的编译定义将只应用于使用它的目标。
  • PUBLIC,编译定义将应用于给定的目标和使用它的所有其他目标。

NOTE:将项目中的源代码与平台相关性最小化,可使移植更加容易。