第43例:特性检测——用CheckIncludeFile、CheckSymbolExists摸清系统底细
写跨平台代码,最头疼的是什么?
不是业务逻辑,是系统差异。
你在Linux上用的dlopen,到了Windows就变成LoadLibrary。你写的#include <unistd.h>,在MSVC下直接编译失败。这种问题,我早年做嵌入式Linux移植时踩过无数次坑。后来学乖了——与其硬编码假设,不如让CMake在配置阶段就把系统特性摸清楚。
今天要讲的,就是CMake里做特性检测的几把利器:CheckIncludeFile、CheckSymbolExists、CheckFunctionExists 等模块。说白了,就是让构建系统自动探测目标平台支持什么、不支持什么,然后通过宏定义或条件编译来适配。
为什么需要特性检测?
你想想看,一个C/C++项目要跑在Linux、macOS、Windows、甚至各种RTOS上。每个系统的头文件位置不同,API集合不同,甚至同一个函数的行为都有差异。
我见过最粗暴的做法:在代码里写一堆#ifdef _WIN32、#ifdef __linux__。这当然能工作,但维护起来就是噩梦。每加一个新平台,就要改一堆地方。
更好的做法是:让CMake去检测,然后把结果通过target_compile_definitions传给编译器。代码里只需要检查#ifdef HAVE_STDIO_H这种统一的宏即可。平台差异被隔离在CMakeLists.txt里,代码本身保持干净。
核心模块一览
CMake提供了几个官方模块,专门干这个事。我列个表,你一看就明白:
| 模块名 | 检测内容 | 典型用法 |
|---|---|---|
| CheckIncludeFile | 某个头文件是否存在 | check_include_file("unistd.h" HAVE_UNISTD_H) |
| CheckIncludeFiles | 多个头文件是否同时存在 | check_include_files("stdlib.h;stdint.h" HAVE_STDLIB_STDINT) |
| CheckSymbolExists | 某个符号(函数/变量/宏)是否存在 | check_symbol_exists("printf" "stdio.h" HAVE_PRINTF) |
| CheckFunctionExists | 某个函数是否可链接 | check_function_exists("dlopen" HAVE_DLOPEN) |
| CheckStructHasMember | 结构体是否有某个成员 | check_struct_has_member("struct stat" "st_mtim" "sys/stat.h" HAVE_STAT_ST_MTIM) |
| CheckTypeSize | 类型的大小(字节数) | check_type_size("int" SIZEOF_INT) |
我个人习惯:优先用CheckSymbolExists而不是CheckFunctionExists。因为前者能检测宏定义,后者只能检测实际链接的函数。有些平台把函数实现成宏,CheckFunctionExists会误判。
实战:检测POSIX头文件和函数
假设我们要写一个跨平台的线程库。Linux上用pthread,Windows上用Win32线程。我们需要检测目标系统是否支持pthread.h和pthread_create。
先看CMakeLists.txt怎么写:
cmake_minimum_required(VERSION 3.10)
project(ThreadDetect C)
# 引入检测模块
include(CheckIncludeFile)
include(CheckSymbolExists)
# 检测头文件
check_include_file("pthread.h" HAVE_PTHREAD_H)
check_include_file("windows.h" HAVE_WINDOWS_H)
# 检测符号(函数)
if(HAVE_PTHREAD_H)
check_symbol_exists("pthread_create" "pthread.h" HAVE_PTHREAD_CREATE)
endif()
if(HAVE_WINDOWS_H)
check_symbol_exists("CreateThread" "windows.h" HAVE_CREATE_THREAD)
endif()
# 把检测结果传给源代码
add_executable(myapp main.c)
target_compile_definitions(myapp PRIVATE
HAVE_PTHREAD_H=${HAVE_PTHREAD_H}
HAVE_PTHREAD_CREATE=${HAVE_PTHREAD_CREATE}
HAVE_WINDOWS_H=${HAVE_WINDOWS_H}
HAVE_CREATE_THREAD=${HAVE_CREATE_THREAD}
)
然后在main.c里,我们就可以这样写:
#include <stdio.h>
#if defined(HAVE_PTHREAD_H) && defined(HAVE_PTHREAD_CREATE)
#include <pthread.h>
#define THREAD_FUNC pthread_create
#elif defined(HAVE_WINDOWS_H) && defined(HAVE_CREATE_THREAD)
#include <windows.h>
#define THREAD_FUNC CreateThread
#else
#error "No thread API found!"
#endif
int main() {
printf("Thread API detected successfully.\n");
return 0;
}
嗯,这里要注意:check_symbol_exists的第二个参数是头文件路径,必须用引号括起来。而且这个头文件必须能被#include找到,否则检测会失败。
避坑指南:检测顺序和缓存
我曾经在做一个嵌入式项目时,发现check_include_file总是返回FALSE,但头文件明明就在系统路径里。排查了半天,发现是因为我在project()之前就调用了检测函数。CMake的检测模块需要编译器信息,必须在project()之后才能正常工作。
另一个坑是缓存。CMake会把检测结果缓存到CMakeCache.txt里。如果你改了系统环境(比如安装了新的库),但缓存没清,检测结果还是旧的。这时候需要手动删除缓存,或者用cmake --fresh重新配置。
project()中明确指定语言标准:set(CMAKE_C_STANDARD 11)。
知识体系:特性检测的核心逻辑
我画了一张图,帮你理清整个流程:
这个流程其实很简单:引入模块 → 执行检测 → 缓存结果 → 根据结果设置宏 → 代码里条件编译。每一步都有对应的CMake命令,你只要按顺序写就行。
高级技巧:自定义检测
有时候内置模块不够用。比如你想检测某个库的版本号,或者某个结构体是否有特定字段。这时候可以用try_compile或try_run自己写检测逻辑。
举个例子,检测struct stat是否有st_mtim字段(用于获取文件修改时间,纳秒精度):
include(CheckStructHasMember)
check_struct_has_member("struct stat" "st_mtim" "sys/stat.h" HAVE_STAT_ST_MTIM)
if(HAVE_STAT_ST_MTIM)
target_compile_definitions(myapp PRIVATE HAVE_STAT_ST_MTIM=1)
endif()
然后在代码里:
#ifdef HAVE_STAT_ST_MTIM
// 使用 st_mtim 获取纳秒级时间
printf("mtime: %ld.%ld\n", buf.st_mtim.tv_sec, buf.st_mtim.tv_nsec);
#else
// 回退到秒级精度
printf("mtime: %ld\n", buf.st_mtime);
#endif
target_compile_definitions传给特定目标,而不是用add_definitions全局设置。这样不同目标可以有不同的特性集,避免污染全局命名空间。
总结
特性检测是跨平台开发的基石。CMake提供的CheckIncludeFile、CheckSymbolExists等模块,让你在配置阶段就能摸清目标系统的底细。记住几个要点:
- 所有检测必须在
project()之后调用 - 检测结果会缓存,改环境后记得清缓存
- 优先用
CheckSymbolExists,它能检测宏和函数 - 用
target_compile_definitions传递结果,别用全局宏
把这些模块用好,你的代码就能真正做到「一次编写,到处编译」。而不是「一次编写,到处调试」。
核心口诀:先检测,后编译;有缓存,要清理;宏定义,传目标;跨平台,不再愁。
公众号:蓝海资料掘金营,微信deep3321