一、前言
我们在文章弄清楚object库、静态库以及动态库里面详细介绍了三种主要类型的库的由来以及优势。本文主要介绍使用cmake如何使用源码、object库以及混编的方式生成静态库。
二、源码组织
我们编写代码的时候,经常需要把相同的功能聚合到一个文件夹里面方便组织。使用cmake管理的时候,每个文件夹下面都放一个CMakeLists.txt
来方便管理。
本章代码以及对应的cmake文件组织如下:
可以看到,每个主目录都有一个CMakeLists.txt
把整个工程组织成树状结构。我们试图把add的功能以及mul的功能一起作为一个库对外提供。
代码可以从github clone下来实验:learning cmake
三、使用变量生成库
我们可以使用变量来收集所有需要的源码,然后一起编译成一个静态库。我们先来看顶层的CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(chp2)
set(CXX_STANDARD 17)
set(CXX_STANDARD_REQUIRED TRUE)
set(CXX_EXTENSION FALSE)
add_subdirectory(src)
我们首先设置了要求的cmake最低需要3.20版本,然后创建了一个叫"chp2"的工程。这个工程名对应于Xcode或者Visual Studio里面的工程名,和实际的可执行文件或者库的名称没有直接关系。
set(CXX_STANDARD 17)
设置全局c++的标准为使用17标准,设置完之后,cmake会尽量去找符合标准的编译器和库,但是如果找不到,也不会报错,会降级寻找,假设找到14的版本就用14的版本。假如我们的代码里面用了c++17的东西,也就是低版本的标准无法编译,那我们就加上一个强制要求,这个强制要求就使用
set(CXX_STANDARD_REQUIRED TRUE)
经常有很多编译器的扩展,不在标准c++里面,我们一般不启用。不启用的话使用
set(CXX_EXTENSION FALSE)
假设我们要启动的话,就把FALSE
修改成TRUE
。
上面的set的通用语法是
set(<variable> <value>... [PARENT_SCOPE])
也就是把一个变量设置成对应的值。我们刚才是把cmake的三个标准变量设置成了我们需要的值。需要注意的是<value>...
也就是说set
函数是可以把一个变量设置成一个分号分隔的值。
比如
set(A b c)
那么A的值是"b;c"。所以上面语句等效于set(A b;c)
或者set(A "b;c")
。
最后一句add_subdirectory(src)
是把src
目录加入到编译系统,对应的目录src
里面必须要有CMakeLists.txt
文件。
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
需要注意的是cmake使用add_subdirectory
加入目录的同时也加入了层级隔离:父目录的变量在子目录的cmake里面可以访问,但是子目录的在父目录是不能访问的。类似与一个c++函数一样,在函数里面可以访问全局变量,全局位置却不能直接访问函数内部的变量。
我们看一下src
目录里面的CMakeLists.txt
add_subdirectory(add)
add_subdirectory(mul)
add_library(math STATIC ${add_src} ${mul_src})
target_include_directories(math PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/add/include
${CMAKE_CURRENT_SOURCE_DIR}/mul/include)
首先先把add, mul
两个目录加入了编译系统。同样的,这两个目录里面必须要有CMakeLists.txt
文件。我们先看一下add
目录的CMakeLists.txt
,里面只有一个语句
set(add_src ${CMAKE_CURRENT_SOURCE_DIR}/fadd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/iadd.cpp PARENT_SCOPE)
set
函数我们之前已经接触过,上面的语句就是把add_src
这个变量赋值为后面两个文件的路径。${CMAKE_CURRENT_SOURCE_DIR}
代表当前正在处理的CMakeLists.txt
文件所在目录的绝对地址。所以${CMAKE_CURRENT_SOURCE_DIR}/fadd.cpp
就是fadd.cpp
的绝对路径。那最后的PARENT_SCOPE
代表什么呢?记得我们之前说过,add_subdirectory
创建了变量访问的层级关系,子目录可以读取父目录的变量,但是父目录的CMakeLists.txt
无法读取子目录的变量。PARENT_SCOPE
就是让子目录的变量可以在直接父目录可以访问,这样方便把文件列表传递出去。
再回到src
目录里面的CMakeLists.txt
,两个add_subdirectory
之后就是add_library
命令。我们可以看到,这里面使用了子目录的变量,同时也可以看到cmake里面对于变量的访问是通过${变量名}
的方式。
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>...])
add_library
可以创建很多类型的库,我们这里使用STATIC
关键字表明创建的是一个静态库。静态库需要编译的源文件就是add和mul目录里面的cpp文件。
我们知道,源文件在编译的时候需要做预处理,这一步就需要访问include的头文件。我们怎么样告诉编译系统去哪寻找对应的头文件呢?这里就是使用target_include_directories
来达到这个目标。
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
这里面SYSTEM
表明把对应的目录看成是系统目录,这样就可以使用<...>
的模式来包含。PRIVATE
等几个选项比较重要:
PRIVATE:后面的包含目录只有构建当前目标程序可见,使用该目标文件的使用方并不能看到这些目录。比如我们刚才的例子,
add/include
和mul/include
在编译math
的时候对于编译系统可见,会去这两个目录查找对应的.h
文件。但是假设可执行程序A使用了math库,这两个目录地址并不会传递给A,也就是编译A的时候编译系统并不知道要去这两个目录查找头文件;INTERFACE:告诉使用该目标文件的目标文件去对应的目录查找地址,当前目标文件编译的时候并不知道去对应的目录查找。比如上面的
PRIVATE
改成INTERFACE
的话,编译math的时候就不会知道去这来个目录查找头文件,但是编译可执行文件A的时候知道去这两个目录查找;PUBLIC:当前目标和使用当前目标文件的目标文件都会去对应的目录查找头文件。比如上面的
PRIVATE
改成PUBLIC
的话,编译math的和编译可执行文件A的时候都知道去这两个目录查找对应的include头文件。
上面步骤联合起来,就是先从add和mul收集需要编译的cpp文件,然后创建一个静态库编译对应的文件。我们在chp2_1根目录执行
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
就会在build/src
目录生成libmath.a
。
我们执行命令nm ./build/src/libmath.a
可以得到
fadd.cpp.o:
0000000000000000 T _fadd
iadd.cpp.o:
0000000000000000 T _iadd
fmul.cpp.o:
0000000000000000 T _mul
imul.cpp.o:
0000000000000000 T _imul
说明libmath.a
静态库里面确实把add和mul对应cpp的内容编译进去了。
四、使用链接object库的方式编译静态库
我们在文章弄清楚object库、静态库以及动态库里面详细介绍了object库、静态库以及动态库方面的知识,也知道cpp文件经过编译之后可以生成object文件,object文件进行打包就可以得到静态库。
这一些列操作,我们也可以直接在cmake里面实现。
首先我们看一下chp2_2里面add目录下的CMakeLists.txt
(建议感兴趣的同学先clone源码,然后对着看)
set(add_src ${CMAKE_CURRENT_SOURCE_DIR}/fadd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/iadd.cpp)
add_library(add OBJECT ${add_src})
target_include_directories(add PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
里面第一条和第三条我们都比较熟悉了,在上面一节里面都讲过用途。第二条add_library(add OBJECT ${add_src})
表示把add目录下的cpp生成一个object库,这个库叫做add
。
再来看一下src目录下的CMakeLists.txt
add_subdirectory(add)
add_subdirectory(mul)
add_library(math STATIC)
target_link_libraries(math PRIVATE add mul)
前面两条指令是把add和mul目录加入编译系统,也通过他们各自的CMakeLists.txt
生成了add和mul两个object库。add_library(math STATIC)
生成了一个静态库目标,只是我们没有提供源码。最后一行target_link_libraries
让改静态库链接两个object库。
这里其实是有一个很奇怪的问题:静态库是没有链接过程的,但是这里我们却使用了target_link_libraries
让静态库“链接”两个object库。
这里我们权且把这当成cmake的一个不大明确表达含义的命令复用吧。这个命令实际上在这起到的作用是把add和mul对应的objects打包成静态库。
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
运行下面命令可以生成对应的静态库
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
同样也可以使用nm
命令检查生成的库,可以发现同样包含了add以及mul里面的全部功能。
使用object库有几个好处:
每个object库知道自己的include路径就好了,不需要顶层的target知道;
可以同时使用object生成静态库和动态库,不需要两次经历从cpp到object的过程,一次编译即可;
可以加速大型工程的编译速度。
五、使用object库和源文件生成静态库
本节在src加入了ifma.cpp
以及对应的include目录,代码结构变成
add以及mul目录的CMakeLists.txt
跟chp2_2一样,都生成了object库。我们来看一下src下面的CMakeLists.txt
add_subdirectory(add)
add_subdirectory(mul)
add_library(math STATIC ifma.cpp)
target_include_directories(math PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(math PRIVATE add mul)
变化的是下面两行
add_library(math STATIC ifma.cpp)
target_include_directories(math PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
第一行是在创建静态库目标的时候给出了src目录下的ifma.cpp
源文件,然后第二行包含了对应的目录。也就是其实对于math静态库来说,需要把ifmac.cpp
编译成object文件,然后和add以及mul里面已经编译出来的object文件一起打包成静态库math。
用上面的步骤编译得到静态库,然后使用nm
命令check会发现确实我们需要的目标文件都打包到了静态库里面。
六、使用静态库生成静态库
之前我们使用纯源文件的方式、使用object库的方式以及object库和源码混编的方式生成了静态库。我们是否可以用静态库链接静态库的方式来生成最终的静态库呢?
答案是:不可以。
在chp2_4里面,我们把add和mul两个目录里面的CMakeLists.txt
都修改成了生成静态库:
set(add_src ${CMAKE_CURRENT_SOURCE_DIR}/fadd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/iadd.cpp)
add_library(add STATIC ${add_src})
target_include_directories(add PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
src目录下的CMakeLists.txt
相对于chp2_3没有改变。我们编译一下,然后使用nm ./build/src/libmath.a
ifma.cpp.o:
U _iadd
0000000000000000 T _ifma
U _imul
我们发现静态库是无法把静态库编进去的,哪怕加上whole-archive
也不可以,因为静态库没有链接阶段。在这种使用的情况下,最后使用math库的程序编译的时候还需要加上add和mul对应的静态库才可以。
评论区