Swift Macros: Saját makró fejlesztése Swift 6-ban (2026)
Lépésről lépésre útmutató saját Swift makró írásához Swift 6-ban: deklaráció, SwiftSyntax plugin felépítése, @attached példa, diagnosztikák és tesztelés assertMacroExpansion-nel.
A Swift makrók fordításidőben kódot generáló mechanizmusok, amelyek a forráskód absztrakt szintaxisfáját (AST) alakítják át a SwiftSyntax könyvtár segítségével, mindezt biztonságosan, típusellenőrzött módon, anélkül, hogy futásidejű reflektivitást vagy preprocesszor trükköket igényelnének. A Swift 5.9-ben mutatkoztak be, és Swift 6-ra már a standard könyvtár belső szövete: az #Predicate, az @Observable és a SwiftData @Model mind makrók. Ebben az útmutatóban végigvezetlek azon, hogyan írj saját makrót a nulláról: kezdve a deklaráción át a SwiftSyntax plugin felépítésén, a diagnosztikákon, az unit teszteken és a leggyakoribb buktatókon.
A Swift makrók két fő típusa a @freestanding (önálló kifejezés, pl. #stringify) és az @attached (deklarációhoz kapcsolt, pl. @Observable). Minden makró egy vagy több szerepkört (role) tölt be.
A makrók külön CompilerPlugin targetben élnek, ezért egy Swift Package-ben mindig három target tartozik hozzájuk: macros, a kliens könyvtár, és a tesztek.
A SwiftSyntax 600.x sorozat a Swift 6.0+ fordítóhoz tartozik; mindig azt a verziót kell rögzíteni a Package.swift-ben, ami a célzott Xcode verzióhoz illeszkedik.
A makrók fordításidőben futnak egy sandboxolt folyamatban, így nem érhetnek el fájlrendszert, hálózatot vagy a futásidejű környezetet.
A makró tesztelés szabványos eszköze a SwiftSyntaxMacrosTestSupport modul assertMacroExpansion függvénye, ami a generált forráskódot szövegszinten hasonlítja össze.
Az Xcode 26-ban a makrók aláíratlan plugin-ek, ezért első futtatáskor a felhasználónak engedélyt kell adnia a "Trust & Enable" gombbal. Ez gyakori CI hiba forrása.
Mi az a Swift macro és mikor érdemes használni?
A Swift macro egy fordításidőben végrehajtódó kódgeneráló komponens, amely az aktuális forrásdarab szintaxisfáját bemenetként kapja, és új vagy módosított szintaxiscsomópontokat ad vissza. A makró kimenete mindig Swift forráskód, amit a fordító ezután ugyanúgy típusellenőriz, mintha kézzel írtad volna. Ez a kulcskülönbség a régi C preprocesszor makrókhoz képest: a Swiftben nincs szöveges helyettesítés, csak strukturált AST transzformáció.
Őszintén szólva, Objective-C korszakot megjárt fejlesztőként sokáig idegenkedtem a makróktól. Túl könnyen lett belőlük olvashatatlan káosz. A Swift implementáció azonban más: a makrók nyilvánosan deklarálják, milyen kódot generálnak (a @attached(member, names: ...) szintaxis konkrétan kötelezi a fejlesztőt a generált név megnevezésére), és a fejlesztőeszközök képesek megmutatni a kibővített eredményt. Xcode-ban jobb klikk a makró hívásra, majd "Expand Macro". Mellesleg ez a legjobb tanulási módszer is.
Makrót akkor érdemes írni, ha (1) a boilerplate kódgenerálás típusalapú, és így nem oldható meg generikus függvénnyel; (2) ugyanazt a mintát több tízszer kell ismételni a projektben; (3) a generált kód olvashatóbb, ha látható, mint ha runtime reflection mögé van rejtve. Például egy @CodingKeys makró, ami a property nevekből generál CodingKey enumot, sokkal tisztább, mint a régi kézi sablonok. Ha érdekel az alapok mélyebb háttere, érdemes elolvasni a Swift 6.2 párhuzamossági útmutatót is, mert sok makró (pl. @Observable) szorosan együttműködik a concurrency rendszerrel.
Freestanding vs. attached makrók: melyiket válaszd?
A Swift két makró kategóriát ismer, és a választás a hívási helytől függ. A freestanding makró önálló kifejezés vagy deklaráció pozícióban hívódik a # jellel, például #stringify(x + y) egy kifejezést alakít át (value, "x + y") tuple-lé. Az attached makró egy deklarációhoz csatolódik a @ jellel, és kiegészíti azt, például a @Observable tagokat ad a class-hoz.
Az attached makróknak több szerepkörük lehet egyszerre, és a sorrend számít: először a memberAttribute fut, ami további attributumokat tehet meg a tagokra; utána a member, ami új tagokat ad; majd az accessor, ami getter/setter párokat generál. A @Observable például mind a hármat használja: a property-kre rákapcsolja az @ObservationTracked-et, hozzáad egy _$observationRegistrar tárolt mezőt, és minden trackelt property-re generál egy access/withMutation getter-settert.
Macro package felépítése Swift 6-ban
Minden makró egy Swift Package-ben él, három targettel: a plugin target (típusa .macro), ami fordításidőben futtatható; a kliens könyvtár, ami a publikus makró deklarációkat exportálja; és a teszt target. Az Xcode-ban a "File → New → Package → Swift Macro" sablon ezt automatikusan generálja, de érdemes manuálisan is tudni felépíteni, különösen, ha meglévő projektbe integrálsz.
A Package.swift a következőképp néz ki egy Swift 6.0+ kompatibilis makró csomag esetén:
A SwiftSyntax verziókövetés az egyik leggyakoribb buktató: a 600.0.0 a Swift 6.0-hoz, a 601.0.0 a Swift 6.1-hez tartozik, és így tovább. Ha rosszul választasz, a fordító cryptic "could not load module" hibát dob. (Pont ebbe szaladtam bele egy CI átállásnál tavaly.) Az aktuális kompatibilitási mátrix mindig a swift-syntax GitHub release oldalán található.
Első saját @attached makró: lépésről lépésre
Készítsünk egy @AutoEquatable makrót, ami egy struct minden tárolt property-jére generál egy Equatable conformance-t. A kliens oldal így néz ki:
A makró deklaráció önmagában nem csinál semmit, csak elnevezi a plugin típust, ami megvalósítja. Az #externalMacro a fordítónak szól: amikor ezt a makrót látja, töltse be a megadott plugin modult és hívja meg a megadott típust. A plugin oldali implementáció:
// MyMacrosPlugin/AutoEquatableMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin
public struct AutoEquatableMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let storedProperties = declaration.memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.filter { $0.bindings.allSatisfy { $0.accessorBlock == nil } }
.flatMap { $0.bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } }
let comparisons = storedProperties
.map { "lhs.\($0) == rhs.\($0)" }
.joined(separator: " && ")
let body = storedProperties.isEmpty ? "true" : comparisons
let ext = try ExtensionDeclSyntax("extension \(type.trimmed): Equatable") {
try FunctionDeclSyntax("public static func == (lhs: \(type.trimmed), rhs: \(type.trimmed)) -> Bool") {
"return \(raw: body)"
}
}
return [ext]
}
}
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [AutoEquatableMacro.self]
}
Két dolog érdemes figyelemre. Először, az ExtensionDeclSyntax string-interpolált inicializálója egy DSL: a \(type.trimmed) beilleszti a célzott típus nevét (pl. User), a \(raw: body) pedig nyers szövegként szúrja be a feltétellistát. Másodszor, a stored property szűrés a számított property-ket dobja ki ($0.accessorBlock == nil). Ezt elfelejteni klasszikus hiba, én is rontottam már el így első körben.
SwiftSyntax alapok: AST navigálás és kódgenerálás
A SwiftSyntax minden Swift nyelvi konstrukciót egy immutable Syntax csomópontként reprezentál. A leggyakoribb típusok: DeclSyntax (deklarációk), ExprSyntax (kifejezések), StmtSyntax (utasítások), TypeSyntax (típusok). Mindegyik konkrét altípusra as(...) hívással kell castolni, például node.as(StructDeclSyntax.self).
A fa bejárására két út van. Strukturált bejáráshoz használj SyntaxVisitor-t, amibe override-olod a megfelelő visit(_:) metódust. Egyszerű lekérdezéshez a memberBlock.members, parameterClause.parameters property-k jól működnek. Új csomópont építésre szintén két út van: a builder DSL (mint a fenti ExtensionDeclSyntax { ... }), vagy a nyers "... \(raw: ...)" string interpoláció, amit a fordító a fordítás során parse-ol vissza AST-vé.
A makró segéd-API-ról és a beépített típusokról hivatalos referencia található az Apple SwiftSyntaxMacros dokumentációban. Mivel a SwiftSyntax egy hatalmas API felület (több ezer csomópont típus), érdemes az AST Explorer-t és a generált DocC-t együtt használni.
Diagnosztikák és fordítási hibák kezelése
Egy jó makró nem csak akkor működik jól, ha a bemenet helyes, hanem akkor is, ha rossz. Ha valaki @AutoEquatable-t tesz egy class-ra, ahol nem értelmezett az érték-szemantika, a makró ne crash-eljen, hanem adjon érthető hibaüzenetet. Erre szolgál a MacroExpansionContext.diagnose(_:):
import SwiftDiagnostics
enum AutoEquatableError: String, DiagnosticMessage {
case onlyOnStruct
var message: String {
switch self {
case .onlyOnStruct: return "@AutoEquatable csak struct típusokon használható."
}
}
var diagnosticID: MessageID { .init(domain: "MyMacros", id: rawValue) }
var severity: DiagnosticSeverity { .error }
}
// expansion(...)-ben:
guard declaration.is(StructDeclSyntax.self) else {
context.diagnose(Diagnostic(node: node, message: AutoEquatableError.onlyOnStruct))
return []
}
Az így bemutatott hiba pontosan ott jelenik meg az Xcode-ban, ahol a node mutat. Vagyis a fejlesztő a makró attributumon látja a piros hullámvonalat, nem valami értelmezhetetlen helyen. A FixIt objektummal javaslatot is mellékelhetsz; ez különösen szeretetreméltó UX, amit a SwiftData makrók is használnak.
Hogyan teszteld a makródat?
A makró tesztelés szövegszinten történik: bemenetként megadod a forráskódot, kimenetként az elvárt kibővített forráskódot, és a tesztkönyvtár összehasonlítja a kettőt. A standard eszköz a SwiftSyntaxMacrosTestSupport modulból érkező assertMacroExpansion:
import XCTest
import SwiftSyntaxMacrosTestSupport
import MyMacrosPlugin
final class AutoEquatableTests: XCTestCase {
func testStructExpansion() throws {
assertMacroExpansion(
"""
@AutoEquatable
struct User {
let id: Int
let name: String
}
""",
expandedSource: """
struct User {
let id: Int
let name: String
}
extension User: Equatable {
public static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id && lhs.name == rhs.name
}
}
""",
macros: ["AutoEquatable": AutoEquatableMacro.self]
)
}
}
Az indentáció pontosan kell, hogy egyezzen. Egyetlen extra szóköz is buktatja a tesztet, ezért érdemes mindig először lefuttatni a tesztet, vágni az aktuális kimenetet, és azt visszaragasztani expanded oldalra. Ez gyors visszacsatolási hurok. A Swift Testing keretrendszerrel is működik a támogatás, de XCTest-szel kiforrottabb.
Gyakori hibák és teljesítmény tippek
A makró fejlesztés első hónapjában mindenki ugyanazokba a falakba szalad bele. A leggyakoribb: "Macro implementation could not be loaded". Ez majdnem mindig a SwiftSyntax verzió és a fordító verzió közti eltérés miatt van. Frissítsd a from: verziót, futtass swift package reset-et, majd swift build. Második leggyakoribb: a generált kód nem fordul. Ilyenkor jobb klikk a makró hívásra Xcode-ban, majd "Expand Macro", és nézd meg, mi került oda.
Teljesítmény szempontból két dolgot tarts észben. Először, a makró expansion minden fordításnál lefut, ezért ha valami nagyon drága (pl. regex-elemzés egy hosszú stringen), érdemes lazy lenni. Másodszor, ha SwiftUI projektben sok @Observable-t használsz, az inkrementális fordítás látványosan lassulhat. Apple a Swift 6.1-ben már bevezetett egy plugin-caching mechanizmust, ami ezt enyhíti, de a hatás még mindig mérhető. Ha sokat dolgozol a SwiftData adatperzisztenciával, érdemes a debug build-eknél figyelni a "compile time" pontszámokra.
A makró fejlesztés iteratív munka: ritkán működik elsőre, de minden iteráció gyors. A kulcs a kicsi lépésekben dolgozz filozófia: írj egy szerepkört, írd hozzá a tesztet, futtasd, javítsd, csak utána lépj a következőre.
Gyakran ismételt kérdések
Mi a különbség a Swift macro és a C preprocessor makró között?
A Swift macro AST-szinten dolgozik és a generált kód típusellenőrzött, míg a C preprocessor szöveges helyettesítést végez típus-tudás nélkül. Ennek köszönhetően a Swift makrók biztonságosabbak, és az IDE meg tudja mutatni a kibővített eredményt.
Lehet-e debug-olni egy Swift makrót?
Igen, az Xcode-ban beállíthatsz breakpointot a makró pluginban, ha az "Edit Scheme → Run → Executable" alatt a swift-compiler-plugin processzt választod. Egyszerűbb módszer: print hívások a plugin kódban; a kimenet a build log-ban jelenik meg.
Miért nem töltődik be a makróm Xcode 26 alatt?
Az Xcode 26 biztonsági okokból nem futtat aláíratlan plugin-t első alkalommal: a Build hibalistájában megjelenik egy "Trust & Enable Plugin" gomb, amit meg kell nyomni. CI környezetben a defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES paranccsal lehet ezt megkerülni.
Hány makrót használjak egy projektben?
Annyit, amennyi indokolt, de nem több. Saját tapasztalatom: ha egy makró kevesebb mint 3 helyen használt, valószínűleg túl korai absztrakció. A standard @Observable és @Model használata viszont általában mindig megéri.
Tud-e egy makró egy másik fájlt olvasni?
Nem. A makró pluginok sandboxolt folyamatban futnak, és nem férnek hozzá a fájlrendszerhez, hálózathoz vagy környezeti változókhoz. Ez szándékos biztonsági döntés: a fordítás determinisztikus és reprodukálható kell legyen.