11、接口库:INTERFACE IMPORTED库。如何创建一个纯头文件库或定义编译需求集合,实现模块化解耦。

说实话,我第一次接触 CMake 的 INTERFACE 库时,心里想的是:「这不就是个空壳子吗?」

后来在项目里被折腾惨了,才明白这个「空壳子」有多重要。今天咱们就把它彻底聊透。

什么是 INTERFACE 库?

说白了,INTERFACE 库就是一个「只提要求,不干活」的库。它没有源文件,不生成任何 .lib 或 .so 文件。它存在的唯一意义,就是告诉链接它的目标:「你得满足这些编译需求」。

我习惯把它比作一份合同。你签了这份合同,就得按合同上的要求来——比如加某个头文件路径、定义某个宏、链接某个第三方库。

核心概念: INTERFACE 库只传播编译需求,不产生构建产物。

为什么需要它?

你想想看,一个纯头文件的模板库,比如 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:「嘿,有这么个东西存在,它的要求是这样的」。

我的习惯: 如果外部库是纯头文件的,我通常会在一个 FindXXX.cmake 模块里创建 INTERFACE IMPORTED 库,这样其他模块通过 find_package 就能直接使用。

创建项目内的 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 自动把所有的编译定义、链接库、头文件路径一股脑传过来。

注意: INTERFACE 库的依赖是传递的。如果你链接了 A,A 又链接了 B,那么你的目标也会继承 B 的需求。这在带来便利的同时,也可能导致「隐式依赖」——我曾经排查过一个编译错误,最后发现是三层 INTERFACE 库传递了一个不兼容的编译选项。

实战:封装一个第三方纯头文件库

假设你下载了 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 库传播机制 BackendInterface cxx_std_17 OpenGLBackend USE_OPENGL=1 链接 OpenGL::GL VulkanBackend USE_VULKAN=1 链接 Vulkan::Vulkan 继承 继承 MyApp(主程序) 选择 VulkanBackend 或 OpenGLBackend 条件链接 传播到 MyApp 的编译需求 cxx_std_17 + USE_VULKAN=1 + Vulkan::Vulkan 链接 MyApp 无需知道后端细节,CMake 自动传递所有需求 INTERFACE 库 可执行目标 传播结果

总结一下

INTERFACE 库是 CMake 里最被低估的特性之一。它让你能:

  • 封装纯头文件库,无需源文件
  • 定义可复用的编译需求集合
  • 实现模块化解耦,主程序不关心后端细节
  • 通过生成器表达式处理构建/安装路径差异

我个人建议,任何需要传播编译需求的地方,优先考虑 INTERFACE 库。它比手动设置 target_include_directories 要优雅得多,也比全局设置 include_directories 要安全得多。

避坑指南: 我曾经在一个项目里,把 INTERFACE 库当普通库用,给它加了源文件。结果 CMake 报错说「INTERFACE 库不能有源文件」。记住:INTERFACE 库就是纯接口,不要试图让它编译任何东西。

好了,INTERFACE 库就聊到这儿。下次你遇到纯头文件库,或者想定义一组编译需求让多个目标共享,记得用这个「空壳子」——它比你想象的要强大得多。


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