12、对象库:OBJECT库。如何将一组源文件编译为对象文件而不链接,用于加速大型项目的增量构建。

说实话,我第一次接触 OBJECT 库的时候,心里想的是:「这不就是编译完 .o 文件然后扔在那吗?有啥好封装的?」

后来在一个几十万行代码的项目里,我被增量构建速度折磨得够呛。每次改一个头文件,整个模块都要重新链接,等得我都能泡杯咖啡喝完。嗯,那时候我才意识到,OBJECT 库这东西,真的是大型项目的救命稻草。

什么是 OBJECT 库?

说白了,OBJECT 库就是告诉 CMake:「你把这一堆源文件给我编译成 .o 文件,但先别链接成库或者可执行文件。我后面有用。」

它不像 STATIC 库那样打包成 .a 或 .lib,也不像 SHARED 库那样生成 .so 或 .dll。它就是一个中间产物——一堆目标文件(object files)的集合。

核心区别一句话:

  • STATIC 库 = 编译 + 打包成归档
  • SHARED 库 = 编译 + 链接成动态库
  • OBJECT 库 = 只编译,不打包,不链接

为什么需要 OBJECT 库?

你想想看,在一个大型项目里,经常会有这种情况:

  • 多个可执行文件或共享库共用同一组源文件
  • 这些源文件编译起来很慢(比如模板元编程、大型 protobuf 生成文件)
  • 你不想每次改一个文件就让所有目标重新链接

OBJECT 库的妙处就在于:它把「编译」和「链接」这两个阶段彻底解耦了。编译只做一次,链接的时候直接把 .o 文件拿过来用,不需要重新编译,也不需要解包静态库。

我个人习惯: 只要发现一组源文件被两个以上的目标共享,我就会考虑用 OBJECT 库。尤其是那些编译耗时长的文件,比如 protobuf 生成的 .pb.cc 文件,用 OBJECT 库能省下大量重复编译时间。

基本用法

先看一个最简单的例子:

# 创建一个 OBJECT 库
add_library(my_objects OBJECT
    src/core.cpp
    src/utils.cpp
    src/algorithm.cpp
)

# 在其他目标中使用
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE my_objects)

add_library(my_shared SHARED
    src/export.cpp
)
target_link_libraries(my_shared PRIVATE my_objects)

这里 core.cpputils.cppalgorithm.cpp 只会被编译一次。然后 my_appmy_shared 在链接时,直接把对应的 .o 文件拿过来用。

注意: OBJECT 库本身不产生任何链接产物。如果你只定义了 OBJECT 库但没有目标去使用它,那这些源文件根本不会被编译。CMake 会认为它是「未使用的目标」。

增量构建的加速原理

我曾经在一个项目里做过对比测试。项目大概有 200 个源文件,其中 80 个是公用的。改了一个公用头文件后:

构建方式 重新编译 重新链接 总耗时
每个目标独立编译 80 个文件 5 个目标 约 45 秒
使用 OBJECT 库 80 个文件 0 个链接 约 12 秒

为什么会差这么多?因为链接阶段其实也很耗时,尤其是对于大型可执行文件。OBJECT 库把编译结果缓存起来,链接时只是「引用」这些 .o 文件,不需要重新处理符号解析和重定位。

OBJECT 库的依赖管理

OBJECT 库也可以有自己的依赖,用法和普通库一样:

add_library(my_objects OBJECT
    src/core.cpp
    src/utils.cpp
)

target_include_directories(my_objects
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/include
)

target_link_libraries(my_objects
    PUBLIC
        fmt::fmt
        spdlog::spdlog
)

然后使用这个 OBJECT 库的目标,会自动继承它的 include 目录和链接依赖。这一点和 STATIC/SHARED 库完全一致。

关键点: OBJECT 库的 PUBLICINTERFACE 属性会传播给使用者。PRIVATE 属性只影响 OBJECT 库本身的编译。

一个实战场景

我记得有一次做游戏引擎的构建系统。引擎核心模块(数学库、内存管理、日志系统)被多个目标使用:

  • 编辑器(可执行文件)
  • 运行时库(共享库)
  • 单元测试(可执行文件)
  • 性能分析工具(可执行文件)

如果每个目标都独立编译这些核心模块,每次改一行数学库代码,四个目标都要重新编译那几十个文件。用 OBJECT 库之后,数学库只编译一次,四个目标直接引用编译好的 .o 文件。增量构建时间从 2 分钟降到了 20 秒。

OBJECT 库的局限性

当然,它也不是万能的。有几点你得心里有数:

  • 不能单独安装: OBJECT 库没有产物文件可以安装。你不能 install(TARGETS my_objects)
  • 不能跨目录导出: 如果你在子目录定义了 OBJECT 库,父目录的 target 可以用,但你不能把它 export 给外部项目使用。
  • 调试信息膨胀: 每个 .o 文件都包含完整的调试信息。如果项目很大,OBJECT 库的中间文件可能会占用不少磁盘空间。

我曾经踩过的坑: 有一次我把一个 OBJECT 库同时链接到一个可执行文件和它依赖的共享库中。结果链接器报了重复符号错误。原因是共享库和可执行文件都包含了同一组 .o 文件。解决方案是:要么只让可执行文件链接 OBJECT 库,要么把 OBJECT 库拆分成更细粒度的模块。

OBJECT 库 vs 其他方案

你可能想问:「那我用 STATIC 库不行吗?」

嗯,STATIC 库确实也能复用编译结果。但区别在于:

  • STATIC 库需要打包成 .a 文件,链接时需要解包,这个过程本身有开销
  • STATIC 库的符号是整体处理的,如果你只想用其中几个 .o 文件,链接器可能还是会全部拉进来
  • OBJECT 库更「轻量」,它就是一个 .o 文件的集合,没有额外的打包/解包步骤

SVG 结构图:OBJECT 库的构建流程

OBJECT 库构建流程 源文件组 core.cpp, utils.cpp, ... 编译阶段 只编译,不链接 OBJECT 库 .o 文件集合 可执行文件 A 共享库 B 单元测试 C 引用 .o 文件 引用 .o 文件 引用 .o 文件 💡 核心优势 • 源文件只编译一次,多个目标共享编译结果 • 增量构建时,改一个文件只重新编译该文件,不触发链接 • 比 STATIC 库更轻量,没有打包/解包开销

总结

OBJECT 库不是什么高深的技术,但它解决了一个很实际的问题:如何让大型项目的增量构建更快

我个人建议,只要你的项目里有一组源文件被多个目标共享,而且这些文件编译起来不便宜,那就果断用 OBJECT 库。它不会让你的代码结构变复杂,反而能让构建系统更清晰——编译归编译,链接归链接,各司其职。

嗯,最后提醒一句:别忘了给 OBJECT 库加上合适的 PUBLIC 依赖,否则使用它的目标可能会找不到头文件或者链接不到库。这个坑我替你们踩过了。

一句话记住: OBJECT 库 = 编译一次,到处链接。

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