cover_image

掌握Swift宏:深入探索SwiftSyntax与宏的实用指南

狐友靳凯 搜狐技术产品 2025年01月22日 23:32

欢迎来到 Swift 宏的世界。我们将探索 Swift 5.9 版本中引入的一项革命性特性——宏(Macro),它为我们提供了一种在编译时处理源代码的能力。宏可以帮助我们自动化代码生成,减少模板代码,并使得代码更加简洁和易于理解。我们将从宏的基本概念开始,逐步深入了解关联宏(attached macro)和独立宏(freestanding macro)的区别与应用。接着,我们会学习如何通过 SwiftSyntax 库来解析和操作 Swift 代码的抽象语法树(AST),这是实现宏功能的基础。

在本篇详细的指南中,我们将深入探索Swift宏和SwiftSyntax库,逐步掌握Swift宏的精髓。以下是我们将要遵循的学习路线:

  • 宏的基础知识:我们将从宏的定义入手,探讨它们在编译时如何转换源代码,以及如何利用宏减少重复代码的编写;
  • 宏的分类:我们将区分并理解关联宏(attached macro)和独立宏(freestanding macro)的不同用途和特性;
  • 宏的实现细节:通过具体的代码示例,我们将学习如何实现一个宏,包括宏的声明与实现的分离,以及如何遵循相应的协议;
  • 深入SwiftSyntax:我们将深入了解SwiftSyntax库,探讨它如何提供操作Swift源代码的高级API,并学习如何使用这些工具来解析和生成代码。

01

宏Macro是什么

宏在编译时会将你的源代码进行转换,这样你就可以避免手动编写重复的代码。在编译过程中,Swift会在通常的代码构建之前将代码中的宏展开。扩展宏总是一种增量操作:宏会添加新的代码,但不会删除或修改现有的代码。

宏的输入和宏展开后的输出都会被检查,以确保它们是语法上正确的 Swift 代码。同样,您传递给宏的值以及由宏生成的代码中的值也会被检查,以确保它们具有正确的类型。此外,如果宏的实现在展开宏时遇到错误,编译器会将其视为编译错误。这些保证使得更易于理解使用了宏的代码,并且使得更易于识别诸如错误地使用宏或存在 bug 的宏实现等问题。

苹果按照宏不同的使用场景,将宏分成两个大类:关联宏(attached macro)和独立宏(freestanding macro) 。

了解了宏的基本概念后,我们进一步探讨宏的分类,特别是关联宏(attached macro)的特点和应用。

1.1关联宏(attached macro)

必须和另一个已有的类型或者是声明关联使用,以「@」号开头;每个宏都有一个或多个角色,这些角色在宏声明的开始部分以属性的形式定义。每个角色需要遵循不同的协议,实现对应函数,在函数内部实现宏展开的内容。

角色协议描述
@attached(peer)PeerMacro为关联的声明添加一段新的声明
@attached(accessor)AccessorMacro为关联的声明添加存取代码(get、set 等)
@attached(memberAttribute)MemberAttributeMacro为关联的类型或扩展添加新特性
@attached(member)MemberMacro为关联的类型或扩展添加新的声明
@attached(conformance)ExtensionMacro为关联的类型或扩展添加新的协议遵循

举例:你可以使用OptionSet协议来表示bitset类型,其中每个比特代表一个集合的成员。在自定义类型中采用这个协议可以让你对这些类型执行集合相关的操作,如成员关系测试、并集和交集。

考虑下面的代码,它没有使用宏:

struct SundaeToppingsOptionSet {
    let rawValue: Int
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

在这段代码中, SundaeToppings 选项集中的每个选项都包含对初始化函数的调用,这很冗余且需要手动操作。在添加新选项时很容易出错,比如在行尾输入错误的数字。

这里有一个使用宏的代码版本:

@OptionSet<Int>
struct SundaeToppings {
    private enum OptionsInt {
        case nuts
        case cherry
        case fudge
    }
}

版本的 SundaeToppings 调用了 @OptionSet 宏。该宏读取私有枚举中的case列表,为每个选项生成一组常量,并添加对 OptionSet 协议的遵从。

以下是扩展后的 @OptionSet 宏的示例:

struct SundaeToppings {
    private enum OptionsInt {
        case nuts
        case cherry
        case fudge
    }


    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppingsOptionSet { }

了关联宏,Swift 还提供了独立宏(freestanding macro),它们在用法上有所不同。

1.2独立宏(freestanding macro)

独立宏在使用上无需和任何类型关联,以「#」号开头。独立宏可以声明一个新的类型,或者作为一段代码(表达式)的替换

角色协议描述
@freestanding(expression)ExpressionMacro创建一个有返回值的表达式
@freestanding(declaration)DeclarationMacro创建一个或多个声明
func myFunction() {
    print("Currently running \(#function)")
    #warning("Something's wrong")
}

第一行代码中, #function 调用了 Swift 标准库中的 function() 宏。当您编译此代码时,Swift 会调用该宏的实现,将 #function 替换为当前函数的名称。当您运行此代码并调用 myFunction() 时,它会打印 "Currently running myFunction()"。在第二行代码中, #warning 调用了 Swift 标准库中的 warning(_:) 宏以生成自定义编译时警告。

独立的宏可以产生一个值,比如 #function 就会这样做,也可以在编译时执行一个操作,比如 #warning 就会这样做。

1.3命名规则

在命名规则上:

  • 关联宏使用大写驼峰式命名法,类似于结构和类的名称;
  • 独立宏的名称使用小写驼峰式命名法,类似于变量和函数的名称。

02

宏展开

当编写使用宏的 Swift 代码时,编译器会调用宏的实现来展开它们。

具体来说,Swift 以如下方式扩展宏:

  • 编译器会读取代码,并创建一个内存中的语法表示;
  • 编译器将内存中的部分表示发送给宏实现程序,该程序会展开宏;
  • 编译器将宏调用替换为其展开形式;
  • 编译器继续进行编译,使用扩展后的源代码。
let magicNumber = #fourCharacterCode("ABCD")

#fourCharacterCode接受一个长度为 4 个字符的字符串,并返回一个无符号 32 位整数,该整数对应于字符串中字符的 ASCII 值之和。某些文件格式使用这样的整数来标识数据,因为它们格式紧凑且在调试器中仍有可读性。

要扩展上面代码中的宏,编译器会读取 Swift 文件并创建一个称为抽象语法树(AST)的内存中表示形式。AST 使代码结构显式化,这使得编写与该结构交互的代码变得更加容易——比如编译器或宏实现。以下是简化了一些额外细节的上述代码的 AST 表示形式:

图片

上图显示了该代码在内存中的结构表示方式。AST中的每个元素都对应于源代码的一部分。“Constant declaration”AST元素下面有两个子元素,代表常量声明的两个部分:其名称和值。“Macro call”元素下面有子元素,代表宏的名称和传递给宏的参数列表。

在构建AST的过程中,编译器会检查源代码是否是合法的Swift代码。例如, #fourCharacterCode 需要一个单个参数,该参数必须是一个字符串。如果你尝试传递一个整数参数,或者忘记了字符串字面量的结尾的引号( " ),那么在编译过程中的这个阶段就会出现错误。

编译器会找到代码中你调用宏的位置,并加载实现这些宏的外部二进制文件。对于每个宏调用,编译器会将部分AST传递给该宏的实现。以下是部分AST的表示形式:

图片

使用#fourCharacterCode宏时,其实现代码在展开宏时会将该部分AST作为输入。宏的实现代码仅操作其接收到的输入部分AST,这意味着无论前后的代码是什么,宏始终以相同的方式展开。这种限制有助于简化宏展开的可理解性,并有助于加快代码构建速度,因为Swift可以避免展开未发生变化的宏。

Swift通过限制实现宏的代码来帮助宏作者避免意外读取其他输入:

  • 传递给宏实现的AST仅包含表示宏的AST元素,而不包含该宏前后的任何代码;
  • 宏实现在沙箱环境中运行,从而防止它访问文件系统或网络。

除了这些保护措施外,宏的作者还应确保不会读取或修改除宏输入以外的任何内容。例如,宏的展开不应依赖于当前的日期和时间。

执行 #fourCharacterCode 会生成一个包含扩展代码的新AST。以下是该代码返回给编译器的内容:

图片

当编译器接收到这个扩展时,它会用包含宏调用的AST元素替换包含宏扩展的AST元素。在宏扩展之后,编译器再次检查以确保程序仍然是语法上正确的Swift代码,并且所有类型都是正确的。这会产生一个可以像往常一样编译的最终AST:

图片

这个AST与下面的Swift代码相对应

let magicNumber = 1145258561 as UInt32

这个例子中,输入源代码只有一条宏定义,但一个真正的程序可能包含多个相同的宏定义和对不同宏的多次调用。编译器会逐个展开宏。

如果一个宏出现在另一个宏内部,则首先展开外部宏 - 这样可以使外部宏在展开内部宏之前对其进行修改。

掌握了宏的展开机制后,我们现在来看看如何在实际中创建和使用宏。

03

创建宏

在大多数 Swift 代码中,当您实现一个符号(例如函数或类型)时,通常没有单独的声明。但是,对于宏,声明和实现是分开的。宏的声明包含其名称、所使用的参数以及可以在何处使用以及生成何种代码。宏的实现包含通过生成 Swift 代码来展开宏的代码。

完整的实现一个宏,需要包含三个部分:

  • 实现宏:利用SwiftSyntax库,分析现有代码,遵循特定协议,撰写宏实现expansion函数,完成特点功能代码添加;
  • 声明宏:声明宏的类型及角色,指定宏的参数,实现模块名称和模块类型等信息;
  • 创建插件:创建插件列表和添加新的宏。

首先通过命令行工具创建Macro模版:

swift package init --type macro

3.1实现宏

/// Implementation of the `stringify` macro, which takes an expression
/// of any type and produces a tuple containing the value of that expression
/// and the source code that produced the value. For example
///
///     #stringify(x + y)
///
///  will expand to
///
///     (x + y, "x + y")
public enum StringifyMacroExpressionMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  )
 -> ExprSyntax {
    guard let argument = node.argumentList.first?.expression else {
      fatalError("compiler bug: the macro does not have any arguments")
    }

    return "(\(argument), \(literal: argument.description))"
  }
}

同类型的宏实现需要遵守对应的协议,下图展示了Swift宏协议之间的继承关系:

图片

箭头指向代表protocol协议的继承关系,子协议指向父协议,与前述章节介绍的两类宏及其附属的不同的角色宏一致。

3.2声明宏

// MARK: - Stringify Macro

/// "Stringify" the provided value and produce a tuple that includes both the
/// original value as well as the source code that generated it.
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (TString) = #externalMacro(module: "MacrosImplementation", type: "StringifyMacro")


3.2.1引入宏声明
你使用 macro 关键字引入一个宏声明。
第一行指定了宏的名称及其参数 - 名称是 stringify ,并且它不接受任何参数。第二行使用了 Swift 标准库中的 externalMacro(module:type:) 宏,告诉 Swift 该宏的实现位置在哪里。在此情况下, MacrosImplementation 模块包含一个名为 StringifyMacro 的类型,该类型实现了 #stringify 宏。

宏总是以 public 的形式声明。由于声明宏的代码与使用该宏的代码位于不同的模块中,因此无法在任何地方应用非公开宏。

3.2.2宏的角色

宏声明定义了宏的角色——在源代码中可以调用该宏的位置以及该宏可以生成的代码类型。每个宏都有一个或多个角色,您可以在宏声明的开始部分作为属性来编写这些角色。一个macro可以添加多个宏的角色,这在关联宏中比较常见
@attached(member)
@attached(extensionconformancesOptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")
个声明中出现了两次 @attached 属性,一次对应每个宏角色。第一次使用( @attached(member))表示宏向您应用的类型添加了新的成员。@OptionSet 宏添加了 init(rawValue:) 初始化器,该初始化器是 OptionSet 协议所要求的,以及一些额外的成员。第二次使用( @attached(extension, conformances: OptionSet) )告诉您, @OptionSet 添加了对 OptionSet 协议的遵循。@OptionSet 宏将您应用宏的类型扩展为添加对 OptionSet 协议的遵循。
3.2.3宏的角色附加属性
宏的声明还提供了有关宏生成的符号名称的信息。当宏声明提供一个符号列表时,可以保证生成的只有使用这些符号的声明,这有助于您理解和调试生成的代码。独立宏没有附加属性,下面针对关联宏做介绍。
对于关联宏,peer、member和accessor宏角色需要一个names:参数,列出宏生成的符号的名称。如果宏在扩展内部添加声明,extension宏角色也需要一个names:参数。当宏声明包含names:参数时,宏实现必须只生成具有与该列表匹配的名称的符号。也就是说,宏不需要为每个列出的名称生成符号。这个参数的值是一个列表,包含以下一个或多个:
  • named 其中name是固定符号的名称,用于预先知道的名称

@attached(member, names: named(_storage))
public macro DictionaryStorage() = #externalMacro(module: "MacrosImplementation", type: "DictionaryStorageMacro")
_storage就是要添加的属性的名称;
extension DictionaryStorageMacroMemberMacro {
  public static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  )
 throws -> [DeclSyntax] {
    return ["\n  var _storage: [String: Any] = [:]"]
  }
}
  • overloaded 重载的名称与现有符号相同
@attached(peer, names: overloaded)
public macro AddAsync() = #externalMacro(module: "MacrosImplementation", type: "AddAsyncMacro")
AddAsyncMacro会对现有的函数,添加一个与现有函数同名同参数的异步函数,所以是被认为重载overloaded。
  • prefixed 其中prefix在符号名称前,用于以固定字符串开头的名称;

  • suffixed 其中后缀附加在符号名称之后,用于以固定字符串结尾的名称;

  • arbitrary 在宏扩展之前无法确定的任意名称。举例

@attached(member, names: named(RawValue), named(rawValue),
        named(`init`), arbitrary)
@attached(extensionconformancesOptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")
上述声明中, @attached(member) 宏在 names: 标签之后为每个由 @OptionSet 宏生成的符号添加了参数。宏为名为 RawValuerawValueinit 的符号添加了声明,因为这些名称在宏声明之前就已经知道了,因此宏声明直接列出了这些符号。
宏声明还包括在名称列表之后的 arbitrary ,这样宏就可以生成在使用宏时才确定名称的声明。例如,当 @OptionSet 宏应用于上面的 SundaeToppings 时,它生成与枚举情况相对应的类型属性,nutscherryfudge

3.3创建插件

@main
struct SwiftMacrosLibraryPluginCompilerPlugin {
    let providingMacros: [Macro.Type] = [
     ...
        StringifyMacro.self,
    ...
    ]
}

个Package中可以包含多个Swift Macro,这些Macro都需要添加到providingMacros中。

从“实现宏”到最后的“创建插件”,这个流程顺序是编写代码的流程,实际代码执行时,执行顺序是倒叙的,即从创建插件开始查找、执行;

创建宏需要深入理解SwiftSyntax库,它为宏的实现提供了基础架构。

04

SwiftSyntax

SwiftSyntax(https://github.com/apple/swift-syntax)库提供了用于检查、处理和操作Swift源代码的高级、安全且高效的API数据结构和算法。SwiftSyntax库是Swift解析器、swift-format和Swift宏等工具的基础。

Swift Macro 与 SwiftSyntax 之间存在密切的关系,因为 SwiftSyntax 提供了一组库,它们可以在源码精确的树状表示(即 SwiftSyntax 树)上工作。这个树状结构构成了 Swift 宏系统的基础,宏扩展节点被表示为 SwiftSyntax 节点,而宏生成的也是一个 SwiftSyntax 树,以便插入到源文件中。

SwiftSyntax 允许开发者使用抽象语法树(AST)以结构化的方式与 Swift 代码进行交互。宏的实现使用 SwiftSyntax 模块来处理 Swift 代码,无论是独立宏(Freestanding Macros)还是附加宏(Attached Macros),它们的实现都需要符合 SwiftSyntax 中相应的协议。

具体来说,Swift Macro 的实现依赖于 SwiftSyntax 提供的接口和协议,例如 ExpressionMacroMemberMacro 等,这些协议定义了宏如何扩展抽象语法树(AST)。宏的实现会接收到使用宏的代码的 AST,并在宏的库内部调用相应的方法(如 expansion(of:in:)),来找到宏的参数并计算结果,最终返回一个新的 AST 节点。

图片

4.1Syntax Nodes 语法节点

语法树由称为语法节点的元素组成。为了帮助对语法节点进行分类和组织,SwiftSyntax为语法节点定义了一个层次结构的协议。

在这个层次结构的顶部是 SyntaxProtocol 协议。每一种协议都有对应的结构体类型遵循该协议

public struct SyntaxSyntaxProtocolSyntaxHashable {
  let data: SyntaxData

  /// Needed for the conformance to ``SyntaxProtocol``.
  ///
  /// Needed for the conformance to ``SyntaxProtocol``. Just returns `self`.
  public var _syntaxNode: Syntax {
    return self
  }

  init(_ data: SyntaxData) {
    self.data = data
  }
}

Syntax节点表示一棵节点树,其中_syntaxNode属性遵守SyntaxProtocol,返回自己;通过data这个属性,所有语法节点都可以访问其底层的SyntaxData,其中包含关于节点结构、位置和原始语法的基本信息。

SyntaxProtocol进一步细化,包含以下子协议:

  • DeclSyntaxProtocol对于像structclassenumprotocol  这样的声明;

    • struct DeclSyntax: DeclSyntaxProtocol
  • StmtSyntaxProtocol对于像ifswitchdo这样的语句;

    • struct StmtSyntax: StmtSyntaxProtocol
  • ExprSyntaxProtocol对于像函数调用、字面量和闭包这样的表达式;

    • struct ExprSyntax: ExprSyntaxProtocol
  • TypeSyntaxProtocol对于像Array<String>[String: String]some Protocol这样的类型;

    • struct TypeSyntax: TypeSyntaxProtocol
  • PatternSyntaxProtocol对于像case (_, let x)这样的模式。

    • struct PatternSyntax: PatternSyntaxProtocol

语法节点构成了语法树的“分支”,因为它们通常是一组或多个语法节点的高级集合。这些分支共同构成了源代码的语法结构。这种结构被编译器和静态分析器用于以高级方式处理源代码。

有一种特殊的语法节点是SyntaxCollection(protocol SyntaxCollection: SyntaxProtocol),它代表具有可变数量子节点的语法。例如,代码块值可以在一对大括号之间包含零个或多个语句。为了表示这些子节点,CodeBlockSyntax值有一个statements访问器,它返回一个CodeBlockItemListSyntax值。这个语法集合中的元素是 CodeBlockItemSyntax 值。

用一个简单的图来解释这个概念

+----------------+        +----------------+        +------------------------+
CodeBlockSyntax |------>| statements     |------>| CodeBlockItemListSyntax |
+----------------+        +----------------+        +------------------------+
|                |        |                |        |                     |
| statements     |        | elements       |        | elements            |
+----------------+        +----------------+        +------------------------+
                                                 |                |
                                                 |                |
                                                 +----------------+
                                                 |                |
                                                 |                |
                                                 +----------------+
                                                 |                |
                                                 |                |
                                                 +----------------+
                                                 |                |
                                                 |                |
                                                 +----------------+
|                     |  |                     |  |                     |
CodeBlockItemSyntax |  | CodeBlockItemSyntax |  | CodeBlockItemSyntax |
+---------------------+  +---------------------+  +---------------------+

这个图中:

  1. CodeBlockSyntax:代表整个代码块,它是一个语法节点,可以包含零个或多个语句;
  2. statements:是CodeBlockSyntax的一个访问器,用来获取代码块中的所有语句;
  3. CodeBlockItemListSyntax:是statements返回的值,代表代码块中所有语句的集合;
  4. elements:是CodeBlockItemListSyntax中的元素,每个元素都是一个CodeBlockItemSyntax
  5. CodeBlockItemSyntax:代表单个的代码块项,可能是一个语句、一个声明等。

这个结构允许SwiftSyntax库以一种灵活的方式来处理代码块中的语句,因为语句的数量可以是零个或多个,而SyntaxCollection正是为了处理这种可变数量的子节点而设计的。

4.2常见的DeclSyntax类型

SwiftSyntax中定义了很多符合DeclSyntaxProtocol的结构体,代表了Swift语法中的各种声明类型。每个结构体对应于一种特定的声明,并包含表示该声明不同部分的属性。下面是对每个结构体的简要解释:

  1. AccessorDeclSyntax:表示属性或下标中的访问器声明(例如get、set);

  2. ActorDeclSyntax:表示一个actor声明,它是一个引用类型,保护它的可变状态不受数据竞争的影响;

  3. AssociatedTypeDeclSyntax:表示协议中的关联类型声明;

  4. ClassDeclSyntax:表示一个类声明;

  5. DeinitializerDeclSyntax:表示类的反初始化声明;

  6. EditorPlaceholderDeclSyntax:表示编辑器上下文中声明的占位符;

  7. EnumCaseDeclSyntax:表示枚举中的枚举case声明;

  8. EnumDeclSyntax:表示枚举声明;

  9. ExtensionDeclSyntax:表示类型的扩展声明;

  10. FunctionDeclSyntax:表示函数声明;

  11. IfConfigDeclSyntax:表示声明的条件编译块(#if, #else, #endif);

  12. ImportDeclSyntax:表示一个导入声明;

  13. InitializerDeclSyntax:表示初始化器声明;

  14. MacroDeclSyntax:表示一个宏声明;

  15. MacroExpansionDeclSyntax:表示声明上下文中的宏扩展;

  16. MissingDeclSyntax:表示语法树中缺失的声明;

  17. OperatorDeclSyntax:表示操作符声明;

  18. PoundSourceLocationSyntax:表示#sourceLocation指令;

  19. PrecedenceGroupDeclSyntax:表示操作符的优先级组声明;

  20. ProtocolDeclSyntax:表示协议声明;

  21. StructDeclSyntax:表示结构体声明;

  22. SubscriptDeclSyntax:表示下标声明;

  23. TypeAliasDeclSyntax:表示typealias声明;

  24. VariableDeclSyntax:表示变量声明(let或var)。

这些结构体都提供了对应声明类型的结构化表示,便于以抽象语法树的形式操作和分析Swift源代码。

4.3Syntax Tokens 语法标记

语法树的叶子节点包含 TokenSyntax 类型的值。一个token语法值代表语法中的一个基本单元以及与其相关的一些细微差别,比如标识符以及其周围的空白。所有token的组合代表了源代码的词法结构。这种结构被linter和格式化器用来分析源代码的文本内容

struct TokenSyntaxSyntaxProtocolSyntaxHashable

TokenSyntax 遵循协议SyntaxProtocol和SyntaxHashable,SyntaxHashable遵循Hashable协议,

表示单个标记的语法节点。语法树的所有源代码都由标记布局节点表示,不要单独包含任何源代码。

4.4Syntax Trivia 语法知识

琐碎的内容包括空白、注释、编译器指示和错误的源代码。琐碎的内容对文档的语法结构贡献最大,但通常与它的语义结构关系不大。因此,编译器等工具在处理源代码时通常会忽略琐碎的内容。然而,对于编辑器、IDE、格式化器和重构引擎等工具来说,维护琐碎的内容是很重要的。SwiftSyntax使用 Trivia 类型明确表示琐碎的内容。与标记语法相关的琐碎内容可以使用 leadingTriviatrailingTrivia 访问器进行检查。

例如下面代码,创建AttributeSyntax节点,其中内容一个开头从新的一行开始且空格为2的,标识是“DictionaryStorageProperty”:

      AttributeSyntax(
        leadingTrivia: [.newlines(1), .spaces(2)],
        attributeName: IdentifierTypeSyntax(
          name: .identifier("DictionaryStorageProperty")
        )
      )

05

实现宏的流程

1.分析设计宏要实现的功能是什么?

  • 确定是要为现有结构增加功能(此时应使用关联宏),还是实现一个完全独立的新功能(此时应使用独立宏)。

2.遵守对应宏协议,实现expansion函数。

  • 分析源代码结构的函数declaration是类型/扩展,还是函数?是否符合当前宏的使用条件,即进行类型判断,安全防护,抛出错误处理;
  • 通过分析现有类型的AST结构,添加需要实现的功能。一般包括:函数签名和返回类型提取;函数名称提取;
  • 从新的声明中移除宏声明,即@xxxx#xxxxx这类在源代码开头添加的宏,避免宏被重复展开;
  • 设置新声明参数,生成新声明。

在深入了解了实现宏的流程之后,我们现在将聚焦于关联宏的一个具体实例——@attached(peer)宏。

这种宏作为关联宏家族中的一员,允许我们在现有代码的基础上扩展新的功能,而无需改动原始代码结构。@attached(peer)宏特别适用于那些需要在相同作用域内为现有声明生成新的声明的场景,比如增加额外的方法或属性。

接下来,我们将以@attached(peer)宏为例,详细展示如何在实际中应用我们之前讨论的宏实现流程,从分析设计宏的功能开始,一直到遵守对应宏协议,实现expansion函数的具体步骤。这将帮助我们更好地理解如何在Swift中有效地使用关联宏,以及如何通过SwiftSyntax库来精确地操作和生成代码。让我们继续探索,深入了解@attached(peer)宏的实际应用和强大功能。

06

@attached(peer)

Peer Macros是一种宏,它能够在相同作用域内为现有声明生成新的声明,特别适用于增强代码组织和可维护性。

这个角色除了能关联到常见的类型、参数、方法以外,甚至能关联 import 和操作符的声明,并为其添加新的声明。以下面的方法为例,

定义了一个 Swift 宏 AddAsyncMacro,它是一个用于扩展非异步函数以支持 async 属性的宏。在这个例子中,AddAsyncMacro 会在原始的非异步函数上生成一个新的 async 函数版本。

添加后的展开效果如图所示:

图片

由于我们是在相同作用域在添加新的函数,所以适合使用关联宏peer类型。

接下来实现PeerMacro协议,即实现expansion函数,不同的协议类型,需要分别实现不同参数的expansion函数

public protocol PeerMacroAttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingPeersOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  )
 throws -> [DeclSyntax]
}

释其中参数和返回值含义:

  • of node: AttributeSyntax:此参数代表宏的属性语法节点,也就是代码中应用的宏标签。在 AST(抽象语法树)中,它对应于代表该宏Attribute的位置;
  • providingPeersOf declaration: some DeclSyntaxProtocol:此参数代表应用宏的声明语法节点。在 AST 中,它通常对应于一个具体的声明节点,如函数声明 (FunctionDecl) 或变量声明 (VarDecl)。这个参数提供了宏操作的目标,你可以在其中创建新的“同类”声明;
  • in context: some MacroExpansionContext:此参数提供宏扩展执行的上下文信息,包含可用的符号、诊断信息等。虽然它不直接对应于 AST 中的一个节点,但它为宏的扩展过程提供环境支持;
  • 返回值是DeclSyntax类型数组,DeclSyntax 就是专门用来表示声明(Declarations)的节点类型。声明可以是变量、函数、类、结构体、枚举等 Swift 代码中的元素。因此,返回值代表你要添加的内容结构。

在分析时,在实现expansion函数之前,我们通常会使用工具如Swift AST Explorer(https://swift-ast-explorer.com/)来分析源代码结构,这有助于我们更好地理解AST并实现宏。

首先利用Swift AST Explorer(https://swift-ast-explorer.com/),分析源代码结构:

@AddAsync
func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void {
  completionBlock(true)
}

数AST结构如下图所示,添加了关键的标注信息,对应SwiftSynatx中对应的类型或结构字段名。

图片

首先简要分析此AST结构:

1.CodeBlockItemList:这是一个包含多个代码块项目的列表;
2.CodeBlockItem:这是CodeBlockItemList中的一个项目;
3.FunctionDecl:这是一个函数声明;
4.AttributeList:这是一个宏的属性列表,包含了修饰函数的宏的信息。Attribute:这是一个属性。**@**:这是一个属性标记,表示该参数是逃逸的。IdentifierType:这是一个标识符类型;
5.DeclModifierList:这是一个声明修饰符列表,包含了函数的修饰符。func:这是函数关键字。d:这是函数的名称;
6.FunctionSignature:这是函数签名;
7.FunctionParameterClause:这是函数参数的括号部分;
8.FunctionParameterList:这是函数参数的列表;
9.FunctionParameter:这是单个函数参数。

结合实现expansion函数的流程,将一步步展示如何实现这个功能的PeerMacro:

1.判断声明类型应该是函数,否则抛出错误;
 // Only on functions at the moment.
    guard var funcDecl = declaration.as(FunctionDeclSyntax.selfelse {
      throw CustomError.message("@addAsync only works on functions")
    }
expansion函数的declaration参数类型是DeclSyntaxProtocol,是通用类型,而函数类型FunctionDeclSyntax是遵循DeclSyntaxProtocol的结构体,所示需要使用as进行类型转换。

2.特定限制检查,该宏应用于满足特定条件的函数,包括:不是异步函数将completion处理程序作为最后一个参数并返回Void;

此处需要重点分析AST结构中,signature下面的parameterClause和returnClause参数:
  • parameterClause参数是FunctionParameterClauseSyntax类型,也是遵循SyntaxProtocol的结构体,该结构体通常用于解析和生成 Swift 代码中的函数参数子句,例如函数的参数列表部分;
  • parameterClause.parameters是FunctionParameterListSyntax类型,是一个集合类型,查看AST图片结构中可以看到多个FunctionParameter,其中每一个参数就是一个FunctionParameterSyntax结构体,用于表示 Swift 语法中的函数参数。例如当前示例中d函数参数a: Int,参数 b: String
  • returnClause参数是ReturnClauseSyntax类型,ReturnClauseSyntax 是一个结构体,遵循 SyntaxProtocolSyntaxHashable 协议,它用于表示 Swift 语法中的返回子句。例如当前示例中d函数返回 -> Void 可以被解析为一个 ReturnClauseSyntax 实例,其中 Void 是返回类型
// This only makes sense for non async functions.
    if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil {
      throw CustomError.message(
        "@addAsync requires an non async function"
      )
    }

    // This only makes sense void functions
    if funcDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" {
      throw CustomError.message(
        "@addAsync requires an function that returns void"
      )
    }

    // Requires a completion handler block as last parameter
    guard let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last?.type.as(AttributedTypeSyntax.self),
      let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as(FunctionTypeSyntax.self)
    else {
      throw CustomError.message(
        "@addAsync requires an function that has a completion handler as last parameter"
      )
    }

    // Completion handler needs to return Void
    if completionHandlerParameter.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" {
      throw CustomError.message(
        "@addAsync requires an function that has a completion handler that returns Void"
      )
    }
数completionHandlerParameterAttribute是signature.parameterClause集合中最后一个元素(即d函数的completionBlock参数)的类型的type就是,如下图所示局部展开的AST中的AttriubutedType字段。
图片

在分析了参数和AST结构之后,我们分享一些有用的AST查看技巧,这将帮助我们更有效地理解和操作AST。

  • AST展示的节点名称通常在末尾去掉Syntax,例如AttributedType实际上代表的是AttributedTypeSyntax
  • 点击AST结构展开或收起,鼠标点中其中节点会在左侧展示对应节点的子属性,这些属性不直接展示在AST结构中。

completionHandlerParameterAttribute的baseType如下图所示,是FunctionType。对应示例中d函数completionBlock: @escaping (Bool) -> Void中闭包参数,即(Bool) -> Void

图片
3.在分析源代码声明基础上,添加新功能;
通过前面分析,completionHandlerParameterAttribute和completionHandlerParameter两个参数,分别拿到completionBlock闭包和其闭包参数,然后返回类型和结果处理:确定返回类型是否为Result类型,如果是则提取Result类型。
    let returnType = completionHandlerParameter.parameters.first?.type
//parameters是TupleTypeElementListSyntaxSyntaxCollection,returnType是Void
    let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result"
    let successReturnType = isResultReturn ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument : returnType
//
这里特别区分不同:
func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result<String, Error>) -> Void) -> Void {
      completionBlock(.success("a: \(a), b: \(b), value: \(value)"))
    }
转成异步函数,宏会根据闭包的返回类型(Void 或 Result)生成不同的异步函数体。
func c(a: Int, for b: String, _ value: Double) async throws -> String {
  try await withCheckedThrowingContinuation { continuation in
    c(a: a, for: b, value) { returnValue in

      switch returnValue {
      case .success(let value):
        continuation.resume(returning: value)
      case .failure(let error):
        continuation.resume(throwing: error)
      }
    }
  }
}
果闭包返回类型是 Result,使用 try await withCheckedThrowingContinuation 处理可能的错误。修改返回类型为闭包的返回类型(如果是 Result,则提取 Result 的第一个泛型参数)。
如果闭包返回类型是 Void,使用 await withCheckedContinuation 处理无错误的情况。直接调用 continuation.resume(returning: ()) 处理 Void 返回类型。
接下来,移除源代码声明中的completionHandler参数,因为转换后不再需要:
    // Remove completionHandler and comma from the previous parameter
    var newParameterList = funcDecl.signature.parameterClause.parameters
    newParameterList.removeLast()
    var newParameterListLastParameter = newParameterList.last!
    newParameterList.removeLast()
    newParameterListLastParameter.trailingTrivia = []
    newParameterListLastParameter.trailingComma = nil
    newParameterList.append(newParameterListLastParameter)
4.从新的声明中移除宏声明,避免宏被重复展开;
    let newAttributeList = funcDecl.attributes.filter {
      guard case let .attribute(attribute) = $0,
        let attributeType = attribute.attributeName.as(IdentifierTypeSyntax.self),
        let nodeType = node.attributeName.as(IdentifierTypeSyntax.self)
      else {
        return true
      }

      return attributeType.name.text != nodeType.name.text
    }
部分代码完全几乎可以不做修改,在很多同类型Peer宏展开中使用,与具体的功能无关。
5.设置新声明参数,返回新的声明内容
    let callArguments: [String] = newParameterList.map { param in
      let argName = param.secondName ?? param.firstName

      let paramName = param.firstName
      if paramName.text != "_" {
        return "\(paramName.text): \(argName.text)"
      }

      return "\(argName.text)"
    }

    let switchBody: ExprSyntax =
      """
            switch returnValue {
            case .success(let value):
              continuation.resume(returning: value)
            case .failure(let error):
              continuation.resume(throwing: error)
            }
      """


    let newBody: ExprSyntax =
      """

        \(raw: isResultReturn ? "try await withCheckedThrowingContinuation { continuation in" : "await withCheckedContinuation { continuation in")
          \(raw: funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "")

      \(raw: isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))")
          }
        }

      """



这段代码中,我们首先创建了一个callArguments数组,它包含了新异步函数调用时所需的所有参数。然后,我们根据闭包的返回类型(是否为Result类型)来决定如何生成异步函数体。如果闭包返回Result类型,我们将使用try await withCheckedThrowingContinuation来处理可能的错误,并根据Result的值来决定是继续执行还是抛出错误。如果闭包返回Void类型,我们将使用await withCheckedContinuation来处理无错误的情况。
接下来,我们更新函数声明,添加异步关键字(async),并根据需要添加throws关键字
    // add async
    funcDecl.signature.effectSpecifiers = FunctionEffectSpecifiersSyntax(
      leadingTrivia: .space,
      asyncSpecifier: .keyword(.async),
      throwsSpecifier: isResultReturn ? .keyword(.throws) : nil
    )

    // add result type
    if let successReturnType {
      funcDecl.signature.returnClause = ReturnClauseSyntax(leadingTrivia: .space, type: successReturnType.with(\.leadingTrivia, .space))
    } else {
      funcDecl.signature.returnClause = nil
    }

    // drop completion handler
    funcDecl.signature.parameterClause.parameters = newParameterList
    funcDecl.signature.parameterClause.trailingTrivia = []

    funcDecl.body = CodeBlockSyntax(
      leftBrace: .leftBraceToken(leadingTrivia: .space),
      statements: CodeBlockItemListSyntax(
        [CodeBlockItemSyntax(item: .expr(newBody))]
      ),
      rightBrace: .rightBraceToken(leadingTrivia: .newline)
    )

    funcDecl.attributes = newAttributeList

    funcDecl.leadingTrivia = .newlines(2)

    return [DeclSyntax(funcDecl)]
后,我们更新函数的参数列表,移除原有的completion handler参数,并设置新的函数体和属性。
通过这些步骤,我们成功地将一个非异步函数转换为一个异步函数,并确保了代码的正确性和可读性。
构建由几个层级组成:
  • ExprSyntax:表示一个表达式,例如变量赋值、函数调用、算术运算等;
  • CodeBlockItemSyntax:表示代码块中的一个条目,可以是语句(StmtSyntax)、声明(DeclSyntax)或表达式(ExprSyntax)。ExprSyntax可以作为 CodeBlockItemSyntax 的一部分,表示代码块中的具体操作;
  • CodeBlockSyntax:表示一个代码块,通常包含一系列CodeBlockItemSyntax组成;
  • DeclSyntax:表示一个声明,例如函数声明、变量声明、类型声明等。包含 CodeBlockSyntax 作为其主体部分。
通过这种方式,你可以构建复杂的声明、代码块和表达式结构。

07

小结

通过本文的深入探讨,我们不仅学习了Swift宏的基础知识,还掌握了如何在实际项目中有效运用宏,以及如何通过SwiftSyntax库来增强我们的代码处理能力。从宏的功能分析到expansion函数的具体实现,我们了解了如何为现有函数添加异步版本,并通过PeerMacro协议来实现这一功能。我们讨论了如何检查函数的签名、参数和返回类型,并根据这些信息生成新的声明。同时,我们也学习了如何从源代码中移除宏声明,避免宏的重复展开,并设置了新的声明参数,以生成和返回新的声明内容。

本文旨在为读者提供一个关于Swift宏和SwiftSyntax库的全面视角,帮助您理解如何在Swift中实现和使用宏,提高代码的组织性和可维护性,同时在编译时生成更高效、更简洁的代码。随着Swift语言的不断发展,宏的运用将为开发者带来更多的灵活性和能力,助力构建更加强大的Swift应用程序。希望本文能成为您在Swift宏世界中探索和实践的起点,激励您在项目中充分发挥宏的潜力。

08

参考

  • https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/

  • https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/

  • https://developer.apple.com/documentation/swift/applying-macros/

  • https://swift-ast-explorer.com/

  • https://github.com/swiftlang/swift-evolution/blob/main/proposals/0402-extension-macros.md



继续滑动看下一个
搜狐技术产品
向上滑动看下一个