# Swift Macro --- ## Agenda * 事前準備 * Install * SPM Setup * 實作 macro * Error Handle * Test --- ## The First Time We Met Macro ---- ## C [Macro](https://gcc.gnu.org/onlinedocs/cpp/Macros.html) 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 ```clike= #define BUFFER_SIZE 1024 foo = (char *) malloc (BUFFER_SIZE); foo = (char *) malloc (1024); ``` ---- ### Function-like Macros ```clike= #define lang_init() c_init() lang_init() c_init() ``` ---- ### Stringizing ```clike= #define xstr(s) str(s) #define str(s) #s #define foo 4 str (foo) → "foo" xstr (foo) → xstr (4) → str (4) → "4" ``` ---- ### Concatenation ```clike= #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](https://speakerdeck.com/cocoaheadsukraine/type-safe-metaprogramming-in-swift-lets-talk-about-sourcery-by-krzysztof-zablocki) ---- ### 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](https://swift-ast-explorer.com/) ---- ### 略懂 Swift Syntax ---- ### 常用 Syntax * Decl * DeclSyntax * DeclSyntaxProtocol * DeclGroupSyntax * Expr * ExprSyntax ---- ### Decl(Declaration) > 宣告 ---- ### ClassDecl(class 宣告) ```swift= class Temp { let a = 1 } ``` ---- ### VariableDecl(變數宣告) ```swift= let a = 1 ``` ---- ### DeclSyntax & DeclSyntaxProtocol ```swift= public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable {} ``` ```swift= public struct ClassDeclSyntax: DeclSyntaxProtocol, SyntaxHashable {} ``` ---- ### 哪些屬於 DeclSyntax(1/2) * ClassDecl * VariableDecl * ... ---- ### 哪些屬於 DeclSyntax(2/2) ```swift= 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 ```swift= extension ActorDeclSyntax: DeclGroupSyntax {} extension ClassDeclSyntax: DeclGroupSyntax {} extension EnumDeclSyntax: DeclGroupSyntax {} extension ExtensionDeclSyntax: DeclGroupSyntax {} extension ProtocolDeclSyntax: DeclGroupSyntax {} extension StructDeclSyntax: DeclGroupSyntax {} ``` ---- ### Expr(Expression) > 表達式 表述式 ---- ### IntegerLiteralExpr ```swift= 1 // let a = 1 ``` ---- ### SequenceExpr ```swift= 1 + 2 ``` ---- ### FunctionCallExpr ```swift= print("Hello World") ``` ---- ## WHY? ---- ```swift= public func expansionDecl() throws -> DeclSyntax {} public func expansionExpr() throws -> ExprSyntax {} ``` ---- ```swift= 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 * toolchain [swift5.9_2023_06_07](https://download.swift.org/development/xcode/swift-DEVELOPMENT-SNAPSHOT-2023-06-07-a/swift-DEVELOPMENT-SNAPSHOT-2023-06-07-a-osx.pkg) * XCode 15 Beta ---- ## Setup ```sh export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw /Library/Developer/Toolchains/swift-5.9-DEVELOPMENT-SNAPSHOT-2023-06-05-a.xctoolchain/Info.plist) ``` ---- ## Check ```sh 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= // 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= // swift-tools-version: 5.9 ``` ---- ### import CompilerPluginSupport ```swift= import CompilerPluginSupport ``` ---- ### OS 最低版本需求 ```swift= platforms: [ .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13), ], ``` ---- ### Import SwiftSyntax ```swift= dependencies: [ .package (url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-Swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), ], ``` ---- ### 實作 macro 的地方 ```swift= .macro(name: "WWDCMacrosPlugin") ``` ---- ### 定義 Macro Interface 的地方 ```swift= .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 ```swift= // .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 ```swift= // .macro(name: "WWDCMacrosPlugin") #if canImport(SwiftCompilerPlugin) import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ OneMacro.self, ] } #endif ``` ---- ### 3. 定義 marco interface ```swift= // .target(name: "WWDCMacros") @freestanding(expression) public macro one() = #externalMacro( module: "WWDCMacrosPlugin", type: "OneMacro" ) ``` ---- ### 4. 使用(展開) ```swift= import WWDCMacros let a = #one() ``` ```swift= import WWDCMacros let a = 1 ``` --- ## Error Handle * throw `Swift.Error` * provide `DiagnosticMessage` ---- ### throw `Swift.Error` ```swift= static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] throw MyError.caseA } ``` ---- ### provide `DiagnosticMessage`(1/2) ```swift= 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) ```swift= 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 ```swift= import SwiftSyntaxMacrosTestSupport func testOne() { assertMacroExpansion( "#one()", expandedSource: """ 1 """, macros: ["one": OneMacro.self] ) } ``` --- ## 參數解析 ---- ### Format * Macro * (attached)目標 * context * Code Gen ---- ### DeclarationMacro ![](https://hackmd.io/_uploads/ryZ3xdVw3.png) * node: Macro * `-> [DeclSyntax]`: Code Gen ---- ### MemberAttributeMacro ![](https://hackmd.io/_uploads/S1YATwEw3.png) * node: Macro * delaration: 目標 * member: 子目標 * `-> [AttributeSyntax]`: Code Gen ---- ![](https://hackmd.io/_uploads/BJfzZIdPn.png) ---- ### node ```swift= @memberAttribute ``` ---- ### declaration ```swift= //@memberAttribute struct A { var a: Int var b: Int @abc var c: Int } ``` ---- ### member ```swift= var a: Int ``` ```swift= var b: Int ``` ```swift= @abc var c: Int ``` ---- ### Code Gen ```swift= return ["@abc"] ``` --- ## Freestanding Macro ```swift= 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 ```swift= public struct SomwMacro: PeerMacro { public static func expansion( of node: AttributeSyntax, // ... ) } ``` ---- ### AttributeSyntax > @MyMacro(123, 456) * atSign: `@` * attributeName: `MyMacro` * leftParen: `(` * argument: `123, 456` * rightParen: `)` ---- ```swift= @attached(xxx) macro test<T>(_ v1: Int = 0, _ v2: T) @test("a") syntax.attributeName = "test" // T ??? syntax.argument[0] = "a" ``` ---- ```swift= // 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 ```swift= public struct MyExpressionMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> ExprSyntax { ``` ```swift= let a = #one let a = 1 ``` ---- ### DeclarationMacro ```swift= public struct MyDeclarationMacro: DeclarationMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { ``` ```swift= #tuple(2) struct Tuple2<V0, V1> { let v0: V0 let v1: V1 } ``` ---- ### CodeItemMacro ```swift= public struct MyCodeItemMacro: CodeItemMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> [CodeBlockItemSyntax] { ``` ---- ### CodeItemMacro(Package.swift) ```swift= let settings: [SwiftSetting] = [ .enableExperimentalFeature("CodeItemMacros"), ] .target( name: "WWDCMacros", dependencies: [ "WWDCMacrosPlugin", ], swiftSettings: settings ), ``` ---- ### CodeBlockItemSyntax(1/2) ```swift= import Foundation let a = 1 print(a) ``` ```swift= CodeBlockItem(Decl(import Foundation)) CodeBlockItem(Decl(let a = 1)) CodeBlockItem(Expr(print(a))) ``` ---- ### CodeBlockItemSyntax(2/2) ```swift= func test() { line1 line2 line3 } ``` ```swift= func test() { CodeBlockItem(line1) CodeBlockItem(line2) CodeBlockItem(line3) } ``` ---- ### Peer ```swift= struct MyPeerMacro: PeerMacro { static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] ``` ```swift= @Peer func abc() {} func abc() {} /* func _abc() {} */ ``` ---- ### Accessor ```swift= struct MyAccessorMacro: AccessorMacro { static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AccessorDeclSyntax] ``` ```swift= @Accessor var a: Int var a: Int /* {...} */ ``` ---- ### MemberAttribute ```swift= struct MyMemberAttributeMacro: MemberAttributeMacro { static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AttributeSyntax] { ``` ```swift= @MemberAttribute struct A { var a: Int } struct A { /* @xxx */ var a: Int } ``` ---- ### Member ```swift= struct MyMemberMacro: MemberMacro { static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { ``` ```swift= @Member struct A { } struct A { /* func dump() {} */ } ``` ---- ### Conformance(1/2) ```swift= struct MyConformanceMacro: ConformanceMacro { static func expansion( of node: AttributeSyntax, providingConformancesOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { ``` ---- ### Conformance(2/2) ```swift= @Conformance struct A {} struct A: Codable {} ``` ```swift= @Conformance struct A<T> {} struct A : Codable where T: Codable {} ``` ---- ### Conformance(extension) ```swift= @Conformance struct A {} // struct A: Codable {} extension A: Codable {} ``` ```swift= @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。 ---- ```swift= @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(&lt;some prefix&gt;)|attached only, can start with `$`| |suffixed(&lt;some suffix&gt;)|attached only| |named(&lt;some name&gt;)|| |arbitrary|| ---- ### overloaded ```swift= @attached(peer, named: overloaded) public macro ToAsync() @ToAsync func call(callback: (T) -> ()) {} func call(callback: (T) -> ()) {} func call() async -> T {} ``` ---- ### named ```swift= @attached(member, named: named(init)) public macro Init() @Init struct A { } struct A { init() {} } ``` ---- ### arbitrary ```swift= @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 性能提升的新途径](https://www.fatbobman.com/posts/mastering-Observation/) ---- ### OptionSet * OptionSetMacro * ConformanceMacro * MemberMacro ---- ### [PowerAssert](https://github.com/kishikawakatsumi/swift-power-assert) ![](https://hackmd.io/_uploads/Bka50wpw3.png) ---- ### [swift-request](https://github.com/ailtonvivaz/swift-request) ```swift= @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) ```kotlin= public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); } ``` ---- ### [#Preview](https://developer.apple.com/documentation/swiftui/previews-in-xcode) ---- ### [MacroExample](https://github.com/DougGregor/swift-macro-examples) --- ### 禁止使用外部資訊 * 只能使用編譯器提供的資訊 * 否則,工具將無法得知何時需要重新展開巨集 * 巨集無法存取檔案系統或網路 * 即使沙箱無法阻止你,但你仍不應該: * 插入像現在的時間、進程 ID 或隨機數之類的 API 結果 * 在展開之間在全域變數中儲存資訊 --- ## Ideas --- ### Point(2/3/4)D ---- ### DeclarationMacro ```swift= #Point(2) class Point2D { let v0: Double let v1: Double } ``` --- ## RawJSON [String: Any] ```swift= @attached(accessor) @attached(memberAttribute) @attached(member) macro RawJSON(name: String? = nil) ``` ---- ### member ```swift= @RawJSON struct A { } struct A { let dict: [String: Any] init(_ dict: [String: Any]) { self.dict = dict } } ``` ---- ### memberAttribute ```swift= @RawJSON struct A { var a: Int @RawJSON("B") var b: String } struct A { @RawJSON var a: Int @RawJSON("B") var b: String } ``` ---- ### accessor ```swift= @RawJSON("B") var b: String var b: String { get { dict["B"] as! String } } ``` --- ## PublicInit ---- ### member ```swift= @PublicInit public struct A { let a: Int } public struct A { let a: Int public init(a: Int) { self.a = a } } ``` ---- ### peer(@Default) ```swift= 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](https://github.com/krzysztofzablocki/Swift-Macros) --- ## End --- ## Other ```swift= context.makeUniqueName() return \(literal: xxx)" let location = contenxt.location(of: syntax)! location.file location.line ```
{"breaks":true,"title":"Swift Macro","lang":"zh-TW","dir":"ltr","description":"Swift Macro 入門","contributors":"[{\"id\":\"6883ab5f-8423-424e-bfcb-d0002f96698f\",\"add\":34041,\"del\":14254}]"}
    485 views