一、库的构建:四种库类型详解
说到CMake的库构建,我个人觉得这是整个构建系统里最核心的部分之一。你想想看,一个项目少则几个模块,多则上百个模块,怎么组织这些代码?库就是答案。
CMake提供了四种库类型:STATIC、SHARED、MODULE、OBJECT。每种都有它的用武之地。我在实际项目中踩过不少坑,今天一并分享给你。
1.1 静态库(STATIC)
静态库说白了就是把一堆目标文件打包成一个 .a(Linux)或 .lib(Windows)文件。链接时直接复制到可执行文件里。
适用场景:工具库、基础算法库、不想暴露源码的内部模块
# CMakeLists.txt
add_library(my_math STATIC
src/add.cpp
src/sub.cpp
src/mul.cpp
)
target_include_directories(my_math PUBLIC include)
嗯,这里要注意:静态库的链接顺序很重要。我曾经在项目里遇到过一个诡异的链接错误,折腾了两天才发现是库的依赖顺序写反了。GCC的链接器是从右向左解析符号的,所以被依赖的库要放在右边。
我的习惯:小项目用静态库就够了,省去动态库的部署烦恼。但超过10个模块的项目,我会考虑用动态库。
1.2 动态库(SHARED)
动态库是运行时加载的,多个程序可以共享同一份代码。Linux下是 .so,Windows下是 .dll,macOS下是 .dylib。
add_library(my_engine SHARED
src/engine.cpp
src/renderer.cpp
)
# Windows下需要导出符号
if(WIN32)
target_compile_definitions(my_engine PRIVATE MY_ENGINE_EXPORTS)
endif()
我记得第一次做动态库时,在Windows上折腾了半天——明明编译成功了,运行时就报找不到符号。后来才明白,Windows下需要显式声明 __declspec(dllexport) 和 __declspec(dllimport)。
我曾经踩过的坑:动态库的ABI兼容性问题。升级编译器版本后,旧动态库和新编译的程序不兼容,运行时直接崩溃。所以动态库的接口设计要格外小心,尽量用C风格接口或者保持稳定的C++ ABI。
1.3 模块库(MODULE)
模块库是动态库的一种特殊形式。它不会被链接到可执行文件里,而是在运行时通过 dlopen(Linux)或 LoadLibrary(Windows)手动加载。
add_library(my_plugin MODULE
src/plugin.cpp
)
# 不产生导入库,只生成 .so/.dll
set_target_properties(my_plugin PROPERTIES
PREFIX ""
SUFFIX ".so"
)
你想想看,什么时候用模块库?插件系统!比如你的编辑器支持第三方插件,每个插件就是一个模块库。主程序启动后扫描插件目录,动态加载。
核心区别:SHARED库会被链接器处理,生成导入库(.lib/.dll.a);MODULE库只生成 .so/.dll,不生成导入库。
1.4 对象库(OBJECT)
对象库是CMake 2.8.8引入的特性。它不打包成 .a 或 .so,而是直接提供 .o 文件给其他目标使用。
# 创建对象库
add_library(my_objects OBJECT
src/obj1.cpp
src/obj2.cpp
)
# 在其他目标中使用
add_library(my_lib STATIC
$<TARGET_OBJECTS:my_objects>
src/extra.cpp
)
add_executable(my_app
$<TARGET_OBJECTS:my_objects>
src/main.cpp
)
我个人特别喜欢对象库。为什么?因为它避免了重复编译。假如你有10个源文件,其中5个要被静态库和动态库同时使用。传统做法是编译两次,对象库只需要编译一次。
使用技巧:对象库配合生成器表达式($<...>)使用,可以灵活控制哪些源文件进入哪些目标。
1.5 四种库的对比
| 类型 | 输出文件 | 链接方式 | 运行时加载 | 典型用途 |
|---|---|---|---|---|
| STATIC | .a / .lib | 编译时链接 | 不需要 | 工具库、内部模块 |
| SHARED | .so / .dll | 编译时链接 | 自动加载 | 公共库、第三方库 |
| MODULE | .so / .dll | 不链接 | 手动加载 | 插件系统 |
| OBJECT | .o / .obj | 编译时合并 | 不需要 | 避免重复编译 |
1.6 知识体系图
下面这张图展示了四种库类型在构建流程中的位置和关系:
1.7 实战建议
说了这么多,到底怎么选?我根据经验给你几个原则:
- 小项目(少于10个源文件):全部用STATIC,简单直接
- 中等项目(10-50个源文件):核心模块用STATIC,公共接口用SHARED
- 大型项目(50+源文件):按功能拆分成多个SHARED库,插件用MODULE
- 需要避免重复编译:用OBJECT库共享编译结果
重要提醒:静态库和动态库混用时,要注意全局变量的重复定义问题。我曾经在一个项目里,静态库和动态库都包含了同一个全局单例,结果运行时出现了两个实例,排查了好久才发现。
嗯,四种库类型就讲到这里。每种都有它的设计哲学,理解透了,你的CMake构建就能游刃有余。