19、CMake进阶:静态库与共享库:add_library的多种模式(STATIC, SHARED, OBJECT, INTERFACE),库的导出与安装。
好,咱们今天聊点实在的。库这个东西,说白了就是代码的集装箱。你写了一大堆功能,总不能每次都从头编译吧?打包成库,复用起来就方便多了。CMake 里的 add_library 命令,就是干这个活的。
我个人习惯,在项目一开始就把库的类型想清楚。是静态链接,还是动态加载?或者只是个接口定义?选错了,后面改起来挺麻烦的。今天我就把四种模式掰开揉碎了讲给你听。
19.1 四种库模式:STATIC、SHARED、OBJECT、INTERFACE
先看个总览表格,心里有个谱:
| 模式 | 生成产物 | 链接时机 | 典型用途 |
|---|---|---|---|
| STATIC | .a / .lib | 编译时 | 小型工具库、嵌入式裸机项目 |
| SHARED | .so / .dll | 运行时 | 插件系统、动态更新模块 |
| OBJECT | .o / .obj 集合 | 编译时(无归档) | 避免重复编译、加速构建 |
| INTERFACE | 无二进制产物 | 编译时(纯头文件) | 头文件库、纯模板库 |
19.1.1 STATIC 模式
静态库,说白了就是把一堆 .o 文件打包成一个 .a 文件。链接时,链接器会把需要的代码直接拷贝到最终的可执行文件里。
我在嵌入式项目里用得最多。为什么?因为嵌入式环境往往没有动态加载器,而且静态库能减少运行时依赖。你想想看,一个传感器驱动库,静态链接进去,烧写到 Flash 里就完事了,多省心。
add_library(my_driver STATIC
src/gpio.c
src/i2c.c
src/spi.c
)
target_include_directories(my_driver PUBLIC include)
这里有个坑:静态库的链接顺序很重要。我曾经在调试一个 USB 协议栈时,因为库顺序写反了,链接器报了一堆未定义引用。折腾了半天才发现是顺序问题。GCC 的链接器是从右向左解析符号的,所以被依赖的库要放在右边。
19.1.2 SHARED 模式
共享库,生成的是 .so 文件。它不会把代码拷贝到可执行文件里,而是在运行时动态加载。好处是节省空间,多个进程可以共用一份代码。
嗯,这里要注意:嵌入式 Linux 项目里,共享库很常见。比如你有一个算法库,需要经常更新,用共享库就方便了——只替换 .so 文件,不用重新编译整个应用。
add_library(my_algorithm SHARED
src/fft.c
src/filter.c
)
target_link_libraries(my_app PRIVATE my_algorithm)
但共享库有个烦人的问题——ABI 兼容性。我记得有一次,我升级了编译器版本,结果 .so 文件里的符号修饰变了,应用加载时直接崩溃。从那以后,我养成了一个习惯:对外发布的共享库,一定要用 -fvisibility=hidden 控制符号导出。
19.1.3 OBJECT 模式
OBJECT 库是个好东西,但很多人不知道。它不生成 .a 或 .so,而是生成一堆 .o 文件。这些 .o 文件可以被其他目标直接引用,避免了重复编译。
为什么会有人用这个?我举个例子。你有一个大型项目,包含多个可执行文件,它们都共用同一组源文件。如果用 STATIC 库,每次修改一个源文件,整个 .a 都要重新打包。用 OBJECT 库,每个 .o 文件独立存在,链接时直接拿过来用,构建速度能快不少。
add_library(common_objects OBJECT
src/ring_buffer.c
src/crc32.c
src/queue.c
)
add_executable(app1 main1.c $<TARGET_OBJECTS:common_objects>)
add_executable(app2 main2.c $<TARGET_OBJECTS:common_objects>)
注意那个 $<TARGET_OBJECTS:common_objects> 语法,这是 CMake 的生成器表达式。我第一次看到时也愣了一下,后来发现它其实就是把 OBJECT 库里的 .o 文件路径展开。
19.1.4 INTERFACE 模式
INTERFACE 库最特殊——它没有源文件,也没有二进制产物。它就是个「标签」,用来传递编译选项、头文件路径、链接依赖。
说白了,它就是给头文件库用的。比如你写了一个纯模板的数学库,或者一个只有头文件的日志库,用 INTERFACE 就对了。
add_library(my_header_only INTERFACE)
target_include_directories(my_header_only INTERFACE include)
target_compile_definitions(my_header_only INTERFACE USE_FAST_MATH=1)
然后其他目标链接它时,会自动继承这些属性:
target_link_libraries(my_app PRIVATE my_header_only)
这样 my_app 就能找到头文件,并且预定义了 USE_FAST_MATH 宏。干净利落。
19.2 库的导出与安装
库写好了,怎么给别人用?这就涉及到导出和安装了。导出是给 CMake 的 find_package 用的,安装是把文件放到系统目录下。
19.2.1 安装库文件
先看安装。用 install() 命令指定目标文件和安装路径:
install(TARGETS my_driver
ARCHIVE DESTINATION lib # 静态库放 lib/
LIBRARY DESTINATION lib # 共享库也放 lib/
PUBLIC_HEADER DESTINATION include # 头文件放 include/
)
这里有个细节:ARCHIVE 对应静态库,LIBRARY 对应共享库,RUNTIME 对应 DLL(Windows 上)。别搞混了。
19.2.2 导出 CMake 包
安装文件只是第一步。要让别人用 find_package(my_driver) 找到你的库,还得导出 CMake 配置。
我建议用 install(EXPORT) 配合 export() 命令:
install(TARGETS my_driver
EXPORT my_driverTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
install(EXPORT my_driverTargets
FILE my_driverTargets.cmake
NAMESPACE my_driver::
DESTINATION lib/cmake/my_driver
)
这样,别人用的时候:
find_package(my_driver REQUIRED)
target_link_libraries(my_app PRIVATE my_driver::my_driver)
那个 :: 语法是 CMake 的命名空间约定,表示这是一个导入的目标,而不是普通的库名。我个人觉得这个设计挺好的,一眼就能看出是外部依赖。
NAMESPACE,结果用户链接时找不到目标。排查了半天才发现是命名空间没对上。记住,find_package 导入的目标名是 命名空间+目标名,必须一致。
19.3 知识体系总览
说了这么多,我画个图帮你理清思路。这张图展示了四种库模式的关系,以及导出安装的流程:
好了,关于 add_library 的四种模式和库的导出安装,就聊到这里。每种模式都有它的适用场景,选对了能让项目结构清晰、构建高效。下次你写 CMakeLists.txt 时,不妨多想想:这个库到底该用哪种模式?