第1章 跨平台编译:平台检测、编译器检测、系统特性检测

跨平台编译,说白了就是让同一份代码能在Windows、Linux、macOS甚至嵌入式系统上都能编译通过。我刚开始接触CMake时,觉得这玩意儿不就是个构建工具嘛,后来被各种平台差异折磨过之后,才真正体会到它的价值。

嗯,这一章我们重点聊三个核心问题:怎么知道当前是什么平台?用什么编译器?系统支持哪些特性?这三个问题搞定了,跨平台编译就成功了一半。

跨平台编译核心知识体系 平台检测 WIN32 / UNIX / APPLE 编译器检测 MSVC / GCC / Clang 特性检测 CheckIncludeFile / CheckSymbolExists CMAKE_SYSTEM_NAME WIN32 / CMAKE_SYSTEM_VERSION CMAKE_CXX_COMPILER_ID MSVC / GNU / Clang / AppleClang CheckIncludeFile CheckSymbolExists / CheckTypeSize 目标:一套CMakeLists.txt,处处编译

1.1 平台检测——先搞清楚你在哪

CMake里做平台检测,最常用的就是看 CMAKE_SYSTEM_NAME 这个变量。它会告诉你当前是Windows、Linux、Darwin(macOS)还是其他系统。

核心变量: CMAKE_SYSTEM_NAME 的值包括:

  • Windows — 所有Windows版本
  • Linux — 各种Linux发行版
  • Darwin — macOS / iOS
  • Android — Android系统
  • Emscripten — WebAssembly编译环境

我个人习惯这样用:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "当前是Windows平台")
    # 设置Windows特有的编译选项
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "当前是Linux平台")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "当前是macOS平台")
endif()

除了 CMAKE_SYSTEM_NAME,CMake还预定义了一些快捷变量:

变量名 说明 典型值
WIN32 Windows平台时为真 TRUE / FALSE
UNIX Unix-like系统时为真(含Linux、macOS) TRUE / FALSE
APPLE Apple平台时为真(macOS、iOS等) TRUE / FALSE
MINGW MinGW编译环境时为真 TRUE / FALSE
CYGWIN Cygwin环境时为真 TRUE / FALSE

小技巧: 我建议优先用 CMAKE_SYSTEM_NAME 做精确判断,而不是 WIN32UNIX。为什么?因为 WIN32 在64位Windows上也是真,容易让人误解。而且 CMAKE_SYSTEM_NAME 能区分Linux和macOS,这在处理系统API差异时特别有用。

1.2 编译器检测——知道是谁在干活

平台搞清楚了,接下来得知道用什么编译器。CMake通过 CMAKE_CXX_COMPILER_ID 来识别编译器类型。

我在项目中遇到过这样的情况:同样的代码,用MSVC编译没问题,换到GCC就报一堆警告。所以编译器检测是必须的。

if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    # Visual Studio编译器
    add_compile_options(/W4 /utf-8)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    # GCC
    add_compile_options(-Wall -Wextra -std=c++17)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    # Clang(macOS默认)
    add_compile_options(-Wall -Wextra -stdlib=libc++)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
    # macOS自带的Clang(注意和普通Clang有区别)
    add_compile_options(-Wall -Wextra)
endif()

注意: macOS上的Clang和普通Clang的 COMPILER_ID 不一样。macOS自带的是 AppleClang,而通过Homebrew安装的是 Clang。我曾经因为这个差异,在macOS上折腾了半天才发现是编译器版本问题。

除了 CMAKE_CXX_COMPILER_ID,还有几个相关变量:

  • CMAKE_CXX_COMPILER_VERSION — 编译器版本号,比如 "12.0.5"
  • CMAKE_CXX_COMPILER — 编译器的完整路径
  • CMAKE_CXX_COMPILER_FRONTEND_VARIANT — 编译器前端变体(比如MSVC的clang-cl)

你想想看,如果不知道编译器版本,有些新特性就不敢用。比如C++20的某些特性,GCC 10才支持,GCC 9就不行。这时候版本检测就派上用场了:

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "10.0")
    add_compile_definitions(HAS_CXX20_FEATURES)
endif()

1.3 系统特性检测——别想当然

平台和编译器都知道了,但还不够。有些系统特性不是所有平台都有的。比如某些POSIX函数在Windows上就不存在,或者某些头文件在Linux上有但在macOS上路径不同。

CMake提供了 CheckIncludeFileCheckSymbolExistsCheckTypeSize 等模块来做这件事。

1.3.1 检查头文件是否存在

include(CheckIncludeFile)

check_include_file("pthread.h" HAVE_PTHREAD_H)
check_include_file("windows.h" HAVE_WINDOWS_H)
check_include_file("dlfcn.h" HAVE_DLFCN_H)

if(HAVE_PTHREAD_H)
    # 有pthread,可以用多线程
    find_package(Threads REQUIRED)
endif()

1.3.2 检查符号是否存在

include(CheckSymbolExists)

check_symbol_exists("clock_gettime" "time.h" HAVE_CLOCK_GETTIME)
check_symbol_exists("gettimeofday" "sys/time.h" HAVE_GETTIMEOFDAY)

if(NOT HAVE_CLOCK_GETTIME AND HAVE_GETTIMEOFDAY)
    # 没有clock_gettime就用gettimeofday替代
    add_compile_definitions(USE_GETTIMEOFDAY)
endif()

核心原则: 不要假设某个函数或头文件一定存在。用CMake的检测机制去确认,然后把结果通过 add_compile_definitions 传给源码。这样源码里就可以用 #ifdef 来做条件编译。

1.3.3 检查类型大小

include(CheckTypeSize)

check_type_size("long" SIZEOF_LONG)
check_type_size("long long" SIZEOF_LONG_LONG)
check_type_size("void*" SIZEOF_POINTER)

message(STATUS "long的大小: ${SIZEOF_LONG} 字节")
message(STATUS "指针的大小: ${SIZEOF_POINTER} 字节")

嗯,这里要注意:check_type_size 的结果会缓存到CMakeCache中,所以第一次运行后,后续再跑就不会重复检测了。这个设计挺好的,能加快配置速度。

1.4 综合实战——一个跨平台的配置示例

把上面三个部分串起来,写一个完整的跨平台CMakeLists.txt片段:

cmake_minimum_required(VERSION 3.16)
project(CrossPlatformDemo CXX)

# 1. 平台检测
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    add_compile_definitions(PLATFORM_WINDOWS)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    add_compile_definitions(PLATFORM_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    add_compile_definitions(PLATFORM_MACOS)
endif()

# 2. 编译器检测
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    add_compile_options(/W4 /utf-8)
else()
    add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 3. 特性检测
include(CheckIncludeFile)
include(CheckSymbolExists)

check_include_file("pthread.h" HAVE_PTHREAD_H)
check_include_file("dlfcn.h" HAVE_DLFCN_H)
check_symbol_exists("clock_gettime" "time.h" HAVE_CLOCK_GETTIME)

# 把检测结果传给源码
configure_file(config.h.in config.h)

# 添加可执行文件
add_executable(myapp main.cpp)

对应的 config.h.in 模板文件:

#ifndef CONFIG_H
#define CONFIG_H

#cmakedefine PLATFORM_WINDOWS
#cmakedefine PLATFORM_LINUX
#cmakedefine PLATFORM_MACOS

#cmakedefine HAVE_PTHREAD_H
#cmakedefine HAVE_DLFCN_H
#cmakedefine HAVE_CLOCK_GETTIME

#endif // CONFIG_H

个人经验: 我建议把平台检测、编译器检测、特性检测分开写,不要混在一起。这样后期维护时,想改某个部分一眼就能找到。我曾经接手过一个项目,所有检测都写在一个巨大的if-else里,改起来简直想骂人。

1.5 避坑指南

最后分享几个我踩过的坑:

  • 不要用 CMAKE_SYSTEM 做平台判断。 这个变量是 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_VERSION 的组合,格式不固定,容易出错。
  • 交叉编译时注意 CMAKE_SYSTEM_NAME 的值。 如果你在Linux上交叉编译到ARM,CMAKE_SYSTEM_NAME 是目标平台的名称,不是宿主机的。
  • 特性检测的结果要缓存。check_* 系列函数时,CMake会自动缓存结果。但如果你手动写检测逻辑,记得用 set(... CACHE BOOL "...") 来缓存。
  • AppleClang 和 Clang 不是一回事。 我之前在macOS上用 if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 死活不生效,后来才发现是 AppleClang

跨平台编译说白了就是「先问清楚,再动手」。平台、编译器、特性,这三件事搞清楚了,你的CMakeLists.txt就能在多个平台上跑得稳稳当当。


公众号:蓝海资料掘金营,微信deep3321