Swift Macros - 宏之协议

235 阅读8分钟

Swift 宏的强大源于其背后一套精巧严谨的协议体系。这些协议定义了:

  • 宏的行为规范:如何与编译器通信,如何生成语法树
  • 宏的能力边界:什么宏可以插入什么样的结构
  • 宏的输入输出约束:需要接受什么样的输入,返回什么样的输出

在 Swift 中, “宏 = 协议方法的实现” 。宏不会在运行时参与逻辑,而是在编译期间将协议方法转换为结构化代码。

本篇将深入解析这些协议的共性特征与调用方式,为你在后续实现各种角色宏打下统一的基础。

Swift 宏协议的共性特征

Swift 宏虽然分工明确(表达式宏、声明宏、成员宏等),但它们的实现方式高度统一,主要体现为以下特征:

编号特征描述
1方法统一命名为 expansion所有宏协议都实现 static func expansion(...) 作为展开主入口。
2支持 throws 异常机制展开过程中可中止并抛出诊断错误。
3必带 context 参数提供编译期上下文信息,是宏的“工具箱”。
4必带 node 参数表示宏的调用现场,如 #宏名(...)@宏名
5输入输出皆为 Syntax 类型宏只操作语法树,输入输出都是 SwiftSyntax 节点。
6仅在编译期执行宏不能访问运行时信息,所有逻辑基于静态源码。
7返回类型严格固定每种宏角色返回类型不同,且不可交叉使用。

1. 所有宏都实现 static func expansion(...)

Swift 宏协议统一使用 expansion 方法命名,使得不同类型的宏拥有相似的签名与调用习惯,极大降低学习与维护成本。

 // 各协议方法签名示例
 protocol ExpressionMacro {
    static func expansion(...) throws -> ExprSyntax
 }
 ​
 protocol DeclarationMacro {
    static func expansion(...) throws -> [DeclSyntax]
 }
  • 方法总是 static,因为宏不依赖实例
  • 输入是调用现场 node + 编译上下文 context
  • 输出是结构化语法树,如 ExprSyntaxDeclSyntax

2. 宏支持 throws,可中止并报告错误

所有宏的 expansion 方法都支持 throws,允许在发现语义错误时立即中止,并通过 context.diagnose(...) 抛出诊断信息,提升宏的可维护性与用户友好度。

错误提示.png

只需要在适当的地方抛出异常,你可以自行编辑异常的message,以便使用者更好的理解该异常。

 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        throw ASTError("错误提示: the macro does not have any arguments")
    }
 }

你可以通过自定义错误类型(如 ASTError)提供清晰的人类可读信息,IDE 也会高亮定位到宏调用位置,提升调试体验。

3. context 宏的工具箱

每个宏都会收到一个 context 参数(类型为 some MacroExpansionContext),这是宏与编译器交互的主要手段,具备多项能力:

 public protocol MacroExpansionContext: AnyObject {
  func makeUniqueName(_ name: String) -> TokenSyntax
  func diagnose(_ diagnostic: Diagnostic)
  func location(of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode) -> AbstractSourceLocation?
  var lexicalContext: [Syntax] { get }
 }

它是宏与编译器沟通的桥梁,也是实现宏逻辑动态化的关键接口。以下是 Swift 宏系统中 MacroExpansionContext 协议四个核心成员的作用详解,按重要性分层说明:

3.1 命名避冲突:makeUniqueName(_:)

自动生成唯一标识符,避免命名冲突

 // 使用场景:临时变量、缓存值、内部标识符等场景。
 let uniqueVar = context.makeUniqueName("result")
 // 输出结果可能是 `result_7FE3A1` 之类的唯一名称

3.2 诊断报告:diagnose(_:)

核心作用:编译时错误报告系统

  • 多级诊断:支持 error / warning / note 三种严重级别
  • 精准定位:关联到具体语法节点(如高亮错误位置)
  • 修复建议:可附加自动修复方案(FixIt)
 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
     
        context.diagnose(Diagnostic(node: node, message: MacroDiagnostic.deprecatedUsage))
        throw ASTError("错误提示: xxxxxx")
    }
 }

某些宏过期时,可以通过 context.diagnose(...) 给于警告提醒。

警告提醒.png

DiagnosticMessage

这里的 Diagnostic.message 需要一个实现 DiagnosticMessage 协议的实例。

 public protocol DiagnosticMessage: Sendable {
 /// The diagnostic message that should be displayed in the client.
 var message: String { get }
 ​
 /// See ``MessageID``.
 var diagnosticID: MessageID { get }
 ​
 var severity: DiagnosticSeverity { get }
 }
  • message:诊断信息的信息

  • diagnosticID:诊断 ID

  • severity:诊断严重程度

     public enum DiagnosticSeverity {
        case error   // 编译错误,阻止构建。
        case warning // 编译警告,不阻止构建。
        case note     // 提示信息,常用于补充说明。
     }
    

3.3 源码定位:location(of:at:filePathMode:)

可定位到调用宏的具体源代码行列,便于诊断、代码导航、日志标注等用途:

 public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
 ) throws -> ExprSyntax {   
    let loc = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID )
    ......
 }

func location( of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode ) -> AbstractSourceLocation?

AbstractSourceLocation 返回值中,可以获取以下信息:

public struct AbstractSourceLocation: Sendable {
/// 文件位置
public let file: ExprSyntax

/// 行的位置
public let line: ExprSyntax

/// 字符位置
public let column: ExprSyntax
  • 四种定位模式

    enum PositionInSyntaxNode {
        case beforeLeadingTrivia  // 包含注释/空格
        case afterLeadingTrivia   // 实际代码起始处
        case beforeTrailingTrivia // 实际代码结束处
        case afterTrailingTrivia  // 包含尾部注释
    }
    
  • 路径显示控制

    • .fileID"ModuleName/FileName.swift"(安全格式)
    • .filePath → 完整系统路径(调试用)

3.4 词法作用域追踪:lexicalContext

核心作用:获取词法作用域上下文

以数组形式,记录从当前节点向外的层层包裹结构;

经过脱敏处理(如移除函数体、清空成员列表)。

// 检查是否在类方法中
let isInClassMethod = context.lexicalContext.contains { 
    $0.is(FunctionDeclSyntax.self) && 
    $0.parent?.is(ClassDeclSyntax.self) != nil
}

4. node 调用现场信息

每个宏的 expansion 方法,除了 context 外,还会接收一个 node 参数,类型通常是 some SyntaxProtocol(如 FreestandingMacroExpansionSyntaxAttributeSyntax 等)。

它代表了宏的调用现场——也就是源码中触发宏展开的那段语法结构。

简单理解:node 就是“#宏名(...)”或“@宏名” 这一整段的解析结果。

以自由宏为例,node 类型通常是 FreestandingMacroExpansionSyntax,它包含了调用宏时的所有组成元素:

public protocol FreestandingMacroExpansionSyntax: SyntaxProtocol {
  var pound: TokenSyntax { get set }  // "#" 符号
  var macroName: TokenSyntax { get set }  // 宏名
  var genericArgumentClause: GenericArgumentClauseSyntax? { get set } // 泛型参数
  var leftParen: TokenSyntax? { get set }  // 左括号 "("
  var arguments: LabeledExprListSyntax { get set }  // 参数列表
  var rightParen: TokenSyntax? { get set }  // 右括号 ")"
  var trailingClosure: ClosureExprSyntax? { get set }  // 尾随闭包
  var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax { get set }  // 多个尾随闭包
}

具体能做什么?

通过解析 node,可以在宏内部获取宏调用时传递的信息,从而进行自定义生成:

  • 提取参数:解析 arguments,得到用户传入的内容;
  • 读取宏名:从 macroName 获取调用者使用的名字(有些宏支持重名扩展);
  • 处理泛型:如果 genericArgumentClause 存在,可以根据泛型参数生成不同代码;
  • 解析闭包:支持分析和利用用户传递的尾随闭包;
  • 实现自定义行为:比如根据传入参数数量、类型、值,决定生成什么样的代码。

示例

public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
) throws -> ExprSyntax {
    // 取出第一个参数
    guard let firstArg = node.arguments.first?.expression else {
        throw ASTError("缺少参数")
    }
    
    // 根据参数生成不同表达式
    return "print((firstArg))"
}

小结: node = 宏调用时的源码快照context = 辅助功能工具箱

两者结合使用,才能让宏既能理解调用现场,又能灵活地生成对应代码。

5. 输入输出皆基于 Syntax 节点

Swift 宏以结构化 AST(抽象语法树)为基础,输入输出都基于 SwiftSyntax 类型,例如:

  • 输入:AttributeSyntaxFreestandingMacroExpansionSyntaxDeclSyntaxProtocol
  • 输出:ExprSyntax[DeclSyntax][AccessorDeclSyntax] 等。

这种设计保证了宏生成的代码具备:

  • 与手写代码一致的结构完整性;
  • 良好的可分析性与可重构性;
  • 自动享受 IDE 语法高亮、错误检测等支持。

Swift 宏不是简单拼接字符串,而是真正生成 AST。

6. 宏只运行于编译时

Swift 宏只能在编译期运行,这意味着它们不能访问运行时信息、全局变量、实例状态或外部服务。所有宏的行为都必须建立在静态源代码、类型系统和语法结构之上。

这为宏提供了如下保证:

  • 可预测性:展开结果与运行环境无关,确保行为一致;
  • 可分析性:工具链可以分析宏行为,进行语法检查与补全;
  • 可维护性:宏代码不会隐藏运行时副作用,有利于重构和测试。

开发者在编写宏时,也应遵循“编译时思维”,尽可能将逻辑转化为静态分析与结构转换。

7. 每种宏的返回类型固定

每个宏协议都明确限定了其 expansion 方法的返回类型,这种限制具有强约束力:

宏协议返回类型
ExpressionMacroExprSyntax
DeclarationMacro[DeclSyntax]
MemberMacro[DeclSyntax]
AccessorMacro[AccessorDeclSyntax]
BodyMacro[CodeBlockItemSyntax]
ExtensionMacro[ExtensionDeclSyntax]
MemberAttributeMacro[AttributeSyntax]

这种强约束带来:

  • 类型安全;
  • 生成结果合法;
  • 避免不同宏角色混淆使用。

比如:成员宏只能生成成员声明,不能直接生成表达式或代码块。

总结

Swift 宏协议的结构化设计,使得宏具备了安全、清晰、灵活的特性。无论你编写哪种类型的宏,理解 expansion 的统一调用模式、context 工具箱能力、node 的语法抽象、以及 Syntax 类型的输入输出机制,都是构建可靠宏逻辑的基础。

在接下来的章节中,我们将深入每一种宏协议(如 ExpressionMacroDeclarationMacro 等),并结合实际案例,帮助你实现更多有趣且实用的 Swift 宏。