4.2.1 模块映射语法参考
7 分钟阅读
原文链接: https://docs.swift.org/latest/documentation/packagemanagerdocs/modulemapreference/
4.2.1 模块映射语法参考
定义 C 和 C++ 模块接口所用的模块映射指令、属性和语法参考。
概述
模块映射使用一种领域特定语言来描述 C、Objective-C 和 C++ 头文件如何组织成模块。本参考文档记录了模块映射声明的语法和语义。
关于创建模块映射的操作步骤,参见创建模块映射。关于模块映射的疑难排查与测试,参见调试模块映射。
Clang Modules 文档是模块映射的官方规范。下面的内容针对常见的 Swift 使用场景对该规范做了归纳。
模块声明
模块声明定义一个具名模块并指定其内容。
基本语法
module ModuleName {
declarations
}
模块名必须是有效的标识符。它可以包含字母、数字和下划线,但不能以数字开头。模块名必须与 Swift Package Manager 中的产品名或目标名一致。
框架模块
框架模块使用一种专门的语法,用于 macOS 和 iOS 框架:
framework module FrameworkName {
umbrella header "FrameworkName.h"
export *
module * { export * }
}
framework 关键字表明该模块代表一个框架包。module * { export * } 模式会为所有头文件自动创建并导出子模块。
显式子模块
子模块把一个模块划分为更小的单元:
module ParentModule {
explicit module SubmoduleA {
header "SubA.h"
export *
}
explicit module SubmoduleB {
header "SubB.h"
export *
}
}
explicit 关键字要求客户端显式导入该子模块。代码通过 import ParentModule.SubmoduleA 来导入显式子模块。
不使用 explicit 关键字时,导入父模块会自动导入该子模块。
头文件声明
头文件声明指定哪些头文件属于某个模块。
header 指令
把一个头文件包含进模块:
header "HeaderName.h"
该路径相对于模块映射文件所在的位置。编译器会解析该头文件,并在导入模块时让其中的声明可用。
umbrella header 指令
包含一个自身又包含所有其他公共头文件的头文件:
umbrella header "ModuleName.h"
伞形头文件为模块的全部功能提供单一入口。这样的头文件通常用 #include 指令包含所有其他公共头文件。
private header 指令
包含一个仅在模块实现内部可见的头文件:
private header "InternalAPI.h"
私有头文件对导入该模块的客户端不可见。对模块内其他文件需要访问的实现细节,请使用私有头文件。
textual header 指令
包含一个以文本方式包含、而不是作为模块一部分编译的头文件:
textual header "Macros.h"
文本头文件通常用于只含宏的头文件,或者无法独立解析的头文件。编译器会把这些头文件的内容直接包含进每个翻译单元,而不是把它们作为模块的一部分预编译。
exclude header 指令
显式地把某个头文件从伞形结构中排除:
umbrella header "Module.h"
exclude header "Internal.h"
当你使用的伞形头文件包含了你不希望进入模块的文件时,用 exclude header 省略特定头文件。
umbrella directory 指令
包含某个目录中的所有头文件:
umbrella "include"
编译器会包含指定目录及其子目录中找到的所有头文件。它比伞形头文件少见,因为它对哪些头文件是公共的控制更弱。
导出声明
导出声明控制哪些模块会被重新导出给客户端。
export * 指令
重新导出所有已导入的模块:
export *
当客户端导入你的模块时,它们也能看到你的模块所导入模块中的声明。这是 C 库最常见的导出模式。
选择性导出
重新导出指定的模块:
module MyModule {
header "MyModule.h"
export Foundation
export Darwin
}
只有指定的模块对客户端可见。已导入但没有导出的模块仍然是私有的实现细节。
链接声明
链接声明指定链接器应当包含的库。
基本语法
link "libraryname"
库名不要带 lib 前缀和文件扩展名。例如,对 libz.a 或 libz.dylib 使用 link "z"。
构建系统会自动把这些信息传给链接器。
框架链接
对于 macOS 和 iOS 框架:
link framework "FrameworkName"
这会告诉链接器链接指定的框架。
要求
要求指定模块可用所必须满足的条件。
语言要求
cplusplus:要求支持 C++ 语言cplusplus11:要求 C++11 或更高版本cplusplus14:要求 C++14 或更高版本cplusplus17:要求 C++17 或更高版本cplusplus20:要求 C++20 或更高版本objc:要求支持 Objective-C 语言opencl:要求支持 OpenCL
示例:
module CppLibrary {
header "CppLibrary.h"
requires cplusplus11
export *
}
如果构建时不满足这些要求,编译器会拒绝该模块。
特性要求
blocks:要求支持 blocks(闭包)gnuinlineasm:要求支持 GNU 内联汇编
使用取反运算符来排除某些特性:
requires !gnuinlineasm
组合要求
用逗号组合多个要求:
requires cplusplus, !blocks
所有要求都必须被满足,模块才可用。
模块属性
属性用于修改模块的行为和语义。
[system] 属性
把模块标记为系统模块:
module SystemLibrary [system] {
header "system.h"
export *
}
系统模块会得到特殊对待:
- 系统头文件中的编译器警告会被抑制
- Swift 对某些 Objective-C 类型的导入方式不同(
NSUInteger变成Int而不是UInt) - 该属性确保本地构建与客户端构建的行为一致
对于 SDK 提供的库和系统头文件,请使用 [system]。
[extern_c] 属性
表明 C++ 代码采用 C 链接方式:
module [extern_c] CLibrary {
header "clib.h"
export *
}
在 C++ 环境中包装 C 库时,这很有用。
[no_undeclared_includes] 属性
阻止隐式包含头文件:
module StrictModule [no_undeclared_includes] {
header "StrictModule.h"
export *
}
客户端必须显式导入它们使用的所有模块。这让依赖变得显式,避免无意中依赖传递包含。
配置宏
配置宏允许基于预处理宏定义提供不同的模块变体。
config_macros 指令
声明会影响模块接口的宏:
module ConfigurableModule {
header "ConfigurableModule.h"
config_macros exhaustive DEBUG_MODE, FEATURE_X
export *
}
exhaustive 关键字列出所有会影响该模块 API 的宏。编译器会为不同的宏组合创建不同的模块变体。
如果没有声明配置宏,编译器就假定该模块的接口与预处理状态无关。
冲突声明
冲突声明用于指定互不兼容的模块。
conflict 指令
声明某个模块与另一个模块冲突:
module NewModule {
header "NewModule.h"
conflict OldModule, "Use NewModule instead"
export *
}
如果在同一个翻译单元中同时导入这两个模块,编译器会产生错误。错误信息会说明两个模块为何冲突,以及应当改用什么。
extern 模块
extern 模块用来引用其他文件中的模块映射。
extern module 指令
引用另一个模块映射中定义的模块:
extern module ExternalModule "path/to/other.modulemap"
这样就可以把庞大的模块层级拆分到多个文件中组织。该路径相对于当前的模块映射文件。
use 声明
use 声明用于指定模块之间的依赖关系。
use 指令
声明某个模块依赖于另一个模块:
module DependentModule {
header "DependentModule.h"
use Foundation
export *
}
这类声明只起说明作用,有助于记录模块依赖。大多数模块映射并不需要显式的 use 声明。
完整语法示例
下面是一个综合运用多种特性的模块映射:
module CompleteExample [system] {
// 主模块
umbrella header "CompleteExample.h"
export *
// 配置
config_macros exhaustive DEBUG, LOGGING_ENABLED
// 链接
link "example"
// 要求
requires !gnuinlineasm
// 显式子模块
explicit module Utilities {
header "Utilities.h"
export *
}
// 私有实现子模块
explicit module Private {
private header "CompleteExample_Private.h"
export *
}
// C++ 子模块
explicit module CPP {
header "CompleteExample_CPP.hpp"
requires cplusplus11
export *
}
}
// 向后兼容模块
module CompleteExampleOld {
header "CompleteExample_Old.h"
conflict CompleteExample, "Use CompleteExample instead of CompleteExampleOld"
export *
}
模块映射文件命名
模块映射文件必须遵循特定的命名约定:
module.modulemap:标准的公共模块映射module.private.modulemap:用于实现细节的私有模块映射
Swift Package Manager 默认查找 module.modulemap。
系统库包装
module SystemLib [system] {
header "shim.h"
link "systemlib"
export *
}
垫片头文件包含真正的系统头文件,让编译器能通过搜索路径找到它。
C++ 库 {#C++-library}
module CppLib {
header "CppLib.hpp"
requires cplusplus11
export *
}
requires cplusplus11 指令确保该模块只在 C++11 或更高版本模式下可用。
带子模块的框架
framework module MyFramework {
umbrella header "MyFramework.h"
export *
module * { export * }
explicit module Private {
private header "MyFramework_Private.h"
export *
}
}
公共头文件会被自动模块化,而私有头文件需要显式导入。
多个库
module CompositeLib {
module Core {
header "Core.h"
link "core"
export *
}
module Utils {
header "Utils.h"
link "utils"
export *
}
}
每个子模块可以链接各自的库。
诊断指令
模块映射不支持 #if 或 #ifdef 这类预处理指令。模块映射中的所有内容始终生效。
要处理平台特定的头文件,可以为每个平台使用单独的模块映射,或者使用要求:
module CrossPlatform {
// 公共头文件
header "Common.h"
// 通过要求处理平台特定部分
module Darwin {
requires objc
header "Darwin.h"
export *
}
export *
}
模块映射的解析
编译器解析模块映射时具有以下特征:
- 注释使用 C++ 风格(
//)或 C 风格(/* */)。 - 标识符遵循 C 的命名规则(字母、数字、下划线)。
- 字符串字面量使用双引号。
- 字符串字面量中的路径相对于该模块映射文件。
- 字符串字面量之外的空白不具意义。
- 模块映射文件必须是有效的 UTF-8。
局限与约束
模块映射有若干局限:
- 模块名不能包含句点(请改用子模块)。
- 头文件路径必须相对于模块映射文件。
- 不支持模块之间的循环依赖。
- 模块映射中不支持条件编译指令。
- 模块映射假定头文件格式良好,并且可以独立解析。
与 Swift Package Manager 的集成
Swift Package Manager 使用模块映射在 C、C++ 库与 Swift 之间架起桥梁:
- artifact bundle 中的
moduleMapPath字段指定模块映射的位置。 - 系统库目标把模块映射放在目标目录中。
- 构建系统会自动添加
-fmodule-map-file=标志。 - 模块名必须与二进制目标或系统库目标的名称一致。
关于创建模块映射的操作步骤,参见创建模块映射。
最佳实践
编写模块映射时请遵循以下做法:
- 对 SDK 和系统库模块使用
[system]。 - 只要涉及 C++ 代码,就指定
requires cplusplus。 - 多公共头文件的库使用伞形头文件。
- 保持模块名简洁且具描述性。
- 让模块名与产物名完全一致。
- 所有头文件引用都使用相对路径。
- 用
exhaustive记录配置宏。 - 在所有目标平台上测试模块映射。
- 对可选功能使用显式子模块。
- 保持模块映射小而聚焦。