Basic 03-static-library
# 介绍
本节主要展示一个 hello world 的例子,创建并链接一个静态库。这是一个简化的例子,展示了库和可执行二进制文件在同一个 build 文件夹中。它们会作为 02-sub-projects 章节的子项目出现。
本例的目录🌲:
.
├── CMakeLists.txt # 包含了本项目运行所需要的CMake命令
├── include
│ └── static
│ └── Hello.h # main.cpp, Hello.cpp包含的头文件
└── src
├── Hello.cpp # Hello.h头文件对应的cpp文件
└── main.cpp
2
3
4
5
6
7
8
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#Generate the static library from the library sources
add_library(hello_library STATIC
src/Hello.cpp
)
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################
# Add an executable with the above sources
add_executable(hello_binary
src/main.cpp
)
# link the new hello_library target with the hello_binary target
target_link_libraries( hello_binary
PRIVATE
hello_library
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 概念解释
# 创建一个静态库 (Static Library)
add_library() 函数使用源文件创建库,需要指定:
- 静态库的目标名 (注意该目标对应的库会默认以 “目标名.xxx” 的方式来命名,静态库 “lib 目标名.a”, 共享库 “lib 目标名.so”)
- 库类型 什么是静态库?什么是动态库?见共享库和静态库的区别
- 构成该库的源文件
add_library(hello_library STATIC # 生成libhello_library.a
src/Hello.cpp
)
2
3
使用建议
我们直接将 main 中被包含的源文件由 add_library 制作为库 (比如这个例子中的 libhello_library.a ),然后在 main 的目标上添加链接库属性 使用add_link_libraries ,这是现代 CMake 的推荐做法,而不使用之前 Basic 01-hello-cmake 和 Basic 02-hello-headers 中使用 add_execute 函数添加 main 的头文件对应的源文件供链接使用。
# 指定 Including Directories
在这个例子中我们使用 target_include_directoris() 函数将声明我们的目标包含 include 目录。
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
2
3
4
注意
hello_library 是项目的名称,在这里还是 library target 的名称,当然两者可以不同,他们之间没有仍和联系
因为是可见性为 PUBLIC,根据继承关系 (什么是继承关系可以看我下面的讲解 PUBLIC VS INTERFACE VS PRIVATE),会使得:
- 编译 target 对应的目标库
hello_library.a时会使用这个包含路径 - 编译任何的其他目标,并且这些目标链接了这个库 (比如这个例子中
target_link_libraries(helllo_binary, hello_library)) 时也会包含这个路径${PROJECT_SOURCE_DIR}/include。
简单解释一下 target_include_directories 就是:
PRIVATE - the directory is added to this target’s include directories
INTERFACE - the directory is added to the include directories for any targets that link this library.
PUBLIC - As above, it is included in this library and also any targets that link this library.
(emmm.... 感觉翻译过来就变味了,所以留了原文在这里)
使用建议:
对于公共的头文件,建议把它放在 ./include 文件夹下定义的文件夹中。因为在通过给 target_include_directories 传递包含目录时,我们经常传递包含目录的根目录 ( ./include ) 给目标,因此在 cpp 的源文件中包含头文件时,你必须要从 ./include 下面的目录开始包含。
比如这个例子中,我们的源文件的头文件格式都是:
#include "static/Hello.h"
即从 static 开始,这样做的好处在于:可以防止发生你的头文件名重名。
PUBLIC VS INTERFACE VS PRIVATE:
(关于这里,在 Google 游荡了一天下午,终于算是有点理解了。cmake 真的好复杂啊!其实是我菜😭)
建议直接读 blog:CMake: Public VS Private VS Interface (opens new window) 和 https://kubasejdak.com/modern-cmake-is-like-inheritance (opens new window), 读完其中一个你就懂了。(如果不懂就多读几遍,建议读第一个,我感觉更容易理解些)
要理解上面提到的三者,首先要理解 Modern CMake 是由什么组成的?
Modern CMake = targets + properties
- target: targets 是 cmake 中最基本的单元,它是一个构建流程的构建目标。
笔记
最常见的 target 有可执行目标文件目标 (executabel object target) 和库目标 (library target)。
# 如何创建target
# target为myExecutable对应的可执行文件名为myExecutable
add_executable(myExecutable
main.cpp
)
# target为libA,对应的库为libA.a
add_library(libA
sourceA.cpp
)
add_library(libB
sourceB.cpp
sourceB_impl.cpp
)
add_library(libC
sourceC.cpp
)
# 如何关联不同target
target_link_libraries(myExecutable libA)
target_link_libraries(libA libB)
target_link_libraries(libB libC)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
为了构建 myExecutabel 需要和 libA 链接,为了构建 libA 需要和 libB 链接,为了构建 libB,需要和 libC 链接。
propeties: 每个 target 都有自己的 properties, 最常见的有:
- compilation flags
- linking flags
- preprocessor flags
- C/C++ standard
- include directories
在 cmake 中可以使用以下函数构建:
| propeties | setting method |
|---|---|
| include directories | target_include_directories |
| preprocessor flags | target_compile_definitions |
| compilation flags | target_compile_options |
| linking flags | target_link_options |
| C++ standard | set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) |
Modern CMake 的 target_xxxx_xxxx 和 target_xxxx_xxxx 中还引入了可见性: PUBLIC , PRIVATE and INTERFACE .
- PRIVATE property is only for the internal usage by the property owner,
- PUBLIC property is for both internal usage by the property owner and for other targets that use it (link with it, inherit from it)
- INTERFACE property is only for usage by other libraries.
上面的这三句话中的 other targets 是继承关系中的子对象 (类似于 CPP 中类的继承关系)
接下来就可以解析可见性 PUBLIC , PRIVATE and INTERFACE 为什么像是继承关系了,以 target_include_directories 和 target_link_libraries 为例:
# 解析 target_include_directories
在 CMake 中任何的 target 在预处理阶段,预处理器都会根据 INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES 两个目标的属性来决定搜索哪些头文件进行预处理。
这就很自然的会让人联想到,target 的 INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES 是怎么来的呢?谁让 target 有了这两个属性呢?
答案是:target_include_directories
target_include_directories 根据可见性 <PUBLIC|PRIVATE|INTERFACE> 指定了目标两个属性 ( INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES ).
INCLUDE_DIRECTORIES将被用于生成当前targetINTERFACE_INCLUDE_DIRECTORIES将被用于生成 "其他" 目标,并且这些 "其他" 目标依赖于当前target。
通过这些配置,使得不同目标的继承关系之间变的清楚明了。
下面是对 target_include_directories 中可见性 PUBLIC,PRIVATE 和 INTERFACE 的解析:
- PUBLIC
target_include_directories(target1
PUBLIC
[items1...]
)
target_link_libraries(target2
PRIVATE
target1
)
2
3
4
5
6
7
8
9
注意
target_link_libraries 可以帮助我们把 target1 的 INTERFACE_INCLUDE_DIRECTORIES 属性传递给 target2 INCLUDE_DIRECTORIES
所有的包含路径 (items1) 将被用当前目标 (target1) 和依赖于当前目标 (target1) 的其他目标 (target2)。
换句话说会把这些路径添加到当前目标 (target1) 的 INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES 两个属性中。
并且 target2 获取 target1 的 INTERFACE_INCLUDE_DIRECTORIES 属性,将路径添加到自己的 INCLUDE_DIRECTORIES 属性中。
- PRIVATE
target_include_directories(target1
PRIVATE
[items1...]
)
target_link_libraries(target2
PRIVATE
target1
)
2
3
4
5
6
7
8
9
所有的包含路径 (items1) 将只被用来编译生成当前目标 (target1) 所需要的源文件。
换句话说会把这些路径添加到当前目标 (target1) 的 INCLUDE_DIRECTORIES 属性中,不会添加到 INTERFACE_INCLUDE_DIRECTORIES 属性中。
所以 target2 根据 target1 的 INTERFACE_INCLUDE_DIRECTORIES 属性,就不能将路径 (items1) 添加到自己的 INCLUDE_DIRECTORIES 属性中。
- INTERFACE
target_include_directories(target1
INTERFACE
[items1...]
)
target_link_libraries(target2
PRIVATE
target1
)
2
3
4
5
6
7
8
9
所有的包含路径 (items1) 只会添加到依赖于当前目标 (target1) 的其他目标 (target2) 的包含路径中。
换句话说会把这些路径 (items1) 添加到当前目标 (target1) 的 INTERFACE_INCLUDE_DIRECTORIES 属性中,不会添加到 INCLUDE_DIRECTORIES 属性中。
所以在编译 target1 的源文件时,target1 对应的源文件无法获取路径 (items1)
(别忘了有两种方法指定生成 target 对应的二进制文件或者库需要的源文件,它们分别是 add_executabel and add_library() )
但是 target2 根据 target1 的 INTERFACE_INCLUDE_DIRECTORIES 属性,可以将路径添加到自己的 INCLUDE_DIRECTORIES 属性中。
我们可以在本节的 CMakeList.txt 中证明这一点是正确的,比如我们可以修改本节的 CMakeLists:
target_include_directories(hello_library
PRIVATE
${PROJECT_SOURCE_DIR}/include
)
2
3
4
我们把包含目录的可见性改为 PRIVATE,那么:
target_link_libraries(hello_binary
PRIVATE
hello_library
)
2
3
4
中的可执行目标 hello_binary 无法继承来自于 hello_libray 中的包含路径,因为目标 hello_library 的 INTERFACE_INCLUDE_DIRECTORIES 属性中没有包含路径 {PROJECT_SOURCE_DIR}/include 。
这会导致使用 CMake 构建完 Makefile 后,使用 GNU Make 构建时会产生 main.cpp 无法找到头文件的路径👀,在我的 linux 上运行的到如下错误:
$ mkdir build
$ cd build
$ cmake ..
$ make
Scanning dependencies of target hello_lib
[ 25%] Building CXX object CMakeFiles/hello_lib.dir/src/Hello.cpp.o
[ 50%] Linking CXX static library libhello_lib.a
[ 50%] Built target hello_lib
Scanning dependencies of target hello_binary
[ 75%] Building CXX object CMakeFiles/hello_binary.dir/src/main.cpp.o
/home/tartarus/cmake/cmake-examples/01-basic/C-static-library/src/main.cpp:1:10: fatal error: static/Hello.h: No such file or directory
1 | #include "static/Hello.h"
| ^~~~~~~~~~~~~~~~
compilation terminated.
make[2]: *** [CMakeFiles/hello_binary.dir/build.make:63: CMakeFiles/hello_binary.dir/src/main.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:78: CMakeFiles/hello_binary.dir/all] Error 2
make: *** [Makefile:84: all] Error 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 解析 target_link_libraries
target_link_libraries(destination_target
<PRIVATE | PUBLIC | INTERFACE>
source_target
)
2
3
4
5
target_link_libraries 的两个作用:
target_link_libraries 是不同 target 之间,包含路径的传递桥梁 (source_target 的
INTERFACE_INCLUDE_DIRECTORIES属性会传递给 destination_target 的INCLUDE_DIRECTORIES属性)。
示例见解析 target_include_directories在链接阶段,我们也需要确定给出的 source_target 是否需要被链接。
我也分别解释一下三个可见性的作用:
- PUBLIC: source_target 的目标文件在链接阶段会和 destination_target 一起进行链接,并且如果其他目标依赖于 destination_target,那么也会提供 source_target 供他们链接使用。
- PRIVATE: source_target 的目标文件在链接阶段会和 destination_target 一起进行链接。但是如果其他目标依赖于 destination_target,不会提供 source_target 供他们链接使用。
- INTERFACE: 如果其他目标依赖于 destination_target,那么也会提供 source_target 供他们链接使用,但是 source_target 的目标文件在链接阶段不会和 destination_target 一起进行链接
举例可以看我提供的链接:blog:CMake: Public VS Private VS Interface (opens new window)
# 链接库文件
当我们创建可执行文件 hello_binary 时,因为 main.cpp 中用到了 #include "static/Hello.h" ,而 Hello.cpp 已经被我们制作成了库文件 hello_library.a 对应的目标时 hello.library,因此我们还需要告诉链接器在链接阶段链接这个库文件:
add_executable(hello_binary
src/main.cpp
)
target_link_libraries(hello_binary
PRIVATE
hello_library
)
2
3
4
5
6
7
8
比如本例中连接器就通过如下的指令链接 hello_library.o:
/usr/bin/c++ CMakeFiles/hello_binary.dir/src/main.cpp.o -o hello_binary -rdynamic libhello_library.a
当然了,正如我们在解析 target_link_libraries 讨论过的, target_link_libraries 还可以帮助我们把库目标 hello_binary 的 INTERFACE_INCLUDE_DIRECTORIES 属性传递给目标 hello_binary 的 INCLUDE_DIRECTORIES 的属性,使得生成目标 hello_binary 时能够提供给编译器正确的包含路径。
# 构建本例
$ mkdir build
$ cd build
$ cmake ..
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/lib/ccache/cc
-- Check for working C compiler: /usr/lib/ccache/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/lib/ccache/c++
-- Check for working CXX compiler: /usr/lib/ccache/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tartarus/cmake/cmake-examples/01-basic/C-static-library/build
$ make
Scanning dependencies of target hello_lib
[ 25%] Building CXX object CMakeFiles/hello_lib.dir/src/Hello.cpp.o
[ 50%] Linking CXX static library libhello_lib.a
[ 50%] Built target hello_lib
Scanning dependencies of target hello_binary
[ 75%] Building CXX object CMakeFiles/hello_binary.dir/src/main.cpp.o
[100%] Linking CXX executable hello_binary
[100%] Built target hello_binary
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
从 verbose 输出中有几个地方比较有特点需要解释一下,这样可以对 cmake 中一些命令背后做了什么有个大体的了解:
// 编译汇编Hello.cpp为Hello.cpp.o
/usr/lib/ccache/c++ -I/home/tartarus/cmake/cmake-examples/01-basic/C-static-library/include -o CMakeFiles/hello_library.dir/src/Hello.cpp.o -c /home/tartarus/cmake/cmake-examples/01-basic/C-static-library/src/Hello.cpp
// 将Hello.cpp.o制作为静态库libhello_library.a
/usr/bin/ar qc libhello_library.a CMakeFiles/hello_library.dir/src/Hello.cpp.o
/usr/bin/ranlib libhello_library.a
// 编译汇编的到mian.cpp.o
/usr/lib/ccache/c++ -I/home/tartarus/cmake/cmake-examples/01-basic/C-static-library/include -o CMakeFiles/hello_binary.dir/src/main.cpp.o -c /home/ tartarus/cmake/cmake-examples/01-basic/C-static-library/src/main.cpp
// 链接生成可执行文件
/usr/lib/ccache/c++ CMakeFiles/hello_binary.dir/src/main.cpp.o -o hello_binary libhello_lib.a
2
3
4
5
6
7
8
9
10
11
12