Clang Module 是大约 2013 年左右呈现的,它的呈现是为了处理传统依据 C 言语的编程言语的头文件包括的坏处。也是现代 Apple 平台软件开发必定会用到的一个技术,了解 Clang Module 对咱们安排代码结构,理解 Xcode 编译流程,优化编译速度,定位编译过错等都会有帮助。

传统头文件包括的坏处

传统的头文件包括,存在以下几个首要的问题:

编译功能问题

关于传统头文件包括,预处理器会将头文件的内容复制粘贴过来替换 #include 预处理指令。而许多头文件内都会包括相同的其它头文件,例如底层依靠,体系库等,这样造成了不同的源文件中会呈现重复的内容。也就是说,关于 M 个源文件,假如有 N 个头文件,复杂度量级是 M x N 的,编译器会对每个重复的内容都进行一次文本剖析,做了大量重复的工作,拖慢了编译时间。

例如体系的 Foundation 结构,结构内嵌套包括了 800 个以上的其它头文件,整个结构的巨细在 9MB 以上,作为最最根底的结构,几乎每个源文件都会包括 Foundation.h。关于传统的头文件包括,Foundation.h 的内容及其包括的其它头文件会被不断地重复进行词法剖析语义剖析,拖慢编译速度。

脆弱性

脆弱性是由于 #include 替换得到的内容会受到其它预处理指令的影响。例如头文件中有某个符号 XXX,假如在包括这个头文件之前,存在相似 #define XXX "other text" 这样的宏定义,就会导致头文件中的所有 XXX 都被替换为 “other text”,而导致编译过错。

#define XXX "other text"
#include "XXX.h"

有必要运用常规方案来处理一些问题

传统的头文件包括无法处理头文件重复包括的问题,因而咱们都用一种常规来防止重复包括。

#ifndef __XXXX_H__
#define __XXXX_H__
// 头文件内容
#endif

尽管现代开发东西都能主动生成这些,但仍是存在必定的不方便。

另外为了处理宏在多个库之间重名的问题,咱们都会把宏的称号起的很长很长,添加前缀和后缀。

对东西的迷惑性

在 C 言语为根底的言语中,软件库的边界不是很清晰,比如很难区分一个头文件到底是哪个言语的,由于 C、C++、Objective-C 等言语的头文件都是 .h。也很难弄清楚一个头文件到底是属于哪个库的。这关于开发依据这些软件库的东西带来了必定的难度。

Clang Module 能处理什么问题?

语义导入

Clang Module 从传统头文件包括的文本导入改善成了更强健,功率更高的语义导入。当编译器看到一个 Module 导入指令时,编译器会去加载一个二进制文件,这个二进制文件供给了这个模块所有 API 的信息,这些 API 能够直接给其它代码运用。

编译功能提升

Clang Module 提升了编译功能,每个模块只需求编译一次,然后会生成一个模块的二进制表明(.pcm,预编译模块,下文会阐明),并缓存到磁盘上。下次遇到 import 这个模块时,编译器不需求再次编译 Module,而是直接读取这个缓存的二进制表明即可。

上下文无关

Clang Module 处理了脆弱性的问题,每个 Module 都是一个独立的实体,会被隔离地、独立的编译,是上下文无关的。当 import 模块时,会忽略 import 上下文的其它的预处理指令,这样 import 之前的预处理指令不会对模块导入发生任何影响。

每个模块都是一个自包括的个体,他们上下文无关,相互隔离,因而不在需求运用一些常规方法来防止呈现一些问题,由于这些问题已经不会呈现了。

自己制作一个模块

为了能对有一个直观的了解,咱们能够自己着手制作一个 模块。用 Xcode 创立一个新的 iOS app 工程作为测验运用。然后在工程根目录下新建一个 group 命名为 Frameworks。

在指令行中进入 Frameworks 文件夹,新建一个 Dog.framework 文件夹,名字能够随意,这儿是随便起的。

mkdir Dog.framework

然后回到 Xcode 中,在 Frameworks 目录上右击鼠标,挑选 Adds files to … 把 Dog.framework 添加到 Frameworks 目录内。
此刻编译会报错 Framework not found Dog,接下来咱们看看怎样制作出一个 Xcode 能正确识别并编译的模块。

在 Dog.framework 中新建 Dog.swift 文件并添加以下内容:

// Dog.swift
import Foundation
public class Dog: NSObject {
    public func bark() {
        print("bark")
    }
    @objc func objcBark() {
        print("objc bark")
    }
}

接下来咱们来为这个 framework 生成接口文件。在指令行中履行以下指令:

swiftc -module-name Dog -c Dog.swift -target arm64-apple-ios16.2-simulator -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk -emit-module -emit-objc-header -emit-objc-header-path Dog-Swift.h

swiftc 是 Swift 言语的编译器,它底层也调用了 clang。下面对参数逐个进行阐明:

  • -module-name Dog 模块的称号,运用者能够经过 import + 这个称号来引进模块。
  • -c Dog.swift 指定要编译的源文件。
  • -target arm64-apple-ios16.2-simulator 指定生成方针的架构。
  • -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk 指定要链接进来的 SDK,这儿运用的是 iOS 16.2 的模拟器版别。
  • -emit-module 会生成一个 .swiftdoc 文件和一个 .swiftmodule 文件。
  • -emit-objc-header 生成 Objective-C 头文件,仅包括被标记为 @objc 的符号。
  • -emit-objc-header-path 指定 Objective-C 头文件的途径,这儿咱们遵循了 Xcode 的常规,运用 ”模块名+Swift.h“ 来命名。

尽管需求的文件已经生成了,可是并不是 Xcode 支撑的 module 目录结构,无法被 Xcode 读取。咱们能够经过观察 Xcode 创立的 Framework 来了解这种结构,来创立正确的结构。

在 Dog.framework 文件夹中创立 Headers 文件夹,然后把 Dog-Swift.h 移动到 Headers 文件夹中。然后在 Dog.framework 文件夹中再创立一个 Modules 文件夹,然后在 Modules 文件夹中创立 Dog.swiftmodule 文件夹,把 Dog.swiftdoc 和 Dog.swiftmodule 移动到 Dog.swiftmodule 文件夹中。最后把这两个文件重命名为 arm64.swiftdoc 和 arm64.swiftmodule。

当时 Dog.framework 的目录结构为:

Dog.framework/
|---- Dog
|---- Headers
|    |---- Dog-Swift.h
|---- Modules
     |---- Dog.swiftmodule
         |---- arm64.swiftdoc
         |---- arm64.swiftmodule 

现在接口已经有了,可是还没有二进制库文件,依然无法编译经过,下面咱们来生成二进制库文件。

履行以下指令:

swiftc -module-name Dog -parse-as-library -c Dog.swift -target arm64-apple-ios16.2-simulator -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk -emit-object

这个指令中有许多参数跟上一个指令是一样的,在此不做重复阐明,仅阐明下上个指令中没有的参数。

  • -parse-as-library 让编译器把文件解释为一个库,而不是一个可履行文件。
  • -emit-object 输出方针文件。

这个指令履行完今后会生成 Dog.o 这个方针文件,然后咱们需求把方针文件归档为库。

libtool -static Dog.o -arch_only arm64 -o Dog

这儿为了简化流程挑选了创立静态库,而不是动态链接库,因而运用 -static。这时在 Dog.framework 中会呈现 Dog 这个二进制静态库文件。此刻咱们在 ViewController 中 import Dog 然后编译工程,就能编译经过了。阐明当时这种目录结构能够让 Xcode 正确找到模块所需文件。

Module map

接下来咱们来试一试用 Objective-C 来调用 Dog 模块会怎样。

在上面的工程中再创立一个 Objective-C 类,命名为 OCObject,并让 Xcode 主动创立头文件桥接文件,并添加以下代码:

// OCObject.h
@interface OCObject : NSObject
- (void)doSomething;
@end
// OCObject.m
#import "OCObject.h"
#import <Dog/Dog-Swift.h>
@implementation OCObject
- (void)doSomething {
    Dog *dog = [[Dog alloc] init];
    [dog objcBark];
}
@end

会发现此刻是能够打印出 objc mark 的。然后把 #import <Dog/Dog-Swift.h> 替换成规范的模块导入语法 @import Dog;,编译却报错了,提示 ”Module ‘Dog’ not found“。

这时由于 framework 中缺少了一个重要的 modulemap 文件,Xcode 就无法找到模块。#import <Dog/Dog-Swift.h> 之所以有效是由于它本身是一个向前兼容的句子,假如 framework 支撑模块,则导入模块,假如 framework 不支撑模块,它会像 #include 一样去搜索途径中找到这个头文件,直接把文本内容粘贴到这儿。

Module map 指明晰 framework 中的头文件逻辑结构应该如何映射为模块。参阅用 Xcode 创立 framework 时主动创立的 module map 文件,会发现在 Modules 文件夹下有一个 module.modulemap 文件,其内容如下:

framework module ObserveModuleStructure {
  umbrella header "ObserveModuleStructure.h"
  export *
  module * { export * }
}
module ObserveModuleStructure.Swift {
  header "ObserveModuleStructure-Swift.h"
  requires objc
}

经过参阅 clang 的文档,来对这个语法逐个进行阐明:

  • framework module XXXX 定义了一个 framework 语义的模块
  • umbrella header "XXXX.h" 阐明把 XXXX.h 文件作为模块的 unbrella header,伞头文件相当于模块中所有公共头文件的一个集合,方便运用者导入。
  • export * 将所有子模块中的符号进行重导出到主模块中
  • module * { export * } 定义子模块,这儿为 * 则是为 umbrella header 中的每个头文件都创立一个子模块。

依据这个语法编写自己的 module map 文件,途径为 Dog.framework/Modules/module.modulemap:

// Dog.framework/Modules/module.modulemap
framework module Dog {
    umbrella header "Dog.h"
    export *
    module * { export * }
}
module Dog.Swift {
    header "Dog-Swift.h"
    requires objc
}

此刻依然编译报错,还需求一个 unbrella header 文件,创立一个 Dog.h 文件放到 Dog.framework/Headers/ 中,内容为空即可。然后就能够编译经过,打印出 bark objc。

Module Map 言语语法

官方把这种语法叫做模块映射言语(Module Map Language)。

依据 Clang 的文档,模块映射言语在 Clang 的大版别之间可能不会保持稳定,因而在平常的开发中,让 Xcode 去主动生成就好。

模块声明

[framework] module module-id [extern_c] [system] {
    module-member
}

framework

framework 代表这个模块是是一个 Darwin 风格的 framework。Darwin 风格的 framework 首要呈现在 macOS 和 iOS 操作体系中,它的全部内容都包括在一个 Name.framework 文件夹中,这个 Name 就是 framework 的名字,这个文件夹的内容布局如下:

Name.framework/
    Modules/module.modulemap    framework 的模块映射
    Headers/                    包括了 framework 中的头文件
    PrivateHeaders/             包括了 framework 中私有的头文件
    Frameworks/                 包括嵌入的其它 framework
    Resources/                  包括额定的资源
    Name                        指向同享库的符号链接

system

system 指定了这个模块是一个体系模块。当一个体系模块被重编译后,模块的所有头文件都会被作为体系头文件,这样一些正告就不会呈现。这和在头文件中放置 #pragma GCC system_header 等效。

extern_c

extern_c 指明晰模块中包括的 C 代码能够被 C++ 运用。当这个模块被编译用来给 C++ 调用时,所有模块中的头文件都会被包括在一个隐含的 extern "C" 代码块中。

模块体

模块体包括了 header、requires 等常见的声明和子模块声明,例如:

framework module Dog {
    umbrella header "Dog.h"
    requires objc
    module * { export * }
}

header

header 指定了要把哪些头文件映射为模块。umbrella header 则是指定了综合性伞头文件。

requires

requires 声明指定了导入这个模块的编译单元需求满足的条件。这个条件有言语、平台、编译环境和方针特定功能等。例如 requires cplusplus11 表明模块需求在支撑 C++11 的环境中运用,requires objc 表明模块需求在支撑 Objective-C 言语的环境中运用。

module

module 用来声明模块中的子模块,假如是 module * 则代表模块中的每个头文件都会作为一个子模块。

子模块声明

在主模块的模块体中嵌套地声明模块就是子模块。例如在 MyLib 模块中声明一个子模块 A,写法如下:

module MyLib {
    module A {
        header "A.h"
        export *
    }
}

explicit

explicit 润饰符是用来润饰子模块的。假如想运用被 explicit 润饰的子模块,有必要在 import 时指定子模块的名字,像这样 import modulename.submodulename,或许这个子模块已经被其它已导入的模块重导出过。

export

export 指定了将哪个模块的 API 进行从头导出,成为 export 地点的模块的 API。

export_as

export_as 将当时模块的 API 经过另一个指定的模块导出。

module MyFrameworkCore {
    export_as MyFramework
}

上面的例子中,MyFrameworkCore 中的 API 将经过 MyFramework 导出。

模块映射言语还包括许多其它的声明句子,例如 useconfig_macrslinkconflict 等,由于在 iOS 开发中呈现的不是许多,这儿就不做逐个阐明,有爱好能够检查 Clang 的官方文档。

Clang Module 的缓存机制

Clang 能够经过读取 modulemap 文件的内容将 modulemap 中指定的模块编译成预编译模块(Precompiled Module),后缀名是 .pcm。

clang -cc1 -emit-obj use.c -fmodules -fimplicit-module-maps -fmodules-cache-path=prebuilt -fdisable-module-hash

上面的指令经过指定参数 implicit-module-maps 让编译器依据必定的规则自己去查找 modulemap 文件,经过指定参数 modules-cache-path 告知编译器预编译模块的缓存途径。Clang 会依据 modulemap 中的信息编译各个模块,将生成的 .pcm 文件放到 prebuilt 目录下。

.pcm 文件以一种编译器能够轻松读取并解析的格局保存了模块的信息,之后编译器在编译其它模块时假如遇到了需求依靠这个模块,则能够快速的从 .pcm 中读取模块信息而不需求从头编译模块。

在 Xcode 中运用 Clang Module

用 Xcode 创立的结构或库都是默认敞开 Clang Module 支撑的,也就是在 Build Settings 中,Defines Module 的设置为 YES。假如是很老的库可能没有敞开,手动把 Defines Module 设置为 YES 即可。

当 Defines Modules 是 YES 是,Xcode 在编译工程时会给 clang 指令添加 -fmodules 等模块相关参数,敞开模块支撑。

结语

许多时分,开发东西都对咱们躲藏了许多底层的细节,了解这些细节,能够帮助咱们了解底层的原理,剖析并处理一些扎手的问题。Clang 是 Apple 平台上重要的东西,值得咱们去研究探索。感谢您的阅读,假如文章有不正确的当地,或许您有自己的见地,欢迎发表谈论来讨论。

参阅资料

  • clang.llvm.org/docs/Module…
  • clang.llvm.org/docs/PCHInt…
  • bignerdranch.com/blog/it-loo…
  • nachbaur.com/2019/03/11/…
  • samsymons.com/blog/unders…