前言

本年的 WWDC 重头戏无疑是苹果初次发布的 Vision Pro 头显,以及配套的 visionOS,感谢苹果爸爸的努力,iOS开发总算又双叒叕 有人要辣!

除此之外,苹果也发布了一系列基建更新,其间包含 Xcode 15、Swift 5.9、iOS 17 等等,而 Swift 5.9 引进了一个重大更新——Swift 宏(Macro)。

关于宏的概念和运用在其他言语中并不陌生,假设写过 objective-c 的话,那最常见到的宏莫过于下面这一对

// 出自 FLEX 源码
#define weakify(var) __weak __typeof(var) __weak__##var = var;
#define strongify(var) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong typeof(var) var = __weak__##var; \
_Pragma("clang diagnostic pop")
#endif

它们的用法也很简略

// 出自 FLEX 源码
weakify(self);
xxx = [NSNotificationCenter.defaultCenter
    addObserverForName:UIContentSizeCategoryDidChangeNotification
    object:nil queue:nil usingBlock:(NSNotification *note) { 
                strongify(self)
                
    }
];

C 言语的宏,其本质是文本替换,说白了便是一堆支撑必定插值才干的替换规矩,并在编译进程中的预处理阶段将用到的宏依照规矩替换成对应代码,然后参与后续的编译进程,这就导致这种宏存在以下问题

  1. 缺少类型查看和编译校验,不能限制宏的运用规模,也感知不了宏翻开后的源码是否满意语法语义要求

  2. 宏对源码的改动缺少上下文信息,在运用到宏的当地,宏针对源码详细做了什么,很难被开发者轻松区别

  3. 运用到宏的源码,假设想要了解宏翻开后的代码,或许进一步针对这些代码进行断点调试,难度很大

Swift 宏的特色

而 Swift 宏一举处理了上述问题,并提出了宏规划的四个准则

  1. Dinstinctive use Sites

  2. Complete, type-checked, validated

  3. Inserted in predictable ways

  4. Macors are not magic

这些准则在本年 WWDC 的 Expand on Swift macros 有详细解说,我这儿就用直观的办法展现一下 Swift 宏的特色。

首要是 Swift 宏在 Xcode 里的展现,用到的 Swift 宏都会用 # 或许 @ 进行标识,就像下面这样,一目了然

其次,Swift 宏具有类型查看和语法树查看才干,能够限制宏的运用场景,还能够清晰宏承受的参数类型,从这一点看,宏的完结和函数十分相似

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDC23MacroMacros", type: "StringifyMacro")

而关于宏的不合理运用,能够抛出确诊信息,又称为 Diagnostic,能够支撑宏开发者自界说,还支撑 FixIt 的才干。

终究,Xcode 供给了预览宏翻开的才干,直接对用到的宏右键就能看到预览按钮,预览速度也十分快,再也不必人肉剖析宏翻开的代码了。

Xcode 关于宏的支撑十分强壮,乃至到了能针对宏翻开后的源码进行断点调试的地步,能够说是十分贴心了。

Swift 宏的安全性

Swift 宏在供给足够灵活性和易用性的一起,还供给了安全的代码转化才干,保证了宏关于源码有限的修正才干,这种安全代码转化主要体现在

  • Swift 宏只能新增源码,不能删除或许修正已有的源码,然后保证宏的代码刺进是可猜测的

例如下面这段代码,无论中间的 macro 是什么,都不会影响到 start 和 finish 办法的调用。

  • Swift 宏内部运用 SwiftSyntax 拜访源码和拼装新代码,保证了翻开前后语法树的结构不会被破坏,也完结了类型查看和错误提示才干,这一点咱们在后边的实战环节再翻开叙述
// Swift 宏完结的中心办法之一
// DeclGroupSyntax 和 AttributeSyntax 都是 SwiftSyntax 供给的 Swift 语法树的模型类
static func expansion<
  Declaration: DeclGroupSyntax,
  Context: MacroExpansionContext
>(
  of node: AttributeSyntax,
  providingMembersOf declaration: Declaration,
  in context: Context
) throws -> [DeclSyntax]
  • Swift 宏的翻开进程是安全的

这儿先简略介绍下 Swift 宏的翻开进程

  1. Swift 编译器从源码中提取宏的调用,转化为原始语法树,发送给包含宏完结的编译器插件(Compiler Plug-in)

  2. 编译器插件在 独立进程安全沙盒 中获取到原始语法树后,调用宏界说的完结

  3. 宏的完结完结对原始语法树的翻开(expansion),并生成新的语法树

  4. 编译器插件将新的语法树序列化后刺进到源码中,参与后续的编译进程

能够看到,编译器插件是一个独立进程,在这个进程里,Swift 限制了宏与外界交换信息的才干,例如文件读取、网络请求等等,假设调用相关系统 API,例如经过 FileManager 读取磁盘文件,将会直接回来错误。然后杜绝了咱们在运用外部宏的进程中,无意中被外部宏不合法攻击的风险(在宏插件中挖矿,想想还挺刺激)。

"The file “xxx” couldn’t be opened because you don’t have permission to view it."

Swift 官方也清晰主张咱们不要在宏完结傍边运用除了编译器供给的信息以外的其他任何信息,咱们需求保证宏自身是一个纯函数(pure functions),只需编译器传入的语法树没有发生改变,咱们的输出就不应该改变,这能够协助编译器优化宏的翻开,例如缓存翻开后的代码等等。

而假设咱们运用了额定的上下文信息,比方随机数(每次调用随机回来一段代码完结)、日期(依据单双号回来 true 和 false ),或许结合多个宏的上下文信息(经过宏来悄悄搜集源码细节),这种宏的行为将是不可预知的。

开发 Swift 宏

环境预备

讲完了 Swift 宏的特色和安全性,总算咱们能够进入到实战环节了,工欲善其事必先利其器,首要咱们需求做好以下预备

  • macOS Ventura 13.3 以上操作系统

  • Xcode 15 以上,本文运用的版本是15.0 beta (15A5160n)

  • Swift 入门级语法(把握 Hello World 的 4 种写法)

然后咱们需求初始化一个 Swift 宏开发工程。

  1. 直接翻开 Xcode 15,File -> New -> Package
  1. 挑选 Swift Macro 模版,然后给咱们的工程起一个名字,我这儿就叫做 “SwiftMacroKit”
  1. 翻开工程,大功告成

这是一个 SPM 办理的工程,假设翻开 Package 文件,咱们能够看到它依靠了前面说到的 SwiftSyntax 库

dependencies: [
        // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
        .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
    ],

除此之外,工程其实还依靠了两个库, SwiftSyntaxBuilder 和 SwiftSyntaxMacros ,这三个库的职责别离是

  • SwiftSyntax:供给 Swift 语法树支撑

  • SwiftSyntaxMacros:供给完结宏所需求的协议和类型

  • SwiftSyntaxBuilder:为翻开新代码供给快捷的语法树创立 API

整个工程的源码主要有 4 个部分,截图暗示如下

它们别离是

  • SwiftMacroKit:包含 Swift 宏的界说,注意这儿不供给完结,可是会将界说与完结连接起来

  • SwiftMacroKitClient:一个测验工程(所以称为 Client),能够在 main 函数里测验 SwiftMacroKit 界说的宏

  • SwiftMacroKitMacros:宏的中心完结部分,终究打包成 macro 产物供给给其他模块运用,例如在 SwiftMacroKit 中引证

  • SwiftMacroKitTests:Swift 宏的测验模块,苹果官方引荐咱们选用 TDD 的办法开发咱们的宏,后边会讲到

Swift 宏分类

在开发榜首个宏之前,咱们还需求了解 Swift 供给了哪些宏。依照不同宏在源码中扮演的角色(role),以及在源代码中能够扩展的不同位置,Swift 将宏分为了两大类

  • 独立宏(Freestanding):顾名思义,能够独立存在的宏,不依靠于已有的代码完结

  • 绑定宏(Attached):需求绑定到特定源码位置的宏,包含类型、枚举、办法、函数等等

独立宏

进一步地,独立宏能够分为

  • 表达式宏(Expression)

  • 声明宏(Declaration)

表达式与声明是编程言语中两个特定的概念,为了偷懒,我这儿就直接把 chatgpt 的解说搬过来了。

在核算机编程中,表达式和声明是两个不同的概念。

表达式表明核算和回来一个值的特定操作或句子。表达式能够包含根本的算术操作(例如:+、-、*、/ 等),逻辑操作(例如:&&、||、!等),还能够包含函数调用、变量赋值、字面量等。表达式通常会发生一个成果值,并且能够把这个值传递给其他的表达式或句子进行操作。

声明用于将一个实体引进到程序中,例如变量、常量、函数、类、结构体、枚举等。声明供给了实体的界说,包含其称号、类型、作用域等信息,并且告知编译器如安在程序中创立该实体。声明并不履行任何操作,并且不会发生成果值。而是界说程序中特定实体的特色和行为,以及该实体怎么履行操作。

总体来说,表达式和声明在编程言语中具有不同的作用和功能。表达式用于履行操作并输出成果值,声明用于引进实体,并界说其特色和行为。这两个概念在程序规划中都很重要,因为它们答应开发人员界说程序的运行和行为,并履行特定的核算和操作。

简略而言,表达式和声明的差异在于,表达式有一个回来值成果,而声明会引进新的实体,例如特色、类型等等,能够看到下面别离举了表达式和声明的三个例子,注意到,表达式的回来值是能够作为参数传递给其他函数的,而声明没有回来值,所以不能作为参数传递

var x: Int // 声明一个名为 x 的变量,类型为整数
x = 10 // 表达式,将 x 的值设置为 10
print(var y: Int) // Error: Expected expression in list of expressions
print(x = 10) // 打印成果:()

不过了解这些其实也没什么用,后边举个实践例子就能看明白了,现在咱们只需求记住,独立宏分为表达式宏和声明宏,它们能够在代码中翻开为表达式和声明。

绑定宏

绑定宏也能够进一步分为 5 类,别离标识了绑定宏所绑定的目标

  • 对等宏(peer)

  • 拜访器宏(accessor)

  • 成员宏(member)

  • 成员特色宏(memberAttribute)

  • 一致性宏(conformance)

这些绑定宏的命名和它们的用法休戚相关,咱们就留到实战环节去解说,需求说明的一点是,这儿的中文称号都是我自己机翻的,不代表官方用法,假设担心发生误导,主张以英文为准。

实战环节

实战环节,我会依照上面的宏分类,顺次以一个业务场景和技术领域里存在的问题为线索,别离完结出对应类别的宏。

表达式宏(Expression Macro)

因为独立表达式宏是榜首个解说的宏,所以咱们会叙述的细致一些。

宏界说

咱们以官方供给的 demo 为例,翻开 SwiftMacroKit,也便是界说宏的文件,咱们就能够看到模版主动生成的 stringify 宏。

/// A macro that produces both a value and a string containing the
/// source code that generated the value. For example,
///
///     #stringify(x + y)
///
/// produces a tuple `(x + y, "x + y")`.
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "SwiftMacroKitMacros", type: "StringifyMacro")

能够看到,榜首行的 @freestanding(expression) 润饰符标明晰这个宏是一个独立表达式宏,本质上这儿的 freestanding 扮演的便是宏的角色。

第二行,咱们用到了 macro 关键字来声明一个名为 stringify 的宏,它承受一个泛型参数 T,和一个 T 类型的参数 value,一起回来一个元组,别离包含一个 T 类型的实例和一个 String 字符串。整个宏界说十分相似于一个函数,这样必定程度上协助咱们降低了学习本钱。

func stringify<T>(_ value: T) -> (T, String)

接下来,在等号右侧,咱们用到了 externalMacro 关键字,它相似一个独立表达式宏,因为它用到了 # 前缀符号。externalMacro 用于引证一个外部模块的宏完结,这儿咱们将 stringify 的完结绑定到 SwiftMacroKitMacros 模块的 StringifyMacro,待会咱们会看到 StringifyMacro 是什么。

通常咱们都会选用这样的办法,在咱们自己的工程里引进外部宏,苹果官方也说到了咱们能够引进闭源完结的宏,或许直接在当前模块完结宏,因为我还没有悉数测验,所今后续就都以这种办法引进宏。

用法

咱们还没有说到 stringify 宏的用法,它主要将传入的参数,与其对应的字符串表达拼装成一个元组,作为表达式的回来值回来,例如下面这段示例代码

let 大锤 = 80
let 小锤 = 40
let 一杯宫殿玉液酒 = 180
let 一副拐 = 220
let (result, code) = #stringify(大锤 + 小锤 + 一杯宫殿玉液酒 + 一副拐)
print("\(code) = \(result)")

它将打印如下内容 (对不起,玩个烂梗)

大锤 + 小锤 + 一杯宫殿玉液酒 + 一副拐 = 520

宏的完结

接下来,咱们就来看看 stringify 宏是怎么完结的,咱们翻开 SwiftMacroKitMacro.swift

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
/// 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 struct StringifyMacro: ExpressionMacro {
    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))"
    }
}
@main
struct SwiftMacroKitPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
    ]
}

能够看到,模板代码乃至连注释都写好了,详细解说了 StringifyMacro 的作用。咱们直接看下代码。

首要看到,在代码最底部,有一个 @main 润饰的结构体 SwiftMacroKitPlugin,实践上这便是 SwiftMacroKitMacro 的 main 函数,它完结了 CompilerPlugin 协议,并经过 providingMacros 对外暴露了 StringifyMacro 的类型,因为一切的宏协议的中心办法都是静态办法,咱们在这儿不需求初始化任何宏目标,直接回来类型即可,这也契合咱们将宏视为纯函数的规划准则。

而假设这儿咱们不暴露对应的宏,那么外部就无法引证到,咱们用到宏的当地就会报错

终究让咱们看看 StringifyMacro 这个结构体,它完结了 ExpressionMacro 协议,标明它是一个表达式宏,同样的,后续咱们会看到,前面说到的悉数 7 大类宏都有对应的协议

  • 独立宏

  • 绑定宏

而一切这些宏协议,都会供给一个中心的 expansion 办法用于完结宏翻开,这儿咱们看到,SwiftMacroKitPlugin 就完结了这样一个 expansion 办法

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))"
}

咱们先看下办法签名里的入参

  • node:代表宏自身,这儿也便是 stringify 这个宏,咱们能够拜访到 node 的宏称号、参数列表等等

  • context:宏上下文,它能够协助咱们抛出 Diagnostic 确诊信息,还能够定位源码文件名和行数等等上下文信息

在办法完结中,咱们就经过 node 的 argumentList 特色,取到了榜首个参数及其表达式,依据前面的例子,以及咱们的断点成果,能够看到 argument 长这样

能够看到,这儿的 argument 实践上便是一个 SyntaxTree,上面有许多节点,包含了源码中的一切语法信息,咱们能够依据咱们的需求去遍历获取想要的内容。

在这儿,咱们直接将 argument 和 argument 的 description 特色,也便是 argument 的字符串表明,经过终究一行的字面量代码,组合为元组回来。没错,这儿咱们像写代码相同,把元组作为”字符串”回来了,十分简略直接。

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

但咱们会发现,整个 expansion 办法需求回来一个 ExprSyntax,也便是表达式语法实体,而咱们却回来了一个”字符串”,这样不会类型不匹配吗?

实践上,这儿并不是回来字符串,想要验证这一点,咱们能够换成下面的写法

let result = "Hello world" // result is String
return result

编译后咱们就会发现 Xcode 因为类型不匹配直接报错了。

实践上在这儿,咱们是在经过 字符串****字面量 创立语法树,当咱们编译宏的时分,Swift Parser 也会主动将这段字面量代码转化为 SyntaxTree,也便是 ExpreSyntax,这一特功能够协助咱们在不必深化了解语法树的基础上,也能经过字面量完结宏的完结,究竟复制粘贴谁还不会了

咱们一起会发现,回来值的第二个参数用到了 literal 关键字,literal 能够协助咱们对传入的字符串变量里的字符进行转义以保证编码正确,例如,假设咱们想让元组第二个字符串参数一直回来一个双引号

假设咱们直接用原始字符串的值

return "(\(argument), \(raw: "\""))"

那么咱们终究会发现代码翻开后变成了这样,双引号直接导致语法错误。

let (result, code) = (大锤 + 小锤 + 一杯宫殿玉液酒 + 一副拐, ")

但假设咱们用 literal 关键字,就能够得到正确的翻开成果

public static func expansion(...) {
                  
        return "(\(argument), \(literal: "\""))"
}
// 宏翻开后
let (result, code) = (大锤 + 小锤 + 一杯宫殿玉液酒 + 一副拐, #"""#)

当然知道这些其实也没什么用,只需记住带上 literal 大概率就不会有字符串转义问题就能够了,就算真的遇到了也会很简略发现问题所在。

宏的测验与调试

以上便是 stringify 的悉数完结了,接下来咱们应该持续解说其他类型的宏,但为了便利理解,有必要在这儿提一下怎么测验和调试宏。

前面说到,咱们是经过在 expansion 函数中断点的办法,来确认怎么从 node 参数获取到咱们想要的宏入参 ,而为了完结断点调试,就必须履行单元测验,在这儿也便是 SwiftMacroKitTests.swift 文件。

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self,
]
final class SwiftMacroKitTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            #stringify(a + b)
            """,
            expandedSource: """
            (a + b, "a + b")
            """,
            macros: testMacros
        )
    }
}

文件现已供给了两个单元测验办法,咱们只看其间一个即可,这儿用到了 assertMacroExpansion 办法,它能够依据传入的原始代码和预期的翻开后代码,对宏进行测验,测验阶段,咱们就能够在宏的 expansion 完结中参加断点调试了。assertMacroExpansion 还需求传入待测验的宏,这儿传入的是文件最最初界说的 testMacros,假设咱们参加了新的宏,也需求更新这儿才干测验。

不过这儿需求吐槽的是,像这种字符串代码的传入办法真的十分简略犯错,也不是很直观,假设仅仅为了断点调试,仍是比较麻烦的,期望今后能找到其他更好的调试办法。

声明宏(Declaration Macro)

其实独立表达式宏许多时分和一个函数界说很像,所以仍是很简略上手的,接下来咱们看独立宏的另一个类别,声明宏。

让咱们考虑这样一个场景,咱们有一些蛇形命名的字符串常量,期望将它们界说为常量,常量名是驼峰命名的,大概像这样

struct Constaints {
    public static var appIcon = "app_icon"
    public static var emptyImage = "empty_image"
    public static var errorTip = "error_tip"
}

每次新增一个常量,都需求咱们从头界说变量名,会很麻烦,咱们就能够用声明宏来处理这个问题。

首要让咱们界说一个声明宏,能够看到咱们承受一个 String 类型的参数。

@freestanding(declaration)
public macro Constant(_ value: String) = #externalMacro(module: "SwiftMacroKitMacros", type: "ConstantMacro")

接下来,咱们去完结这个 ConstantMacro 类,别忘了在 SwiftMacroKitPlugin 中增加咱们新创立的宏。

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

ConstantMacro 的完结十分简略,咱们需求完结 DeclarationMacro 协议的 expansion 办法。

public struct ConstantMacro: DeclarationMacro {
    public static func expansion<Node, Context>(of node: Node, in context: Context) throws -> [DeclSyntax] where Node : FreestandingMacroExpansionSyntax, Context : MacroExpansionContext {
        guard
            let name = node.argumentList.first?
                .expression
                .as(StringLiteralExprSyntax.self)?
                .segments
                .first?
                .as(StringSegmentSyntax.self)?
                .content.text
        else {
            fatalError("compiler bug: invalid arguments")
        }
        let camelName = name.split(separator: "_")
            .map { String($0) }
            .enumerated()
            .map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() }
            .joined()
        return ["public static var \(raw: camelName) = \(literal: name)"]
    }
}

在办法完结里,咱们首要经过 node.argumentList 获取到入参字符串 name。

怎么确认拜访途径呢,当然是经过断点调试,配合上控制台打印。因为咱们想获取到 “error_tips” 这个字符串自身,所以咱们需求将 expression 经过 as() 函数转化为字符串字面量表达式符号(StringLiteralExprSyntax),然后从 segments 中获取到榜首段字符串,终究从 content 中取出字符串。

(关于 segments 的处理涉及到 SwiftSyntax 傍边的字符串字面量的概念,为了简略处理,咱们这儿假设传入的字面量不包含阶段和插值)

然后经过一系列高阶函数,咱们将 name 转化为驼峰命名的 camelName。

在回来值这儿,DeclarationMacro 和 ExpressionMacro 略有不同。因为表达式只能有一个回来值,所以 ExpressionMacro 只能回来一个 ExprSyntax,而 DeclarationMacro 答应咱们创立多个声明,所以回来的是 DeclSyntax 数组。

在这儿,咱们就简略将 camelName 和 name 组合起来,构成一个静态特色回来出去,就完结了声明宏。

让咱们看看实践运用的作用。

struct Constaints {
    #Constant("app_icon")
    #Constant("empty_image")
    #Constant("error_tip")
}

不出意外的话,Xcode 会抛出三个错误,大概意思是 name 不契合 Constant 宏。

呈现这个错误的原因是因为声明宏要求咱们翻开宏今后所得到的 name 称号,无论是变量名、办法名仍是类型名,都应该清晰它的命名规矩,以避免命名抵触。

例如下面这种代码,咱们经过 Constant 所生成的 errorTip 变量将与 errorTip() 办法抵触。

struct Constaints {
    #Constant("error_tip")
    func errorTip() -> String {
    }
}

所以 Swift 宏规定了以下几种命名规矩

  • overloaded:与绑定的命名完全一致

  • prefixed:增加具有相同根本称号的声明,但增加了指定的前缀

  • suffixed:与 prefixed 相似,运用后缀而不是前缀

  • named:宏增加具有特定的、固定的根本称号的声明

  • arbitrary:宏增加了一些其他称号的声明,这些称号无法运用任何这些规矩来描述

其间前四种都是限制宏称号在特定规模的,官方文档说到运用这些规矩能提升编译器对宏的处理功能,咱们会在后边经过实践案例解说。而这儿,因为咱们的变量名只能依据传入的静态字符串决议,所以咱们挑选终究一种规矩 arbitrary。

@freestanding(declaration, names: arbitrary)
public macro Constant(_ value: String) = #externalMacro(module: "SwiftMacroKitMacros", type: "ConstantMacro")

要注意,表达式宏并不强制要求 names,因为表达式宏不太或许引进新的称号,而声明宏则是强制要求的,否则就会报错。

这儿还有一个问题,咱们关于 names 的限制是在声明宏的当地,可是实践上,真正决议称号规矩的是宏的完结,这样相当于由宏的调用方反向限制宏的完结方,感觉规划上并不是很合理。依照 Swift 宏的规划文档 看,本意是期望依据 names 挑选性翻开宏,但从现在表现来看,并没有发现这种挑选性的存在。

终究让咱们看下宏翻开后的预览作用,十分完美~

对等宏(Peer Macro)

看完了独立宏,咱们接下来看看绑定宏,绑定宏的运用看起来就比较高级了,首要咱们看看对等宏,这儿 Peer 的意思是说,对等宏翻开后的代码将与原绑定的元素处于同一层级,例如都在 toplevel,或许都在一个类、一个枚举里。

让咱们考虑一个实践问题,假设咱们在一个模块内界说了一个基类 Merchant。

class Merchant: MerchantInterface {
    var name: String = ""
    func product(num: Int) {
        // ...
    }
}

模块内部分办法本来只承受基类作为参数,但随着业务发展,现在咱们答应外部也传入和基类 Merchant 结构相同、但完结不同的示例,换句话说,咱们期望笼统出一个协议 MerchantInterface,让外部能够传入自界说的模型参数。

public protocol MerchantInterface {
    var name: String  {
        get
    }
    func product(num: Int)
}

咱们期望能根据 Merchant 的完结,主动地生成和更新对应的 MerchantInterface 协议类,避免重复单调的协议声明进程,这种场景就能够用对等宏来完结。

咱们将依据 Merchant 的完结,取出它的特色和办法,组合成一个 MerchantInterface 协议,翻开后它将附加在 Merchant 的后边,一起 Merchant 也能够完结这个协议,详细作用见下图。

咱们需求完结一个 InterfaceGenMacro 的对等宏,宏的声明和注册咱们就不赘述了,直接看下宏的中心完结吧。

public struct InterfaceGenMacro: PeerMacro {
    public static func expansion<Context, Declaration>(of node: AttributeSyntax, providingPeersOf declaration: Declaration, in context: Context) throws -> [DeclSyntax] where Context : MacroExpansionContext, Declaration : DeclSyntaxProtocol {
                ...
    }
}

InterfaceGenMacro 遵从了 PeerMacro 协议,所以要完结对应的 expansion 办法。咱们能够将咱们的诉求拆分为三个部分

  • 发生协议名

  • 发生协议中的变量声明

  • 发生协议中的办法声明

首要看下协议名,咱们假定咱们的 Merchant 只能是类,经过断点调试咱们能够知道,expansion 办法的 declaration 参数会是一个 ClassDeclSyntax 类型的实例,所以咱们先进行一次转化。

guard
    let classDecl = declaration.as(ClassDeclSyntax.self)
else {
    fatalError("compiler bug: invalid declaration")
}

稍后在这儿咱们能够抛出 Diagnostic,给出详细的错误信息。

接下来依据 ClassDeclSyntax 的结构,咱们很简略知道怎么获取类名,然后就能够确认协议的称号。

let className = classDecl.identifier.text // \(raw: className)Interface

然后,对 ClassDeclSyntax 进一步剖析能够发现,它还有一个 members 的目标,其间包含两个 MemberDeclListItemSyntax 元素,从它们的 decl 目标名 VariableDeclSyntax 和 FunctionDeclSyntax 咱们能够判别出一个是变量,一个是办法(从后边的字符串常量也能够剖析出来)。

memberBlock: MemberDeclBlockSyntax
├─leftBrace: leftBrace
├─members: MemberDeclListSyntax
 ├─[0]: MemberDeclListItemSyntax
  ╰─decl: VariableDeclSyntax
    ├─bindingKeyword: keyword(SwiftSyntax.Keyword.var)
    ╰─bindings: PatternBindingListSyntax
      ╰─[0]: PatternBindingSyntax
        ├─pattern: IdentifierPatternSyntax
         ╰─identifier: identifier("name")
        ├─typeAnnotation: TypeAnnotationSyntax
         ├─colon: colon
         ╰─type: SimpleTypeIdentifierSyntax
           ╰─name: identifier("String")
        ╰─initializer: InitializerClauseSyntax
          ├─equal: equal
          ╰─value: StringLiteralExprSyntax
            ├─openQuote: stringQuote
            ├─segments: StringLiteralSegmentsSyntax
             ╰─[0]: StringSegmentSyntax
               ╰─content: stringSegment("")
            ╰─closeQuote: stringQuote
 ╰─[1]: MemberDeclListItemSyntax
   ╰─decl: FunctionDeclSyntax
     ├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
     ├─identifier: identifier("product")
     ├─signature: FunctionSignatureSyntax
      ╰─input: ParameterClauseSyntax
        ├─leftParen: leftParen
        ├─parameterList: FunctionParameterListSyntax
         ╰─[0]: FunctionParameterSyntax
           ├─firstName: identifier("num")
           ├─colon: colon
           ╰─type: SimpleTypeIdentifierSyntax
             ╰─name: identifier("Int")
        ╰─rightParen: rightParen
     ╰─body: CodeBlockSyntax
       ├─leftBrace: leftBrace
       ├─statements: CodeBlockItemListSyntax
       ╰─rightBrace: rightBrace
╰─rightBrace: rightBrace

咱们先处理变量名。

let variables = classDecl.memberBlock.members
    .compactMap({ member -> PatternBindingListSyntax? in
        member.decl.as(VariableDeclSyntax.self)?.bindings
    }).compactMap { bindings -> (String, String)? in
        guard
            // 获取变量名
            let variable = bindings.first?.pattern
                .as(IdentifierPatternSyntax.self)?
                .identifier.text,
            //获取类型
            let type = bindings.first?.typeAnnotation?.type.description
        else {
            return nil
        }
        return (variable, type)
    }
    .map({ "    var \($0.0): \($0.1) { get }" })
    .joined(separator: "\n")

能够看出,咱们从 VariableDeclSyntax 中别离提取出变量名和类型,组成元组后,经过字符串插值,组合成变量声明,然后拼接成一个完整的字符串。

接下来是办法声明的处理。

let functions = classDecl.memberBlock.members
    .compactMap({ member -> FunctionDeclSyntax? in
        member.decl.as(FunctionDeclSyntax.self)
    }).compactMap({ funcDecl in
        var new = funcDecl
        new.body = nil // 去除办法体
        return new.description
    })
    .map({
        // 移除前后空格和换行符,并增加缩进
        "    \($0.trimmingCharacters(in: .whitespacesAndNewlines))"
    }).joined(separator: "\n")

为了生成协议中不带办法体的办法声明,这儿我选用了一个 tricky 的办法,将 FunctionDeclSyntax 的 body 直接设置为空,终究经过裁剪拼接,生成办法声明。

终究,咱们只需求将协议名、变量声明和办法声明组合起来,就完结了宏的完结,而翻开作用就和前面展现的截图一模相同。

return ["""
        public protocol \(raw: className)Interface {
        \(raw: variables)
        \(raw: functions)
        }
        """
]

当然,为了便于解说,这儿的处理仍是比较简略的,还有许多问题需求考虑,例如

  • 经过默认值进行类型揣度的变量 var a = “ABC”

  • 带默认参数的办法 func(a: Int = 1024)

  • 区别变量的拜访权限,只读 or 可读可写

这些都能够经过进一步完善 expansion 办法来完结。

抛出 Diagnostic

接下来,咱们期望在开发者误用咱们的宏的时分给出详细的提示,例如针对结构体或许枚举的时分。实践上咱们现在也有错误处理,便是代码最初的 fatalError。

guard
    let classDecl = declaration.as(ClassDeclSyntax.self)
else {
    fatalError("compiler bug: invalid declaration")
}

想要抛出确诊信息,能够凭借 expansion 的 context 参数,有两种办法。

  • 直接自界说 Error 并抛出
enum SwiftMacroKitError: CustomStringConvertible, Error {
    case unsupportType
    var description: String {
        switch self {
        case .unsupportType:
            return "不支撑的类型"
        }
    }
}
{
    context.addDiagnostics(from: SwiftMacroKitError.unsupportType, node: node)
}

其间 node 参数需求给出 SyntaxTree 中的节点,错误信息也会附着在对应节点上,终究作用长这样

  • 经过自界说 DiagnosticMessage 完结
enum SwiftMacroKitDiagnostic: String, DiagnosticMessage {
    case unsupportType
          // 确诊信息类型,warning/error
    var severity: DiagnosticSeverity { return .error }
    var message: String {
        switch self {
        case .unsupportType:
            return "不支撑的类型"
        }
    }
          // 确诊唯一标识
    var diagnosticID: MessageID {
        MessageID(domain: "SwiftMacroKitDiagnostic", id: rawValue)
    }
}
{
    context.diagnose(Diagnostic(node: node._syntaxNode, message: SwiftMacroKitDiagnostic.unsupportType))
}

相比于榜首种办法,这种办法能够抛出更丰厚的信息,例如确诊信息的锚点代码、高亮代码、FixIt 等等,但我没有深化测验,这儿就不细说了。

拜访器宏(Accessor Macro)

拜访器宏,顾名思义,能够拓展生成一个特色的拜访器,包含 get、set、willSet、didSet 等等。在工程中,经常有一些变量需求实时耐久化到本地,例如 UserDefault 傍边,曾经咱们能够经过 PropertyWrapper 来完结,而现在,咱们也能够经过拜访器宏来完结。

让咱们先看看终究作用,能够看到,咱们供给了一个 UserDefault 宏,并直接用变量名作为 key,完结了 flag 变量和 tag 变量的耐久化读写逻辑,当然,咱们也能够给 UserDefault 加上参数,完结自界说 key 的设置。

细心的话会发现这儿生成的 key 前面有一个空格,但宏翻开的完结并没有参加空格,测验了许多办法都去不掉,感觉是 Swift 宏的 bug,待我后续 oncall 完再贴定论吧, 先记个 TODO

和之前相同,咱们仍是需求完结一个 UserDefaultMacro 类,它完结了 AccessorMacro 协议。

public struct UserDefaultMacro: AccessorMacro {
    public static func expansion<Context, Declaration>(of node: AttributeSyntax, providingAccessorsOf declaration: Declaration, in context: Context) throws -> [AccessorDeclSyntax] where Context : MacroExpansionContext, Declaration : DeclSyntaxProtocol {
    }
}

咱们首要需求剖析 declaration,找到变量名和对应的类型

    let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first,
    let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
    let type = binding.typeAnnotation?.as(TypeAnnotationSyntax.self)?.type.as(OptionalTypeSyntax.self)?.wrappedType.description
else {
    fatalError("compiler bug: unknown error")
}

接下来便是完结 getter 和 setter

let getter = AccessorDeclSyntax.init(accessorKind: .keyword(.get), bodyBuilder: {...})
let setter = AccessorDeclSyntax.init(accessorKind: .keyword(.set), bodyBuilder: {...})

咱们没有像之前直接用字符串字面量生成代码,而是调用了 AccessorDeclSyntax 的初始化办法来构建 Syntax,它承受一个拜访器类别,这儿咱们用 keyword 别离指定了 get 和 set。

第二个参数是一个 resultBuilder,假设熟悉 SwiftUI 的话应该不会感到陌生,在 bodyBuilder 里咱们能够履行多行表达式,终究都会合并为一个成果回来出去,例如下面这样的写法,能够在 setter 里生成三行代码

let setter = AccessorDeclSyntax.init(accessorKind: .keyword(.set), bodyBuilder: {
    DeclSyntax(stringLiteral: "print(\"before set \(name)\")")
    DeclSyntax(stringLiteral: """
    UserDefaults.standard.setValue(newValue, forKey: \"\(name)\")
    """)
    DeclSyntax(stringLiteral: "print(\"after set \(name)\")")
})

当然这儿咱们并不需求,咱们直接经过字符串字面量就生成好了耐久化代码。

let getter = AccessorDeclSyntax.init(accessorKind: .keyword(.get), bodyBuilder: {
    DeclSyntax(stringLiteral: """
    UserDefaults.standard.value(forKey: \"\(name)\") as? \(type)
    """)
})
let setter = AccessorDeclSyntax.init(accessorKind: .keyword(.set), bodyBuilder: {
    DeclSyntax(stringLiteral: """
    UserDefaults.standard.setValue(newValue, forKey: \"\(name)\")
    """)
})

以上便是拜访器宏的一个简略运用。

成员特色宏(Member Attribute Macro)

咱们的 UserDefaultMacro 现已十分好用了,可是假设特色多了,咱们的代码或许会变成这样。

这样当然是不能忍的,有没有办法主动给一切特色加上 @UserDefault 宏呢?咱们能够用成员特色宏来完结,成员特色宏能够给一个类型的成员参加新的特色,其间就包含参加宏,当然这儿也体现出宏翻开的一个”递归”的特色,也便是能够在一个宏的翻开中引进新的宏,新的宏还会进一步翻开,就像下面这样

榜首层宏翻开后

第二层宏翻开后,得到了终究的代码。

代码完结也十分简略,咱们乃至不需求新创立宏,直接拓展 UserDefaultMacro,让它完结 MemberAttributeMacro 协议即可。

extension UserDefaultMacro: MemberAttributeMacro {
    public static func expansion<Declaration, MemberDeclaration, Context>(of node: AttributeSyntax, attachedTo declaration: Declaration, providingAttributesFor member: MemberDeclaration, in context: Context) throws -> [AttributeSyntax] where Declaration : DeclGroupSyntax, MemberDeclaration : DeclSyntaxProtocol, Context : MacroExpansionContext {        
        return [.init(stringLiteral: "@UserDefault")]
    }
}

当然这儿的完结比较简略,没有考虑许多鸿沟 case,比方假设特色现已参加了 UserDefaultMacro,就不能重复增加了,至于为什么不考虑,详细原因见下图

成员宏(Member Macro)

成员宏与成员特色宏很像,但作用不同,成员宏能够为类型增加新成员,成员包含特色、办法、枚举等等。

众所周知,Swift 中的 struct 和 class 天然生成有一个不同点,便是在打印目标的时分,struct 能够打印出特色细节,而 class 只有一个类型,像下面这样

struct Product {
    var name: String = "Apple Vision Pro"
    var price: Int = 25888
}
class BusinessModel {
    var count: Int = 0
    var tag: String?
}
print(Product()) // Product(name: "Apple Vision Pro", price: 25888)
print(BusinessModel()) // SwiftMacroKitClient.BusinessModel

而 class 假设也想具有这一特性,就需求开发者自己遵从 CustomStringConvertible 协议,然后完结一个 description 的字符串特色,这样就能够定制字符串的内容了。但一般在开发中,咱们并不想高度定制化的 description,只需能像 struct 相同打印出一切特色即可,这一需求就能够用成员宏来完结。

咱们界说一个 DescriptionMacro,让它承继自 MemberMacro。

public struct DescriptionMacro: MemberMacro {
    public static func expansion<Declaration, Context>(of node: AttributeSyntax, providingMembersOf declaration: Declaration, in context: Context) throws -> [DeclSyntax] where Declaration : DeclGroupSyntax, Context : MacroExpansionContext {
        guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
            fatalError("compiler bug: unknown error")
        }        
    }
}

榜首步咱们要求被绑定的元素是 ClassDeclSyntax,这儿能够依照前面所说,抛出 Diagnostic。

然后还需求校验类是否遵从了 CustomStringConvertible 协议,咱们仍是需求凭借断点调试来确认节点和类型信息。

guard
    let inheritedTypeCollection = classDecl.inheritanceClause?.inheritedTypeCollection,
    inheritedTypeCollection.compactMap({
        $0.typeName.as(SimpleTypeIdentifierSyntax.self)
    }).contains(where: { typeSyntax in
        typeSyntax.name.text == "CustomStringConvertible"
    })
else {
    fatalError("compiler bug: need `CustomStringConvertible` conformance")
}

接下来咱们取出类名和一切特色名,并开端拼装代码

let className = classDecl.identifier.text
let variables = classDecl.memberBlock.members
    .compactMap({ member -> PatternBindingListSyntax? in
        member.decl.as(VariableDeclSyntax.self)?.bindings
    }).compactMap { bindings -> String? in
        bindings.first?.pattern
            .as(IdentifierPatternSyntax.self)?
            .identifier.text
    }
    .map({ "\($0): \\(\($0))" })
    .joined(separator: ", ")

终究,只需求依照 struct 的打印格局,发生出对应的 description 界说即可。

return [
    """
    var description: String {
        "\(raw: className)(\(raw: variables))"
    }
    """
]

让咱们看看终究作用

不过成员宏因为或许引进新成员,进一步引进新的命名,所以也需求指定命名规矩,咱们先回想一下前面说到的规矩。

  • overloaded:与绑定的命名完全一致

  • prefixed:增加具有相同根本称号的声明,但增加了指定的前缀

  • suffixed:与 prefixed 相似,运用后缀而不是前缀

  • named:宏增加具有特定的、固定的根本称号的声明

  • arbitrary:宏增加了一些其他称号的声明,这些称号无法运用任何这些规矩来描述

那么这儿因为 DescriptionMacro 必定会引进的是 description,所以适用于 named 规矩。

@attached(member, names: named(description))
public macro Description() = #externalMacro(module: "SwiftMacroKitMacros", type: "DescriptionMacro")

假设咱们的宏会依据办法名或许类型名增加固定前缀或许后缀,就能够用 suffixed 和 prefixed,假设咱们新增的称号与绑定的命名一致,就能够用 overloaded。

当然假设咱们声明晰对应的命名规矩,却又不按规矩办事,Xcode 也会直接报错。(仅仅这个模糊的错误信息或许会让你摸不着头脑)

@__swiftmacro_19SwiftMacroKitClient13BusinessModel11DescriptionfMm_.swift:1:5: Declaration name 'descriptions' is not covered by macro 'Description'

可是这些规矩并不能完全处理命名抵触的问题,让咱们看看下面这个 DescriptionMacro 的完结。

return [
    """
    var description: String {
        var content = "\(raw: className)"
        content += "(\(raw: variables))"
        return content
    }
    """
]

咱们生成的代码里引进了 content 局部变量,用来暂存和拼接需求打印的内容,大部分情况下这并没有什么问题,但假设遇到下面这样的 BusinessModel 就会出问题了。

@Description
class BusinessModel {
    var count: Int = 0
    var tag: String?
    var content: Data? // 同名 content 特色
}

咱们直接看下打印成果

print(BusinessModel()) // BusinessModel(count: 0, tag: nil, content: BusinessModel)

content 明明是空 Data,成果却打印出来了 BusinessModel,让咱们看下宏翻开后的代码

没错,因为局部变量优先级更高,咱们生成的代码错误地将局部变量 content 带入到了字符串里。所以为了避免这种变量命名的抵触,Swift Macro 供给了一个办法用于生成绝无仅有的、与上下文变量名不会抵触的变量名,用法也很简略。

let contentVariable = context.makeUniqueName("content")
return [
    """
    var description: String {
        var \(raw: contentVariable) = "\(raw: className)"
        \(raw: contentVariable) += "(\(raw: variables))"
        return \(raw: contentVariable)
    }
    """
]

而宏翻开后的代码就变成了这样,这么一长串变量名就很难抵触了。

var description: String {
    var $s19SwiftMacroKitClient13BusinessModel11DescriptionfMm_7contentfMu_ = "BusinessModel"
    $s19SwiftMacroKitClient13BusinessModel11DescriptionfMm_7contentfMu_ += "(count: \(count), tag: \(tag), content: \(content))"
    return $s19SwiftMacroKitClient13BusinessModel11DescriptionfMm_7contentfMu_
}

一致性宏(Conformance Macro)

DescriptionMacro 现已很优异了,可是仍是有一个小瑕疵,那便是它要求被绑定的类型要遵从 CustomStringConvertible 协议,为什么不能直连续协议都帮咱们完结好呢?

一致性宏就能够做到这一点,它能够给类型新增协议,乃至协议的泛型束缚都能够,也便是咱们常见到的 “where xxx“ 句子,让咱们看下怎么完结。

咱们仍然凭借 DescriptionMacro 来完结,直接遵从 ConformanceMacro 协议即可。

extension DescriptionMacro: ConformanceMacro {
    public static func expansion<Declaration, Context>(of node: AttributeSyntax, providingConformancesOf declaration: Declaration, in context: Context) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] where Declaration : DeclGroupSyntax, Context : MacroExpansionContext {
        return [("CustomStringConvertible", nil)]
    }
}

能够看到,ConformanceMacro 的 expansion 函数需求回来一个元组数组,元组内榜首个元素是协议声明,第二个便是泛型束缚句子,这儿咱们不需求就直接回来 nil。

现在关于想要完结 CustomStringConvertible 协议的类,咱们就只需求用一个 DescriptionMacro 来润饰即可。

@Description
class BusinessModel {
    var count: Int = 0
    var tag: String?
    var content: Data?
}

让咱们看看翻开作用,是不是很完美~

总结

以上便是我根据 WWDC2023 放出的 Swift Macro 相关信息总结的一切内容了,总结起来,Swift Macro 能做的事情有点相似元编程,也便是通进程序来生成程序,而在 Swift Macro 呈现之前,咱们也有相似的处理方案,比方 Sourcery、SwiftGen 等等。

现在 Swift Macro 能够部分地代替它们的才干,有了 Apple 生态的加持,Swift Macro 与现有工程的结合或许是它的一部分优势,比方咱们上面所开发的 SwiftMacroKit,是能够经过 SPM 分发到其他工程的,乃至直接本地途径依靠就能够运用。

再比方 Xcode 原生支撑宏翻开预览、调试等等才干,以及对语法树的拜访和生成,都是第三方东西有完结本钱的当地。

但就现在而言,Swift Macro 也有必定局限性,以及不完善的当地,从我自身的体会简略总结了几点

  • 因为 Macro 的安全性规划,导致宏无法与被绑定源码外的信息交互,例如工程信息、磁盘数据乃至远端数据等等,导致适用规模不如第三方东西

  • Macro 的开发和调试本钱相对较高,需求对 SwiftSyntax 有必定了解才干开宣布功能齐备的宏

  • Macro 还处于初期阶段,存在许多 bug 和问题(当然也或许单纯是我不会用),影响运用体会,包含但不限于

* 字面量生成的代码,关于缩进的处理比较简陋

* 宏翻开后有时分会发生奇奇怪怪的空格

* Xcode 的宏翻开预览 bug,有时分会遇到无法翻开的问题,这一点很影响实践开发

* 独立声明宏生成带默认值的实例特色后,Xcode 会提示未初始化

从功能考量动身,Swift Macro 也不适合大规模运用,究竟宏需求在每次编译运行时单独履行必定的代码逻辑,从本质上说这是一种用(编译)时刻换(模版代码)空间、以及(开发者)开发时刻的战略,假设咱们在宏傍边进行了十分复杂的逻辑,终究仅仅仅仅生成一段固定格局的代码,也许直接经过主动化脚本一次性生成好代码会是更优解。

SwiftMacroKit 源码已上传至 GitLab


相关资料

  • Expand on Swift macros

  • Write Swift macros

  • swift-evolution/proposals/0389-attached-macros.md

  • swift-evolution/proposals/0397-freestanding-declaration-macros.md

  • swift-evolution/proposals/0382-expression-macros.md

  • swift-evolution/visions/macros.md

  • A Possible Vision for Macros in Swift – Pitches – Swift Forums

  • A possible vision for macros in Swift GitHub