4. CMake跨平台实战:Windows/Linux/macOS下的CMake配置差异、编译器检测、平台特定代码

说实话,CMake 这东西,刚接触时觉得它就是个“编译脚本生成器”。但用久了你会发现,它其实是跨平台开发的“粘合剂”。今天我们就来聊聊,怎么用 CMake 把 Windows、Linux、macOS 这三个平台伺候舒服了。

4.1 为什么需要跨平台 CMake 配置?

我刚开始做跨平台项目时,犯过一个低级错误——在 Windows 上写死了路径分隔符 \\,结果 Linux 同事一编译就报错。后来我学乖了:平台差异不是 bug,是特性。你想想看,每个平台都有自己的 ABI、动态库后缀、默认编译器,CMake 就是用来统一管理这些差异的。

核心原则:CMake 脚本应该像“交通规则”——告诉编译器怎么走,而不是替它开车。

4.2 编译器检测与平台变量

CMake 内置了一堆变量,帮你识别当前在哪个平台上干活。我个人习惯用 CMAKE_SYSTEM_NAME 来做分支判断,它比 WIN32、APPLE 这些更直观。

# 检测操作系统
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "当前是 Windows 平台")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "当前是 Linux 平台")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "当前是 macOS 平台")
endif()

# 检测编译器
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    message(STATUS "编译器:Visual Studio")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    message(STATUS "编译器:GCC")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    message(STATUS "编译器:Clang")
endif()

小技巧:在 macOS 上,CMAKE_CXX_COMPILER_ID 可能是 AppleClang 而不是 Clang。记得区分一下,否则可能踩坑。

4.3 平台特定代码的三种处理方式

我在项目中遇到过最头疼的事:同一段代码,Windows 上跑得好好的,Linux 上就崩。后来我总结出三种处理平台特定代码的方式,按推荐程度排序:

4.3.1 方式一:CMake 变量控制(推荐)

把平台差异抽象成 CMake 变量,C++ 代码里只引用这些变量。这样代码干净,也容易维护。

# CMakeLists.txt
if(WIN32)
    set(PLATFORM_LIBS ws2_32 winmm)
    add_definitions(-DPLATFORM_WINDOWS)
elseif(APPLE)
    set(PLATFORM_LIBS "-framework CoreFoundation -framework Cocoa")
    add_definitions(-DPLATFORM_MACOS)
else()
    set(PLATFORM_LIBS pthread dl)
    add_definitions(-DPLATFORM_LINUX)
endif()

target_link_libraries(myapp ${PLATFORM_LIBS})
// main.cpp
#ifdef PLATFORM_WINDOWS
    #include <windows.h>
    void sleep_ms(int ms) { Sleep(ms); }
#elif PLATFORM_MACOS
    #include <unistd.h>
    void sleep_ms(int ms) { usleep(ms * 1000); }
#else
    #include <unistd.h>
    void sleep_ms(int ms) { usleep(ms * 1000); }
#endif

4.3.2 方式二:单独文件编译(适合大块差异)

如果某个平台的代码特别多,比如 Windows 上的注册表操作,Linux 上的 udev 设备枚举,那就拆成独立文件,用 CMake 条件编译。

# CMakeLists.txt
if(WIN32)
    set(PLATFORM_SOURCES
        platform_win.cpp
        platform_win.h
    )
elseif(APPLE)
    set(PLATFORM_SOURCES
        platform_mac.mm
        platform_mac.h
    )
else()
    set(PLATFORM_SOURCES
        platform_linux.cpp
        platform_linux.h
    )
endif()

target_sources(myapp PRIVATE ${PLATFORM_SOURCES})

注意:macOS 上如果用到 Objective-C,文件后缀必须是 .mm,否则编译器不认。我曾经因为这个折腾了一下午。

4.3.3 方式三:预处理器宏(不推荐大范围使用)

偶尔用用还行,但别满屏都是 #ifdef。代码可读性会变得很差,维护起来像在考古。

#if defined(_WIN32) || defined(_WIN64)
    #define PATH_SEPARATOR "\\"
#else
    #define PATH_SEPARATOR "/"
#endif

4.4 平台差异实战:动态库与静态库

嗯,这里要注意:Windows 上的动态库需要 __declspec(dllexport) 和 __declspec(dllimport),而 Linux/macOS 只需要 __attribute__((visibility("default")))。我建议用 CMake 统一处理:

# CMakeLists.txt
if(WIN32)
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()

add_library(mylib SHARED
    mylib.cpp
    mylib.h
)

# 或者手动控制导出
target_compile_definitions(mylib PRIVATE
    $<$<BOOL:${WIN32}>:MYLIB_EXPORTS>
)
// mylib.h
#ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
#else
    #define MYLIB_API __declspec(dllimport)
#endif

#ifdef __GNUC__
    #define MYLIB_API __attribute__((visibility("default")))
#endif

MYLIB_API void hello();

4.5 知识体系结构图

下面这张图,是我自己总结的 CMake 跨平台配置的核心逻辑。你看一眼就能明白整个流程:

CMake 跨平台配置核心逻辑 CMakeLists.txt CMAKE_SYSTEM_NAME 检测 Windows Linux macOS 编译器:MSVC 动态库:.dll 编译器:GCC 动态库:.so 编译器:Clang 动态库:.dylib 跨平台可执行文件 / 库

4.6 避坑指南:我踩过的三个坑

  • 路径分隔符:我曾经在 Windows 上用 / 写路径,结果某些老旧库不认。后来统一用 CMake 的 file(TO_CMAKE_PATH) 转换,再也没出过问题。
  • 大小写敏感:Linux 文件系统大小写敏感,Windows 不敏感。我有个同事把头文件命名为 MyHeader.H,代码里写 #include "myheader.h",Windows 上编译通过,Linux 上直接报错。
  • 动态库运行时路径:Windows 上动态库放 exe 同目录就行,Linux 上要设置 LD_LIBRARY_PATH,macOS 上可以用 @rpath。我建议在 CMake 里用 target_link_options 统一设置 rpath,省得用户手动配环境变量。

我的习惯:每次新建跨平台项目,我都会先在三个平台上各编译一次,确保 CMake 脚本没问题。别等到项目做大了再回头修,那代价太大了。

4.7 总结

CMake 跨平台配置,说白了就是“检测差异、抽象差异、隔离差异”。你只要记住三点:用 CMAKE_SYSTEM_NAME 做分支,用 CMake 变量控制平台特定代码,用条件编译处理小差异。做到这三点,你的项目就能在 Windows、Linux、macOS 上丝滑运行。

可可精品资料库