2023-08-28
CMake
Usage Requirements 是 CMake 中一个非常重要的概念。例如,使用 CMake 最常见的需求就是让某个 target 链接到它所依赖的 target(库)——通过 target_link_libraries()
这个命令。在使用这个命令的时候可以使用三种关键字 PUBLIC
、PRIVATE
和 INTERFACE
中的一种来指定如何链接到这个库:
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
这三个关键字是什么意思呢?实际上,它们控制了一个库的 Usage Requirements 应当如何传递。因此,想要理解这三个关键字,首先要理解什么是一个 target 的 Usage Requirements,这就是本文的核心内容。
不过,在讲述 Usage Requirements 之前,需要先明白一个 target 的 Build Specifications 是什么。
Target 的 Build Specifications
所谓 Build Specifications,就是指一个 target 应当如何被编译。具体来说,就是下面这三个:
- 头文件目录,即编译器
-I
选项所指定的内容; - 预处理器定义,即编译器
-D
选项所指定的内容; - 编译选项,如
-std=c++17
。
当我们编译(不考虑链接)一个源文件的时候,必然要考虑上述三种 Build Specifications,如果配置不当,就会编译失败或编译结果不符合预期。
CMake 提供了以下三个命令,分别用于为一个 target 设置上述三种 Build Specifications:
它们的实际作用,就是为某个 target 分别设置以下三个 property 的内容:
当这个 target 被编译的时候,这三个 property 的内容就会被 CMake 拿来设置编译器的 -I
、-D
和其他编译选项。因此,这三个 property 的内容也就是这个 target 的 Build Specifications,它们决定了这个 target 应当如何被编译。
同时,上述三种 property 还有三个对应的 INTERFACE_
版本:
这三个 property 和不带 INTERFACE_
的三个 property 的区别在于:它们不决定当前 target 如何被编译,而是决定当前 target 的依赖者(消费者)如何编译。这个很好理解:当你依赖某个 target 的时候,你很有可能需要增加一些头文件目录、宏定义和编译选项,才能正常和这个 target 链接起来。这三个 property 的内容,就是这个 target 的 Usage Requirements。(谁使用我,谁就要按这些要求来编译。)
如何设置一个 target 的 Usage Requirements
前面说的 target_include_directories()
、target_compile_definitions()
和 target_compile_options()
这三个命令都支持PRIVATE
、PUBLIC
和 INTERFACE
三种 mode:
PRIVATE
模式只设置 非INTERFACE_
版 的 property;INTERFACE
模式只设置INTERFACE_
版 的 property;PUBLIC
模式同时设置两种版本的 property。
例如:
target_compile_definitions(archive
PRIVATE BUILDING_WITH_LZMA
INTERFACE USING_ARCHIVE_LIB
)
上述命令使得 archive
这个 target 的 COMPILE_DEFINITIONS
这个 property 的内容增加 BUILDING_WITH_LZMA
,这是 archive 自己的 Build Specifications;同时使它的 INTERFACE_COMPILE_DEFINITIONS
这个 property 的内容增加 USING_ARCHIVE_LIB
,这是 archive 的 Usage Requirements。
这样,编译器在编译 archive
的时候,就会增加 -DBUILDING_WITH_LZMA
这个定义;而假如另一个 target myapp
链接到了 archive
,那么编译器在编译 myapp
的时候,就会增加 -DUSING_ARCHIVE_LIB
这个定义。这就是这六种 target 的实际意义,确实非常好用。
Usage Requirements 的传递
在实际 C/C++ 的开发中经常出现一个库依赖另一个库的情况。我们就假设是以下这种情况:
依赖 依赖
A ---> B ---> C
会出现以下三种情况中的一种:
- B 依赖 C 仅仅是为了自己的需要(实现),A 甚至意识不到 C 的存在,此时 C 的 Usage Requirements 不需要让 A 也遵守——此时应当使用
target_link_libraries(B PRIVATE C)
,让 C 的 Usage Requirements 只传递给 B,不继续传递下去。 - B 在自己的接口和实现中都使用了 C 的内容,那边 B 的编译肯定需要遵守 C 的 Usage Requirements;而此时 A 使用了 B 提供的接口,那必然也能意识到 C 的存在,也就需要遵守 C 的 Usage Requirements ——此时应当使用
target_link_libraries(B PUBLIC C)
,让 C 的 Usage Requirements 不仅传给 B,还传给 A。 - B 自身的编译不需要使用 C,C 仅仅是 B 接口的一部分。例如一个纯头文件的库 B,B 本身实际上没法编译 —— 此时应当使用
target_link_libraries(B INTERFACE C)
,让 C 的 Usage Requirements 能传递给 A。
也就是说,target_link_libraries()
这个命令的 PUBLIC
、PRIVATE
和 INTERFACE
控制了一个 target 的 Usage Requirements 是如何被传递的。这就解释了文章开头所说的。
总结
理解 CMake Usage Requirements 的核心是理解编译和链接过程,CMake 不过将其进行抽象,用 CMake 自己的语言表达出来罢了。对于一个合格的 C/C++ 开发者来说,编译和链接过程是必然需要深刻理解的,否则在 CMake 的使用过程中就会一头雾水,就像我以前一样 :)