Swift Opt-In Reflection Metadata
01 何が問題だったのか
Swiftのコンパイラが生成するメタデータには、大きく分けて次の二種類があります。
- Core Metadata: 型情報のレコードなど、言語ランタイムの動作に必須のもの。常に出力され、明らかに使われていない場合しかストリップできません。
- Reflection Metadata: 型のフィールド名やフィールド型への参照を含む、ランタイムのリフレクション用の情報。
Mirror(reflecting:)経由でアクセスされるもので、言語機能自体は依存していません。
このProposalが扱うのは後者のReflection Metadataです。
Reflection Metadataには、これまで次のような問題がありました。
- 特定の型だけリフレクションを有効にする手段がありません。モジュール全体で有効化するか、コンパイラフラグ(
-disable-reflection-metadataなど)で全面的に無効化するかの二択でした。 - APIが内部でリフレクションを使っているかどうかを、シグネチャから知ることができません。APIが「ブラックボックス」になってしまっています。
- その結果、開発者は「使っているかわからないのでとりあえず全面的に有効にする」か、「APIの実装を推測して必要そうなモジュールだけ有効にする」かを選ばざるを得ません。前者はバイナリサイズの肥大化とリバースエンジニアリングの容易化につながり、後者は推測を誤ると実行時に不具合が出ます。
- APIによってリフレクションへの依存の仕方が異なります。
print/debugPrint/dumpのようにリフレクションが無くても動くが出力が簡素になるものもあれば、SwiftUIのようにユーザモジュールのリフレクションを使って状態変化の検出や再レンダリングを行い、無いと正しく動かないものもあります。 - リフレクションを誤ってオフにしたモジュールをリフレクション前提のAPIと組み合わせた場合でも、コンパイル時には何も警告されません。たとえばSwiftUIのステート変更でViewが再描画されない、といった形で実行時にだけ症状が現れ、原因の特定が難しくなります。
- 逆に、リフレクションを有効にしたモジュールで生成されたReflection Metadataは、Full Type Metadataから参照されてしまうため、デッドコード除去でもバイナリから落とし切れないケースが多く、不要なメタデータが残り続けます。
要するに、「この型にはリフレクションが必要」「このAPIはリフレクションを要求する」という依存関係を静的に表現する仕組みが無いため、安全性・バイナリサイズ・コードの秘匿性のいずれも妥協せざるを得ない状況でした。
02 どのように解決されるのか
新しいマーカープロトコル Reflectable を導入し、Reflection Metadataへの依存を型システム上で表現できるようにします。型が Reflectable に適合している場合にのみ、その型のReflection Metadataをコンパイラ(IRGen)が出力する、という仕組みです。これにより、実行時の問題だったものをコンパイル時のチェックに引き上げることを目指します。
このProposalはレビュー後に再検討のため差し戻された(Returned for revision) もので、Swiftに正式に取り込まれたわけではありません。以下はあくまで提案時点の設計です。
APIによる要求の表現
APIの作者は、リフレクションを必要とする関数のジェネリック要求として Reflectable を書けるようになります。利用者側はその型を Reflectable に適合させない限り、APIに渡すことができません。
// ライブラリ側
public func foo<T: Reflectable>(_ t: T) { ... }
// 利用者側
struct Bar: Reflectable {}
foo(Bar()) // OK。Bar のReflection Metadataが出力される
Reflectable への適合は型宣言の場所でのみ許可され、他モジュールから取り込んだ型にあとから適合させることはできません。リフレクションが有効になっていないモジュールの型に適合を付けられると、意図しない挙動につながるためです。
一方、プロトコルを介した間接的(transitive)な適合は許されます。APIの作者は、リフレクションに依存しているという事実を実装の詳細として隠せます。
// ライブラリ
public protocol Foo: Reflectable {}
public func consume<T: Foo>(_ t: T) {}
// 利用者
struct Bar: Foo {} // Reflection Metadataが出力される
consume(Bar())
SwiftUIの View プロトコルを Reflectable に適合させれば、SwiftUIを使うすべてのViewでリフレクションが自動的に有効になります。
// SwiftUI 側(想定)
protocol SwiftUI.View: Reflectable {}
// 利用者側
struct SomeModel {} // こちらはReflection Metadataなし
struct SomeView: SwiftUI.View {
var body: some View { Text("Hello, World!") }
}
この状態でユーザモジュールのリフレクションがコンパイラフラグによって無効化されていた場合、コンパイラはエラーを出します。「リフレクションを要求するAPIを使っているのにリフレクションを切っている」という矛盾を、実行時ではなくコンパイル時に検出できます。
Reflectable へのキャスト
実行時にリフレクションが利用可能かを確認できるよう、Reflectable への条件付きキャスト・強制キャスト・is 判定を許可します。成功するのはReflection Metadataが存在する型の場合のみです。
public func conditionalUse<T>(_ t: T) {
if let _t = t as? Reflectable {
// リフレクションを使う実装
} else {
// フォールバック実装
}
}
public func forceUse<T>(_ t: T) {
debugPrint(t as! Reflectable) // リフレクションが無ければクラッシュ
}
public func testIsReflectable<T>(_ t: T) -> Bool {
return t is Reflectable
}
これらのキャストは swift_reflectableCast という新しいランタイム関数を介して実装されます。Mirror.children.count ではフィールドが無い型とリフレクションが無い型を区別できなかったため、この判別手段を導入するという位置づけです。
キャストはコンパイル時に静的に可視である必要があり、as Reflectable のような暗黙変換は禁止されます。ジェネリックな経路で Reflectable に変換しようとするコードはコンパイルエラーになります。
func cast<T, U>(_ x: U) -> T { return x as! T }
let a = cast(1) as Reflectable // エラー: 'as?' または 'as!' を使うこと
let b: Reflectable = cast(1) // 同上
新しいランタイム関数を必要とするため、キャスト側はアベイラビリティチェックによりゲートされます。
リフレクション出力のモードとフラグ
Swift 6での既定挙動を変えるため、リフレクション出力は次の三つのモードに整理されます。
- Reflection Disabled(
-disable-reflection-metadata/-reflection-metadata-for-debugger-only)- Swift 6より前: デバッグ情報がフル(
-g/-gdwarf-types)なら出力、そうでなければ出力しない。モジュール内にReflectable適合型があればコンパイルエラー。 - Swift 6以降: no-op(opt-inモードが既定になるため)。
- Swift 6より前: デバッグ情報がフル(
- Opt-In Reflection(
-enable-upcoming-feature OptInReflection)- デバッグ情報が無効なら、
Reflectable適合型のみ出力。 - デバッグ情報が有効なら、すべての型について出力。
- Swift 6より前はupcoming feature flagで明示的に有効化、Swift 6以降は既定で有効。
- デバッグ情報が無効なら、
- Fully enabled(
-enable-full-reflection-metadata)- すべての型についてReflection Metadataを出力。
Reflectableへの適合はすべての型に自動合成されるため、リフレクション要求付きAPIもそのまま使えます。Swift 6より前の既定挙動。
- すべての型についてReflection Metadataを出力。
Swift 6以降は -disable-reflection-metadata と -emit-reflection-for-debugger を非推奨化し、no-opとして扱います。必要なときにリフレクションが無い、という事態を避けるためです。
デバッガ用には別の扱いがあります。-gdwarf-types や -g が有効な場合、Reflection Metadataは常に保持されます。ただし、nominal type descriptor経由ではアクセスできない形で保持されるため、ReleaseビルドとDebugビルドでAPIの挙動が変わらないようになっています。
標準ライブラリ側の扱い
Mirror(reflecting:) に Reflectable 制約は付けません。リフレクションをオプショナルに使いたいコードの自由度を残すためです。リフレクションが必須のAPIは、自身のシグネチャで Reflectable を要求することが推奨されます。
結果として、Swift 6でopt-inモードが既定になると、dump / debugPrint / String(describing:) のような標準ライブラリAPIは、Reflectable に適合していない型に対しては限定的な出力しか返さなくなります。ライブラリ作者は、Swift 6に備えて自身のAPIに Reflectable 要求を付けていくことが期待されます。
Future Directions(speculative)
今回扱うReflection MetadataはField Descriptor Metadataのみですが、将来的にはメソッドやcomputed propertyなど他の種類のメタデータが追加される可能性があり、その場合も Reflectable でまとめて表現できる想定です。また、この仕組みが整えば、Codable のコード自動生成をリフレクションベースの実装に置き換えることも現実的になります。いずれもspeculativeな見通しで、このProposal自体で約束されるものではありません。