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.cpp、utils.cpp、algorithm.cpp 只会被编译一次。然后 my_app 和 my_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 库的 PUBLIC 和 INTERFACE 属性会传播给使用者。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 库不是什么高深的技术,但它解决了一个很实际的问题:如何让大型项目的增量构建更快。
我个人建议,只要你的项目里有一组源文件被多个目标共享,而且这些文件编译起来不便宜,那就果断用 OBJECT 库。它不会让你的代码结构变复杂,反而能让构建系统更清晰——编译归编译,链接归链接,各司其职。
嗯,最后提醒一句:别忘了给 OBJECT 库加上合适的 PUBLIC 依赖,否则使用它的目标可能会找不到头文件或者链接不到库。这个坑我替你们踩过了。
一句话记住: OBJECT 库 = 编译一次,到处链接。