第1章 跨平台编译:平台检测、编译器检测、系统特性检测
跨平台编译,说白了就是让同一份代码能在Windows、Linux、macOS甚至嵌入式系统上都能编译通过。我刚开始接触CMake时,觉得这玩意儿不就是个构建工具嘛,后来被各种平台差异折磨过之后,才真正体会到它的价值。
嗯,这一章我们重点聊三个核心问题:怎么知道当前是什么平台?用什么编译器?系统支持哪些特性?这三个问题搞定了,跨平台编译就成功了一半。
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 做精确判断,而不是 WIN32 或 UNIX。为什么?因为 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提供了 CheckIncludeFile、CheckSymbolExists、CheckTypeSize 等模块来做这件事。
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_NAME和CMAKE_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