-
Expand on Swift macros
Discover how Swift macros can help you reduce boilerplate in your codebase and adopt complex features more easily. Learn how macros can analyze code, emit rich compiler errors to guide developers towards correct usage, and generate new code that is automatically incorporated back into your project. We'll also take you through important concepts like macro roles, compiler plugins, and syntax trees.
Chapitres
- 0:00 - Introduction
- 0:51 - Why macros?
- 2:13 - Design philosophy
- 4:48 - Translation model
- 6:18 - Macro roles
- 17:48 - Macro implementation
- 33:36 - Writing correct macros
- 38:42 - Wrap up
Ressources
Vidéos connexes
WWDC23
-
Rechercher dans cette vidéo…
-
-
0:44 - The #unwrap expression macro, with a more complicated argument
let image = #unwrap(request.downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap" -
0:50 - Existing features using expansions (1)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } } -
1:11 - Existing features using expansions (2)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] // Begin expansion for Codable private enum CodingKeys: String, CodingKey { case id, title, description, measuredIngredients } init(from decoder: Decoder) throws { … } func encode(to encoder Encoder) throws { … } // End expansion for Codable static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } } -
3:16 - Macros inputs are complete, type-checked, and validated
#unwrap(1 + ) // error: expected expression after operator @AddCompletionHandler(parameterName: 42) // error: cannot convert argument of type 'Int' to expected type 'String' func sendRequest() async throws -> Response @DictionaryStorage class Options { … } // error: '@DictionaryStorage' can only be applied to a 'struct' -
3:45 - Macro expansions are inserted in predictable ways
func doThingy() { startDoingThingy() #someUnknownMacro() finishDoingThingy() } -
4:51 - How macros work, featuring #stringify
func printAdd(_ a: Int, _ b: Int) { let (result, str) = #stringify(a + b) // Begin expansion for "#stringify" (a + b, "a + b") // End expansion for "#stringify" print("\(str) = \(result)") } printAdd(1, 2) // prints "a + b = 3" -
5:43 - Macro declaration for #stringify
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) -
7:11 - What’s an expression?
let numPixels = (x + width) * (y + height) // ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression // ^~~~~~~~~ But so is this // ^~~~~ And this -
7:34 - The #unwrap expression macro: motivation
// Some teams are nervous about this: let image = downloadedImage! // Alternatives are super wordy: guard let image = downloadedImage else { preconditionFailure("Unexpectedly found nil: downloadedImage was already checked") } -
8:03 - The #unwrap expression macro: macro declaration
/// Force-unwraps the optional value passed to `expr`. /// - Parameter message: Failure message, followed by `expr` in single quotes @freestanding(expression) macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped -
8:21 - The #unwrap expression macro: usage
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap" -
9:09 - The #makeArrayND declaration macro: motivation
public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } public struct Array3D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1, width2: Int public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index { Index(storageIndex: (i0 * width1 + i1) * width2 + i2) } public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element { get { self[makeIndex(i0, i1, i2)] } set { self[makeIndex(i0, i1, i2)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } -
10:03 - The #makeArrayND declaration macro: macro declaration
/// Declares an `n`-dimensional array type named `Array<n>D`. /// - Parameter n: The number of dimensions in the array. @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int) -
10:15 - The #makeArrayND declaration macro: usage
#makeArrayND(n: 2) // Begin expansion for "#makeArrayND" public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } } // End expansion for "#makeArrayND" #makeArrayND(n: 3) #makeArrayND(n: 4) #makeArrayND(n: 5) -
11:23 - The @AddCompletionHandler peer macro: motivation
/// Fetch the avatar for the user with `username`. func fetchAvatar(_ username: String) async -> Image? { ... } func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } } -
11:51 - The @AddCompletionHandler peer macro: macro declaration
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") -
11:59 - The @AddCompletionHandler peer macro: usage
/// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler" -
12:36 - The @DictionaryStorage accessor macro: motivation
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } } -
13:04 - The @DictionaryStorage accessor macro: declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil) -
13:20 - The @DictionaryStorage accessor macro: usage
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" } -
13:56 - The @DictionaryStorage member attribute macro: macro declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) -
14:46 - The @DictionaryStorage member attribute macro: usage
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } -
15:52 - The @DictionaryStorage member macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) -
16:26 - The @DictionaryStorage member macro: usage
// The @DictionaryStorage member macro @DictionaryStorage struct Person: DictionaryRepresentable { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } -
16:59 - The @DictionaryStorage conformance macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) -
17:09 - The @DictionaryStorage conformance macro: usage
struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } -
17:28 - @DictionaryStorage starting point
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } } -
17:32 - @DictionaryStorage ending point
@DictionaryStorage struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" } -
17:35 - @DictionaryStorage ending point (without expansions)
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } -
18:01 - Macro implementations
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro( module: "MyLibMacros", type: "StringifyMacro" ) -
19:18 - Implementing @DictionaryStorage’s @attached(member) role (1)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } -
19:52 - Code used to demonstrate SwiftSyntax trees
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } -
22:00 - Implementing @DictionaryStorage’s @attached(member) role (2)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } -
24:29 - A type that @DictionaryStorage isn’t compatible with
@DictionaryStorage enum Gender { case other(String) case female case male // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" } -
25:17 - Expansion method with error checking
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard declaration.is(StructDeclSyntax.self) else { let structError = Diagnostic( node: attribute, message: MyLibDiagnostic.notAStruct ) context.diagnose(structError) return [] } return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } enum MyLibDiagnostic: String, DiagnosticMessage { case notAStruct var severity: DiagnosticSeverity { return .error } var message: String { switch self { case .notAStruct: return "'@DictionaryStorage' can only be applied to a 'struct'" } } var diagnosticID: MessageID { MessageID(domain: "MyLibMacros", id: rawValue) } } -
29:32 - Parameter list for `ArrayND.makeIndex`
FunctionParameterListSyntax { for dimension in 0 ..< numDimensions { FunctionParameterSyntax( firstName: .wildcardToken(), secondName: .identifier("i\(dimension)"), type: TypeSyntax("Int") ) } } -
30:17 - The #unwrap expression macro: revisited
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap" -
30:38 - Implementing the #unwrap expression macro: start
static func makeGuardStmt() -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } """ } -
30:57 - Implementing the #unwrap expression macro: the message string
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ } -
31:21 - Implementing the #unwrap expression macro: the variable name
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { return """ guard let \(wrapped) else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ } -
31:44 - Implementing the #unwrap expression macro: interpolating a string as a literal
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ } -
32:11 - Implementing the #unwrap expression macro: adding an expression as a string
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ } -
33:00 - Implementing the #unwrap expression macro: inserting the file and line numbers
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax, in context: some MacroExpansionContext) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " let originalLoc = context.location(of: originalWrapped)! return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: \(originalLoc.file), line: \(originalLoc.line) ) } """ } -
34:05 - The #unwrap expression macro, with a name conflict
let wrappedValue = "🎁" let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was \(wrappedValue)", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap" -
34:30 - The MacroExpansion.makeUniqueName() method
let captureVar = context.makeUniqueName() return """ { [\(captureVar) = \(originalWrapped)] in \(makeGuardStmt(wrapped: captureVar, …)) \(makeReturnStmt(wrapped: captureVar)) } """ -
35:44 - Declaring a macro’s names
@attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int) -
38:28 - Macros are testable
import MyLibMacros import XCTest import SwiftSyntaxMacrosTestSupport final class MyLibTests: XCTestCase { func testMacro() { assertMacroExpansion( """ @DictionaryStorage var name: String """, expandedSource: """ var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } """, macros: ["DictionaryStorage": DictionaryStorageMacro.self]) } }
-