第 5 步:深入了解 CMake 库概念

虽然可执行文件大多是通用的,但库的形式多种多样。仅举几例,就有静态归档库、共享对象库、模块库、对象库、仅含头文件的库,以及描述高级 CMake 属性并可被其他目标继承的库。

在这一步中,您将了解 CMake 可以描述的最常见的一些库类型。这将涵盖 add_library() 在项目内的大多数用法。从依赖项导入的库(或由项目导出以供作为依赖项使用)将在后续步骤中介绍。

背景

正如我们在 Step1 中所学到的,add_library() 命令的第一个参数是所要创建的库目标的名称。第二个参数是可选的 <type>,以下值均有效:

STATIC

静态库(Static Library):用于链接其他目标时的目标文件归档。

SHARED

共享库(Shared Library):一种可由其他目标链接并在运行时加载的动态库。

MODULE

模块库(Module Library):一种插件,不能被其他目标链接,但可以使用类似 dlopen 的功能在运行时动态加载。

OBJECT

对象库(Object Library):一组尚未归档或链接到库中的目标文件。

INTERFACE

接口库(Interface Library):一种库目标,它为依赖项指定使用需求,但不编译源文件,也不会在磁盘上生成库工件。

此外,还有 IMPORTED 库,它们描述了从外部项目或模块导入当前项目的库目标。我们将在后续步骤中简要介绍这些内容。

MODULE 库最常见于插件系统,或作为 Python 或 Javascript 等运行时加载语言的扩展。它们的作用与普通共享库非常相似,只是不能被其他目标直接链接。它们非常相似,因此我们不再深入讨论。

练习 1 - 静态库与共享库

虽然 add_library() 命令支持显式设置 STATICSHARED,且有时这是必要的,但对于大多数既能作为静态库又能作为共享库操作的“普通”库,最好将第二个参数留空。

当未给定类型时,add_library() 将根据 BUILD_SHARED_LIBS 的值创建 STATICSHARED 库。如果 BUILD_SHARED_LIBS 为真,则创建 SHARED 库,否则为 STATIC

add_library(MyLib-static STATIC)
add_library(MyLib-shared SHARED)

# Depends on BUILD_SHARED_LIBS
add_library(MyLib)

这是一种理想的行为,因为它允许打包人员确定生成哪种类型的库,并确保依赖项链接到该版本的库,而无需修改其源代码。在某些上下文中,完全静态构建是合适的,而在其他上下文中,共享库则是首选。

注意

CMake 默认不定义 BUILD_SHARED_LIBS 变量,这意味着在没有项目或用户干预的情况下,add_library() 将产生 STATIC 库。

通过将 add_library() 的第二个参数留空,项目为打包人员和下游依赖项提供了额外的灵活性。

目标

MathFunctions 构建为共享库。

注意

在 Windows 上,您可能会看到关于空 DLL 的警告,因为 MathFunctions 没有导出任何符号。

有用资源

要编辑的文件

没有需要编辑的文件。

开始

Help/guide/tutorial/Step5 目录包含了 Step4 的完整推荐解决方案。这一步是关于构建 MathFunctions 库,不需要进行任何 TODO 操作。您可以直接进入构建步骤。

构建并运行

我们可以使用预设进行配置,通过 -D 标志启用 BUILD_SHARED_LIBS

cmake --preset tutorial -DBUILD_SHARED_LIBS=ON

然后我们可以使用 -t 仅构建 MathFunctions 库。

cmake --build build -t MathFunctions

验证是否为 MathFunctions 生成了共享库,然后重置 BUILD_SHARED_LIBS,可以通过使用 -DBUILD_SHARED_LIBS=OFF 重新配置,或者删除 CMakeCache.txt 来实现。

解决方案

此练习无需对项目进行任何更改。

练习 2 - 接口库

接口库是指那些仅传达其他目标使用需求的库,它们本身不构建或产生任何工件。因此,接口库的所有属性都必须是接口属性本身,并使用 INTERFACE 作用域关键字来指定。

add_library(MyInterface INTERFACE)
target_compile_definitions(MyInterface INTERFACE MYINTERFACE_COMPILE_DEF)

C++ 开发中最常见的接口库类型是仅包含头文件的库。此类库不构建任何东西,只提供发现其头文件所需的标志。

目标

将一个仅含头文件的库添加到教程项目中,并在 Tutorial 可执行文件中使用它。

有用资源

要编辑的文件

  • MathFunctions/MathLogger/CMakeLists.txt

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

开始

在之前关于 target_sources(FILE_SET) 的讨论中,我们提到如果文件集的名称与文件集的类型相同,我们可以省略 TYPE 参数。我们还提到,如果我们想将当前源目录作为唯一的基目录,可以省略 BASE_DIRS 参数。

我们准备引入第三个快捷方式:如果头文件打算被安装(例如库的公共头文件),我们只需要包含 FILES 参数即可。

本练习中的 MathLogger 头文件仅由 MathFunctions 实现内部使用。它们将不会被安装。这应该会使 target_sources(FILE_SET) 的调用变得非常简短。

注意

编译器依赖扫描程序会发现这些头文件,以确保正确的增量构建。无论如何,在这种情况下列出头文件是很有用的,因为该列表可用于生成某些 IDE 所依赖的元数据。

您可以开始编辑 Step5 目录。完成 TODO 1TODO 7

构建并运行

预设已更新为使用 mathfunctions::sqrt 而不是 std::sqrt。我们可以像往常一样进行构建和配置。

cmake --preset tutorial
cmake --build build

验证 Tutorial 的输出现在是否使用了日志框架。

解决方案

首先,我们添加一个名为 MathLogger 的新 INTERFACE 库。

TODO 1:点击显示/隐藏答案
TODO 1: MathFunctions/MathLogger/CMakeLists.txt
add_library(MathLogger INTERFACE)

然后我们添加适当的 target_sources() 调用来捕获头文件信息。我们给这个文件集命名为 HEADERS,这样我们就可以省略 TYPE;我们不需要 BASE_DIRS,因为我们将使用当前源目录的默认值;我们也可以省略 FILES 列表,因为我们不打算安装该库。

TODO 2:点击显示/隐藏答案
TODO 2: MathFunctions/MathLogger/CMakeLists.txt
target_sources(MathLogger
  INTERFACE
    FILE_SET HEADERS
)

现在我们可以将 MathLogger 库添加到 MathFunctions 的链接库中,并将 MathLogger 文件夹添加到项目中。

TODO 3-4: 点击显示/隐藏答案
TODO 4: MathFunctions/CMakeLists.txt
add_subdirectory(MathLogger)

最后,我们可以更新 MathFunctions.cxx 以利用新的记录器。

TODO 5-7: 点击显示/隐藏答案
TODO 5: MathFunctions/MathFunctions.cxx
#include <cmath>
#include <format>

#include <MathLogger.h>
TODO 6: MathFunctions/MathFunctions.cxx
mathlogger::Logger Logger;
TODO 7: MathFunctions/MathFunctions.cxx
Logger.Log(std::format("Computing sqrt of {} to be {}\n", x, result));

练习 3 - 对象库

对象库有几种高级用途,但也存在一些难以在本教程范围内完全列举的棘手细微差别。

add_library(MyObjects OBJECT)

对象库最明显的缺点是对象本身不能被传递链接。如果一个对象库出现在目标的 INTERFACE_LINK_LIBRARIES 中,链接该目标的依赖项将“看不到”这些对象。在这种情况下,对象库的作用类似于 INTERFACE 库。一般情况下,对象库仅适用于通过 target_link_libraries() 进行 PRIVATEPUBLIC 使用。

对象库的一个常见用例是将多个库目标合并为一个归档或共享库对象。即使在单个项目中,库也可能出于多种原因(例如属于组织内不同的团队)而作为不同的目标进行维护。然而,有时希望将它们作为单个面向消费者的二进制文件进行分发。对象库使这成为可能。

目标

MathFunctions 库添加多个对象库。

有用资源

要编辑的文件

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.h

  • Tutorial/Tutorial.cxx

入门

我们已经提供了 MathFunctions 库的几个扩展(我们可以想象这些扩展来自我们组织内的其他团队)。花点时间查看 MathFunctions/MathExtensions 中提供的目标。然后完成 TODO 8TODO 11

构建和运行

无需重新配置,我们可以像往常一样进行构建。

cmake --build build

验证 Tutorial 的输出现在是否包含验证消息。还要花点时间检查 build/MathFunctions/MathExtensions 下的构建目录。您应该发现,与 MathFunctions 不同,没有任何对象库产生归档文件。

解决方案

首先,我们将所有对象库的链接添加到 MathFunctions。它们是 PUBLIC 的,因为我们希望将对象作为其自身构建步骤的一部分添加到 MathFunctions 库中,并且我们希望该库的使用者能够使用这些头文件。

然后我们向项目添加 MathExtensions 子目录。

TODO 8-9: 点击显示/隐藏答案
TODO 9: MathFunctions/CMakeLists.txt
add_subdirectory(MathExtensions)

为了让使用者能够使用这些扩展,我们在 MathFunctions.h 头文件中包含了它们的头文件。

TODO 10: 点击显示/隐藏答案
TODO 10: MathFunctions/MathFunctions.h
#include <OpAdd.h>
#include <OpMul.h>
#include <OpSub.h>

最后,我们可以利用 Tutorial 程序中的这些扩展。

TODO 11: 点击显示/隐藏答案
TODO 11: Tutorial/Tutorial.cxx
double const checkValue = mathfunctions::OpMul(outputValue, outputValue);
std::cout << std::format("The square of {} is {}\n", outputValue,
                         checkValue);