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设置VERSION和SOVERSION属性。
小技巧:在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,但后缀不同(.avs.so) - 动态库设置了版本号,方便后续管理
- 安装规则区分了RUNTIME(Windows的dll)、LIBRARY(Linux的so)、ARCHIVE(静态库和导入库)
经验之谈:如果你同时构建静态库和动态库,并且它们共享同一份源码,一定要确保两者的编译选项一致。我曾经遇到过静态库用了-fPIC而动态库没用的bug,链接时各种诡异错误。后来我统一在全局加了set(CMAKE_POSITION_INDEPENDENT_CODE ON),世界清净了。
11.5 知识体系总览
下面这张图总结了本章的核心内容,帮你理清思路:
这张图把四种库类型和它们的特性都串起来了。你可以看到,add_library是核心入口,四种类型各有侧重。底部的BUILD_SHARED_LIBS是一个全局开关,但我不建议过度依赖它。
11.6 避坑指南
最后,分享几个我踩过的坑:
- Windows上的DLL导出:在Windows上构建动态库,记得在头文件里加
__declspec(dllexport)和__declspec(dllimport)。CMake有GenerateExportHeader模块可以自动生成,省心不少。 - 静态库的符号冲突:如果你链接了多个静态库,它们之间有同名符号,链接器可能会报错。我一般用
OBJECT库来避免这个问题。 - MODULE库的路径问题:用
dlopen加载MODULE时,路径是相对于当前工作目录的。我建议用CMAKE_CURRENT_BINARY_DIR来明确指定路径。
好了,库的构建就讲到这里。记住一句话:选对库类型,能让你的项目结构清晰、维护方便。选错了,后面全是坑。