Swift Macro


Agenda

  • 事前準備
  • Install
  • SPM Setup
  • 實作 macro
  • Error Handle
  • Test

The First Time We Met Macro


C Macro

A macro is a fragment of code which has been given a name. Whenever the name is used, it is replaced by the contents of the macro.


Kind

  • Object-like Macros
  • Function-like Macros

Object-like Macros

#define BUFFER_SIZE 1024 foo = (char *) malloc (BUFFER_SIZE); foo = (char *) malloc (1024);

Function-like Macros

#define lang_init() c_init() lang_init() c_init()

Stringizing

#define xstr(s) str(s) #define str(s) #s #define foo 4 str (foo) → "foo" xstr (foo) → xstr (4) → str (4) → "4"

Concatenation

#define COMMAND(NAME) { #NAME, NAME ## _command } COMMAND (quit), COMMAND (help), { "quit", quit_command }, { "help", help_command },

用途?


Code Gen?


Type-safe metaprogramming in Swift? Let's talk about sourcery


What is metaprogramming?

  • Metaprogramming means that a program can be designed to read, generate, analyse or transform other programs, and even modify itself while running.
  • But in reality the most common use case is to generate boilerplate code automatically

Why would we want metaprogramming:

  • If we analyze our day to day job, we repeat same code patterns:
    • Equality
    • Hashing
    • Data Persistance
    • JSON parsing
    • Writing and updating testing Mocks / Stubs

Why would we want metaprogramming:

  • Metaprogramming allows us to:
    • Adhere to DRY principle
    • Write once,
    • test once
    • Ease maintenance costs
    • Avoid human mistakes
    • Experiment with interesting architecture choices

Avoiding human mistakes

  • don't have a problem writing boilerplate code like Equatable implementation, its simple after all
    • The problem isn't writing it.
    • The problem is that when you change it e.g. add new variables
    • You might not realize you forgot to update your boilerplate
    • This can cause hard to track bugs

事前準備


AST Explorer


略懂 Swift Syntax


常用 Syntax

  • Decl
    • DeclSyntax
    • DeclSyntaxProtocol
    • DeclGroupSyntax
  • Expr
    • ExprSyntax

Decl(Declaration)

宣告


ClassDecl(class 宣告)

class Temp { let a = 1 }

VariableDecl(變數宣告)

let a = 1

DeclSyntax & DeclSyntaxProtocol

public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable {}
public struct ClassDeclSyntax: DeclSyntaxProtocol, SyntaxHashable {}

哪些屬於 DeclSyntax(1/2)

  • ClassDecl
  • VariableDecl

哪些屬於 DeclSyntax(2/2)

public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable { public init?<S: SyntaxProtocol>(_ node: S) { switch node.raw.kind { case .accessorDecl, .actorDecl, .associatedtypeDecl, .classDecl, .deinitializerDecl, .editorPlaceholderDecl, .enumCaseDecl, .enumDecl, .extensionDecl, .functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typealiasDecl, .variableDecl: self._syntaxNode = node._syntaxNode default: return nil } } }

DeclGroupSyntax

extension ActorDeclSyntax: DeclGroupSyntax {} extension ClassDeclSyntax: DeclGroupSyntax {} extension EnumDeclSyntax: DeclGroupSyntax {} extension ExtensionDeclSyntax: DeclGroupSyntax {} extension ProtocolDeclSyntax: DeclGroupSyntax {} extension StructDeclSyntax: DeclGroupSyntax {}

Expr(Expression)

表達式 表述式


IntegerLiteralExpr

1 // let a = 1

SequenceExpr

1 + 2

FunctionCallExpr

print("Hello World")

WHY?


public func expansionDecl() throws -> DeclSyntax {} public func expansionExpr() throws -> ExprSyntax {}

import SwiftSyntaxBuilder public func expansionDecl() throws -> DeclSyntax { // x // return "1" return "class Temp {}" } public func expansionExpr() throws -> ExprSyntax { // x // return "class Temp {}" return "1" }

Install


swift 5.9


Setup

export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw /Library/Developer/Toolchains/swift-5.9-DEVELOPMENT-SNAPSHOT-2023-06-05-a.xctoolchain/Info.plist)

Check

swift --version
Apple Swift version 5.9-dev (LLVM 464b04eb9b157e3, Swift 7203d52cb1e074d)
Target: x86_64-apple-macosx12.0

SPM Setup

Package@swift-5.9.swift

// swift-tools-version: 5.9 import PackageDescription import CompilerPluginSupport let package = Package( name: "MacroPrac", platforms: [ .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13), ], dependencies: [ .package (url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-Swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), ], targets: [ .executableTarget( name: "MacroMain", dependencies: [ "WWDCMacros", ]), .macro( name: "WWDCMacrosPlugin", dependencies:[ .product(name: "SwiftSyntaxMacros",package:"swift-syntax"), .product(name: "SwiftCompilerPlugin",package: "swift-syntax"), ]), .target( name: "WWDCMacros", dependencies: [ "WWDCMacrosPlugin", ]), .testTarget( name: "Macro123Tests", dependencies: [ "WWDCMacrosPlugin", .product(name: "SwiftSyntaxMacros",package:"swift-syntax"), .product(name: "SwiftSyntaxMacrosTestSupport",package:"swift-syntax"), ]), ])

Swift Tool Version

// swift-tools-version: 5.9

import CompilerPluginSupport

import CompilerPluginSupport

OS 最低版本需求

platforms: [ .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13), ],

Import SwiftSyntax

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

實作 macro 的地方

.macro(name: "WWDCMacrosPlugin")

定義 Macro Interface 的地方

.target(name: "WWDCMacros")

實作 Macro


Macro Type

  • freestanding
    • #someFreestanding()
  • attached
    • @SomeAttached var a: Int

Macro Role(freestanding)

  • expression
  • declaration
  • codeItem
    • new

Macro Role(attached)

  • peer
  • accessor
  • memberAttribute
  • member
  • conformance
    • extension
      • system
      • new

Macro Role

macro protocol
@freestanding(expression) ExpressionMacro
@freestanding(declaration) DeclarationMacro
@attached(peer) PeerMacro
@attached(accessor) AccessorMacro
@attached(memberAttribute) MemberAttributeMacro
@attached(member) MemberMacro
@attached(conformance) ConformanceMacro

1. 實作 Macro

// .macro(name: "WWDCMacrosPlugin") import SwiftCompilerPlugin import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros public struct OneMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> ExprSyntax { return "1" } }

2. 註冊 Macro

// .macro(name: "WWDCMacrosPlugin") #if canImport(SwiftCompilerPlugin) import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ OneMacro.self, ] } #endif

3. 定義 marco interface

// .target(name: "WWDCMacros") @freestanding(expression) public macro one() = #externalMacro( module: "WWDCMacrosPlugin", type: "OneMacro" )

4. 使用(展開)

import WWDCMacros let a = #one()
import WWDCMacros let a = 1

Error Handle

  • throw Swift.Error
  • provide DiagnosticMessage

throw Swift.Error

static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] throw MyError.caseA }

provide DiagnosticMessage(1/2)

static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] let error = Diagnostic( node: attribute, message: MyLibDiagnostic.someError ) context.diagnose(structError) return [] }

provide DiagnosticMessage(2/2)

enum MyLibDiagnostic: String, DiagnosticMessage { case someError var severity: DiagnosticSeverity { .error } var message: String { switch self { case .someError: return "someError" } } var diagnosticID: MessageID { MessageID(domain: "MyLibMacros", id: rawValue) } }

Test

import SwiftSyntaxMacrosTestSupport func testOne() { assertMacroExpansion( "#one()", expandedSource: """ 1 """, macros: ["one": OneMacro.self] ) }

參數解析


Format

  • Macro
  • (attached)目標
  • context
  • Code Gen

DeclarationMacro

  • node: Macro
  • -> [DeclSyntax]: Code Gen

MemberAttributeMacro

  • node: Macro
  • delaration: 目標
    • member: 子目標
  • -> [AttributeSyntax]: Code Gen


node

@memberAttribute

declaration

//@memberAttribute struct A { var a: Int var b: Int @abc var c: Int }

member

var a: Int
var b: Int
@abc var c: Int

Code Gen

return ["@abc"]

Freestanding Macro

public struct SomeMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, // ... ) }

FreestandingMacroExpansionSyntax

#free<T1, T2>(v1, v2)

  • poundToken: #
  • macro: free
  • genericArguments: <T1, T2>
  • leftParen: (
  • argumentList: v1, v2
  • rightParen: )

FreestandingMacroExpansionSyntax

#free {}

  • trailingClosure: {...}

FreestandingMacroExpansionSyntax

#free {} v2: {}

  • additionalTrailingClosures

Attached Macro

public struct SomwMacro: PeerMacro { public static func expansion( of node: AttributeSyntax, // ... ) }

AttributeSyntax

@MyMacro(123, 456)

  • atSign: @
  • attributeName: MyMacro
  • leftParen: (
  • argument: 123, 456
  • rightParen: )

@attached(xxx) macro test<T>(_ v1: Int = 0, _ v2: T) @test("a") syntax.attributeName = "test" // T ??? syntax.argument[0] = "a"

// macro test<T>(_ v1: Int = 0, _ v2: T) // @test("a") let v1, v2: Syntax if syntax.count == 1 { v1 = 0 v2 = syntax.argument[0] } else syntax.count == 2 { v1 = syntax.argument[0] v2 = syntax.argument[1] }

Macro 簡易展開說明


ExpressionMacro

public struct MyExpressionMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> ExprSyntax {
let a = #one let a = 1

DeclarationMacro

public struct MyDeclarationMacro: DeclarationMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] {
#tuple(2) struct Tuple2<V0, V1> { let v0: V0 let v1: V1 }

CodeItemMacro

public struct MyCodeItemMacro: CodeItemMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> [CodeBlockItemSyntax] {

CodeItemMacro(Package.swift)

let settings: [SwiftSetting] = [ .enableExperimentalFeature("CodeItemMacros"), ] .target( name: "WWDCMacros", dependencies: [ "WWDCMacrosPlugin", ], swiftSettings: settings ),

CodeBlockItemSyntax(1/2)

import Foundation let a = 1 print(a)
CodeBlockItem(Decl(import Foundation)) CodeBlockItem(Decl(let a = 1)) CodeBlockItem(Expr(print(a)))

CodeBlockItemSyntax(2/2)

func test() { line1 line2 line3 }
func test() { CodeBlockItem(line1) CodeBlockItem(line2) CodeBlockItem(line3) }

Peer

struct MyPeerMacro: PeerMacro { static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax]
@Peer func abc() {} func abc() {} /* func _abc() {} */

Accessor

struct MyAccessorMacro: AccessorMacro { static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AccessorDeclSyntax]
@Accessor var a: Int var a: Int /* {...} */

MemberAttribute

struct MyMemberAttributeMacro: MemberAttributeMacro { static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AttributeSyntax] {
@MemberAttribute struct A { var a: Int } struct A { /* @xxx */ var a: Int }

Member

struct MyMemberMacro: MemberMacro { static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] {
@Member struct A { } struct A { /* func dump() {} */ }

Conformance(1/2)

struct MyConformanceMacro: ConformanceMacro { static func expansion( of node: AttributeSyntax, providingConformancesOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {

Conformance(2/2)

@Conformance struct A {} struct A: Codable {}
@Conformance struct A<T> {} struct A : Codable where T: Codable {}

Conformance(extension)

@Conformance struct A {} // struct A: Codable {} extension A: Codable {}
@Conformance struct A<T> {} // struct A : Codable where T: Codable {} extension A : Codable where T: Codable {}

Role composition

Macro 可以有多個 role


Role composition

在使用 macro 的地方,Swift 將展開所有適用的 role。
至少必須有一個適用的 role。


@attached(member) @attached(memberAttribute) @attached(accessor) public macro composition() = #externalMacro( module: "WWDCMacrosPlugin", type: "CompositionMacro" )

Making names Visible

必須在 macro interface 定義會產生的名稱


Name Specifiers

overloaded attached only
prefixed(<some prefix>) attached only, can start with $
suffixed(<some suffix>) attached only
named(<some name>)
arbitrary

overloaded

@attached(peer, named: overloaded) public macro ToAsync() @ToAsync func call(callback: (T) -> ()) {} func call(callback: (T) -> ()) {} func call() async -> T {}

named

@attached(member, named: named(init)) public macro Init() @Init struct A { } struct A { init() {} }

arbitrary

@attached(peer, named: arbitrary) public macro ToPublic(_ name: String) @ToPublic("A") private var a = 1 private var a = 1 public var A = a

Macros


深度解读 Observation —— SwiftUI 性能提升的新途径


OptionSet

  • OptionSetMacro
    • ConformanceMacro
    • MemberMacro

PowerAssert


swift-request

@Service(resource: "quotes") protocol QuoteService { @GET("random") func getRandomQuotes(@QueryParam limit: Int?) async throws -> [Quote] @GET("{id}") func getQuote(@Path by id: String) async throws -> Quote }

Retrofit(kotlin)

public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }

#Preview


MacroExample


禁止使用外部資訊

  • 只能使用編譯器提供的資訊
    • 否則,工具將無法得知何時需要重新展開巨集
  • 巨集無法存取檔案系統或網路
  • 即使沙箱無法阻止你,但你仍不應該:
    • 插入像現在的時間、進程 ID 或隨機數之類的 API 結果
    • 在展開之間在全域變數中儲存資訊

Ideas


Point(2/3/4)D


DeclarationMacro

#Point(2) class Point2D { let v0: Double let v1: Double }

RawJSON [String: Any]

@attached(accessor) @attached(memberAttribute) @attached(member) macro RawJSON(name: String? = nil)

member

@RawJSON struct A { } struct A { let dict: [String: Any] init(_ dict: [String: Any]) { self.dict = dict } }

memberAttribute

@RawJSON struct A { var a: Int @RawJSON("B") var b: String } struct A { @RawJSON var a: Int @RawJSON("B") var b: String }

accessor

@RawJSON("B") var b: String var b: String { get { dict["B"] as! String } }

PublicInit


member

@PublicInit public struct A { let a: Int } public struct A { let a: Int public init(a: Int) { self.a = a } }

peer(@Default)

macro<T> Default(_ value: T) // return [] @PublicInit public struct A { @Default(1) let a: Int } public struct A { let a: Int public init(a: Int = 1) { self.a = a } }

Swift-Macros


End


Other

context.makeUniqueName() return \(literal: xxx)" let location = contenxt.location(of: syntax)! location.file location.line
Select a repo