一、库的构建:四种库类型详解

说到CMake的库构建,我个人觉得这是整个构建系统里最核心的部分之一。你想想看,一个项目少则几个模块,多则上百个模块,怎么组织这些代码?库就是答案。

CMake提供了四种库类型:STATICSHAREDMODULEOBJECT。每种都有它的用武之地。我在实际项目中踩过不少坑,今天一并分享给你。

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 知识体系图

下面这张图展示了四种库类型在构建流程中的位置和关系:

CMake 四种库类型构建流程 源文件 (.cpp/.c) 编译 (compile) 目标文件 (.o/.obj) STATIC (.a/.lib) SHARED (.so/.dll) MODULE (.so/.dll) OBJECT (.o/.obj) 源文件 编译 目标文件 库文件

1.7 实战建议

说了这么多,到底怎么选?我根据经验给你几个原则:

  1. 小项目(少于10个源文件):全部用STATIC,简单直接
  2. 中等项目(10-50个源文件):核心模块用STATIC,公共接口用SHARED
  3. 大型项目(50+源文件):按功能拆分成多个SHARED库,插件用MODULE
  4. 需要避免重复编译:用OBJECT库共享编译结果

重要提醒:静态库和动态库混用时,要注意全局变量的重复定义问题。我曾经在一个项目里,静态库和动态库都包含了同一个全局单例,结果运行时出现了两个实例,排查了好久才发现。

嗯,四种库类型就讲到这里。每种都有它的设计哲学,理解透了,你的CMake构建就能游刃有余。


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