4.2.1 模块映射语法参考

原文链接: 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 "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 记录配置宏。
  • 在所有目标平台上测试模块映射。
  • 对可选功能使用显式子模块。
  • 保持模块映射小而聚焦。