Spec-Zone.ru › Swift Language

Макросы

Используйте макросы для генерации кода во время компиляции.

Макросы преобразуют ваш исходный код во время компиляции, позволяя избежать написания повторяющегося кода вручную. Во время компиляции Swift расширяет все макросы в вашем коде перед построением кода обычным образом.

Диаграмма, демонстрирующая общий обзор расширения макросов. Слева — стилизованное представление Swift-кода. Справа — тот же код с несколькими добавленными строками макросом.

Расширение макроса всегда является операцией добавления: макросы добавляют новый код, но никогда не удаляют и не изменяют существующий код.

И входные данные макроса, и выходные данные расширения макроса проверяются на синтаксическую правильность Swift-кода. Аналогично, значения, которые вы передаёте макросу, и значения в коде, сгенерированном макросом, проверяются на соответствие правильных типов. Кроме того, если в реализации макроса возникает ошибка при расширении этого макроса, компилятор обрабатывает это как ошибку компиляции. Эти гарантии упрощают понимание кода, использующего макросы, и облегчают выявление проблем, таких как неправильное использование макроса или ошибка в реализации макроса.

Swift имеет два вида макросов:

  • Самостоятельные макросы появляются сами по себе, без привязки к объявлению.

  • Прикреплённые макросы изменяют объявление, к которому они прикреплены.

Вы вызываете прикреплённые и самостоятельные макросы немного по-разному, но оба они следуют той же модели расширения макросов, и вы реализуете их оба с помощью того же подхода. В следующих разделах подробно описаны оба вида макросов.

Самостоятельные макросы

Чтобы вызвать самостоятельный макрос, запишите знак числа (#) перед его именем, а любые аргументы макроса — в скобках после имени. Например:

func myFunction() {
    print("Currently running \(#function)")
    #warning("Something's wrong")
}

В первой строке #function вызывается макрос function() из стандартной библиотеки Swift. При компиляции этого кода Swift вызывает реализацию этого макроса, которая заменяет #function именем текущей функции. При выполнении этого кода и вызове myFunction(), он выведет «В настоящее время выполняется myFunction()». Во второй строке #warning вызывается макрос warning(_:) из стандартной библиотеки Swift для создания пользовательского предупреждения во время компиляции.

Самостоятельные макросы могут производить значение, как делает #function, или могут выполнять действие во время компиляции, как делает #warning.

Прикреплённые макросы

Чтобы вызвать прикреплённый макрос, запишите знак «@» (@) перед его именем, а любые аргументы макроса — в скобках после имени.

Прикреплённые макросы изменяют объявление, к которому они прикреплены. Они добавляют код к этому объявлению, например, определение нового метода или добавление соответствия протоколу.

Например, рассмотрим следующий код, который не использует макросы:

struct SundaeToppings: OptionSet {
    let rawValue: Int
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

В этом коде каждый из вариантов в наборе SundaeToppings включает вызов инициализатора, что повторяется и происходит вручную. Легко допустить ошибку при добавлении нового варианта, например, напечатав неправильное число в конце строки.

Вот версия этого кода, которая использует макрос вместо этого:

@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

Эта версия SundaeToppings вызывает макрос @OptionSet. Макрос считывает список случаев в закрытом перечислении, генерирует список констант для каждого варианта и добавляет соответствие протоколу OptionSet.

Для сравнения, вот что выглядит расширенная версия макроса @OptionSet. Вы не пишете этот код, и вы увидите его только если специально попросите Swift показать расширение макроса.

struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }

    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppings: OptionSet { }

Весь код после закрытого перечисления получен из макроса @OptionSet. Версия SundaeToppings, использующая макрос для генерации всех статических переменных, более читабельна и проще в обслуживании, чем вручную написанная версия, представленная ранее.

Объявления макросов

В большинстве Swift-кода, когда вы реализуете символ, такой как функция или тип, нет отдельного объявления. Однако для макросов объявление и реализация разделены. Объявление макроса содержит его имя, параметры, которые он принимает, где его можно использовать и какой код он генерирует. Реализация макроса содержит код, который расширяет макрос, генерируя Swift-код.

Вы вводите объявление макроса с ключевым словом macro. Например, вот часть объявления макроса @OptionSet, используемого в предыдущем примере:

public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

Первая строка указывает имя макроса и его аргументы — имя OptionSet, и он не принимает никаких аргументов. Во второй строке используется макрос externalMacro(module:type:) из стандартной библиотеки Swift, чтобы указать Swift, где находится реализация макроса. В данном случае модуль SwiftMacros содержит тип, названный OptionSetMacro, который реализует макрос @OptionSet.

Поскольку OptionSet является прикреплённым макросом, его имя использует верхний регистр с прописной буквы, как и имена структур и классов. Самостоятельные макросы имеют имена в нижнем регистре с прописной буквы, как имена переменных и функций.

Примечание: макросы всегда объявляются как public. Поскольку код, который объявляет макрос, находится в другом модуле, чем код, использующий этот макрос, нет места, где можно было бы применить непубличный макрос.

Объявление макроса определяет роли макроса — места в исходном коде, где этот макрос может быть вызван, и типы кода, которые макрос может генерировать. Каждый макрос имеет одну или несколько ролей, которые вы пишете как часть атрибутов в начале объявления макроса. Вот немного больше объявления для макроса @OptionSet, включая атрибуты для его ролей:

@attached(member)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

Атрибут @attached появляется дважды в этом объявлении, по одному для каждой роли макроса. Первое использование, @attached(member), указывает, что макрос добавляет новые члены к типу, к которому он применяется. Макрос @OptionSet добавляет инициализатор init(rawValue:), который необходим протоколу OptionSet, а также некоторые дополнительные члены. Второе использование, @attached(extension, conformances: OptionSet), говорит вам, что @OptionSet добавляет соответствие протоколу OptionSet. Макрос @OptionSet расширяет тип, к которому вы применяете макрос, чтобы добавить соответствие протоколу OptionSet.

Для самостоятельного макроса вы пишете атрибут @freestanding, чтобы указать его роль:

@freestanding(expression)
public macro line<T: ExpressibleByIntegerLiteral>() -> T =
        /* ... location of the macro implementation... */

Макрос #line выше имеет роль expression. Макрос выражения производит значение или выполняет действие во время компиляции, например, генерирует предупреждение.

В дополнение к роли макроса, объявление макроса предоставляет информацию об именах символов, которые генерирует макрос. Когда объявление макроса предоставляет список имён, гарантируется, что он будет генерировать только объявления, которые используют эти имена, что помогает понять и отладить сгенерированный код. Вот полное объявление макроса @OptionSet:

@attached(member, names: named(RawValue), named(rawValue),
        named(`init`), arbitrary)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

В объявлении выше макрос @attached(member) включает аргументы после метки names: для каждого из символов, которые генерирует макрос @OptionSet. Макрос добавляет объявления для символов, названных RawValue, rawValue и init — так как эти имена известны заранее, объявление макроса явно их перечисляет.

Объявление макроса также включает arbitrary после списка имён, что позволяет макросу генерировать объявления, имена которых не известны до тех пор, пока вы не используете макрос. Например, когда макрос @OptionSet применяется к SundaeToppings выше, он генерирует свойства типа, которые соответствуют случаям перечисления, nuts, cherry и fudge.

Дополнительную информацию, включая полный список ролей макросов, см. в <doc:Attributes#attached> и <doc:Attributes#freestanding> в Атрибуты.

Расширение макросов

При построении Swift-кода, использующего макросы, компилятор вызывает реализацию макросов для их расширения.

Диаграмма, показывающая четыре шага расширения макросов. Входные данные — Swift-исходный код. Он превращается в дерево, представляющее структуру кода. Реализация макроса добавляет ветви к дереву. Результатом является Swift-исходный код с дополнительным кодом.

В частности, Swift расширяет макросы следующим образом:

  1. Компилятор считывает код, создавая внутри памяти представление синтаксиса.

  2. Компилятор отправляет часть представления внутри памяти в реализацию макроса, которая расширяет макрос.

  3. Компилятор заменяет вызов макроса его расширенной формой.

  4. Компилятор продолжает компиляцию, используя расширенный исходный код.

Для прохождения конкретных шагов рассмотрите следующее:

let magicNumber = #fourCharacterCode("ABCD")

Макрос #fourCharacterCode принимает строку длиной в четыре символа и возвращает целое число без знака 32 бита, которое соответствует значениям ASCII в строке, соединённых вместе. Некоторые форматы файлов используют целые числа такого типа для идентификации данных, потому что они компактны, но всё ещё читаемы в отладчике. В разделе <doc:Macros#Implementing-a-Macro> ниже показано, как реализовать этот макрос.

Для расширения макросов в коде выше компилятор считывает Swift-файл и создаёт внутри памяти представление этого кода, известное как абстрактное синтаксическое дерево (AST). AST делает явной структуру кода, что упрощает написание кода, взаимодействующего с этой структурой, — например, компилятора или реализации макроса. Вот представление AST для кода выше, немного упрощённое путём пропуска некоторых дополнительных деталей:

Диаграмма дерева с константой в качестве корневого элемента. Константа имеет имя, магическое число и значение. Значение константы — вызов макроса. Вызов макроса имеет имя, fourCharacterCode, и аргументы. Аргумент — строковый литерал ABCD.

На диаграмме выше показано, как структура этого кода представлена в памяти. Каждый элемент в AST соответствует части исходного кода. Элемент AST «Объявление константы» имеет два дочерних элемента под ним, которые представляют две части объявления константы: её имя и её значение. Элемент «Вызов макроса» имеет дочерние элементы, которые представляют имя макроса и список аргументов, передаваемых макросу.

В рамках построения этой абстрактной синтаксической древесины (AST) компилятор проверяет, что исходный код является корректным Swift-кодом. Например, #fourCharacterCode принимает один аргумент, который должен быть строкой. Если вы попытались передать целочисленный аргумент или забыли кавычку (") в конце строковой константы, в этом месте процесса вы получите ошибку.

Компилятор находит места в коде, где вы вызываете макрос, и загружает внешний двоичный файл, реализующий эти макросы. Для каждого вызова макроса компилятор передает часть AST реализации этого макроса. Вот представление этой частичной AST:

Деревовидная диаграмма, где вызов макроса является корневым элементом. Вызов макроса имеет имя, четырехсимвольный код и аргументы. Аргументом является строковая константа ABCD.

Реализация макроса #fourCharacterCode читает эту частичную AST в качестве входных данных при развёртывании макроса. Реализация макроса работает только с частичной AST, полученной в качестве входных данных, что означает, что макрос всегда разворачивается одинаково независимо от того, какой код находится перед и после него. Это ограничение помогает сделать развёртывание макросов более понятным и ускоряет сборку вашего кода, поскольку Swift может избежать развёртывания макросов, которые не изменились. Swift помогает авторам макросов избежать случайного чтения других входных данных, ограничивая код, реализующий макросы:

  • AST, передаваемая в реализацию макроса, содержит только элементы AST, представляющие макрос, а не любой код, предшествующий или следующий за ним.

  • Реализация макроса выполняется в защищённой среде, которая предотвращает доступ к файловой системе или сети.

Помимо этих мер предосторожности, автор макроса несёт ответственность за то, чтобы не читать и не изменять ничего за пределами входных данных макроса. Например, развёртывание макроса не должно зависеть от текущего времени суток.

Реализация макроса #fourCharacterCode генерирует новую AST, содержащую развернутый код. Вот, что этот код возвращает компилятору:

Деревовидная диаграмма с целочисленной константой 1145258561 типа UInt32.

Когда компилятор получает это развёртывание, он заменяет элемент AST, содержащий вызов макроса, элементом, содержащим развёртывание макроса. После развёртывания макроса компилятор снова проверяет, чтобы программа по-прежнему была синтаксически корректным Swift-кодом, и все типы были верными. Это даёт окончательную AST, которую можно скомпилировать как обычно:

Деревовидная диаграмма, где константа является корневым элементом. Константа имеет имя, магическое число и значение. Значением константы является целочисленная константа 1145258561 типа UInt32.

Эта AST соответствует Swift-коду, подобному этому:

let magicNumber = 1145258561 as UInt32

В этом примере исходный код содержит только один макрос, но реальная программа может содержать несколько экземпляров одного и того же макроса и несколько вызовов разных макросов. Компилятор разворачивает макросы по одному.

Если один макрос находится внутри другого, сначала разворачивается внешний макрос — это позволяет внешнему макросу изменить внутренний макрос перед его развёртыванием.

Реализация макроса

Для реализации макроса вам необходимо создать две компоненты: тип, выполняющий развёртывание макроса, и библиотеку, объявляющую макрос для экспорта его как API. Эти части создаются отдельно от кода, использующего макрос, даже если вы разрабатываете макрос и его клиенты вместе, поскольку реализация макроса выполняется в рамках сборки клиентов макроса.

Для создания нового макроса с помощью Swift Package Manager запустите swift package init --type macro — это создаст несколько файлов, включая шаблон для реализации и объявления макроса.

Чтобы добавить макросы в существующий проект, отредактируйте начало вашего файла Package.swift следующим образом:

  • Установите версию Swift Tools 5.9 или более позднюю в комментарии swift-tools-version.
  • Импортируйте модуль CompilerPluginSupport.
  • Включите macOS 10.15 в качестве минимальной целевой версии в список platforms.

Код ниже показывает начало примера файла Package.swift.

// swift-tools-version: 5.9

import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyPackage",
    platforms: [ .iOS(.v17), .macOS(.v13)],
    // ...
)

Далее, добавьте целевой объект для реализации макроса и целевой объект для библиотеки макроса в ваш существующий файл Package.swift. Например, вы можете добавить что-то вроде следующего, изменив имена, чтобы соответствовать вашему проекту:

targets: [
    // Macro implementation that performs the source transformations.
    .macro(
        name: "MyProjectMacros",
        dependencies: [
            .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
            .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
        ]
    ),

    // Library that exposes a macro as part of its API.
    .target(name: "MyProject", dependencies: ["MyProjectMacros"]),
]

Код выше определяет два целевых объекта: MyProjectMacros содержит реализацию макросов, а MyProject делает эти макросы доступными.

Реализация макроса использует модуль SwiftSyntax, чтобы взаимодействовать со Swift-кодом структурированным образом, используя AST. Если вы создали новый пакет макросов с помощью Swift Package Manager, сгенерированный файл Package.swift автоматически включает зависимость от SwiftSyntax. Если вы добавляете макросы в существующий проект, добавьте зависимость от SwiftSyntax в свой файл Package.swift:

dependencies: [
    .package(url: "https://github.com/swiftlang/swift-syntax", from: "509.0.0")
],

В зависимости от роли вашего макроса, есть соответствующий протокол из SwiftSyntax, которому соответствует реализация макроса. Например, рассмотрим #fourCharacterCode из предыдущего раздела. Вот структура, реализующая этот макрос:

import SwiftSyntax
import SwiftSyntaxMacros

public struct FourCharacterCode: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression,
              let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
              segments.count == 1,
              case .stringSegment(let literalSegment)? = segments.first
        else {
            throw CustomError.message("Need a static string")
        }

        let string = literalSegment.content.text
        guard let result = fourCharacterCode(for: string) else {
            throw CustomError.message("Invalid four-character code")
        }

        return "\(raw: result) as UInt32"
    }
}

private func fourCharacterCode(for characters: String) -> UInt32? {
    guard characters.count == 4 else { return nil }

    var result: UInt32 = 0
    for character in characters {
        result = result << 8
        guard let asciiValue = character.asciiValue else { return nil }
        result += UInt32(asciiValue)
    }
    return result
}
enum CustomError: Error { case message(String) }

Если вы добавляете этот макрос в существующий проект Swift Package Manager, добавьте тип, который действует как точка входа для целевого объекта макроса, и перечислите макросы, определённые этим целевым объектом:

import SwiftCompilerPlugin

@main
struct MyProjectMacros: CompilerPlugin {
    var providingMacros: [Macro.Type] = [FourCharacterCode.self]
}

Макрос #fourCharacterCode — это свободный макрос, генерирующий выражение, поэтому тип FourCharacterCode, который его реализует, соответствует протоколу ExpressionMacro. Протокол ExpressionMacro имеет одно требование: метод expansion(of:in:), который разворачивает AST. Список ролей макросов и соответствующих протоколов SwiftSyntax см. в <doc:Attributes#attached> и <doc:Attributes#freestanding> в Атрибуты.

Для развёртывания макроса #fourCharacterCode Swift отправляет AST кода, использующего этот макрос, в библиотеку, содержащую реализацию макроса. Внутри библиотеки Swift вызывает FourCharacterCode.expansion(of:in:), передавая в качестве аргументов AST и контекст в метод. Реализация expansion(of:in:) находит строку, переданную в качестве аргумента макросу #fourCharacterCode, и вычисляет соответствующее целочисленное значение типа 32-битное беззнаковое целое число.

В примере выше первый блок guard извлекает строковую константу из AST, присваивая этот элемент AST переменной literalSegment. Второй блок guard вызывает частную функцию fourCharacterCode(for:). Оба эти блока генерируют ошибку, если макрос используется неправильно — сообщение об ошибке становится ошибкой компилятора в месте некорректного вызова. Например, если вы попытаетесь вызвать макрос как #fourCharacterCode("AB" + "CD"), компилятор выведет ошибку «Необходима статическая строка».

Метод expansion(of:in:) возвращает экземпляр ExprSyntax, тип из SwiftSyntax, представляющий выражение в AST. Поскольку этот тип соответствует протоколу StringLiteralConvertible, реализация макроса использует строковую константу в качестве лёгкого синтаксиса для создания своего результата. Все типы SwiftSyntax, которые вы возвращаете из реализации макроса, соответствуют протоколу StringLiteralConvertible, поэтому вы можете использовать этот подход при реализации любого типа макроса.

Разработка и отладка макросов

Макросы хорошо подходят для разработки с использованием тестов: они преобразуют одну AST в другую AST, не завися от внешнего состояния и не внося изменений во внешнее состояние. Кроме того, вы можете создавать узлы синтаксиса из строковой константы, что упрощает настройку входных данных для теста. Вы также можете прочитать свойство description элемента AST, чтобы получить строку для сравнения с ожидаемым значением. Например, вот тест макроса #fourCharacterCode из предыдущих разделов:

let source: SourceFileSyntax =
    """
    let abcd = #fourCharacterCode("ABCD")
    """

let file = BasicMacroExpansionContext.KnownSourceFile(
    moduleName: "MyModule",
    fullFilePath: "test.swift"
)

let context = BasicMacroExpansionContext(sourceFiles: [source: file])

let transformedSF = source.expand(
    macros:["fourCharacterCode": FourCharacterCode.self],
    in: context
)

let expectedDescription =
    """
    let abcd = 1145258561 as UInt32
    """

precondition(transformedSF.description == expectedDescription)

В приведенном выше примере для тестирования макроса используется предопределённое условие, но вместо него можно использовать фреймворк для тестирования.

This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API