Swift macro

什么是 Swift macro

Macro 是 Swift 5.9 的新特性之一。

在 OC 中,宏其实就是代码替换,相对比较简单。与 OC 中的宏类似,Swift 也是为了减少重复代码,不同点在于,可以在编译之前动态地操作项目的 Swift 代码,在编译时注入额外的功能。并且,Swift 中的宏可以调试。

注意:宏只会添加新代码,但绝不会修改或删除项目中已有的代码。

image.png

目前在GitHub上已经有很多开发者为 Swift 提供 Macro GitHub - krzysztofzablocki/Swift-Macros: A curated list of awesome Swift Macros

宏的类型

在 Swift 中,宏分类两种:

  • 独立宏 (Freestanding Macro)

  • 附加宏(Attached Macro)

独立宏

类似 OC 中的宏,主要作用是代替代码中的内容。使用 @freestanding 关键字声明,使用的时候以 # 开头。SwiftUI 中预览视图就使用了独立宏:

#Preview {
    ContentView()
}

附加宏

主要作用是为声明的内容添加代码,声明的时候使用 @attached 关键字,使用的时候以 @ 开头。

例:@OptionSet<Int>

创建宏

创建宏主要使用 SwiftSyntax 库。SwiftSyntax 是一组用于解析、检查、生成和转换 Swift 源代码的 Swift 库。

环境要求

Xcode 15+

创建 Package

每个 Swift Macro 都是一个 Package,在 Xcode 中File -> New -> Package,选择 Swift Macro,创建即可。

image.png

可以看到,Xcode 已经为我们创建好了一个 Macro 的 Demo,并且添加了 SwiftSyntax 库的依赖。

image.png
  • [Macro name].swift 宏的声明

  • main.swift 一些测试宏的代码

  • [Macro name]Macro.swift 宏的具体实现

  • [Macro name]Tests.swift 单元测试

声明

声明一个宏主要分为两部分:角色+签名

在刚才创建的 package 中,Xcode 已经在 [Macro name].swift 文件中声明了一个宏。

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

在 main.swift 文件中可以看到stringify这个宏的效果是将表达式 a + b 转换为字符串 "a + b"。

角色

Xcode 提供的示例声明中 @freestanding(expression) 就是一个宏的角色。目前一共有 7 个角色,2 个独立宏,5 个附加宏:

具体的内容及含义,后面会有详细解释。

由于附加宏可能修改属性、方法、协议等内容,所以会给编译器在预编译阶段产生很大的性能负担,不利于增量编译。为了解决这个问题,附加宏在声明的时候需要将修改或者添加的属性通过 names 明确声明出来。

  • named(<declaration-name>) - 特定固定名称

  • arbitrary - 动态名称,例如可能是根据其他参数动态生成的

  • overloaded - 名称不变,只重载参数或者返回

  • prefixed - 通过添加前缀生成的内容,可以用作前缀,允许宏生成以 开头的名称

  • suffixed - 通过添加后缀生成的内容

签名

签名与方法的定义类似。在 Swift 中 func 来定义一个方法,macro 定义一个宏。由于 Swift 中的宏是独立的 Package 所以,宏的定义都是 public 的。最后使用externalMacro指定了宏的实现位置。

实现

根据不同角色,实现的方式有所不同。

根据 externalMacro 指定的信息,可以在 MyMacroMacro.swift 中找到 StringifyMacro。里面定义了宏的实现expansion方法。

可以看到StringifyMacro是一个结构体,遵守ExpressionMacro协议,不同的角色需要遵守不同的协议。

public struct StringifyMacro: ExpressionMacro

协议中有一个expansion方法,每一个 Macro 协议都有一个expansion方法,只是参数及返回值不同。

static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> ExprSyntax

对于协议内的 node 参数部分,可以查看SwiftSyntax 的相关文档,了解树形结构。Swift AST Explorer

可以看到,现在返回了一个字符串,字符串的内部是一段代码,代码的内容是一个元组。元组里有两个值,第一个是 #stringify 传入的参数的值,第二个是 #stringify 传入的参数的描述

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

在 main.swift 中可以看到一个使用示例

let a = 17
let b = 25

let (result, code) = #stringify(a + b)
print("\(code) = \(result)")

示例中,我们可以对#stringify 这个宏展开,展开后可以看到就是我们刚刚返回的字符串内容。

image.png

在 Swift 中编译器在编译时遇到宏的时候,会调用 MyMacroPlugin,生成代码后再进行编译

image.png

独立宏

一个宏只能有一个独立宏角色

@freestanding(expression)

用途

传入一个表达式,输出一个结果。

协议

ExpressionMacro

声明
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")
实现
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))"
    }
}
使用
import MyMacro

let a = 17
let b = 25

let (result, code) = #stringify(a + b)
print("\(code) = \(result)") // a + b = 42
相关文档

暂时无法在飞书文档外展示此内容

@freestanding(declaration)

用途

创建一个或多个声明,比如struct, function, variabletype。SwiftUI 中的 Preview 就是这个角色。

协议

DeclarationMacro

声明
@freestanding(declaration)
public macro todo(_ message: String) = #externalMacro(module: "MyMacroMacros", type: "ToDoMacro")
实现
public struct ToDoMacro: DeclarationMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    guard let messageExpr = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
          messageExpr.segments.count == 1,
          let firstSegment = messageExpr.segments.first,
          case let .stringSegment(message) = firstSegment else {
        fatalError("todo macro requires a non-interpolated string literal")
    }

    context.diagnose(Diagnostic(node: Syntax(node), message: SimpleDiagnosticMessage(
      message: message.description,
      diagnosticID: .init(domain: "test", id: "error"),
      severity: .warning)))
    return []
  }
}
使用
func someFunc() {
    #todo("To do message")
}
相关文档

暂时无法在飞书文档外展示此内容

附加宏

相关文档

暂时无法在飞书文档外展示此内容

@attached(peer)

用途

可以在声明的同级增加代码

协议

PeerMacro

声明
@attached(peer, names: overloaded)
public macro AddCompletionHandler() =
    #externalMacro(module: "MacroExamplesPlugin", type: "AddCompletionHandlerMacro")
实现

内容较长,见 MacroExamples 工程

使用
@AddCompletionHandler
func f(a: Int, for b: String, _ value: Double) async -> String {
    return b
}

被AddCompletionHandler标记的async 方法,会自带一个CompletionHandler

@attached(accessor)

用途

向属性添加get、set 方法,例如 SwiftUI 中的 @State

协议

AccessorMacro

声明
@attached(accessor)
public macro ObservableProperty() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservablePropertyMacro")
实现

内容较长,见 MacroExamples 工程

使用
@ObservableProperty
var name: String = ""

@attached(memberAttribute)

用途

给成员变量添加属性

协议

MemberAttributeMacro

声明
@attached(memberAttribute)
public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(module: "MyMacroMacros", type: "WrapStoredPropertiesMacro")
实现

内容较长,见 MacroExamples 工程

使用
@wrapStoredProperties(#"available(*, deprecated, message: "hands off my data")"#)
struct OldStorage {
    var x: Int
    @available(*, deprecated, message: "hands off my data")
    var y: Int
}

属性 x 在使用的时候会与 y 一样提示 deprecated

@attached(member)

用途

在其所应用的类型/扩展内添加新声明。例如init()

协议

MemberMacro

声明
@attached(member, names: named(init), named(shared))
public macro Singleton() = #externalMacro(module: "MyMacroMacros", type: "SingletonMacro")
实现

内容较长,见 MyMacro 工程

使用
@Singleton
struct MyStruct2 {
    var name: String = ""
}

@attached(extension)

用途

原@attached(conformance)。

conformance 只能给声明的内容添加协议或者 where语句的扩展,但不能添加对应协议的属性,必须要通过其他宏一起使用。例如:

@attached(conformance)
macro AddEquatable() = #externalMacro(...)

@AddEquatable
struct S {}
// expands to
extension S: Equatable {}

所以后续推出了extension 替代 conformance。

协议

ExtensionMacro

声明
protocol MyProtocol {
  func requirement()
}
@attached(extension, conformances: MyProtocol, names: named(requirement))
public macro MyProtocol() = #externalMacro(module: "MyMacroMacros", type: "MyProtocolMacro")
实现
public struct MyProtocolMacro: ExtensionMacro {
    public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
        guard let protocolType = protocols.first else { return [] }
        let decl: DeclSyntax =
          """
          extension \(raw: type.trimmedDescription): \(protocolType) {
            func requirement() {
              print("requirement")
            }
          }

          """

        guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
          return []
        }

        return [
          extensionDecl
        ]
    }

}
使用
@MyProtocol
struct MyStruct3 {
    var name: String = ""
}
相关文档

暂时无法在飞书文档外展示此内容

调试

开发一个宏的时候免不了需要对宏进行测试和调试。调试的方式与代码调试差不多,可以断点也可以 print。

需要注意的是,必须运行单元测试才会进入断点。

相关资料

示例代码

swift-macro-examples.zip

MyMacro.zip

WWDC相关视频

https://developer.apple.com/videos/play/wwdc2023/10164/

WWDC23 - 10164 - What’s new in Swift.srt

https://developer.apple.com/videos/play/wwdc2023/10167

WWDC23 - 10167 - Expand on Swift macros.srt

https://developer.apple.com/videos/play/wwdc2023/10166

WWDC23 - 10166 - Write Swift macros.srt

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容