Swift Macros メモ
Swift Macros雑記
まずはこのページで気になった点をまとめていくよ
マクロを展開することは、常に加算操作です。つまり、マクロは新しいコードを追加しますが、既存のコードを削除したり修正したりすることはありません。
自立型マクロは、宣言に添付されることなく、それ自体独立して表示されます
付属型マクロは、それが添付されている宣言を変更します
この辺の違いがまだよくわからない。
もっとよく読み解く必要がある🤔
自立型マクロ(Freestanding Macros)
それ単体で実行できるMacroのことを指す
使用するには #
を頭につけて呼び出す。
func hoge() {
#something("Hoge")
...
}
自立型マクロは、小文字始まりのキャメルケースを使用する
付属型マクロ(Attached Macros)
struct
等に付与するMacroのこと
使用するには @
を頭につけて呼び出す。
@Something
func hoge() {
...
}
付属型マクロは大文字始まりのキャメルケースを使用する
マクロ宣言
マクロを使用するには実装とは別に宣言が必要になる
- マクロの名前
- 受け取るパラメータ
- 使用できる場所、
- 生成されるコードの種類
の定義が必要
public macro OptionSet<RawType>() =
#externalMacro(module: "SwiftMacros", type: "OptionSetMacro")
macro
キーワードを使用することで宣言を行える。
マクロの実装がどこにあるかをSwiftに伝えるため #externalMacro(module:type:)
マクロを使用する。
この場合 SwiftMacros
モジュールは、@OptionSet
マクロを実装する OptionSetMacro
という名前の型を含んでいます。
マクロ属性 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)
は、@OptionSet
が OptionSet
プロトコルへの準拠を追加することを定義している。
参照コード
@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 { }
マクロ属性 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:
ラベルの後に RawValue
、rawValue
、init
という名前のシンボルに対する宣言を追加している。
これらの名前は前もってわかっているので、マクロ宣言の時にはそれらを明示的にリストアップすることができる。
また、名前のリストの最後に 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 { }
マクロ属性 Part.3
自立型マクロの場合、@freestanding 属性を記述してその役割を指定する。
@freestanding(expression)
public macro line<T: ExpressibleByIntegerLiteral>() -> T =
/* ... マクロ実装の場所... */
上記の #line
マクロは、expression
の役割を担っていることを宣言している。
expression
定義されたマクロは、値を生成したり、警告を生成するようなコンパイル時のアクションを実行できる。
そのほかの属性については、属性(Attributes)の属性(Attributes)のattachedと属性(Attributes)のfreestanding を参照してください。
マクロ展開
マクロを使用した Swift のコードをビルドする時、コンパイラはマクロを展開するためにマクロの実装を呼ぶ。
以下のように展開されます:
- コンパイラは、コードを読み、構文のメモリ内表現を作成する
- コンパイラは、メモリ内表現の一部をマクロの実装に送信し、マクロの実装はマクロを展開する
- コンパイラは、マクロの呼び出しを展開した形に置き換える
- コンパイラは、展開されたソースコードを使用してコンパイルを続行する
- マクロの実装に渡される AST は、マクロを表す AST 要素のみを含み、その前後に来るコードは一切含まれない
- マクロ実装は、ファイルシステムやネットワークにアクセスできないサンドボックス環境で実行される
マクロの展開が現在の時刻に依存することはない。
抽象構文木(AST)の中でMacroが実行、結果が返るとASTを書き換えてくれるみたい。
実際のプログラムでは、同じマクロの複数のインスタンスと異なるマクロへの複数の呼び出しがあるかもしれません。コンパイラは、マクロを 1 つずつ展開します。
あるマクロが別のマクロの中にある場合、外側のマクロが最初に展開されます。これにより、展開される前に、外側のマクロが内側のマクロを変更することができます。
マクロ実装
マクロを実装するには展開する型と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)],
// ...
)
次に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"),
],