11. 多库管理:多个 add_library()、库之间的依赖关系、target_link_libraries() 详解、库的可见性
好,咱们今天聊点实在的。当你一个项目里只有一两个 .so 库的时候,CMakeLists.txt 怎么写都行。但一旦项目膨胀起来——比如你同时要编译一个核心算法库、一个网络协议库、一个日志工具库,再加上第三方的预编译库——这时候,多库管理的能力就成了分水岭。
我见过不少团队,一开始图省事,把所有源文件塞进一个 add_library() 里。结果呢?编译慢、耦合高、想单独替换某个模块都费劲。说白了,这就是欠了技术债。今天咱们就把这笔债还上。
11.1 多个 add_library() 的正确姿势
先看一个最简单的多库场景。假设你有三个模块:core、network、utils。每个模块都有自己的源文件。
# CMakeLists.txt
add_library(core STATIC
src/core/engine.cpp
src/core/manager.cpp
)
add_library(network STATIC
src/network/http_client.cpp
src/network/websocket.cpp
)
add_library(utils STATIC
src/utils/logger.cpp
src/utils/string_helpers.cpp
)
每个库独立编译,互不干扰。这有什么好处?我举个例子。有一次我在项目中需要单独替换网络层,因为要适配一个新的 IoT 协议。如果所有代码都在一个库里,我得小心翼翼地改,生怕影响其他模块。但拆成多个库之后,我只需要重新编译 network 库,core 和 utils 纹丝不动。编译时间从 3 分钟降到了 30 秒。
11.2 库之间的依赖关系
库拆开了,问题也来了:它们之间怎么互相调用?比如 network 库要用 core 库里的数据结构,utils 库里的日志函数。
这时候就需要 target_link_libraries() 登场了。它用来声明库之间的依赖关系。
target_link_libraries(network PUBLIC core)
target_link_libraries(network PRIVATE utils)
这两行代码,就是告诉 CMake:
- network 依赖于 core(PUBLIC)
- network 依赖于 utils(PRIVATE)
你可能会问:PUBLIC 和 PRIVATE 到底有什么区别?别急,咱们下一节细讲。这里先记住一个原则:依赖关系要明确,不要偷懒写成 PUBLIC 一把梭。
11.3 target_link_libraries() 详解
这个函数是 CMake 里最常用、也最容易用错的函数之一。它的完整签名是这样的:
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]
)
每个 <item> 可以是:
- 另一个 CMake 目标(比如你定义的库名)
- 一个库文件的绝对路径
- 一个库文件名(如
log) - 一个
-l开头的链接器标志
咱们重点说说最常见的用法——链接另一个 CMake 目标。这时候,CMake 会自动处理头文件搜索路径、编译定义、链接库路径等所有传递性依赖。
# 假设 app 是一个可执行文件或共享库
target_link_libraries(app
PRIVATE core
PUBLIC network
INTERFACE utils
)
嗯,这里要注意:INTERFACE 比较特殊。它表示这个依赖只对使用 app 的目标可见,app 自己并不链接它。这个用法在纯头文件库或者接口库中很常见。
11.4 库的可见性:PUBLIC / PRIVATE / INTERFACE
这是多库管理的核心,也是很多人搞不清楚的地方。我尽量用大白话讲明白。
想象一下,你有一个库 A,它依赖了库 B。现在有一个库 C,它要链接 A。
- PRIVATE:A 依赖 B,但 C 不需要知道 B 的存在。B 是 A 的内部实现细节。
- PUBLIC:A 依赖 B,而且 C 也需要用到 B 的东西(比如头文件、符号)。B 是 A 的接口的一部分。
- INTERFACE:A 自己不链接 B,但任何链接 A 的目标都必须链接 B。这通常用于纯头文件库或者接口定义库。
咱们用代码说话:
# 库 A 的实现用到了 B
add_library(A STATIC a.cpp)
target_link_libraries(A PRIVATE B)
# 库 C 链接 A,但 C 不需要知道 B
add_library(C STATIC c.cpp)
target_link_libraries(C PRIVATE A)
# 这里 C 不会自动链接 B,因为 B 是 A 的 PRIVATE 依赖
再看 PUBLIC 的例子:
# 库 A 的头文件里引用了 B 的类型
add_library(A STATIC a.cpp)
target_link_libraries(A PUBLIC B)
# 库 C 链接 A,C 的源文件里可以直接 #include <B/header.h>
add_library(C STATIC c.cpp)
target_link_libraries(C PRIVATE A)
# 这里 C 会自动获得 B 的头文件搜索路径和链接库
最后是 INTERFACE:
# 一个纯头文件库,没有源文件
add_library(header_only INTERFACE)
target_include_directories(header_only INTERFACE include/)
target_link_libraries(header_only INTERFACE core)
# 任何链接 header_only 的目标,都会自动获得 include/ 目录和 core 库
add_library(app SHARED main.cpp)
target_link_libraries(app PRIVATE header_only)
11.5 实战:一个典型的多库项目结构
咱们把前面讲的知识点串起来,看一个完整的例子。假设你的 Android NDK 项目结构如下:
app/
├── CMakeLists.txt
├── src/
│ ├── core/
│ │ ├── engine.cpp
│ │ └── engine.h
│ ├── network/
│ │ ├── http_client.cpp
│ │ └── http_client.h
│ └── utils/
│ ├── logger.cpp
│ └── logger.h
└── libs/
└── libssl.so (第三方预编译库)
对应的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)
project(MyApp)
# 1. 定义三个内部库
add_library(core STATIC
src/core/engine.cpp
)
add_library(utils STATIC
src/utils/logger.cpp
)
add_library(network STATIC
src/network/http_client.cpp
)
# 2. 声明依赖关系
target_link_libraries(network
PUBLIC core # network 的头文件暴露了 core 的类型
PRIVATE utils # network 内部用 utils 打日志
)
# 3. 导入第三方库
add_library(ssl SHARED IMPORTED)
set_target_properties(ssl PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/libssl.so
)
# 4. 最终的目标:共享库
add_library(myapp SHARED
src/main.cpp
)
target_link_libraries(myapp
PRIVATE network # 只依赖 network,间接获得 core
PRIVATE ssl # 直接依赖第三方库
)
这个结构清晰吗?我觉得挺清晰的。每个库的职责单一,依赖关系明确。如果将来要替换网络层,只需要改 network 库和它的依赖声明,其他代码完全不受影响。
11.6 知识体系总览
为了让你对本章内容有个整体把握,我画了一张图。它展示了多库管理的核心要素和它们之间的关系。
11.7 避坑指南
最后,分享几个我这些年积累的经验,希望能帮你少走弯路。
- 循环依赖是噩梦:库 A 依赖 B,B 又依赖 A。CMake 会直接报错。解决办法是重新设计模块边界,或者把公共部分抽成第三个库。
- 小心 STATIC 库的重复链接:如果多个共享库都链接了同一个静态库,每个共享库都会有一份静态库的副本。这会导致代码膨胀和全局变量重复。我的建议是:静态库只用于内部模块,最终产物用共享库。
- INTERFACE 库没有源文件:它只是一个“标签”,用来传递编译选项和依赖。别想着往里面加 .cpp 文件,CMake 会忽略它们。
- Android NDK 的特殊性:在 Android 平台上,系统库(如 log、z)需要用
find_library()找到路径,再用target_link_libraries()链接。不要直接写-llog,那样不够跨平台。
好了,多库管理这块就聊到这儿。记住一句话:好的依赖管理,是项目长期健康的基石。别图一时省事,给未来的自己挖坑。