Swiftのマクロは、コンパイルの意味解析フェーズ、具体的には構文解析後、最終的な抽象構文木(AST)の型チェック前に展開されます。このタイミングは重要で、マクロ展開が完全な型チェックと意味的検証を受ける必要があるコードを生成できるからです。この段階で動作することで、Swiftは拡張されたコードが言語の型安全性保証を侵害したり、アクセス制御修飾子を回避したりしないことを保証します。
問題は、マクロが新しい構文ノードを生成することによってソースコードを変換し、周囲のレキシカルスコープに既存の変数と衝突する可能性のある識別子を導入することです。もしマクロが単にハードコーディングされた変数名を挿入した場合、呼び出しコンテキストから変数を偶然にキャプチャしたり、シャドーイングしたりする可能性があります。これは、生成されたコードが呼び出し元のロジックと干渉することで微妙なバグやセキュリティの脆弱性を引き起こす原因となります。
これを解決するために、Swiftはすべての合成バインディングに対してユニークな内部識別子を使用する衛生的マクロシステムを採用しています。コンパイラは構文ノードにメタデータを付加し、その元のレキシカルコンテキストを追跡します。これにより、生成された識別子は明示的にアンラッピングされない限り、ユーザーが記述したコードと区別されます。このメカニズムにより、マクロはリスクなしに一時変数を導入できる一方で、必要に応じて明示的なパラメータ渡しを通じて故意に名前をキャプチャすることも可能です。
私たちのチームは、依存性注入のためのSwiftパッケージを構築しており、@Injectableという付加マクロを使用して、複雑なサービスクラスの初期化コードを自動生成していました。このマクロは、構築中に中間依存関係を保持する一時変数を作成する必要がありましたが、containerやserviceのような一般的な変数名がターゲットクラスのスコープに既に存在する危険性がありました。これがジレンマを生じさせました:名前の衝突を避け、クライアントコードや微妙な再割り当てバグを引き起こさずに、安全な初期化コードを生成するにはどうすればよいでしょうか?
当初は、初期化実装を生成するためにシンプルな文字列テンプレートを使用したナイーブなテキストベースのコード生成アプローチを検討しました。主な利点は実装のシンプルさでした。生成されたSwiftコードをすぐに検査し、直接デバッグできたからです。しかし、重大な欠点は衛生保証が存在しないことでした。一時変数名がターゲットクラスの既存のプロパティと衝突しないことを保証するメカニズムがなかったため、コンパイル失敗や、マクロが既存のインスタンス変数を誤って再割り当てすることによる静かなロジックエラーを引き起こす可能性がありました。
次に、成熟したサードパーティのコード生成ツールであるSourceryを評価しました。このツールは、Swiftコンパイラの外部でプリコンパイルステップとして機能します。利点としては、広範なドキュメント、柔軟なステンシルテンプレート、およびインラインコードではなくファイル全体を生成する能力が含まれていました。残念ながら、欠点には、Xcodeでの追加のRun Scriptフェーズが必要な複雑なビルドツールの統合、外部プロセスオーバーヘッドによるビルド時間の大幅な遅延、生成されたコードの型エラーがコンパイル時のみ発生し、元のマクロ呼び出しに対する明確でないソースマッピングを伴うことが含まれていました。
最終的に、私たちはSwift 5.9で導入されたSwiftのネイティブマクロシステムを選択し、サービスクラス宣言に付加されたピアマクロを利用しました。このソリューションは、コンパイラパイプラインに直接統合され、拡張されたコードのコンパイル時型チェックを提供し、SwiftSyntaxライブラリを介して生成された識別子のための組み込み衛生を提供するため選ばれました。結果として、@Injectableマクロが安全に複雑な初期化ロジックを生成できる堅牢な依存性注入フレームワークが実現しました。これにより、約70%のボイラープレートコードが削減され、コンパイル時の安全保証が完全に維持され、マクロ使用箇所への明確なエラーメッセージが提供されました。
最終的な実装では、以前の手動依存性注入セットアップで問題となっていた命名関連のバグのカテゴリ全体を排除しました。ビルド時間はSourceryアプローチと比較して40%改善し、開発者はサービスクラスを安心してリファクタリングできるようになり、マクロ生成された初期化子が手動での同期なしに新しい依存関係に自動的に適応することができました。
なぜSwiftのマクロは既存のコードをその場で変更できないのか、同様の意味を持つ代替パターンは何ですか?
LispやRustの手続き型マクロのように既存の構文ノードをその場で変換することができないのとは対照的に、Swiftのマクロは純粋に加算的です—新しいコードを生成することしかできず、元のソースを変更することはできません。この制限は、Swiftのコンパイルモデルがデバッグ、ソースマッピング、インクリメンタルコンパイルの目的のために元のソースを保持する必要があるために存在します。「変更」セマンティクスを達成するためには、開発者はピアマクロを使用して追加のオーバーロードやラッパータイプを生成し、元の宣言に対して非推奨の注釈を付けて、生成された代替策への移行を導く必要があります。
マクロ展開は生成された式の型推論をどのように扱い、推論が失敗した場合はどうなりますか?
マクロが明示的な型注釈のない式を含むコードに展開されると、Swiftはマクロ展開後に行われる標準型チェックフェーズ中に生成されたASTで型推論を実行します。推論が失敗した場合、コンパイラはエラーロケーションをマクロ呼び出しサイトに関連付ける診断メッセージを出力します。この時、拡張中に付加されたソースロケーションメタデータを使用します。候補者はよく、マクロが明示的に#fileおよび#lineリテラルを生成したり、診断ユーザーに対する出現位置を制御する#sourceLocationディレクティブを使用することができ、エラーが内部マクロ実装の詳細ではなく意味のある位置を指すことを保証できることを見逃します。
展開コンテキストと利用可能な意味情報の観点から、自立型マクロと付加型マクロの違いは何ですか?
自立型マクロ(#で接頭辞されたマクロ)は、表現または文レベルで展開され、周囲の型情報に対するアクセスが限られ、引数の構文のみを受け取ります。一方、付加型マクロ(@で接頭辞されたもの)は、宣言に作用し、付加された宣言の構文、アクセス修飾子、およびmacro宣言のコンテキストパラメータを通じての継承関係を含む豊富な意味情報を受け取ります。初心者はこれらの境界を混同し、型メンバーにアクセスしたり、特定の型スコープ内でネストされた宣言を生成したりするために必要な付加型ピアマクロやメンバーマクロの代わりに自立型マクロを使用しようとすることがよくあります。