Swift Digest
Blog | Swift.org Blog

Mirror の仕組み

How Mirror Works

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

Mirror とは何か

Mirror(reflecting:) イニシャライザは任意の値を受け取り、その値についての情報、とりわけ含まれる子要素を提供する Mirror インスタンスを返します。子要素は値と省略可能なラベルからなり、子要素の値にさらに Mirror を使うことで、コンパイル時に型をいっさい知らないまま、オブジェクトグラフ全体をたどれます。

型の種類ごとに、何が子要素になるかは次のとおりです。

型が CustomReflectable プロトコルに適合していれば、introspection(内部の自動的な走査)に頼らず、独自の表現を提供できます。Array は要素をラベルなしの子要素として、Dictionary はキー/値ペアをラベル付きの子要素として公開しています。それ以外の型については、Mirror が値の実際の内容に基づいて子要素のリストを組み立てます。

Swift と C++ の連携

ここから先で扱うシンボル名・C++ の関数・ランタイムの内部表現は、Mirror内部実装 の詳細です。通常の Swift コードから直接依存すべき公開 API ではなく、将来変わる可能性があります。利用する側は、あくまで MirrorCustomReflectable の公開 API を通して使ってください。

リフレクションの API は一部が Swift、一部が C++ で実装されています。Swift らしいインターフェースを書くには Swift が向いている一方、ランタイムの低レベルな部分は C++ で実装されており、その C++ クラスに Swift から直接アクセスはできません。そこで両者を C の層で橋渡ししています。

両者は、Swift に公開された少数の C++ 関数を介してやり取りします。Swift 側では、通常の名前マングリングの代わりにシンボル名を指定するディレクティブで関数を宣言し、C++ 側でそのシンボル名を持つ関数を用意します。たとえば子要素の個数を取得する関数は次のように宣言されています。

@_silgen_name("swift_reflectionMirror_count")
internal func _getChildCount<T>(_: T, type: Any.Type) -> Int

@_silgen_name 属性は、この関数を _getChildCount の通常のマングリングではなく swift_reflectionMirror_count というシンボル名に対応づけるようコンパイラに指示します(先頭のアンダースコアは標準ライブラリ専用であることを示します)。C++ 側ではこのシンボル名で、Swift の呼び出し規約に合わせて引数を慎重に並べた関数を用意します。Swift と C++ の Mirror の境界は、子要素の個数・各子要素・正規化された型・表示スタイルなどを取得するこうした関数群で構成されます。

型ごとに処理を振り分ける

タプル・構造体・クラス・enum のどれを調べるかによって、子要素の個数の求め方などは異なる処理が必要です。さらに Swift のクラスと Objective-C のクラスでも扱いが変わります。

そこで C++ 側は、上記インターフェースを抽象基底クラス ReflectionMirrorImpl として定義し、型の種類ごとにサブクラス(TupleImpl / StructImpl / ClassImpl / EnumImpl など)を用意しています。call という関数が Swift の型をこれらのサブクラスのインスタンスに対応づけ、そのインスタンスのメソッドを呼ぶことで、適切な実装へ動的ディスパッチします。ReflectionMirrorImpl は次のような形をしています。

struct ReflectionMirrorImpl {
  const Metadata *type;
  OpaqueValue *value;

  virtual char displayStyle() = 0;
  virtual intptr_t count() = 0;
  virtual AnyReturn subscript(intptr_t index, const char **outName,
                              void (**outFreeFunc)(const char *)) = 0;
  virtual const char *enumCaseName() { return nullptr; }
  // ...
};

displayStyle() は型の種類('c' クラス、'e' enum、's' 構造体、't' タプル)を表す 1 文字を返します。count() は子要素の個数、subscript() は指定したインデックスの子要素の値とラベルを返します。Swift・C++ 間の橋渡し関数は、call を介してこれら仮想メソッドを呼び出します。

タプル

タプルの場合、子要素の個数はメタデータ(TupleTypeMetadata)の NumElements フィールドから直接得られます。subscript はもう少し手間がかかり、2 つの仕事をします。

1 つは ラベルの取得 です。タプルのラベルはメタデータの Labels フィールドにスペース区切りで格納されており、目的のインデックスのラベルを探し出します。ラベルがなければ .0 のような番号を生成します。Swift と C++ の境界では ARC のような自動メモリ管理が効かないため、生成した名前を解放するための関数(outFreeFunc)を呼び出し側に渡します。

もう 1 つは 値の取得 です。タプルのメタデータは各要素のオフセットと型を持っているため、タプル値の先頭からオフセットを足して要素へのポインタを求め、その型が持つコピー用の関数を使って値を Any にコピーして返します。

構造体とクラス

構造体は仕組みはタプルと似ていますが、リフレクションを一切サポートしない型がある、フィールドの名前・オフセットの取得に手間がかかる、weak 参照を含み得る、といった違いがあります。

まず、構造体のメタデータにはリフレクション可能かどうかのフラグがあり、不可能なら子要素の個数を 0 とします。フィールドの型情報の取得は _swift_getFieldAt というヘルパー関数に委ねます。これは型と「何番目のフィールドか」を受け取り、フィールド記述子(field descriptor)を探し出して、フィールド名と型をコールバックに渡します。weak 参照を持つフィールドは特別な読み出し処理を通し、それ以外は通常どおり値を Any にコピーします。

クラスは構造体とほぼ同じですが、Objective-C 連携のための差異が 2 点あります。1 つは、Xcode のクイックルックで使う quickLookObject(Objective-C の debugQuickLookObject を呼ぶ)の実装を持つこと。もう 1 つは、Objective-C のスーパークラスを持つ場合、フィールドのオフセットを Objective-C ランタイムから取得することです。

enum

enum はやや特殊です。前述のとおり子要素は最大 1 つで、ラベルは現在のケース名、値は associated value です。たとえば次の enum を考えます。

enum Foo {
  case bar
  case baz(Int)
  case quux(String, String)
}

Foo.bar には子要素がなく、Foo.baz には Int 値を持つ子要素が 1 つ、Foo.quux には (String, String) 値を持つ子要素が 1 つできます。同じ構造体やクラスの値は常に同じフィールドを持ちますが、同じ enum 型でもケースが違えば子要素は変わります。

enum を反映するには 4 つの情報が必要です。ケース名タグ(どのケースかを表す数値)、payload の型、そして payload が indirect かどうか です。タグはメタデータに問い合わせて取得し、残りはタグを「フィールドのインデックス」として _swift_getFieldAt から取得します。count() は payload の型が null かどうかで 01 を返します。

subscript() で値を取り出す際には注意が必要です。enum から payload を取り出す操作は 破壊的(destructive) で、コピーをきれいに取り出す手段がありません。そのため、いったん破壊的に payload を取り出してコピーを作り、すぐ元のタグを戻して値を元の状態に復元する、という手順を踏みます。indirect なケースでは、payload は box に入っているため、box から実体を取り出してから返します。

そのほかの種類

ObjCClassImpl(Objective-C クラス)・MetatypeImpl(メタタイプ)・OpaqueImpl(不透明型)は、いずれもほとんど何もせず子要素を返しません。とくに Objective-C クラスは、ivar の内容について自由度が高く、ダングリングポインタを保持し続けることすら許されるため、その値を子要素として返すと Swift のメモリ安全性を損なう恐れがあります。そのため安全側に倒して、子要素を返さないようにしています。

Swift 側の組み立て

C++ 側がそろえた情報を、Swift 側の Mirror が使いやすい形に組み立てます。subjectType(反映に使う型)を決めたうえで、子要素の個数を取得し、各子要素を 遅延的に 取り出すコレクションを作ります。

let childCount = _getChildCount(subject, type: subjectType)
let children = (0 ..< childCount).lazy.map({
  getChild(of: subject, type: subjectType, index: $0)
})
self.children = Children(children)

subjectType は通常は値の実行時の型ですが、superclassMirror でクラス階層を上にたどる場合はスーパークラスになります。Mirror はこの superclassMirror プロパティを通じて、クラス階層を 1 つ上のクラスのプロパティへと順にたどれるようになっています。最後に C++ 側から得た表示スタイルを displayStyle.class / .enum / .struct / .tuple)に変換して、Mirror のプロパティを埋めます。

まとめ

Swift の豊富な型メタデータは、プロトコル適合の検索やジェネリック型の解決などを支えつつ、ふだんは裏方として存在します。その一部を Mirror 型として利用者に公開することで、任意の値の実行時検査が可能になっています。静的型付けを重んじる Swift で Mirror のような仕組みがあるのは一見不思議に思えますが、実際にはすでに用意されている型メタデータを素直に応用したものにすぎません。Mirror を使うときに裏側で何が起きているのかを知っておくと、その挙動を理解する助けになります。

関連リンク