11、接口库:INTERFACE IMPORTED库。如何创建一个纯头文件库或定义编译需求集合,实现模块化解耦。
说实话,我第一次接触 CMake 的 INTERFACE 库时,心里想的是:「这不就是个空壳子吗?」
后来在项目里被折腾惨了,才明白这个「空壳子」有多重要。今天咱们就把它彻底聊透。
什么是 INTERFACE 库?
说白了,INTERFACE 库就是一个「只提要求,不干活」的库。它没有源文件,不生成任何 .lib 或 .so 文件。它存在的唯一意义,就是告诉链接它的目标:「你得满足这些编译需求」。
我习惯把它比作一份合同。你签了这份合同,就得按合同上的要求来——比如加某个头文件路径、定义某个宏、链接某个第三方库。
为什么需要它?
你想想看,一个纯头文件的模板库,比如 Eigen、nlohmann/json,它们没有 .cpp 文件要编译。但你用它们的时候,还是得告诉编译器:「头文件在哪儿」「需要 C++17 标准」「得链接 pthread」。
这些信息如果散落在各个 CMakeLists.txt 里,维护起来就是一场噩梦。我在一个老项目里见过,同一个头文件路径在 20 多个子目录里重复写——后来换了个目录结构,改到崩溃。
创建 INTERFACE IMPORTED 库
有两种方式创建 INTERFACE 库:一种是自己项目里用的,叫 INTERFACE 库;另一种是引用外部已安装的库,叫 INTERFACE IMPORTED 库。咱们先看后者。
# 创建一个 IMPORTED 的 INTERFACE 库
add_library(MyHeaderOnlyLib INTERFACE IMPORTED)
# 设置头文件路径
set_target_properties(MyHeaderOnlyLib PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include"
)
# 设置编译定义
target_compile_definitions(MyHeaderOnlyLib INTERFACE
USE_MY_LIBRARY=1
)
# 设置编译选项
target_compile_features(MyHeaderOnlyLib INTERFACE
cxx_std_17
)
嗯,这里要注意:IMPORTED 表示这个库不是由当前项目构建的,而是外部提供的。你只是告诉 CMake:「嘿,有这么个东西存在,它的要求是这样的」。
创建项目内的 INTERFACE 库
如果你自己写了一个纯头文件库,不需要 IMPORTED 关键字:
# 创建一个普通的 INTERFACE 库
add_library(MyUtils INTERFACE)
# 指定头文件路径
target_include_directories(MyUtils INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 要求 C++20
target_compile_features(MyUtils INTERFACE
cxx_std_20
)
# 链接其他库(也会传播给使用者)
target_link_libraries(MyUtils INTERFACE
Threads::Threads
)
看到 $<BUILD_INTERFACE:...> 和 $<INSTALL_INTERFACE:...> 了吗?这叫生成器表达式。它让库在构建时和安装后使用不同的头文件路径。我在做 SDK 分发时被这个坑过——构建时路径是 /home/me/project/include,安装后变成 /usr/local/include,不用生成器表达式根本没法搞。
用 INTERFACE 库实现模块化解耦
这是 INTERFACE 库最骚的操作。你可以用它来定义一组「编译需求集合」,然后让不同的目标去满足这些需求。
举个例子:你的项目支持多个后端,每个后端需要不同的编译选项和链接库。
# 定义抽象接口
add_library(BackendInterface INTERFACE)
target_compile_features(BackendInterface INTERFACE cxx_std_17)
# 定义 OpenGL 后端需求
add_library(OpenGLBackend INTERFACE)
target_link_libraries(OpenGLBackend INTERFACE
BackendInterface
OpenGL::GL
glfw
)
target_compile_definitions(OpenGLBackend INTERFACE
USE_OPENGL=1
)
# 定义 Vulkan 后端需求
add_library(VulkanBackend INTERFACE)
target_link_libraries(VulkanBackend INTERFACE
BackendInterface
Vulkan::Vulkan
)
target_compile_definitions(VulkanBackend INTERFACE
USE_VULKAN=1
)
# 主程序选择后端
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE
$<$<BOOL:${USE_VULKAN}>:VulkanBackend>
$<$<BOOL:${USE_OPENGL}>:OpenGLBackend>
)
这样,主程序根本不需要知道后端的具体细节。它只需要说「我要 Vulkan 后端」,CMake 自动把所有的编译定义、链接库、头文件路径一股脑传过来。
实战:封装一个第三方纯头文件库
假设你下载了 nlohmann/json,把它放在 third_party/json/ 目录下。你想让整个项目都能方便地使用它。
# third_party/CMakeLists.txt
add_library(nlohmann_json INTERFACE)
target_include_directories(nlohmann_json INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/json/single_include
)
target_compile_features(nlohmann_json INTERFACE cxx_std_17)
# 使用方
add_executable(MyTool tool.cpp)
target_link_libraries(MyTool PRIVATE nlohmann_json)
就这么简单。任何链接了 nlohmann_json 的目标,自动获得头文件路径和 C++17 要求。
INTERFACE 库的调试技巧
有时候你发现编译报错,怀疑是 INTERFACE 库传递了错误的信息。可以用这个命令查看:
# 查看目标的编译属性
get_target_property(inc MyUtils INTERFACE_INCLUDE_DIRECTORIES)
message("Include dirs: ${inc}")
# 或者用 cmake_print_properties
include(CMakePrintHelpers)
cmake_print_properties(TARGETS MyUtils PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES
INTERFACE_COMPILE_DEFINITIONS
INTERFACE_COMPILE_FEATURES
)
我曾经在一个大型项目里,发现某个 INTERFACE 库莫名其妙地传递了一个 -Werror 编译选项,导致所有链接它的目标都把警告当错误。查了两天,最后用 cmake_print_properties 才发现是三层依赖里的一个 INTERFACE 库设置的。
SVG 结构图:INTERFACE 库的传播机制
总结一下
INTERFACE 库是 CMake 里最被低估的特性之一。它让你能:
- 封装纯头文件库,无需源文件
- 定义可复用的编译需求集合
- 实现模块化解耦,主程序不关心后端细节
- 通过生成器表达式处理构建/安装路径差异
我个人建议,任何需要传播编译需求的地方,优先考虑 INTERFACE 库。它比手动设置 target_include_directories 要优雅得多,也比全局设置 include_directories 要安全得多。
好了,INTERFACE 库就聊到这儿。下次你遇到纯头文件库,或者想定义一组编译需求让多个目标共享,记得用这个「空壳子」——它比你想象的要强大得多。