Swift Macros ฉบับสมบูรณ์: สร้าง Custom Macro ใน Swift 6.2 (2026)

คู่มือสร้าง Custom Macro ใน Swift 6.2 ทีละขั้นตอน ตั้งแต่โครงสร้าง Swift Package จนถึงการทดสอบด้วย SwiftSyntaxMacrosTestSupport พร้อมตัวอย่างครบทั้ง @freestanding และ @attached

Swift Macros 6.2: Custom Macro Guide (2026)

อัปเดตล่าสุด: 8 กรกฎาคม 2026

Swift Macros คือกลไก compile-time metaprogramming ที่ Apple แนะนำครั้งแรกใน Swift 5.9 และเติบโตเต็มที่ใน Swift 6.2 โดยเปิดให้เราเขียนโค้ดที่ generate โค้ด Swift อื่นในช่วงคอมไพล์ผ่าน SwiftSyntax ทำให้ลดการเขียน boilerplate ได้อย่างมหาศาลโดยไม่ต้องพึ่งพา runtime reflection หรือ Objective-C runtime แบบเดิม บทความนี้จะพาคุณสร้าง Custom Macro ตั้งแต่การตั้ง Swift Package จนถึงการทดสอบด้วย SwiftSyntaxMacrosTestSupport พร้อมตัวอย่างครบทั้ง @freestanding และ @attached

  • Swift Macros ทำงานที่ compile time ผ่าน compiler plugin โดยรับ SyntaxNode และ return โค้ด Swift ใหม่ ไม่ใช่ runtime reflection แบบเก่า
  • มี 2 ตระกูลใหญ่: @freestanding (เรียกด้วย #macroName) และ @attached (เรียกด้วย @MacroName) โดย attached แยกย่อยเป็น peer, member, accessor, memberAttribute, extension และ conformance
  • โปรเจกต์ macro ต้องแบ่งเป็น 2 target: MacroPluginProvider (executable ที่ compiler โหลด) และ library target ที่ประกาศ macro declaration ให้ผู้ใช้ import
  • SwiftSyntax เป็น dependency บังคับ ใช้ระบุ syntax tree อย่าง FunctionDeclSyntax หรือ VariableDeclSyntax เพื่อวิเคราะห์และสร้างโค้ด
  • Xcode 26 ต้องอนุญาต macro plugin ในไดอะล็อก "Trust & Enable" ก่อนใช้งานครั้งแรก และ error ทั้งหมดควรใช้ DiagnosticsError เพื่อให้ผู้ใช้เห็น error ที่ตำแหน่ง call site จริง
  • ทดสอบ macro ได้โดย assertMacroExpansion จาก SwiftSyntaxMacrosTestSupport ซึ่งเทียบ input source กับ expanded output แบบ string

Swift Macros คืออะไร และทำไมต้องใช้

เอาล่ะ มาเริ่มจากพื้นฐานกันก่อน Swift Macros คือฟีเจอร์ metaprogramming ที่ Swift 5.9 เพิ่มเข้ามาเพื่อให้เรา generate โค้ด Swift จากภายในโค้ด Swift ด้วยกันในช่วง compile time โดยผ่าน compiler plugin ที่รันแยกจาก main process ของ compiler เอง กลไกทำงานคร่าว ๆ คือคอมไพเลอร์ parse ซอร์สจนได้ syntax tree แล้วส่ง node ที่ถูก annotation ด้วย macro ไปยัง plugin ผ่าน stdio จากนั้น plugin ตอบกลับเป็น syntax tree ใหม่ที่จะถูก splice กลับเข้าไปในโปรแกรมและคอมไพล์ต่อ ผลที่ได้คือ zero-cost abstraction: โค้ดที่ generate ถูกคอมไพล์เหมือนเราเขียนเองด้วยมือ ไม่มี runtime overhead และไม่พึ่งพา Objective-C runtime อย่างที่ @propertyWrapper หรือ Codable รุ่นเก่าเคยทำ

ในสมัย Objective-C เราคุ้นกับ preprocessor macros แบบ #define ที่ทำ textual substitution แบบตาบอด Swift Macros ต่างออกไปสิ้นเชิง เพราะรับ syntactic input เป็น type ที่ define ไว้ใน SwiftSyntax เช่น FunctionDeclSyntax, VariableDeclSyntax, StructDeclSyntax (นี่ทำให้ตรวจสอบและวิเคราะห์โค้ดได้อย่างละเอียด ไม่ใช่แค่ replace ข้อความ) นอกจากนี้ macro ต้อง hygienic โดยธรรมชาติ: identifier ที่ macro สร้างจะไม่ชนกับ scope ภายนอกโดยไม่ตั้งใจ

เหตุผลที่ควรใช้ macros ในโปรเจกต์จริงมีหลายข้อ ตัวอย่างที่ผมใช้บ่อยที่สุดคือลด boilerplate ของ init, Codable, DTO mapping, async wrapper รอบฟังก์ชัน callback-based, และ dependency-injection registration ในโปรเจกต์ SwiftUI ขนาดใหญ่ที่ผมเคยดูแล การย้ายจาก manual init ไปเป็น @AutoInit ตัดโค้ดออกไปหลักพันบรรทัดโดยไม่กระทบ ABI เลย และประเด็นสำคัญคือ macro ไม่ได้ทำให้ compile time พองมากอย่างที่บางคนกลัว เพราะ plugin cache ผลลัพธ์ตาม signature ของ input node

Freestanding vs Attached macros ต่างกันอย่างไร

Swift Macros แบ่งเป็น 2 ตระกูลใหญ่ตามวิธีเรียกใช้ ตระกูลแรกคือ freestanding macros ที่ผู้ใช้เรียกด้วย prefix # เช่น #stringify(x + y) หรือ #URL("https://example.com") โดย macro ตัวเดียวสามารถทำหน้าที่ได้ทั้งเป็น expression (return ค่า) หรือเป็น declaration (สร้าง type/function ใหม่) หรือเป็น codeItem (สร้างชุดคำสั่งใน function body) กลุ่มนี้เหมาะกับงานที่ไม่ผูกกับ symbol อื่น เช่น compile-time validation ของ regex, JSON literal ที่ปลอดภัย, หรือ code generation ระดับ statement

ตระกูลที่สองคือ attached macros ที่ผู้ใช้ติดกับ declaration ผ่าน @ เช่น @AutoInit หรือ @Observable ตระกูลนี้แยกย่อยตาม role ที่ประกาศได้ดังนี้

  • peer: สร้าง declaration ใหม่ที่อยู่ข้างเคียงกับตัวที่ติด attach เช่นสร้าง async version ของ callback function
  • member: เพิ่ม properties หรือ methods เข้าไปใน type ที่ติด attach เช่น auto-generated init
  • accessor: เพิ่ม getter/setter/willSet/didSet ให้ property
  • memberAttribute: attach attribute เพิ่มเติมให้ทุก member ของ type (เช่น attach @Published ให้ทุก property)
  • extension: เพิ่ม extension พร้อม conformance เข้าไปให้ type
คุณสมบัติFreestandingAttached
วิธีเรียก#macroName(...)@MacroName
เห็น context declarationไม่เห็นเห็น declaration ที่ติด attach
Role ที่ใช้ได้expression, declaration, codeItempeer, member, accessor, memberAttribute, extension
เหมาะกับexpression-level codegen, compile-time validationลด boilerplate ของ type, auto-conform protocol
ตัวอย่างในภาษา#warning, #Predicate@Observable, @Model

เลือก freestanding เมื่อ macro ของคุณไม่ต้องรู้อะไรเกี่ยวกับ symbol รอบ ๆ และเลือก attached เมื่อ macro ต้องอ่าน property/method ของ type ที่ติดไป เกณฑ์ง่าย ๆ คือ ถ้าอยากใส่ macro หน้า struct แล้วให้มัน scan properties เพื่อ generate init ก็ต้องเป็น attached member ส่วนถ้าอยากให้ #json("...") ตรวจ JSON literal ที่ compile time ก็คือ freestanding

โครงสร้าง Swift Package สำหรับ Macro

โปรเจกต์ macro ทุกตัวต้องอยู่ใน Swift Package เพราะ compiler ต้อง build plugin เป็น executable แยกจาก client target โครงสร้างมาตรฐานที่ผมใช้ทุกโปรเจกต์คือแบ่งเป็น 3 targets ได้แก่ macro implementation (compiler plugin), macro interface (library), และ test target สำหรับ macro expansion ให้เริ่มด้วยการรัน swift package init --type macro บน Xcode 26 หรือใช้ template "Swift Macro" ใน Xcode ก็ได้ทั้งคู่

// Package.swift
// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyMacros",
    platforms: [.iOS(.v17), .macOS(.v14)],
    products: [
        .library(name: "MyMacros", targets: ["MyMacros"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"),
    ],
    targets: [
        // 1) Compiler plugin (run macro logic)
        .macro(
            name: "MyMacrosImpl",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
            ]
        ),
        // 2) Library ที่ผู้ใช้ import
        .target(name: "MyMacros", dependencies: ["MyMacrosImpl"]),
        // 3) Test target
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosImpl",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
            ]
        ),
    ]
)

จุดสำคัญคือ .macro(...) target ประเภทนี้ Swift Package Manager จะ compile เป็น executable และ compiler โหลดผ่าน -load-plugin-executable ให้อัตโนมัติ ส่วน MyMacros library target มีหน้าที่แค่ประกาศ @freestanding/@attached declaration เพื่อให้ผู้ใช้ import ได้ตามปกติ อย่าใส่ implementation ในนี้ เพราะจะทำให้ macro โผล่เข้าไปในโปรแกรมของ client โดยไม่จำเป็น

สร้าง Freestanding Macro ตัวแรก: #stringify

ตัวอย่างคลาสสิกที่ Apple ใช้สอนคือ #stringify(x + y) ที่ return tuple ของค่าที่คำนวณและ string ของ expression นั้น ตัว macro นี้อธิบายทั้งกลไกการรับ syntax tree และการ return expression syntax ใหม่ครบทุกขั้นตอน

ในไฟล์ Sources/MyMacros/MyMacros.swift (library target) เราประกาศ declaration ก่อน:

// MyMacros.swift - library interface
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
    #externalMacro(module: "MyMacrosImpl", type: "StringifyMacro")

จากนั้นเขียน implementation ใน Sources/MyMacrosImpl/StringifyMacro.swift ซึ่งเป็น compiler plugin target:

// StringifyMacro.swift - implementation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.arguments.first?.expression else {
            fatalError("compiler bug: missing argument")
        }
        return "(\(argument), \(literal: argument.description))"
    }
}

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

สังเกต 3 จุดหลัก จุดแรกคือต้อง conform ExpressionMacro เพราะประกาศเป็น @freestanding(expression) จุดสองคือใช้ string interpolation ของ ExprSyntax ที่ SwiftSyntax เตรียมให้ ทำให้เขียนโค้ดที่ generate เหมือนเขียน template ปกติ จุดสามคือ @main struct ที่ conform CompilerPlugin จะรีจิสเตอร์ทุก macro ให้ compiler รู้จัก การเรียกใช้จากฝั่ง client ก็ตรงไปตรงมามาก:

import MyMacros

let (value, code) = #stringify(2 + 3)
print(value)  // 5
print(code)   // "2 + 3"

Attached Member Macro: @AutoInit สำหรับ initializer อัตโนมัติ

ถัดมาเป็นตัวอย่างที่ผมใช้ในโปรเจกต์จริงเกือบทุกวัน คือ attached member macro ที่ scan properties ของ struct แล้ว generate memberwise initializer ให้แม้ struct นั้นเป็น public ก็ตาม (โดยปกติ Swift generate memberwise init เฉพาะ internal เท่านั้น) ประกาศใน library:

@attached(member, names: named(init))
public macro AutoInit() =
    #externalMacro(module: "MyMacrosImpl", type: "AutoInitMacro")

Implementation ต้อง conform MemberMacro รับ SomeDeclSyntaxProtocol ของ declaration ที่ติด attach แล้ว return array ของ DeclSyntax:

import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoInitMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // 1) รวบรวม stored properties พร้อม type
        let vars = declaration.memberBlock.members.compactMap {
            $0.decl.as(VariableDeclSyntax.self)
        }
        var params: [String] = []
        var assigns: [String] = []
        for varDecl in vars {
            for binding in varDecl.bindings {
                guard
                    let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
                    let type = binding.typeAnnotation?.type.description
                        .trimmingCharacters(in: .whitespaces)
                else { continue }
                params.append("\(name): \(type)")
                assigns.append("self.\(name) = \(name)")
            }
        }
        // 2) ประกอบเป็น init declaration ด้วย SwiftSyntax string interpolation
        let paramList = params.joined(separator: ", ")
        let body = assigns.joined(separator: "\n    ")
        let initDecl: DeclSyntax = "public init(\(raw: paramList)) {\n    \(raw: body)\n}"
        return [initDecl]
    }
}

เพิ่ม AutoInitMacro.self เข้า providingMacros ของ MyMacrosPlugin แล้วใช้ได้ทันที ในฝั่ง client เขียนแค่:

@AutoInit
public struct User {
    public let id: UUID
    public let name: String
    public var email: String
}

// Expand เป็น:
// public init(id: UUID, name: String, email: String) {
//     self.id = id
//     self.name = name
//     self.email = email
// }

Attached Peer Macro: @AddAsync สำหรับ async wrapper

Peer macro สร้าง declaration ที่อยู่ข้างเคียงกับตัวที่ถูก attach ตัวอย่างที่ผมใช้เพื่อ migrate โปรเจกต์เก่าคือ @AddAsync ที่รับ function callback-based รูปแบบ f(args, completion: (T) -> Void) แล้ว generate async version ให้อัตโนมัติ ตัวนี้ช่วย migrate ไปยัง Swift 6.2 Approachable Concurrency ได้เร็วขึ้นมาก (ผมเจอปัญหานี้ตอนย้าย legacy SDK ตัวหนึ่งที่มี callback function หลายร้อยตัว)

@attached(peer, names: overloaded)
public macro AddAsync() =
    #externalMacro(module: "MyMacrosImpl", type: "AddAsyncMacro")
public struct AddAsyncMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let fn = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.notAFunction
        }
        // ดึงพารามิเตอร์ ยกเว้น completion ตัวสุดท้าย
        let params = fn.signature.parameterClause.parameters
        guard let completion = params.last,
              let completionType = completion.type.as(FunctionTypeSyntax.self),
              let resultType = completionType.parameters.first?.type
        else { throw MacroError.needsCompletion }
        let asyncParams = params.dropLast().map { "\($0.firstName.text): \($0.type.description)" }
        let asyncArgs = params.dropLast().map { "\($0.firstName.text): \($0.firstName.text)" }
        let name = fn.name.text
        let paramList = asyncParams.joined(separator: ", ")
        let argList = asyncArgs.joined(separator: ", ")
        let asyncDecl: DeclSyntax = "public func \(raw: name)(\(raw: paramList)) async -> \(raw: resultType.description) { await withCheckedContinuation { cont in \(raw: name)(\(raw: argList), completion: { result in cont.resume(returning: result) }) } }"
        return [asyncDecl]
    }
}

ผลลัพธ์เมื่อผู้ใช้ติด attach:

@AddAsync
func fetchUser(id: UUID, completion: (User) -> Void) { /* legacy */ }

// Expand เป็นฟังก์ชัน peer:
// func fetchUser(id: UUID) async -> User { ... }

การจัดการ Error ด้วย DiagnosticsError

เมื่อ macro รับ input ที่ไม่ถูกต้อง วิธีที่ผิดคือ fatalError หรือ throw error ทั่วไป เพราะ Xcode จะรายงาน error ไปที่ตำแหน่ง macro invocation อย่างเลือน ๆ วิธีที่ถูกกว่าคือส่ง Diagnostic กลับผ่าน context เพื่อชี้ไปที่ node ต้นเหตุพร้อมข้อความที่อ่านง่าย

import SwiftSyntax
import SwiftSyntaxMacros
import SwiftDiagnostics

enum AutoInitDiag: String, DiagnosticMessage {
    case notAStruct
    var severity: DiagnosticSeverity { .error }
    var message: String {
        switch self {
        case .notAStruct: return "@AutoInit ใช้ได้เฉพาะกับ struct เท่านั้น"
        }
    }
    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: rawValue)
    }
}

// ในฟังก์ชัน expansion:
guard declaration.is(StructDeclSyntax.self) else {
    let diag = Diagnostic(node: Syntax(node), message: AutoInitDiag.notAStruct)
    context.diagnose(diag)
    return []
}

ผลลัพธ์คือ Xcode 26 จะ underline @AutoInit เป็นสีแดงพร้อมข้อความไทยที่เราตั้งไว้ ทำให้ผู้ใช้ macro รู้ทันทีว่าใช้ผิดที่ นอกจากนี้เรายัง attach FixIt เพื่อเสนอการแก้ไขได้ ซึ่ง Xcode จะแสดงเป็นปุ่ม Fix ให้ผู้ใช้กดยอมรับ (ประสบการณ์การใช้งาน macro ที่ดีเลยขึ้นอยู่กับ diagnostic ที่เขียนอย่างจริงจัง)

ทดสอบ Macros ด้วย SwiftSyntaxMacrosTestSupport

Test ของ macro ไม่ได้เขียนเป็น XCTestCase ปกติ เพราะเราต้องเทียบ input source กับ expanded output ทาง SwiftSyntax แถม SwiftSyntaxMacrosTestSupport ที่มีฟังก์ชัน assertMacroExpansion เพียงตัวเดียวก็เพียงพอ คู่กับ Swift Testing framework ก็อ่านง่ายและทันสมัย

import SwiftSyntaxMacrosTestSupport
import Testing
@testable import MyMacrosImpl

let testMacros: [String: Macro.Type] = [
    "AutoInit": AutoInitMacro.self,
    "stringify": StringifyMacro.self,
]

@Test func autoInitExpandsToMemberwiseInit() {
    let input = #"""
    @AutoInit
    public struct User {
        public let id: UUID
        public var name: String
    }
    """#
    let expanded = #"""
    public struct User {
        public let id: UUID
        public var name: String

        public init(id: UUID, name: String) {
            self.id = id
            self.name = name
        }
    }
    """#
    assertMacroExpansion(input, expandedSource: expanded, macros: testMacros)
}

@Test func stringifyReturnsExpressionAndSource() {
    assertMacroExpansion(
        "let pair = #stringify(1 + 2)",
        expandedSource: "let pair = (1 + 2, \"1 + 2\")",
        macros: testMacros
    )
}

รันด้วย swift test หรือ ⌘U ใน Xcode ถ้า expanded output ไม่ match SwiftSyntax จะ print diff แบบ character-by-character ทำให้ debug ได้ตรงจุด ผมแนะนำให้เขียน test อย่างน้อยหนึ่ง case ต่อ macro role และหนึ่ง case ต่อ error path เพื่อรัน CI ไม่ให้ regression หลุดออก production (เคยเจอ regression ที่ macro output เปลี่ยน indentation ไปเงียบ ๆ ตอน bump swift-syntax แล้ว snapshot test นี่แหละที่จับได้)

@Observable และตัวอย่าง Macro ในโลกจริง

Macro ที่ใช้กันมากที่สุดในปี 2026 คงหนีไม่พ้น @Observable ซึ่งเป็น attached macro ที่รวม member, memberAttribute และ extension role เข้าด้วยกัน เมื่อคุณ annotate class ด้วย @Observable compiler plugin จะ generate storage แยกต่อ property, wrap accessor ให้เรียก _$observationRegistrar.access() และ conform protocol Observable ให้อัตโนมัติ นี่คือเหตุผลว่าทำไม SwiftUI 5+ ไม่ต้องใช้ @Published อีกต่อไป ทุกอย่างถูก generate ตอน compile ด้วย macro นี้เอง

ตัวอย่างที่ผมเห็นบ่อยในโปรเจกต์เพื่อนร่วมทีมยังมีอีกหลายชนิด เช่น @Model ของ SwiftData ที่ generate storage, indexing, และ CloudKit sync attribute; #Predicate ของ Foundation ที่ compile Swift closure เป็น NSPredicate; และ third-party อย่าง swift-macro-toolkit, MetaCodable ที่ generate Codable จาก property annotation ที่ยืดหยุ่นกว่า standard

อ้างอิงและอ่านเพิ่มเติมสำหรับใครที่อยากศึกษาลึกลงไปกว่านี้ ผมแนะนำ The Swift Programming Language: Macros ที่เป็น official reference, swiftlang/swift-syntax repository ที่มีตัวอย่าง macro implementation ใช้ในตัว compiler เอง, และ SE-0389: Attached Macros proposal ที่อธิบายเหตุผลเบื้องหลังการออกแบบระบบ role ทั้งหมด

ท้ายที่สุด macros ไม่ใช่กระสุนเงินสำหรับ boilerplate ทุกชนิด กฎที่ผมยึดคือใช้ macro เมื่อ pattern ซ้ำเกิน 3 ครั้งในโปรเจกต์ และ pattern นั้นไม่สามารถแสดงออกได้ด้วย generics หรือ protocol extension เท่านั้น การใช้เกินอาจทำให้ code review ยากขึ้นและทำให้ผู้ที่ไม่คุ้น SwiftSyntax เสียเวลา onboarding นาน ให้เริ่มจาก macro เล็ก ๆ ที่แก้ปัญหาเฉพาะจุดก่อน แล้วค่อยขยายเมื่อทีมทั้งหมดคุ้น

คำถามที่พบบ่อย

Swift Macros ทำให้ compile time ช้าลงหรือไม่?

โดยรวมช้าลงเล็กน้อยเฉพาะครั้งแรกที่ build เพราะต้อง compile plugin executable ครั้งเดียว หลังจากนั้น SwiftPM cache ผลลัพธ์และ incremental build เกือบเท่าโค้ดปกติ ในโปรเจกต์ผมเองที่มี ~40 macro invocation ต่อไฟล์ compile time เพิ่มขึ้นไม่ถึง 3% เทียบกับเขียนโค้ด boilerplate มือ

ใช้ Swift Macros ใน iOS 15 หรือรุ่นเก่ากว่าได้ไหม?

ได้ครับ เพราะ macro expansion เกิดที่ compile time ล้วน ๆ ไม่ต้องพึ่งพา runtime symbol ใหม่ ตราบใดที่โค้ดที่ generate ยังใช้ API ที่ support minimum deployment target ของคุณอยู่ ก็รันบน iOS 13 ขึ้นไปได้สบาย ต้องใช้ Xcode 15+ ในการ build เท่านั้น

ทำไม Xcode ขึ้น "external macro implementation type could not be found"?

สาเหตุพบบ่อยที่สุดคือ version ของ swift-syntax ไม่ match กับ toolchain ของ Xcode หรือชื่อ module ใน #externalMacro ไม่ตรงกับชื่อ macro target ใน Package.swift ให้เช็คว่าใช้ swift-syntax 601.0.0+ กับ Xcode 26 และชื่อ module ต้องตรง case-sensitive ทั้ง 2 ที่

เขียน Macro โดยไม่ใช้ SwiftSyntax ได้ไหม?

ไม่ได้ครับ เพราะ compiler ส่ง input มาเป็น SwiftSyntax node เท่านั้น ทางเลือกเดียวคือใช้ helper library อย่าง swift-macro-toolkit ที่ห่อ SwiftSyntax ด้วย API ที่อ่านง่ายกว่า แต่ภายในก็ยังพึ่งพา SwiftSyntax เต็ม ๆ อยู่ดี

Debug macro ใน Xcode ได้อย่างไร?

คลิกขวาที่ macro invocation แล้วเลือก "Expand Macro" เพื่อดูโค้ดที่ generate หากต้องการ step debugger ให้ attach process ของ compiler plugin (executable ชื่อ MyMacrosImpl) จาก Debug ▸ Attach to Process แล้วตั้ง breakpoint ในฟังก์ชัน expansion(...) ก่อน trigger build

Lukas Müller
เกี่ยวกับผู้เขียน Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.