第五步:深入 CMake 库概念

虽然可执行文件大多是千篇一律的,但库却有很多不同的形式。有静态库、共享库、模块库、对象库、仅头文件库,以及描述要由其他目标继承的高级 CMake 属性的库,这仅仅是其中的一小部分。

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

背景

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

STATIC

一个 静态库:一个用于链接其他目标的のでオブジェクトファイルのアーカイブ。

SHARED

一个 共享库:一个动态库,可以被其他目标链接并在运行时加载。

MODULE

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

OBJECT

一个 对象库:一组未归档或未链接到库中的对象文件。

INTERFACE

一个 接口库:一个库目标,它为依赖项指定使用要求,但不编译源文件,也不在磁盘上生成库文件。

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

MODULE 库最常用于插件系统,或作为 Python 或 JavaScript 等运行时加载语言的扩展。它们与普通共享库非常相似,只是不能被其他目标直接链接。它们足够相似,我们在这里不会深入介绍。

练习 1 - 静态库和共享库

虽然 add_library() 命令支持显式设置 STATICSHARED,并且这有时是必要的,但最好将第二个参数留空,以便大多数“普通”库可以作为两者使用。

当未指定类型时,add_library() 将创建 STATICSHARED 库,具体取决于 BUILD_SHARED_LIBS 的值。如果 BUILD_SHARED_LIBS 为 true,将创建一个 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);