cmake-cxxmodules(7)¶
版本 3.28 新增。
C++20 引入了“模块 (modules)”的概念。该设计要求构建系统能够安排编译顺序,以可靠地满足 import 语句。CMake 的实现方式是:在构建过程中让编译器扫描源文件以获取模块依赖关系,整理扫描结果以推断排序约束,并告知构建工具如何动态更新构建图。
编译策略¶
有了 C++ 模块,编译一组 C++ 源文件不再是极易并行的任务了。也就是说,为了提供 C++ 编译器满足其他源文件中 import 语句所需的“BMI”(或“CMI”),任何给定的源文件可能需要先编译另一个源文件。在使用头文件时,源文件可以共享声明,使得任何使用者都能独立编译。而使用模块时,编译器现在会在编译期间根据源文件内容及其 export 语句生成 BMI 文件。这意味着,为了确保构建正确,且无需在每次源文件更改时都重新生成构建图(运行配置和生成步骤),必须在构建阶段从源文件中确定正确的排序。
构建系统必须能够对构建图中的这些编译进行排序。有多种策略适合此目的,但每种策略都有优缺点。CMake 使用“扫描”步骤策略,这是 CMake 用户在构建环境中能感知到的与模块相关的最明显的变化。CMake 提供了多种方法来控制源文件的扫描行为。
扫描控制¶
源文件是否会被扫描以查找 C++ 模块使用情况取决于以下查询。系统会使用第一个能决定是否扫描的查询结果。
如果源文件属于
CXX_MODULES类型的文件集,它将被扫描。如果目标(target)未至少使用 C++20 标准,则不会扫描。
如果源文件的语言不是
CXX,则不会扫描。如果设置了
CXX_SCAN_FOR_MODULES源文件属性,则使用其值。如果设置了
CXX_SCAN_FOR_MODULES目标属性,则使用其值。设置CMAKE_CXX_SCAN_FOR_MODULES变量可在创建所有目标时初始化此属性。否则,如果编译器和生成器支持扫描,则扫描该源文件。请参阅策略
CMP0155。
请注意,任何被扫描的源文件都将从任何统一构建(unity build,请参阅 UNITY_BUILD)中排除,因为与模块相关的语句只能在 C++ 翻译单元中的一个位置出现。
编译器支持¶
CMake 支持扫描 C++ 模块源文件的编译器列表包括:
MSVC 工具集 14.34 及更高版本(随 Visual Studio 17.4 及更高版本提供)
LLVM/Clang 16.0 及更高版本
GCC 14 及更高版本
import std 支持¶
对 import std 的支持仅限于以下工具链和标准库组合:
Clang 18.1.2 及更高版本,配合标准库
libc++或libstdc++MSVC 工具集 14.36 及更高版本(随 Visual Studio 17.6 及更高版本提供)
GCC 15 及更高版本
注意
26.04 之前的 Ubuntu 版本附带了损坏的
libstdc++.modules.json文件。请参阅 Ubuntu 问题 2141579。
CMAKE_CXX_COMPILER_IMPORT_STD 变量列出了在当前活跃的 C++ 工具链中支持 import std 的标准版本。
此外,目前只有 Ninja 生成器 支持 import std,因为 Visual Studio 生成器 不支持为 IMPORTED 目标构建 BMI。
注意
此支持仅在通过 CMAKE_EXPERIMENTAL_CXX_IMPORT_STD 开关启用实验性 import std 支持时提供。
生成器支持¶
支持扫描 C++ 模块源文件的生成器列表包括:
请注意,Ninja 生成器 需要 ninja 1.11 或更高版本。
限制¶
当前 CMake 中的 C++ 模块支持存在一些已知局限性。此处不列出编译器的已知局限性或错误,因为它们可能会随时间改变。
对于所有生成器:
头文件单元 (Header units) 不受支持。
仅支持 Visual Studio 2022 和 MSVC 工具集 14.34 (Visual Studio 17.4) 及更新版本。
不支持导出或安装 BMI 或模块信息。
不支持从带有 C++ 模块(包括
import std)的IMPORTED目标编译 BMI。不会诊断由
PUBLIC模块源使用PRIVATE源提供的模块的情况。
此外,作为一项设计选择,CMake 不会为导入的目标表示配置无关的模块映射。IMPORTED_CXX_MODULES_<CONFIG> 目标属性始终与特定配置绑定。在从/向不感知配置的构建系统导入/导出目标时,这可能会导致一些冲突。未来的工作将缓解这一限制。
用法¶
故障排除¶
本节旨在回答有关 CMake 实现的常见问题,并帮助诊断或解释 CMake C++ 模块支持中的错误。
文件扩展名支持¶
CMake 对任何模块类型的文件扩展名没有强制要求。虽然不同工具链之间存在偏好(例如,MSVC 上为 .ixx,Clang 上为 .cppm),但并没有普遍公认的扩展名。因此,CMake 仅要求该文件被识别为 CXX 语言源文件。默认情况下,任何被识别的扩展名都可以,但 LANGUAGE 属性也可以与任何其他扩展名一起使用。
文件名要求¶
模块的名称与声明该模块的文件名或路径没有任何关系。C++ 标准对此没有要求,CMake 也没有。然而,在缺乏类似 IDE 的“查找符号”功能的开发环境中(例如代码审查平台),在项目中采用某种模式进行命名可能有助于导航。
无模块扫描¶
对于尚未采用模块的项目,一个常见问题是源文件的冗余扫描。这通常发生在 C++20 项目开始使用 CMake 3.28,或者 3.28 感知项目开始使用 C++20 时。这两种情况最终都会将 CMP0155 设置为 NEW,从而默认启用 C++20 或更高版本的 C++ 源扫描。项目关闭此功能的最简单方法是在其顶级 CMakeLists.txt 文件顶部附近添加:
set(CMAKE_CXX_SCAN_FOR_MODULES 0)
请注意,它不应放在缓存中,因为它可能会影响通过 FetchContent 使用它的项目。此外,还应注意那些可能希望为其自身源文件启用扫描的被引用项目(vendored projects),因为这也将改变它们的默认行为。
调试模块构建¶
本节旨在帮助诊断或解释在 CMake 的 C++ 模块支持的构建侧可能出现的常见错误。
导入循环¶
C++ 标准不允许翻译单元的 import 图中存在循环;因此,CMake 也不允许。目前,CMake 将由构建工具根据用于对模块编译排序的动态依赖来检测循环。CMake 问题 26119 跟踪改进此类情况下的用户体验的需求。
内部模块分区扩展¶
在初步研究 C++ 模块构建实现时,似乎存在一种代表分区单元和实现单元交集的翻译单元类型。最初的 CMake 设计包含了对这些翻译单元的特定支持;然而,深入阅读标准后发现,它们实际上并不存在。这些单元本应具有 module M:part; 作为其模块声明语句。问题在于,这正是用于声明不参与主模块外部接口的模块分区的语法。只有 MSVC 支持这种区别。其他编译器不支持,并将此类文件视为内部分区单元,CMake 会引发错误,提示提供模块的 C++ 源文件必须位于类型为 CXX_MODULES 的 FILE_SET 中。
解决方法是不使用此扩展,因为它在不使用扩展的情况下并不能提供更多的表现力。所有实现单元源文件都应只使用 module M; 作为其模块声明语句,无论所定义的实体属于哪个分区。例如:
// module-interface.cpp
export module M;
export int foo();
// module-impl.cpp
module M:part; // module M:part; looks like an internal partition
int foo() { return 42; }
应改为使用明确的接口/实现分离。
// module-interface.cpp
export module M;
export int foo();
// module-impl.cpp
module M;
int foo() { return 42; }
模块可见性¶
CMake 在目标之间及目标内部强制执行模块可见性。这实质上意味着,从目标 T 上的 PRIVATE FILE_SET 提供的模块(例如 I)不能被以下目标导入:
其他依赖于
T的目标;或目标
T本身,如果模块是从PUBLICFILE_SET提供的。
这是因为,通常情况下,从一个模块导入的所有实体也必须能被该模块的所有潜在导入者导入。即使模块 I 仅在没有 export 关键字的模块部分内使用,它也可能以某种方式影响模块内容,使得模块的使用者为了正常工作需要能够传递性地 import 它。由于 CMake 使用模块可见性来确定是否安装模块接口单元,因此 PRIVATE 模块接口单元将不会被安装,这意味着任何导入 I 的已安装模块都无法正常工作。
相反,请仅从实现单元内部导入 PRIVATE C++ 模块,因为它们不会暴露给任何模块的使用者。
设计¶
与其它设计相比,CMake 的 C++ 模块支持设计做出了若干权衡。首先,将涵盖 CMake 选择的设计。随后的章节将涵盖未被 CMake 实现选择的替代设计。
总的来说,这些设计分布在两个轴上:
显示(Explicit)动态 |
显示(Explicit)静态 |
显示(Explicit)固定 |
隐式(Implicit)动态 |
隐式(Implicit)静态 |
隐式(Implicit)固定 |
显示(Explicit)构建直接控制哪些模块对每个翻译单元可见。例如,当编译需要模块
M的源文件时,编译器将被提供信息,说明在导入M模块时应使用的确切 BMI 文件。隐式(Implicit)构建也可以控制模块可见性,但方法是将 BMI 分组到目录中,然后搜索这些目录以查找满足源文件中
import语句的文件。静态(Static)构建使用一组静态的构建命令来完成构建。必须支持在构建时向节点添加边。
动态(Dynamic)构建可以在构建期间创建新的构建命令,并调度构建期间发现的任何工作。
固定(Fixed)构建在生成时就已经知晓了所有模块依赖。
设计目标¶
CMake 构建 C++ 模块的实现专注于以下设计目标:
正确的构建¶
最重要的是,错误的构建对相关人员来说是一种令人沮丧的经历。一个不能检测错误,反而让存在可检测问题的构建运行到完成的构建,是导致漫长调试过程的绝佳方式。CMake 在避免此类情况上更倾向于谨慎。
确定性的构建¶
鉴于磁盘上的构建状态,应该能够确定下一步将发生什么步骤。这并不意味着构建中可并发运行规则的确切顺序是确定性的,而是意味着要完成的工作集及其结果是确定性的。例如,如果任务 A 和 B 之间没有依赖关系,那么 A 就不应对 B 的执行产生影响,反之亦然。
支持生成的源文件¶
代码生成在 C++ 生态系统中非常普遍,因此只支持在配置时已知内容的文件中的模块是不合适的。如果不支持使用或提供模块的生成源文件,代码生成工具实际上就被切断了模块的使用,且生成源文件的任何依赖项也必须提供非模块化的接口使用方式(即提供头文件)。鉴于所有 C++ 实现都使用强模块所有权进行符号重整(name mangling),当此类接口最终引用其他库中的已编译符号时,这就产生了问题。
静态通信¶
构建的不同步骤之间的所有通信都应静态处理。鉴于 CMake 支持的构建工具,为需要在编译期间交互的配套工具建立受控生命周期具有挑战性。无论是 make 还是 ninja,都无法提供一种在构建开始时启动工具并在结束时确保其停止的方法。相反,与编译器的通信通过输入和输出文件进行管理,利用构建工具中的依赖项来保持一切更新。这种方法支持标准的构建调试策略,并允许开发者在调查问题时直接运行构建命令,而无需考虑在后台运行的其他工具。
最小化重生成¶
模块化构建的积极开发不应要求在每次更改时都重新生成构建图。这意味着模块依赖必须在构建图可用之后构建。如果没有这一点,正确的构建将需要在每次编辑模块感知源文件时重新生成构建图,因为任何更改都可能改变模块依赖。
这也意味着所有模块感知的源文件都必须在配置时可知(即使它们尚不存在),以便构建图可以包含用于扫描其依赖项的命令。
注意
已知的 ninja 问题可能导致在两次构建之间,当两个源文件之间的依赖顺序反转(例如 a 导入 b 变为 b 导入 a)时,错误地检测到依赖循环。详情请参阅 ninja 问题 2666。
用例考虑¶
上述设计目标限制了实现方式。此外,CMake 通过诸如 Ninja Multi-Config 和 Visual Studio 生成器 这样的多配置生成器来支持混合配置。本节描述 CMake 如何解决这些限制。
选择的设计¶
CMake 使用的通用策略是“扫描”源文件以提取顺序依赖信息,并通过现有边之间的新边来更新构建图。这是通过获取逐源扫描结果(由 P1689R5 文件表示),然后为每个目标结合其依赖项的信息进行“整理 (collating)”来完成的。整理器的主要任务是生成“模块映射”文件,并将其传递给每个编译规则,其中包含满足 import 语句所需的 BMI 路径,并告知构建工具在编译期间满足这些 import 语句所需的依赖项。整理器还利用构建时信息为模块接口单元、它们的 BMI 生成 install 规则,并为任何具有 C++ 模块的导出目标生成属性。它还强制要求 PRIVATE 模块不得被其他目标或目标内的任何 PUBLIC 模块接口单元使用。
实现细节¶
本节描述 CMake 实际上是如何构建构建图的,各部分之间传递的数据,以及包含该数据的文件。它旨在用作功能文档,并作为调试模块构建时定位各类数据所在位置的指南。
注意
本节记录了内部实现细节,这些细节对 工具链文件 作者或调试模块相关问题时可能很有用。项目无需检查或修改此处提到的任何变量、属性、文件或目标。
工具链(扫描)¶
支持模块的编译器必须同时提供扫描工具。这通常是编译器本身带有一些额外的标志,或者是随编译器提供的工具。扫描的命令模板存储在 CMAKE_CXX_SCANDEP_SOURCE 变量中。命令应将 P1689R5 结果写入 <DYNDEP_FILE> 占位符。此外,命令应将任何发现的依赖提供给 <DEP_FILE> 占位符。这允许构建工具在扫描命令的任何依赖项发生更改时重新运行扫描。
此外,工具链应设置以下变量:
CMAKE_CXX_MODULE_MAP_FORMAT:模块映射格式,描述编译期间导入模块所需的依赖 BMI 文件存在的位置。必须是gcc、clang或msvc之一。CMAKE_CXX_MODULE_MAP_FLAG:用于通知编译器模块映射文件的参数。它应使用<MODULE_MAP_FILE>占位符。CMAKE_CXX_MODULE_BMI_ONLY_FLAG:用于仅从模块接口单元编译 BMI 文件的参数。这用于在使用来自外部项目的模块时,为当前构建使用而编译 BMI 文件。
如果工具链不提供 CMAKE_CXX_MODULE_BMI_ONLY_FLAG,它将无法使用 IMPORTED 目标提供的模块。
工具链(import std)¶
如果工具链支持 import std,它还必须提供一个名为 ${CMAKE_CXX_COMPILER_ID}-CXX-CXXImportStd 的工具链标识模块。
注意
目前只有 CMake 可以提供这些文件,因为它们被包含的方式特殊。一旦 import std 不再是实验性的,外部工具链也可能独立提供支持。
该模块必须提供 _cmake_cxx_import_std 命令。它将接收两个参数:C++ 标准的版本(例如 23)以及一个变量名,用于放置其 import std 支持的结果。该变量应填入 CMake 源代码,该代码声明 __CMAKE::CXX${std} 目标,其中 ${std} 是传入的版本。如果无法创建目标,源代码应改为设置 CMAKE_CXX${std}_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE 变量,内容为当前配置中不支持 import std 的原因。注意,CMake 会使用条件检查来保护返回的代码,以确保目标只被定义一次。
理想情况下,__CMAKE::CXX${std} 目标将是一个附带 std 模块源的 IMPORTED INTERFACE 目标。然而,对于某些实现,可能需要编译对象。当模块使用者通过编译模块期望获得符号时,需要目标文件。有一个担忧是,如果这种情况在一个程序中发生多次,将导致这些符号的重复,这可能会违反 ODR。
例如,如果模块的使用者被期望为该模块提供符号,那么模块的使用就是程序的一个全局属性,无法被抽象化。想象一个库暴露 C API 但内部使用 C++ 模块。如果它应该提供模块符号,任何使用该 C API 的东西如果想为了自身目的使用同一个模块,就需要与其内部模块使用相协作。如果两者最终都为导入的模块提供了符号,可能会发生冲突。
配置(Configure)¶
在配置步骤中,CMake 需要跟踪哪些源文件关心模块。有关每个源文件如何确定其是否关心模块,请参阅 扫描控制。CMake 在其内部目标表示结构(cmTarget)中跟踪这些信息。可以使用 target_sources()、target_compile_features() 和 set_property() 命令修改需要扫描的源文件集。
此外,目标可以使用 CXX_MODULE_STD 目标属性来指示在目标的源文件中需要 import std。
生成(Generate)¶
在生成期间,CMake 需要添加额外的规则,以确保提供模块的源文件可以在导入这些模块的源文件之前被构建。由于 CMake 使用静态构建,构建图必须包含所有可能的扫描和模块生成命令。确保模块提供的命令之间的依赖边将确保构建图正确执行。这意味着,虽然所有源文件都可能被扫描,但只有实际使用的模块才会被生成。
CMake 执行的第一步是为模块提供目标的每次唯一使用生成一个合成目标 (synthetic target)。这些目标基于其他目标,但仅为其他目标提供 BMI 文件,而不是对象文件。这是因为 BMI 文件的兼容性极窄,不能在任意 import 实例之间共享。由于工具链的内部工作方式,对于任何单一编译,对于各种标志通常只能有一组设置,包括用于导入模块的 BMI 文件。例如,所使用的 C++ 标准需要在所有模块之间保持一致,但有许多设置可能导致不兼容。
注意
CMake 目前假设所有用法都是兼容的,并且每个目标只会创建一组 BMI。这可能导致构建失败,因为需要多组 BMI 文件,但 CMake 只提供了一组。请参阅 CMake 问题 25916 以获取消除此假设的进展。
一旦创建了所有合成目标,CMake 就会查看每个包含可能使用 C++ 模块的源文件的目标,并创建一个命令来扫描它们中的每一个。该命令将输出一个 P1689R5 格式的文件,描述它使用和提供(如果有)的 C++ 模块。它还将创建一个命令来为合格的编译整理模块依赖。该命令取决于所有合格源文件的扫描结果、目标本身的信息以及任何提供 C++ 模块的依赖目标的整理结果。整理步骤使用特定于目标的 CXXDependInfo.json 文件,其中包含以下信息:
compiler-*:基本的编译器信息(id、frontend-variant和simulate-id),用于在为编译器生成路径时生成格式正确的路径。cxx-modules:对象文件到FILE_SET信息的映射,用于强制执行模块可见性并为模块接口单元源生成安装规则。module-dir:放置此目标 BMI 文件的位置。dir-{cur,top}-{src,bld}:当前目录(cur)和项目顶级(top)的源(src)和构建(bld)目录,用于为构建工具动态依赖计算准确的相对路径。exports:导出列表,既包含目标又提供 C++ 模块信息,用于在导出的目标上提供IMPORTED目标的准确模块属性。bmi-installation:安装信息,用于生成 BMI 文件的安装脚本。database-info:如果通过EXPORT_BUILD_DATABASE请求,则生成构建数据库信息所需的信息。sources:目标中其他源文件的列表,如果请求,用于添加到构建数据库中。config:目标的配置,用于在生成的导出文件中设置适当的属性。language:整理元数据文件正在描述的语言(例如 C++ 或 Fortran)。include-dirs和forward-modules-from-target-dirs:C++ 中未使用。
cxx-modules 映射中的每个条目记录以下内容:
bmi-only(bool):如果仅 BMI 可用,而非 BMI 源可用,则为 True。compile-features(list[string]):用于构建对象的cmake-compile-features(7)。compile-options(list[string]):用于构建对象的编译选项/标志,派生自compile-features的除外。definitions(list[string]):用于构建对象的预处理器定义。destination(string):源文件的预期安装目标位置。include-directories(list[string]):用于构建对象的包含目录。name(string):拥有源文件的文件集名称。relative-directory(string):源文件将相对于此基路径重定位到安装目标位置。source(string):源文件的路径。type(string):拥有源文件的文件集类型。visibility(string):拥有源文件的文件集可见性。
对于每次编译,CMake 还将提供一个模块映射,该映射将在构建期间由整理命令创建。如何将其提供给编译器由 CMAKE_CXX_MODULE_MAP_FORMAT 和 CMAKE_CXX_MODULE_MAP_FLAG 工具链变量指定。
扫描(Scan)¶
编译器应实现扫描命令。这是因为只有编译器本身才能可靠地回答诸如 __has_builtin 这样的预处理器谓词,从而在面对编译源文件时可能使用的任意标志的情况下提供准确的模块使用信息。
CMake 将这些文件命名为 .ddi 扩展名,代表“动态依赖信息 (dynamic dependency information)”。这些文件采用 P1689R5 格式,并被整理命令用来执行其任务。
整理(Collate)¶
该整理命令执行了使 C++ 模块在构建图中工作的大部分工作。它消耗以下文件作为输入:
它利用来自这些文件的信息生成:
用于依赖目标的整理命令的
CXXModules.json文件用于每次编译的
*.modmap文件,以查找导入模块的 BMI 文件install-cxx-module-bmi-$<CONFIG>.cmake脚本,用于安装任何 BMI 文件(被install脚本包含)target-*-$<CONFIG>.cmake导出文件,用于目标的任何导出,以提供IMPORTED_CXX_MODULES_<CONFIG>属性。CXX_build_database.json构建数据库文件,用于目标设置其EXPORT_BUILD_DATABASE属性时。
在处理过程中,它强制执行以下保证:
C++ 模块有一条规则,即程序中只能存在一个给定名称的模块。这对于私有模块而言并非完全可强制执行,但对于公共模块是可行的。此强制执行由整理命令完成。CXXModules.json 文件的一部分是它提供的每个模块所传递性导入的模块集。当一个模块随后被导入时,整理命令确保所有给定名称的模块都同意提供该模块的给定 BMI 文件。
编译(Compile)¶
编译使用由整理命令生成的模块映射文件,在编译期间查找导入的模块。由于 CMake 仅提供由扫描命令发现的模块位置,任何被扫描命令错过的模块都不会提供给编译。
工具链有可能以不兼容为由拒绝 CMake 提供给编译的 BMI 文件。这是因为 CMake 目前假设所有使用都是兼容的。请参阅 CMake 问题 25916 以获取消除此假设的进展。
安装(Install)¶
在安装期间,包含在构建期间由整理命令编写的安装脚本,以便根据需要安装任何 BMI 文件。这些需要生成,因为在 CMake 的生成阶段不知道 BMI 文件名会是什么(因为 CMake 是以模块名称本身来命名 BMI 文件的)。这些安装脚本以 OPTIONAL 关键字包含,因此不完整的构建也可能导致不完整的安装。
替代设计¶
存在 CMake 未实现的替代设计。本节旨在简要概述,并解释为什么它们没有被选择用于 CMake 的实现。
隐式构建(Implicit Builds)¶
隐式构建使用编译时搜索路径执行模块构建,以简化构建的实现。这当然是可以实现的。然而,CMake 的目标将其排除在解决方案之外。
当构建使用搜索目录管理时,编译器被指示将模块输出文件放入指定目录。这些目录随后作为搜索路径提供给任何允许使用其中模块的编译。
这种策略存在违反正确的构建目标的风险。这源于搜索目录中存在陈旧文件的风险。由于构建系统不知道实际写入的文件,因此很难知道哪些文件可以删除(例如使用 ninja -t cleandead 来删除 ninja 遇到但不再生成的输出)。如果构建系统除了消费者报告“使用文件 X”之外不知道这些输出,删除中间文件也可能导致构建卡住。
还需要对共享输出目录的 BMI 生成命令进行至少某种程度的排序。可以使用单独的目录对模块组进行排序(例如每个目标一个目录);否则,同一目录中的模块不能假设写入共享目录的其他模块会先完成。如果模块路径根据模块依赖图准确分组,那么迈向直接指定文件的显示构建就只有一小步了。
静态扫描(Static Scanning)¶
一种固定构建在生成构建图时执行扫描,并预先包含必要的依赖项。在 CMake 的情况下,它会在生成阶段查看源文件,并直接将依赖项添加到构建图中。这更有可能适用于既是构建系统又是构建工具的场景,在这种场景下,构建图的操纵可以协同完成。
无论是否集成,该策略都需要一个合适的 C++ 解析器来首先提取信息,或者需要工具链合作来获取它。虽然更简单的 C++ 解析器可以获取模块依赖信息,但依赖项可能隐藏在需要理解才能准确判断的预处理器条件之后。当然,选择不支持 import 语句周围的预处理器条件也是一种选择,但这可能会严重限制外部库的支持。
对于 CMake,此策略意味着对模块感知源文件的任何更改可能都需要触发构建图的重生成。良性编辑至少需要触发对更改的导入的“检查”,但如果未更改,可能会跳过实际重生成。这对于既是构建系统又是构建工具的情况可能不那么关键,但这直接违反了最小化重生成目标。
此外,CMake 的支持生成的源文件目标在此策略下将无法支持。CMake 可以推迟扫描直到生成文件可用,但这些源文件在执行此类扫描之前无法编译。这意味着当源文件变得可用时,构建图将有某种无界(但有限)的重生成次数。
模块映射服务(Module Mapping Service)¶
另一种策略是在构建旁边运行一个服务,它可以充当放置和发现模块的“预言机”。编译器被指示用“此源文件正在导出模块 X”和“此源文件正在导入模块 Y”之类的问题向服务提问,并分别接收创建或查找 BMI 的路径。在这种情况下,该服务动态实现了整理逻辑。
特别值得注意的是,这与确定性的构建和静态通信目标相冲突,因为磁盘上的状态可能与实际状态不匹配,并且协调构建工具本身的生命周期与服务非常困难。主要缺少的功能是当构建会话开始和结束时的某种信号,以便此类服务可以知道它是在什么上下文中回答请求的。还需要有一种方法来恢复会话并检测会话何时失效。目前 CMake 支持的任何构建工具都没有此类功能。
还存在与正确的构建目标相冲突的危险。当模块被导入时,编译器会等待响应然后再继续。然而,无法保证该名称的(可见)模块甚至存在,因此它可能会无限期等待。在等待编译报告它创建该模块时,它可能会陷入依赖循环,导致编译挂起直到达到某个资源限制(可能是时间,或者模块的所有可能提供者都没有报告该名称的模块)。当这些编译在等待答案时,存在它们如何影响所使用构建工具的并行限制的问题。等待答案的编译是否计入限制并阻止其他编译启动以潜在地发现该模块?如果不计入,那么这些编译占用的其他资源(例如内存或可用文件描述符)怎么办?
可能的未来增强¶
本节记录了对 CMake 的 C++ 模块支持的可能未来增强功能。此处没有任何内容是未来实现的保证,顺序也是任意的。
批处理扫描(Batch Scanning)¶
可以一次扫描目标内的所有源文件,当源文件共享传递性包含时,这应该会更快。这对增量构建确实有副作用,因为目标中任何源文件的更新意味着目标中的所有源文件都会被再次扫描。鉴于扫描可以如此之快,假设未更改的结果不会触发重新编译,做这样的“额外”扫描应该是可以忽略不计的。
BMI 修改优化(BMI Modification Optimization)¶
目前,与对象文件一样,即使内容没有改变,编译器也会始终更新 BMI 文件。由于模块增加了“非更改”导致(概念上)不必要重新编译的潜在范围,如果 BMI 文件未更改,则避免重新编译模块使用者可能会很有用。这可以通过将编译包装在带有 cmake -E copy_if_different 通道的传递中来实现,并结合 ninja 的 restat = 1 功能,以避免在 BMI 文件实际上没有更改时重新编译导入者。
更容易的源文件指定(Easier Source Specification)¶
CMake 模块支持的最初实现使用了“只需列出源文件;CMake 会弄清楚”的模式。然而,这遇到了与其它元数据要求相关的问题。这些是在实现除了构建模块使用代码之外的 CMake 支持时发现的。
单独 BMI 生成(Separate BMI Generation)¶
CMake 目前使用单一规则来为一次编译生成 BMI 和对象文件。至少 Clang 支持直接从 BMI 编译对象。这将是有益的,因为 BMI 生成通常比编译要快,并且将 BMI 生成作为一个单独的步骤允许导入者在不等待对象也被生成的情况下开始编译。
在当前的实现中不支持这一点,因为只有 Clang 支持直接从 BMI 生成对象。其他编译器要么不支持这种两阶段生成(GCC),要么需要再次从源文件开始对象编译。
在单一目标上与更容易的源文件指定冲突,因为 CMake 必须在生成时(而非构建时)知晓所有 BMI 生成源文件,才能创建两阶段规则。
模块编译术语表¶
- BMI¶
Built Module Interface(内置模块接口)。模块使用者所需的 C++ 模块接口的编译器生成二进制表示。文件扩展名因编译器而异。
- CMI¶
Compiled Module Interface(编译模块接口)。某些编译器使用的 BMI 的替代名称。
- 构建数据库(build database)¶
包含编译命令、模块依赖和分组信息的 JSON 文件。用于 IDE 集成和构建分析。
- 构建系统(build system)¶
一种促进软件构建的工具,它包含构建组件如何相互关联的模型。例如 CMake、Meson、build2 等。
- 构建工具(build tool)¶
一种构建图执行工具。例如 ninja 和 make。一些构建工具本身也是它们自己的构建系统。
- C++ 模块(C++ module)¶
一种用于描述软件片段 API 的 C++20 语言功能。旨在作为此目的头文件的替代品。
- 整理(collate)¶
从扫描的源文件中聚合模块信息,以确保正确的编译顺序并为构建的其他部分(例如安装或构建数据库)提供元数据的过程。
- 发现的依赖(discovered dependencies)¶
在命令处理过程中找到的、不需要显式声明的依赖项。
- 动态依赖(dynamic dependencies)¶
需要单独指令来检测的依赖项,以便后续指令可以满足其依赖关系。
- embarrassingly parallel¶
由于任务间依赖极少,可以轻松划分为许多可并发执行的独立任务的一组任务。
- explicit build¶
一种构建策略,其中模块依赖项是显式指定的,而不是通过发现得到的。
- fixed build¶
一种构建策略,其中所有模块依赖项都会被计算并直接插入到构建图中。
- header unit¶
一种通过
import语句而不是#include预处理器指令使用的头文件。实现可以提供将#include视为import的支持。- implementation unit¶
一个实现了在模块接口单元中声明的模块实体的 C++ 翻译单元。
- implicit build¶
一种构建策略,其中模块依赖项是通过在编译期间搜索 BMI 文件来发现的。
- internal partition unit¶
- module interface unit¶
- module map¶
一种将模块名称映射到 BMI 位置的编译器特定文件。
- module visibility¶
CMake 基于声明作用域(PUBLIC/PRIVATE)对模块强制执行的访问规则。
- ODR¶
单一定义规则(One Definition Rule)。C++ 要求任何实体在每个程序中只能定义一次。
- partition unit¶
一个描述带有分区名称的模块的 翻译单元(即 module MODNAME:PARTITION;)。该分区可能会也可能不会使用
export关键字。如果使用了,它同时也是一个 模块接口单元;否则,它是一个 内部分区单元。- primary module interface unit¶
- scan¶
分析 翻译单元 以发现模块导入和导出的过程。
- static build¶
一种在生成时确定所有编译规则的构建配置。
- strong module ownership¶
C++ 实现已采用了一种模型,即模块“拥有”其中声明的符号。实际上,这意味着模块名称被包含在其中声明的实体的符号修饰(symbol mangling)中。
- synthetic target¶
一种 CMake 生成的构建目标,用于向提供模块的目标的特定使用者提供 BMI。
- translation unit¶
C++ 程序编译的最小组件。通常,每个源文件有一个翻译单元。不使用 C++ 模块的 C++ 源文件可以组合成单个翻译单元。