第11章:库的构建——静态库、动态库、对象库、接口库、导入库、库的链接
说到库,我估计很多刚接触CMake的朋友第一反应就是「哦,就是编译成.a或者.so文件嘛」。嗯,对,但不全对。CMake里对库的管理,其实比你想的要丰富得多。我在一个大型嵌入式项目中就吃过亏——当时只用了静态库,结果链接时符号冲突搞得我焦头烂额。后来才意识到,CMake提供了好几种库类型,各有各的适用场景。
这一章,我们就来把这些库类型挨个捋一遍。说白了,就是让你知道什么时候该用哪种库,以及怎么在CMake里把它们组织好。
11.1 静态库与动态库
这是最基础的两种库类型。静态库在链接时被直接复制到可执行文件中,动态库则在运行时加载。我个人习惯是:小工具用静态库,大型项目用动态库。为什么?你想想看,静态库虽然部署简单,但每个可执行文件都拷贝一份,磁盘和内存都浪费。
在CMake中,创建静态库和动态库非常简单:
add_library(my_static_lib STATIC src/lib.cpp)
add_library(my_shared_lib SHARED src/lib.cpp)
这里有个坑——STATIC和SHARED是关键字,必须大写。我曾经见过同事写成小写,结果CMake报错半天没反应过来。
11.2 对象库
对象库是个好东西。它不生成最终的库文件,而是只编译出目标文件(.o或.obj)。然后你可以把这些目标文件链接到多个目标中,避免重复编译。
我记得在一个游戏引擎项目中,核心算法模块被多个可执行文件和动态库同时使用。如果编译成静态库,每个目标都有一份拷贝,调试时符号表混乱。用对象库就完美解决了这个问题。
add_library(core_algorithms OBJECT src/algorithm1.cpp src/algorithm2.cpp)
add_executable(game_tool main.cpp $<TARGET_OBJECTS:core_algorithms>)
add_library(game_engine SHARED engine.cpp $<TARGET_OBJECTS:core_algorithms>)
注意那个$<TARGET_OBJECTS:core_algorithms>语法,这是生成器表达式。说白了,就是在构建时把对象库的目标文件展开到这里。
11.3 接口库
接口库是个「虚」库——它不编译任何源文件,只传递编译选项、头文件路径和链接依赖。我最早接触接口库是在做跨平台UI库的时候,不同平台需要不同的编译宏和链接库,接口库帮我省了不少事。
add_library(platform_support INTERFACE)
target_include_directories(platform_support INTERFACE include/)
target_compile_definitions(platform_support INTERFACE PLATFORM_SUPPORT=1)
target_link_libraries(platform_support INTERFACE pthread dl)
然后其他目标只需要链接这个接口库,就能自动获得所有依赖:
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE platform_support)
嗯,这里要注意:接口库的INTERFACE关键字表示这些属性只对链接者生效,对接口库本身无效(因为它根本没有编译过程)。
11.4 导入库
导入库用于引用已经构建好的、外部的库文件。比如你下载了一个第三方库的二进制包,里面有头文件和.a/.so文件,就可以用导入库来告诉CMake怎么用它们。
add_library(third_party_lib UNKNOWN IMPORTED)
set_target_properties(third_party_lib PROPERTIES
IMPORTED_LOCATION /path/to/libthird_party.so
INTERFACE_INCLUDE_DIRECTORIES /path/to/include
)
这里UNKNOWN表示让CMake自动检测库类型。你也可以明确指定STATIC或SHARED。我个人建议尽量指定,因为有些场景下自动检测会出错。
11.5 库的链接
链接库是CMake中最常见的操作之一。但很多人只用了target_link_libraries的基本用法,忽略了它的三个关键字:PUBLIC、PRIVATE、INTERFACE。
这三个关键字决定了依赖的传递方式:
| 关键字 | 对当前目标 | 对链接当前目标的目标 |
|---|---|---|
| PRIVATE | 链接 | 不传递 |
| PUBLIC | 链接 | 传递 |
| INTERFACE | 不链接 | 传递 |
举个例子:假设你有一个日志库log_lib,一个网络库net_lib依赖日志库,一个应用app依赖网络库。
add_library(log_lib STATIC log.cpp)
add_library(net_lib STATIC net.cpp)
target_link_libraries(net_lib PUBLIC log_lib) # app也会链接log_lib
add_executable(app main.cpp)
target_link_libraries(app PRIVATE net_lib) # app会链接net_lib和log_lib
如果net_lib对log_lib使用PRIVATE,那么app就不会自动链接log_lib。这在大型项目中非常有用——你可以精确控制哪些依赖是内部实现细节,哪些是公开接口。
11.6 知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
这张图把五种库类型和三种链接方式放在了一起。你可以看到,静态库和动态库是最常用的,对象库适合避免重复编译,接口库用于传递依赖,导入库用于引用外部二进制。链接时,PRIVATE、PUBLIC、INTERFACE三个关键字决定了依赖的传递范围。
好了,关于库的构建就讲到这里。记住一点:没有银弹。每种库类型都有它的适用场景,关键是根据项目需求来选择。我在实际项目中,经常是静态库、动态库、对象库混合使用——核心算法用对象库,公共模块用动态库,小工具用静态库。灵活搭配,才能发挥CMake的真正威力。
公众号:蓝海资料掘金营,微信deep3321