Modules

介绍

大多数软件是由一些软件库构建的,这些库有的是平台提供的,有的是内部库(将软件的一部分代码结构化提供出来),有的是三方库。每一个库都需要访问自己的接口和实现,在c家族语言中,接口的访问需要导入头文件,例如:

        

#include <SomeLib.h>

通过链接相应的库单独的处理实现,例如,传递-lSomeLib给链接器

模块提供了另一种更简单的方法来使用软件库,它提供了更好的编译时可伸缩性,并消除了使用C预处理器访问库的API所固有的许多问题。

当前模型的问题

 c语言与处理提供的#include机制,是一种非常糟糕的方式去访问库的API,有以下几种原因:

  • 编译时可伸缩性:每包含一个头文件,编译器必须预编译、解析头文件以及头文件包含的其他头文件的所有内容。应用中每个翻译单元必须重复这个过程,这涉及到大量的冗余工作。在一个包含N个翻译单元和每个翻译单元中包含M头的项目中,即使大多数M头在多个翻译单元中共享,编译器仍在执行M x N的工作。
  • 脆弱:#include指令被预处理程序视为文本包含,因此在包含时受任何活动宏定义的约束,如果任何活动宏定义与库中的名称发生冲突,它可能会破坏库API或导致库头文件本身的编译失败。对于一个极端的例子,#define std“The c++ Standard”,然后包含一个标准库头文件:结果是c++标准库实现中出现了一系列可怕的失败。更微妙的现实问题发生在两个不同库的头文件由于宏冲突而相互作用时,用户被迫重新排序#include指令或引入#undef指令以中断(无意的)依赖。
  • 传统的解决方法:C程序员已经采用了许多约定来解决C预处理器模型的脆弱性。例如,绝大多数头文件都需要包含保护,以确保多个包含不会破坏编译。宏名使用LONG_PREFIXED_UPPERCASE_IDENTIFIERS来避免冲突,一些库/框架开发人员甚至在头文件中使用__下划线的名称来避免与(按照惯例)宏的“正常”名称发生冲突。这些约定是来自非c语言的开发人员进入的障碍,是更有经验的开发人员的样板,并且使我们的头文件更难看。
  • 工具混乱:在基于c的语言中,,因为库的边界不清楚,与软件库工作的工具时很难构建的。哪些头文件属于某个特定的库,这些头文件应该以什么顺序包含以保证正确编译? 头文件是C, c++, objective - c++,还是这些语言的变体之一?那些头文件中的哪些声明实际上是API的一部分,哪些声明只是因为必须作为头文件的一部分而出现?

语义导入

模块通过用更健壮、更高效的语义模型替换文本预处理器包含模型,改善了对软件库API的访问。从用户的角度来看,代码看起来只有轻微的不同,因为我们使用了import声明而不是#include预处理器指令:

import std.io; // pseudo-code; see below for syntax discussion

然而,这个模块导入的行为与相应的#include <stdio.h>非常不同:当编译器看到上面的模块导入时,它会加载std.io模块的二进制表示,并让应用程序可以直接使用它的API。导入声明之前的预处理器定义对std.io提供的API没有影响,因为模块本身是作为一个独立的模块编译的。此外,当模块被导入时,使用std.io模块所需的任何链接器标志都会自动提供。这个语义导入模型解决了预处理器包含模型的许多问题:

  • 编译时可伸缩性:std.io模块只编译一次,并且将模块导入到翻译单元是一个常量时间操作(独立于模块系统)。这样,每个软件库的API只被解析一次,将M x N的编译问题简化为M + N问题。
  • 脆弱:每个模块都是作为一个独立的实体进行解析的,因此它有一个一致的预处理器环境。这完全消除了对__下划线的名称和类似的防御技巧的需要。此外,当遇到导入声明时,当前的预处理器定义将被忽略,因此一个软件库不会影响另一个软件库的编译方式,从而消除了包含顺序依赖关系。
  • 工具混乱:模块描述了软件库的API,工具可以推理并将模块作为API的表示来呈现。因为模块只能独立构建,所以工具可以依赖模块定义来确保它们获得完整的库API。此外,模块可以指定它们使用的语言,例如,不能意外地尝试将c++模块加载到C程序中。

模块无法解决的问题

许多编程语言都有一个模块或包系统,由于这些语言提供了各种各样的特性,所以定义模块不能做什么很重要。特别地,以下所有的模块都被认为是超出范围的:

  • 重写世界上的代码:要求应用程序或软件库进行剧烈或非向后兼容的更改是不现实的,完全消除头文件也是不可行的。模块必须与现有的软件库互操作,并允许逐步过渡。
  • 版本控制:模块没有版本信息的概念。程序员仍然必须依赖底层语言的现有版本控制机制(如果存在的话)来对软件库进行版本控制。
  • 命名空间:与某些语言不同,模块不包含任何名称空间的概念。因此,在一个模块中声明的结构仍然会与在不同模块中声明的同名结构发生冲突,就像在两个不同的头文件中声明的结构一样。这方面对于向后兼容性很重要,因为(例如)在引入模块时,不能更改软件库中实体的混乱名称。
  • 模块的二进制分布:头文件(尤其是c++头文件)暴露了该语言的全部复杂性。跨架构、编译器版本和编译器供应商维护稳定的二进制模块格式在技术上是不可行的。

使用模块

要启用模块,请传递命令行标志-fmodules。这将使任何支持模块的软件库都可以作为模块使用,并引入任何特定于模块的语法。其他命令行参数将在后面的单独部分中进行描述。

Objective-CObjective-C导入声明

Objective-C提供了通过@import声明导入模块的语法,该声明会导入已命名的模块:

@import std;

上面的@import声明导入了std模块的全部内容(它将包含,例如,整个C或c++标准库),并使其API在当前的翻译单元中可用。为了只导入模块的一部分,可以对特定的子模块使用点语法,例如:

@import std.io;

冗余的导入声明被忽略,并且只要导入声明在全局范围内,在翻译单元内的任何点可以不用导入模块。

目前,导入声明没有C或c++语法。Clang将在c++委员会中跟踪模块提案。请参阅include as imports一节,了解当前模块是如何导入的。

Includes as importsIncludes as imports

模块的主要用户级特性是导入操作,它提供了对软件库API的访问。然而,今天的程序广泛使用#include,假设所有这些代码一夜之间就会改变是不现实的。相反,模块会自动将#include指令转换为相应的模块导入。例如,include指令

#include <stdio.h>

将自动映射到模块std.io的导入。即使在语言中有特定的导入语法,这个特殊的特性对于采用和向后兼容性都很重要:#include 的自动翻译成 import允许应用程序获得模块的好处(对于所有支持模块的库),而不需要对应用程序本身进行任何更改。因此,用户可以很容易地在使用一个编译器时使用模块,而在使用其他编译器时则退回到包含预处理程序的机制。

注意

#include to import的自动映射还解决了一个实现问题:导入带有某个实体定义的模块(例如,一个struct Point),然后解析包含另一个struct Point定义的报头将导致重新定义错误,即使它是相同的struct Point。通过将#include映射到import,编译器可以保证它始终只看到模块中已经解析过的定义。

在构建模块时,也支持#include_next,但有一点需要注意。#include_next的通常行为是在包含路径列表中搜索指定的文件名,从找到当前文件的路径之后的路径开始。因为在模块映射中列出的文件不能通过include路径找到,所以在这样的文件中,对#include_next指令使用了不同的策略:在包含路径列表中搜索指定的头文件名,以找到指向当前文件的第一个包含路径。#include_next将被解释为当前文件已经在该路径中找到。如果这个搜索找到一个由模块映射命名的文件,#include_next指令会被翻译成导入,就像#include指令一样。

模块maps

模块和标头之间的关键链接由模块映射描述,它描述了现有标头集合如何映射到模块的(逻辑)结构上。例如,可以想象一个模块std覆盖了C标准库。每个C标准库头文件(<stdio.h>, <stdlib.h>, <math.h>,等等)都将对std模块做出贡献,将它们各自的api放入相应的子模块(std.io, std.lib, std.math,等等)。拥有一个std模块的头列表,允许编译器将std模块构建为一个独立的实体,并且拥有从头名称到(子)模块的映射,允许将#include指令自动转换为模块导入。

模块映射被指定为单独的文件(每个名为Module .modulemap)以及它们所描述的头文件,这允许它们被添加到现有的软件库中,而无需更改库头文件本身(在大多数情况下是[2])。后面的部分将描述实际的模块映射语言。

注意

要真正看到模块带来的好处,首先必须为底层C标准库以及它所依赖的库和头文件引入模块映射。“平台模块化”一节描述了编写这些模块映射必须采取的步骤。

可以使用模块映射而不使用模块来检查头文件使用的完整性。为此,可以使用- fimplit -module-maps选项来代替-fmodules选项,或者使用-fmodule-map-file=选项来显式指定要加载的模块映射文件。

编译模型

模块的二进制表示是由编译器根据需要自动生成的。当一个模块被导入时(例如,通过一个模块头文件的#include),编译器将生成自己的第二个实例[3],并带有一个新的预处理上下文[4],以解析该模块中的头文件。生成的抽象语法树(AST)随后被持久化到模块的二进制表示中,然后将该模块加载到遇到模块导入的翻译单元中。

模块的二进制表示被持久化在模块缓存中。导入模块将首先查询模块缓存,如果所需模块的二进制表示已经可用,则将直接加载该表示。因此,每个语言配置只解析模块头一次,而不是每个使用模块的翻译单元解析一次。

模块维护对模块构建中每个头文件的引用。如果这些头文件中的任何一个发生了变化,或者某个模块所依赖的任何模块发生了变化,那么该模块将(自动)被重新编译。这个过程不应该需要任何用户干预。

命令行参数

-fmodules

        启用模块特性。

-fbuiltin-module-map

        加载Clang builtins模块映射文件。(等价于-fmodule-map-file=<resource dir>/include/module.modulemap)

-fimplicit-module-maps

        对名为module的模块映射文件(module.modulemap或类似的名字)启用隐式搜索。。这个选项是由-fmodules隐含的。如果使用-fno-implicit-module-maps禁用此功能,则模块映射文件只有在通过-fmodule-map-file显式指定或由另一个模块映射文件传递使用时才会加载。

-fmodules-cache-path=<directory>

   指定模块缓存的路径。如果没有提供,Clang将选择一个适合系统的默认值。

-fno-autolink

禁用导入模块关联的库自动链接。

-fmodules-ignore-macro=macroname

指示模块在选择适当的模块变体时忽略指定的宏。对于在命令行上定义的不影响模块构建方式的宏,可以使用此方法来改进编译模块文件的共享。

-fmodules-prune-interval=seconds

指定尝试修剪模块缓存之间的最小延迟(以秒为单位)。模块缓存修剪试图清除旧的、未使用的模块文件,以便模块缓存本身不会无限制地增长。默认延迟很大(604,800秒,或7天),因为这是一个昂贵的操作。将此值设置为0以关闭修剪。

-fmodules-prune-after=seconds

指定模块缓存中的文件在模块修剪将其删除之前必须未使用的最小时间(以秒为单位)。默认延迟很大(2,678,400秒,或31天),以避免过度的模块重建。

-module-file-info <module file name>

输出有关给定模块文件(扩展名为.pcm)的信息的调试辅助工具,包括构建特定模块变体时使用的语言和预处理器选项。

-fmodules-decluse

启用模块使用声明的 检查。

-fmodule-name=module-id

将源文件视为给定模块的一部分。

-fmodule-map-file=<file>

如果加载了给定模块映射文件的目录或其子目录中的一个头文件,则加载该模块映射文件。

-fmodules-search-all

如果没有找到符号,搜索当前模块映射中引用的模块,但没有为符号导入,因此错误消息可以通过名称引用模块。注意,如果之前没有构建全局模块索引,这可能会花费一些时间,因为它需要构建所有模块。注意,这个选项在模块构建中不适用,以避免递归。

-fno-implicit-modules

构建中使用的所有模块都必须通过-fmodule-file指定。

-fmodule-file=[<name>=]<file>

指定模块名称到预编译模块文件的映射。如果省略名称,则加载模块文件,无论实际是否需要。如果指定了名称,那么映射将被视为另一种预构建模块搜索机制(除了-fprebuilt-module-path),并且只在需要时加载模块。注意,在这种情况下,指定的文件还覆盖了可能嵌入到其他预编译模块文件中的模块路径。

-fprebuilt-module-path=<directory>

指定预构建模块的路径。如果指定了,我们将在这个目录中查找给定的顶级模块名的模块。在这个目录中,我们不需要模块映射来加载预构建的模块,编译器也不会尝试重新构建这些模块。可以多次指定。

-fprebuilt-implicit-modules

启用预构建的隐式模块。如果在预构建模块路径(通过-fprebuilt-module-path指定)中没有找到预构建模块,我们将在预构建模块路径中寻找匹配的隐式模块。

-cc1选项

-fmodules-strict-context-hash

支持对隐式构建中可能影响模块语义的所有编译器选项进行散列。这包括标题搜索路径和诊断。如果命令行参数在整个构建中不相同,使用此选项可能会导致构建的模块数量过多。

使用预构建模块

下面是一些通过不同选项演示预构建模块用法的示例。

首先,让我们为示例设置文件。

/* A.h */
#ifdef ENABLE_A
void a() {}
#endif
/* B.h */
#include "A.h"
/* use.c */
#include "B.h"
void use() {
#ifdef ENABLE_A
  a();
#endif
}
/* module.modulemap */
module A {
  header "A.h"
}
module B {
  header "B.h"
  export *
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值