【05】目标构建
1. 前期准备
在开始之前,我们先了解一些基本的知识并回顾【01】安装与基本介绍的内容作为前期准备
1.1 编译的 4 个步骤
编译型语言编译一般都有 4 个步骤,分别是:,这里的编译仅指将源代码翻译成汇编代码。而我们平时称的编译则指将源代码翻译成可执行文件的整个过程。Ubuntu 下常见的编译工具是 Unix Makefiles,我们在终端输入
make
的时候,用到的就是 Unix Makefiles。而所有的编译工具均会根据指定的或者默认的环境变量,例如在 Makefile 文件中,可能可以找到类似
CC = gcc
CXX = g++
的内容,从而选择指定的编译器。编译器则根据编译工具所制定的规则对一系列源码进行解析、编译、链接,例如可以在 Makefile 文件中指定
CFLAGS = -g -O2
并使用类似
$(CC) $(CFLAGS)
的语句完成对编译器相关编译选项的设置,最终在编译时生成对应的符合要求的目标(可执行文件、动态库、静态库)
1.2 回顾 CMake 的地位
CMake 作为一个跨平台的编译工具,能够使用通用的语法(组态档)生成编译工具(包括Unix Makefiles、Ninja、Visual Studio……)能够解析的文件(建构档),从而完成对项目的构建。
1.3 动态库 / 静态库
1.3.1 是什么
在 Windows 下,经常会看到扩展名为.dll
的文件,称为动态链接库Dynamic-Link Library
。同样,在Ubuntu下/usr/local/lib
目录下也能看到一系列名为*.so
的文件,称为共享对象Shared Object
,这一类都属于动态库。
另外,在 Windows 下也有部分扩展名为*.lib
的文件,Ubuntu 下也许能在/usr/local/lib
目录下看到一些*.a
文件,这些都属于静态库。
提示
Windows 下的*.lib
文件有两种,一种是静态库,一种是DLL导入库,后者居多,不过在 CMake 开发上简单当做静态库也是可以的。
1.3.2 区别
静态库 这种库在编译的时候会直接整合到目标程序中,所以利用静态库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。
动态库 与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个符号而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去解析这个符号并完成动态库地址的重定位,也就是说可执行文件无法单独运行。这样从产品功能升级的角度来说是方便的,只要替换对应动态库即可完成升级,不必重新编译整个可执行文件。
2. 目标构建
2.1 回顾:可执行文件构建
现给定一个文件架构:
.
├── CMakeLists.txt (待创建)
└── main.cpp
这是最简单的一类情况,即仅有一个 *.cpp
构成可执行程序的源文件。对于这种架构的构建方式在【01】安装与基本介绍中已经做过了演示。对于这种通常只需要短短几行 CMake 语句即可完成,首先在当前目录直接创建 CMakeLists.txt,输入:
cmake_minimum_required(VERSION 3.16) # 指定 CMake 最小版本号,小于此版本的无法通过 CMake
project(MyProject) # 定义项目名
add_executable(my_main main.cpp) # 使用 main.cpp 生成可执行文件 my_main
其中my_main
是目标名,在 CMake 中是具有TARGET
属性的变量,即目标,同时也是可执行文件的文件名。CMake 提供了一系列与目标链接有关的操作方式。其中如果my_main
目标依赖了其他的目标,例如main.cpp
文件中使用到了来自其他*.cpp
文件定义的函数,我们还需要在CMakeLists.txt
中输入
target_link_libraries(
my_main
PRIVATE xxxx
)
这里的xxxx
就表示包含了这些函数定义的目标。这里表示需要将my_main
这个目标链接至xxxx
上。
此外,文档中给出了这样一句话:
New in version 3.13: The
<target>
doesn't have to be defined in the same directory as thetarget_link_libraries
call.
翻译过来就是
3.13 版本的新内容:
<target>
不需要在与调用target_link_libraries
命令相同的目录中被定义。
这句话很关键,表示在较新的 CMake 版本中,允许在整个项目中跨CMakeLists.txt
文件进行链接操作,例如,即使上文的my_main
在父目录的作用域,xxxx
在子目录的作用域,my_main
所在的CMakeLists.txt
也能访问到xxxx
这一目标变量。这一点有别于普通的变量,可以当做是具有目标属性的变量的专属功能。
另外代码中还出现了PRIVATE
的标志,这个属于目标的传播方式,在后文会进行介绍。
2.2 普通库目标构建
在文件结构复杂时,或者需要将功能模块隔离、抽象出来时,经常需要为某个模块构建一个库,最终将这些库链接至可执行文件的目标上。
语法是:add_library(目标名 [SHARED | [STATIC]] 源文件)
还是刚才的文件结构,先创建新的文件夹及文件:
.
├── CMakeLists.txt
├── main.cpp
└── MyLib1
├── CMakeLists.txt (待创建)
├── include
│ └── MyLib1.h
└── src
├── a.cpp
├── b.cpp
└── c.cpp
首先项目根目录的CMakeLists.txt
需要添加
add_subdirectory(MyLib1)
来让该模块添加至该项目。此外,每个库需要有一个单独的CMakeLists.txt
进行管理,因此需要在MyLib1
文件夹内创建该文件,内容如下:
# 搜索 src 文件夹下的所有源文件,并添加至局部变量:my_lib_dir
# 题外话:aux 表示 auxiliary 即辅助的意思,表示这个功能是个辅助功能
aux_source_directory(src my_lib_dir)
# SHARED 表示该目标为动态库,STATIC 则表示静态库
add_library(MyLib1 SHARED ${my_lib_dir})
# 以上内容也可以写成下一行,但这么长的名字,谁会这么做呢?
# add_library(MyLib1 SHARED src/a.cpp src/b.cpp src/c.cpp)
注意,此处生成的动态库 libMyLib1.so
无法做到内存上的复用,在多个进程使用到该动态库时,仍然会开辟一块内存存储该动态库内容。为了真正实现内存上的复用,需要使用地址无关代码机制(position-independent code):
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
话说回来,本库内的三个源文件都会包括 include
头文件夹中的内容,因此在编译时需要给目标添加头文件的搜索路径:
target_include_directories(
MyLib1
PUBLIC include # 表示当前 CMakeLists.txt 所在目录下的 include 文件夹
)
上文的 PUBLIC
属性,有时候还会见到 INTERFACE
、PRIVATE
属性,一共 3 个属性。下面对这三个属性做个介绍:
PUBLIC
:在绑定当前目标时给指定的内容设置公有属性,其他目标在链接当前目标时,能访问这些指定的内容PRIVATE
:在绑定当前目标时给指定的内容设置私有属性,其他目标在链接当前目标时,无法访问这些内容INTERFACE
:在绑定当前目标时给指定的内容设置接口属性,通常在接口库中使用。其他目标在链接当前目标时,只允许访问其声明(接口)
最后,如果此目标有依赖
- 本项目中其他目标
- 项目以外第三方目标(例如 OpenCV)
的内容,那必须要添加以下内容:
target_link_libraries(
MyLib1
PUBLIC xxxx
)
同样,为指定目标链接库的语句也提供了 PUBLIC
、PRIVATE
、INTERFACE
三个属性,这里不再赘述。
这里涉及到的两个语句,我们做个汇总:
target_include_directories
:为目标添加需要包含的文件(一般是头文件)的搜索路径,该路径将绑定至目标上target_link_libraries
:为目标添加依赖的库,该库也将绑定至目标上
2.3 接口库目标构建
在程序开发中,有时候会遇到只有头文件(*.h
、*.hpp
),而没有源文件(*.c
、*.cpp
)的情况,而我们知道,一般在使用 add_library
的时候只能为源文件(*.h
以及*.hpp
之类的头文件不属于源文件)生成目标库。
在这种情况下,如果我们需要对只有头文件的库生成目标,并且进行链接,我们需要创建接口库。这种目标由于没有源文件,不会实质性的参与构建(编译),但提供了与普通目标相同的操作方式,因此接口库属于伪目标。
还是刚才的文件结构,先创建只有头文件的文件夹及 CMakeLists.txt
:
.
├── CMakeLists.txt
├── main.cpp
├── MyLib1
│ ├── CMakeLists.txt
│ ├── include
│ │ └── MyLib1.h
│ └── src
│ ├── a.cpp
│ ├── b.cpp
│ └── c.cpp
└── MyLib2
├── CMakeLists.txt (待创建)
└── include
└── MyLib2.hpp
首先项目根目录的 CMakeLists.txt
需要添加
add_subdirectory(MyLib2)
创建接口库的语法与普通库类似,只是少了源文件的添加的步骤:
add_library(MyLib2 INTERFACE)
target_include_directories(
MyLib2
INTERFACE include # 接口库的目标只能使用 INTERFACE 属性
)
target_link_libraries(
MyLib2
INTERFACE xxx
)
2.4 导入目标构建
我们会遇到这种情况,仅提供了若干头文件和若干库文件(例如 *.so
和 *.a
)在这种情况下我们无法通过自己 add_library
从源文件创建目标,我们需要引入导入目标。
假设某家相机厂商的 SDK 提供了以下内容,假设将其放在了项目文件夹的 camera
文件夹下,请根据以下文件结构创建一个 CMake 目标
.
├── include
│ ├── CameraApi.h
│ ├── CameraDefine.h
│ └── CameraStatus.h
└── lib
└── libMVSDK.so
在 camera
文件夹中创建 CMakeLists.txt
文件,写入
add_library(camera SHARED IMPORTED)
set_target_properties(camera PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/include"
IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/libMVSDK.so"
)
对于 INTERFACE_INCLUDE_DIRECTORIES
的属性,我们也可以用常规的写法,写成
target_include_directories(
camera
INTERFACE include
)
注意
- 在设置
IMPORTED_LOCATION
属性的时候需要指定绝对路径 - 对于
lib
目录中存在多个*.so
等动态库的情况下,请找到真正的动态库(其他动态库一般是陪衬的)
3. 补充内容
3.1 其余目标库
根据CMake官网对目标库的划分,可简单分为两类:二进制目标、伪目标,其大致内容如下
├── 二进制目标
│ ├── 可执行文件
│ └── 二进制库
│ ├── 普通库(动态库、静态库)
│ └── 对象库
└── 伪目标
├── 导入目标
├── 别名目标
└── 接口库
3.1.1 对象库
允许将多个源文件设置为一个单独的目标,而不生成可执行文件或普通库。对象库可以被其他目标(例如可执行文件或普通库)链接和使用。
要创建一个对象库,同样可以使用 add_library
命令,并将 OBJECT
关键字与源文件列表一起使用。下面是一个 CMakeLists.txt
示例:
# 添加对象库
add_library(
my_object_lib
OBJECT file1.cpp file2.cpp file3.cpp
)
# 添加可执行文件,并链接对象库
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE my_object_lib)
在上面的示例中,add_library
命令创建了一个名为 my_object_lib
的对象库,并将 file1.cpp
、file2.cpp
和 file3.cpp
作为源文件。
通过这种方式,对象库中的源文件将会被编译为目标文件,但不会生成一个独立的可执行文件或共享库。其他目标可以链接到对象库,并在链接时使用其中的目标文件。
3.1.2 别名目标
允许为现有的目标创建一个可供引用的别名。别名目标可以用于简化构建过程、重命名目标或创建便于引用的名称。
要创建别名目标,可以使用 add_library
、add_executable
或 add_custom_target
命令,并将 ALIAS
关键字与目标名称一起使用。下面是一个 CMakeLists.txt
示例:
# 添加一个库
add_library(my_library STATIC my_source.cpp)
# 创建别名目标
add_library(lib1 ALIAS my_library)
# 添加可执行文件,并链接别名目标
add_executable(my_app main.cpp)
target_link_libraries(
my_app
PRIVATE lib1
)
但要注意,别名目标(例如上文中的 lib1
)不得作为
target_link_libraries
target_include_directories
的目标名,即不得写成
target_include_directories(
lib1
PUBLIC xxx
)
# 或者
target_link_libraries(
lib1
PRIVATE xxx
)
的形式
3.2 设置目标属性
即使用命令 set_target_properties
一般情况下,目标属性通常很少直接使用,一般是间接使用,例如
- 在使用
target_include_directories
的时候,会为目标设置INTERFACE_INCLUDE_DIRECTORIES
属性 - 在使用
target_link_libraries
的时候,会为目标设置INTERFACE_LINK_LIBRARIES
属性
实际上,对于导入目标、接口库目标,我们可以不通过以上命令来实现对应功能,譬如说,为了指定目标包含的目录,原先写为
target_include_directories(
my_target
PUBLIC include
)
现在可以通过使用
set_target_properties(
my_target PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/include
)
或者
set_property(
TARGET my_target PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/include
)
来实现同样功能,此操作在配置导入目标的时候比较常见。此外,CMake 官网提供了非常非常多的属性,可以点击此处查阅。
注意
在设置 INTERFACE_INCLUDE_DIRECTORIES
和 INTERFACE_LINK_LIBRARIES
属性的时候需要指定绝对路径
3.3 非必要不使用全局包含、链接
非必要情况下不要使用
link_library
include_directories
语句,会影响所有子目录的构建,带来污染问题。但有些情况下这些语句可以实现其他功能。例如 target_include_directories
可以将内容绑定到目标并且在导出目标后依赖关系仍然生效,而使用 include_directories
可以避免安装后仍然存在依赖关系。具体原因和实现细节可参考【10】项目的导出与安装 #3.4。
3.4 访问库的方式
当需要访问第三方库的时候,第三方库需要提供头文件(类似于 *.h
和 *.hpp
)和库文件(包括动态库 *.so
和静态库 *.a
两种形式)。
提示
Windows 下的库文件有两种形式
- 动态库
*.dll
以及导入库*.lib
- 静态库
*.lib
在访问本项目的库
A
时,如果已经使用了target_include_directories
的PUBLIC
属性,将头文件路径绑定在了目标A
中,那么目标B
在访问A
的时候,只需要target_link_libraries(B PUBLIC A)
即可访问到
A
所包含的头文件路径。在访问其他项目的第三方库时,用户需要单独获取对应的库文件与头文件路径进行链接,例如 OpenCV:
add_executable(xxx main.cpp) target_include_directories( xxx PUBLIC ${OpenCV_INCLUDE_DIRS} ) target_link_libraries( xxx PRIVATE ${OpenCV_LIBS} )
但实际上,
OpenCV_LIBS
是包含了众多导入目标的列表变量,例如opencv_core;opencv_highgui
,并且列表中的每个变量都设置了INTERFACE_INCLUDE_DIRECTORIES
的属性。不写target_include_directories
命令也是正确的:add_executable(xxx main.cpp) target_link_libraries( xxx PRIVATE ${OpenCV_LIBS} )