11、库的构建:静态库与动态库、add_library的SHARED/STATIC/MODULE/OBJECT类型、BUILD_SHARED_LIBS

说到库的构建,这其实是CMake最核心的功能之一。我刚开始用CMake时,觉得不就是打个包嘛,有啥好学的?直到有一次,我花了一整天排查一个链接错误,最后发现是库类型选错了。嗯,从那以后,我再也不敢小看这个环节了。

说白了,库就是把一堆编译好的目标文件打包在一起,方便别人复用。但怎么打包、打包成什么形式,这里面的门道可不少。

11.1 静态库 vs 动态库:先搞清楚区别

先问个问题:你写了一个工具函数,想让其他项目用。你是直接给源码,还是给编译好的文件?

给源码当然可以,但别人还得自己编译。更常见的做法是:你把代码编译好,打包成库文件。这时候就有两种选择:

  • 静态库(Windows下是.lib,Linux下是.a):编译时直接嵌入到可执行文件中。好处是部署简单,一个exe就搞定。坏处是每个程序都复制一份,体积大,而且更新库得重新编译整个程序。
  • 动态库(Windows下是.dll,Linux下是.so):运行时才加载。好处是多个程序可以共享同一份库文件,更新库时只要替换dll/so就行。坏处是部署时得带着一堆dll,搞不好就出现「找不到dll」的尴尬。

我个人习惯是:工具类、算法类这种不常变的东西,用静态库。插件、界面组件这种需要灵活更新的,用动态库。

核心区别一句话总结:静态库是「复制粘贴」,动态库是「共享引用」。

11.2 add_library:创建库的核心命令

在CMake里,创建库用的是add_library命令。它的基本语法是:

add_library(<name> [STATIC | SHARED | MODULE | OBJECT]
            [EXCLUDE_FROM_ALL]
            [<source>...])

这里的<name>就是库的名字。注意,这个名字在CMake里是逻辑名称,最终生成的文件名会根据平台自动加上前缀和后缀。比如Linux下,add_library(mycalc SHARED calc.cpp)会生成libmycalc.so

我见过不少新手直接写add_library(mycalc calc.cpp),不指定类型。这时候CMake会怎么处理?嗯,它取决于一个全局变量——BUILD_SHARED_LIBS。这个我们后面会讲。

11.3 四种库类型详解

CMake支持四种库类型:STATIC、SHARED、MODULE、OBJECT。每种都有它的用武之地。

类型 生成文件 链接方式 典型用途
STATIC .lib / .a 编译时链接 工具库、算法库
SHARED .dll / .so 运行时加载 公共组件、接口库
MODULE .dll / .so 运行时动态加载 插件系统
OBJECT .obj / .o 不链接,直接使用目标文件 大型项目内部复用

STATIC:静态库

静态库就是把一堆.o文件打包成一个.a.lib。使用静态库的程序,在链接阶段会把库中的代码复制到自己的可执行文件中。

add_library(mylib STATIC
    src/utils.cpp
    src/math.cpp
    src/io.cpp
)

这里有个坑:如果你在Windows上用MSVC,静态库的.lib和动态库的导入库.lib是两码事。前者包含实际代码,后者只包含跳转信息。我曾经被这个搞晕过,后来养成了一个习惯——在CMake里明确指定库类型,绝不依赖默认值。

SHARED:动态库

动态库在编译时只生成一个「引用」,程序运行时才去加载真正的库文件。

add_library(myplugin SHARED
    src/plugin.cpp
    src/interface.cpp
)

在Linux上,动态库还有个「soname」的概念。CMake默认会帮你处理,但如果你需要精细控制,可以用set_target_properties设置VERSIONSOVERSION属性。

小技巧:在Linux上查看一个动态库的soname,可以用objdump -p libxxx.so | grep SONAME。这个值决定了运行时系统找哪个版本的库。

MODULE:模块库

MODULE类型比较特殊。它生成的也是动态库,但不会自动链接到目标程序上。你需要用dlopen(Linux)或LoadLibrary(Windows)手动加载。

add_library(plugin_a MODULE
    src/plugin_a.cpp
)
add_library(plugin_b MODULE
    src/plugin_b.cpp
)

这种模式非常适合插件系统。主程序只需要定义好接口,各个插件独立编译成MODULE,运行时按需加载。我在做一个图像处理软件时就用过这个模式,每个滤镜都是一个MODULE,用户想加新滤镜,丢一个.so文件进去就行,主程序不用改。

OBJECT:对象库

OBJECT类型是CMake 2.8.8引入的。它不生成库文件,而是生成一堆目标文件(.o.obj)。这些目标文件可以被其他目标直接引用。

add_library(core_objects OBJECT
    src/core1.cpp
    src/core2.cpp
)

add_library(shared_lib SHARED $<TARGET_OBJECTS:core_objects>)
add_library(static_lib STATIC $<TARGET_OBJECTS:core_objects>)

你看,同一个core_objects,既可以用在动态库里,又可以用在静态库里。这样就不需要把同一份源码编译两次了。我在一个大项目里用过这个技巧,编译时间直接砍了一半。

注意:OBJECT库的源文件不能有重复的编译选项。如果你需要不同的编译选项,还是老老实实分开编译吧。

11.3 BUILD_SHARED_LIBS:全局开关

如果你在add_library里不指定类型,CMake会看一个全局变量——BUILD_SHARED_LIBS。默认是OFF,也就是生成静态库。

# 在顶层CMakeLists.txt中设置
set(BUILD_SHARED_LIBS ON)

# 下面这个库会变成动态库
add_library(mylib src/mylib.cpp)

# 但你可以单独覆盖
add_library(special_lib STATIC src/special.cpp)

我个人不太推荐依赖这个全局变量。为什么?因为代码的可读性会变差。你想想看,别人看你的CMakeLists.txt,看到add_library(foo foo.cpp),还得去翻上面有没有set(BUILD_SHARED_LIBS ON),多累啊。

我习惯的做法是:每个add_library都明确写出类型。除非整个项目统一用一种库类型,而且你确定以后不会改,那才考虑用BUILD_SHARED_LIBS

11.4 实战:一个完整的库构建示例

说了这么多,我们来写一个完整的例子。假设我们要构建一个数学工具库,包含静态库和动态库两种版本。

cmake_minimum_required(VERSION 3.10)
project(MathLib VERSION 1.0.0 LANGUAGES CXX)

# 头文件路径
include_directories(include)

# 源文件列表
set(MATH_SOURCES
    src/add.cpp
    src/sub.cpp
    src/mul.cpp
    src/div.cpp
)

# 创建静态库
add_library(math_static STATIC ${MATH_SOURCES})
set_target_properties(math_static PROPERTIES
    OUTPUT_NAME "math"
    CLEAN_DIRECT_OUTPUT 1
)

# 创建动态库
add_library(math_shared SHARED ${MATH_SOURCES})
set_target_properties(math_shared PROPERTIES
    OUTPUT_NAME "math"
    CLEAN_DIRECT_OUTPUT 1
    VERSION ${PROJECT_VERSION}
    SOVERSION 1
)

# 安装规则
install(TARGETS math_static math_shared
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

install(DIRECTORY include/ DESTINATION include)

这个例子有几个要点:

  • OUTPUT_NAME让两个库的输出文件名都叫math,但后缀不同(.a vs .so
  • 动态库设置了版本号,方便后续管理
  • 安装规则区分了RUNTIME(Windows的dll)、LIBRARY(Linux的so)、ARCHIVE(静态库和导入库)

经验之谈:如果你同时构建静态库和动态库,并且它们共享同一份源码,一定要确保两者的编译选项一致。我曾经遇到过静态库用了-fPIC而动态库没用的bug,链接时各种诡异错误。后来我统一在全局加了set(CMAKE_POSITION_INDEPENDENT_CODE ON),世界清净了。

11.5 知识体系总览

下面这张图总结了本章的核心内容,帮你理清思路:

库的构建知识体系 add_library STATIC SHARED MODULE OBJECT 特性 • 编译时链接 • 生成 .a / .lib • 可执行文件独立 特性 • 运行时加载 • 生成 .so / .dll • 需管理依赖 特性 • 手动 dlopen • 插件系统首选 • 不自动链接 特性 • 仅生成 .o / .obj • 可被多目标复用 • 避免重复编译 BUILD_SHARED_LIBS 全局开关

这张图把四种库类型和它们的特性都串起来了。你可以看到,add_library是核心入口,四种类型各有侧重。底部的BUILD_SHARED_LIBS是一个全局开关,但我不建议过度依赖它。

11.6 避坑指南

最后,分享几个我踩过的坑:

  • Windows上的DLL导出:在Windows上构建动态库,记得在头文件里加__declspec(dllexport)__declspec(dllimport)。CMake有GenerateExportHeader模块可以自动生成,省心不少。
  • 静态库的符号冲突:如果你链接了多个静态库,它们之间有同名符号,链接器可能会报错。我一般用OBJECT库来避免这个问题。
  • MODULE库的路径问题:用dlopen加载MODULE时,路径是相对于当前工作目录的。我建议用CMAKE_CURRENT_BINARY_DIR来明确指定路径。

好了,库的构建就讲到这里。记住一句话:选对库类型,能让你的项目结构清晰、维护方便。选错了,后面全是坑。


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