Open13

Swift Macros メモ

Hiroya HinomoriHiroya Hinomori

マクロを展開することは、常に加算操作です。つまり、マクロは新しいコードを追加しますが、既存のコードを削除したり修正したりすることはありません。

Hiroya HinomoriHiroya Hinomori

自立型マクロは、宣言に添付されることなく、それ自体独立して表示されます
付属型マクロは、それが添付されている宣言を変更します

この辺の違いがまだよくわからない。
もっとよく読み解く必要がある🤔

Hiroya HinomoriHiroya Hinomori

自立型マクロ(Freestanding Macros)

それ単体で実行できるMacroのことを指す
使用するには # を頭につけて呼び出す。

func hoge() {
  #something("Hoge")
  ...
}

自立型マクロは、小文字始まりのキャメルケースを使用する

Hiroya HinomoriHiroya Hinomori

付属型マクロ(Attached Macros)

struct 等に付与するMacroのこと
使用するには @ を頭につけて呼び出す。

@Something
func hoge() {
  ...
}

付属型マクロは大文字始まりのキャメルケースを使用する

Hiroya HinomoriHiroya Hinomori

マクロ宣言

マクロを使用するには実装とは別に宣言が必要になる

  • マクロの名前
  • 受け取るパラメータ
  • 使用できる場所、
  • 生成されるコードの種類

の定義が必要

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

macro キーワードを使用することで宣言を行える。
マクロの実装がどこにあるかをSwiftに伝えるため #externalMacro(module:type:) マクロを使用する。

この場合 SwiftMacros モジュールは、@OptionSet マクロを実装する OptionSetMacro という名前の型を含んでいます。

Hiroya HinomoriHiroya Hinomori

マクロ属性 Part.1

すべてのマクロには マクロ宣言の冒頭で属性の一部として記述する必要がある。

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

@attached(member) は、マクロを適用する型に新しいメンバを追加することを示す。

例のマクロは OptionSet プロトコルで必要な init(rawValue:) イニシャライザと、いくつかのメンバー変数を追加するので、属性として定義している。

@attached(extension, conformances: OptionSet) は、@OptionSetOptionSet プロトコルへの準拠を追加することを定義している。

参照コード
@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

👇展開時

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 { }
Hiroya HinomoriHiroya Hinomori

マクロ属性 Part.2

マクロが生成するシンボルの名前に関する情報を提供できる。
名前のリストを提供すると、その名前を使用する宣言だけが生成されることが保証され、生成されたコードの理解やデバッグに役立つ。

@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: ラベルの後に RawValuerawValueinit という名前のシンボルに対する宣言を追加している。
これらの名前は前もってわかっているので、マクロ宣言の時にはそれらを明示的にリストアップすることができる。

また、名前のリストの最後に arbitrary が含まれており、これは、マクロを使用するまで名前がわからない宣言をマクロで生成できる。

👇参照コードの例では、@OptionSet マクロが上記の SundaeToppings に適用されると、列挙型のケースである nuts、cherry、fudge に対応したプロパティの宣言をマクロで生成できる。

参照コード
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 { }
Hiroya HinomoriHiroya Hinomori

マクロ属性 Part.3

自立型マクロの場合、@freestanding 属性を記述してその役割を指定する。

@freestanding(expression)
public macro line<T: ExpressibleByIntegerLiteral>() -> T =
        /* ... マクロ実装の場所... */

上記の #line マクロは、expression の役割を担っていることを宣言している。
expression 定義されたマクロは、値を生成したり、警告を生成するようなコンパイル時のアクションを実行できる。

そのほかの属性については、属性(Attributes)属性(Attributes)のattached属性(Attributes)のfreestanding を参照してください。

Hiroya HinomoriHiroya Hinomori

マクロ展開

マクロを使用した Swift のコードをビルドする時、コンパイラはマクロを展開するためにマクロの実装を呼ぶ。

以下のように展開されます:

  1. コンパイラは、コードを読み、構文のメモリ内表現を作成する
  2. コンパイラは、メモリ内表現の一部をマクロの実装に送信し、マクロの実装はマクロを展開する
  3. コンパイラは、マクロの呼び出しを展開した形に置き換える
  4. コンパイラは、展開されたソースコードを使用してコンパイルを続行する
  • マクロの実装に渡される AST は、マクロを表す AST 要素のみを含み、その前後に来るコードは一切含まれない
  • マクロ実装は、ファイルシステムやネットワークにアクセスできないサンドボックス環境で実行される

マクロの展開が現在の時刻に依存することはない。

抽象構文木(AST)の中でMacroが実行、結果が返るとASTを書き換えてくれるみたい。

実際のプログラムでは、同じマクロの複数のインスタンスと異なるマクロへの複数の呼び出しがあるかもしれません。コンパイラは、マクロを 1 つずつ展開します。

あるマクロが別のマクロの中にある場合、外側のマクロが最初に展開されます。これにより、展開される前に、外側のマクロが内側のマクロを変更することができます。

Hiroya HinomoriHiroya Hinomori

マクロ実装

マクロを実装するには展開する型とAPIとして公開するための宣言をするライブラリが必要。

マクロの実装にはSwift Package Managerを用いて行う
すでにあるプロジェクトに取り込む場合もPackage.swiftを持ちいてインポートする

マクロをインポートする際にPackage.swiftには以下の条件が必要になる

  • swift-tools-version コメントで Swift ツールのバージョンを 5.9 以上に設定
  • CompilerPluginSupport モジュールをインポート
  • platforms リストで、最低の展開先として macOS 10.15 を含めます
// swift-tools-version: 5.9

import PackageDescription
import CompilerPluginSupport

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

次にPackage.swiftにtargetを追加

targets: [
    // ソース変換を行うマクロの実装
    .macro(
        name: "MyProjectMacros",
        dependencies: [
            .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
            .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
        ]
    ),

    // APIの一部としてマクロを公開するライブラリ
    .target(name: "MyProject", dependencies: ["MyProjectMacros"]),
]

また、SwiftSyntaxライブラリも必要なので、dependenciesに追加

dependencies: [
    // Sample当時は 509が最新だったようだけど、2024/4時点では510が最新バージョンになっている
    .package(url: "https://github.com/apple/swift-syntax", from: "510.0.0"), 
],