Swift 5.0 日本語化計画 : Swift 5.0
ミラーの仕組み
2018年9月26日 Mike Ash
Swift は静的型付けに非常に重点を置いていますが、型に関する豊富なメタデータもサポートしているため、実行時に任意の値を検査および操作できます。これは Mirror API を通じて Swift プログラマーに公開されています。Mirror のようなものは静的な型を重視した言語でどうやって動くのでしょうか?見てみましょう!
免責事項
ここにはすべて内部実装の詳細を示します。コードはこの執筆時点では最新ですが、変更される可能性があります。メタデータは、ABI の安定性が高まると、フィックスされた信頼性の高いフォーマットになりますが、現時点ではまだ変更される可能性があります。通常の Swift コードを書く場合は、これに頼らないでください。Mirror が提供するものよりも洗練された反映をしたいコードを作成しているなら、これは出発点になりますが、ABI が安定するまで変更を加えて、最新の物にしておく必要があります。Mirror コード自体で作業したい場合、これはどのように一緒に収まるかをわかるはずですが、コードが変わる可能性があることに留意してください。
インターフェース
Mirror(reflecting:) イニシャライザは任意の値を受け入れます。結果である Mirror インスタンスは、その値に関する情報を提供します。子供は、値と optional のラベルで構成されます。コンパイル時に型が何か知らなくても、子供の値に対して Mirror を使用して、オブジェクトグラフ全体を横断することができます。
Mirror は、型が CustomReflectable プロトコルに準拠してカスタムの表現を提供できるようにします。これは、内省から得られるものよりも優れたものを提示したい型に役立ちます。たとえば、Array は CustomReflectable に準拠し、配列の要素をラベルなしの子供として公開します。Dictionary はそれを使用して、キー/値ペアをラベル付きの子供として公開します。
他のすべての型の場合、Mirror は実際の値の内容に基づいて子供のリストを考えつくためにいくつかの魔法を使います。構造体とクラスとでは、格納されたプロパティを子供として表します。タプルについては、タプル要素を表します。列挙型は、列挙型 case と関連する値を、存在する場合表します。
その魔法はどうやって動くの?確認してみましょう!
構造体
reflection (反映) API は、部分的に Swift で、部分的に C++ で実装されています。Swift は Swifty インターフェイスの実装に適しており、多くの作業が簡単になります。Swift 実行時環境の下位レベルは C++ で実装されており、Swift からこれらの C++ クラスに直接アクセスすることはできないため、C のレイヤーが 2 つを接続します。Swift 側は ReflectionMirror.swift で実装されており、C++ 側は ReflectionMirror.mm で実装されています。
2 つの部分は Swift に公開されている小さな C++ 関数セットを介して通信します。Swift の C ブリッジングにビルドされた物を使用するのではなく、カスタムシンボル名を指定する指示で Swift で宣言されており、その名前の C++ 関数が Swift から直接呼び出せるように慎重に作られています。これにより、2 つの部分が、ブリッジング機構が舞台裏で値に対して何をするかを気にせずに直接通信できるようになりますが、Swift がパラメータと戻り値をどのように渡すかについての正確な知識が必要です。あなたがそれを必要とする実行時コードで作業している場合を除き、自宅でこれを試してはいけません。
これの例として、ReflectionMirror.swift の _getChildCount 関数を見てみましょう。
@_silgen_name("swift_reflectionMirror_count")
internal func _getChildCount<T>(_: T, type: Any.Type) -> Int
@_silgen_name 属性は、_getChildCount に適用される通常の Swift マングリング (ずたずたに切る) ではなく、swift_reflectionMirror_count という名前のシンボルにこの関数をマッピングするように Swift コンパイラに通知します。最初のアンダースコアは、この属性が標準ライブラリ用に予約されていることを示しています。C++ 側では、関数は次のようになります。
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERFACE intptr_t swift_reflectionMirror_count(OpaqueValue *value, const Metadata *type, const Metadata *T) {
SWIFT_CC(swift) は、この関数が C/C++ 規約ではなく Swift 呼び出し規約を使用していることをコンパイラーに通知します。SWIFT_RUNTIME_STDLIB_INTERFACE はこれを Swift 側のインタフェースの一部である関数としてマークし、C++ の名前のマングリングを避け、Swift 側が期待するシンボル名を持つように extern "C" としてマークするという効果があります。Swift が Swift 宣言に基づいてこの関数を呼び出す方法に合わせて、C++ パラメータが注意深く配置されています。Swift コードが _getChildCount を呼び出すと、Swift 値へのポインタ、型パラメータの値を含む type、および汎用の <T> に対応する型を含む T を含むvalue で C++ 関数が呼び出されます。
Mirror の Swift と C++ の間の完全なインタフェースは、次の関数で構成されています。
@_silgen_name("swift_reflectionMirror_normalizedType")
internal func _getNormalizedType<T>(_: T, type: Any.Type) -> Any.Type
@_silgen_name("swift_reflectionMirror_count")
internal func _getChildCount<T>(_: T, type: Any.Type) -> Int
internal typealias NameFreeFunc = @convention(c) (UnsafePointer<CChar>?) -> Void
@_silgen_name("swift_reflectionMirror_subscript")
internal func _getChild<T>(
of: T,
type: Any.Type,
index: Int,
outName: UnsafeMutablePointer<UnsafePointer<CChar>?>,
outFreeFunc: UnsafeMutablePointer<NameFreeFunc?>
) -> Any
// Returns 'c' (class), 'e' (enum), 's' (struct), 't' (tuple), or '\0' (none)
@_silgen_name("swift_reflectionMirror_displayStyle")
internal func _getDisplayStyle<T>(_: T) -> CChar
@_silgen_name("swift_reflectionMirror_quickLookObject")
internal func _getQuickLookObject<T>(_: T) -> AnyObject?
@_silgen_name("_swift_stdlib_NSObject_isKindOfClass")
internal func _isImpl(_ object: AnyObject, kindOf: AnyObject) -> Bool
動的送出を魔法のように完了
任意の型から必要な情報を取得する普遍的な唯一の方法はありません。タプル、構造体、クラス、列挙型はすべて、子供の数を調べるなど、これらのタスクの多くで異なるコードを必要とします。Swift クラスと Objective-C クラスの異なる取り扱いなど、さらに細かいことがあります。
これらの関数はすべて、どのような種類の型が調べられているかに基づいて異なる実装に送出するコードが必要です。これはメソッドの動的ディスパッチ (送出) にかなり似ていますが、呼び出すべき実装の選択は、メソッドが使用されているオブジェクトのクラスをチェックするよりも複雑です。反映コードは、上記のインターフェイスの C++ バージョンを含む抽象基本クラスと、さまざまなケースをすべてカバーするサブクラスを含む C++ の動的送出を使用して、問題を単純化しようとします。単一の関数は、Swift 型をこれらの C++ クラスの 1 つのインスタンスにマッピングします。そのインスタンスでメソッドを呼び出すと、適切な実装に送出されます。
マッピング関数は call と呼ばれ、その宣言は次のようになります。
template<typename F>
auto call(OpaqueValue *passedValue, const Metadata *T, const Metadata *passedType,
const F &f) -> decltype(f(nullptr))
passedValue は、渡された実際の Swift 値へのポインタです。T はその値の静的型で、Swift 側の汎用パラメータ <T> に対応します。passedType は Swift 側で明示的に渡され、実際の反映ステップで使用される型です。(この型は、サブクラスのインスタンスのスーパークラス Mirror で作業する場合、オブジェクトの実際の実行時の型とは異なります) 最後に、f パラメータは、呼び出されるもので、この関数が検索する実装オブジェクトへの参照を渡します。この関数は、呼び出されたときに返される f を何であれ返し、これにより、ユーザーが値を簡単に取り戻すことができます。
call の実装はそれほどエキサイティングではありません。大部分は特殊なケースを処理するためのコードが追加された switch 文です。重要なことは、f を ReflectionMirrorImpl のサブクラスのインスタンスで呼び出すことになり、実際の作業を完了させるためにそのインスタンスのメソッドを呼び出すことになります。
これは 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; }
#if SWIFT_OBJC_INTEROP
virtual id quickLookObject() { return nil; }
#endif
virtual ~ReflectionMirrorImpl() {}
};
Swift コンポーネントと C++ コンポーネントの間のインターフェイスとして機能する関数は、call を使用して対応するメソッドを呼び出します。たとえば、swift_reflectionMirror_count の例を次に示します。
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERFACE
intptr_t swift_reflectionMirror_count(OpaqueValue *value,
const Metadata *type,
const Metadata *T) {
return call(value, T, type, [](ReflectionMirrorImpl *impl) {
return impl->count();
});
}
タプルの反映
タプルの反映から始めましょう。これは、おそらくまだ最も単純なものですが、まだ作業をしています。それがタプルであることを示す 't' の表示スタイルを返すことからそれは始まります:
struct TupleImpl : ReflectionMirrorImpl {
char displayStyle() {
return 't';
}
このようにハードコーディングされた定数を使用することは珍しいことですが、C++ には正確に 1 つの場所があり、Swift にはこの値を参照する場所が 1 つあり、通信にはブリッジを使用していないということを考えると、合理的な選択です。
次は count メソッドです。現時点では、type は実際には Metadata * ではなく TupleTypeMetadata * であることがわかります。TupleTypeMetadata には、タプル内の要素数を保持する NumElements フィールドがあります。これで完了です。
intptr_t count() {
auto *Tuple = static_cast<const TupleTypeMetadata *>(type);
return Tuple->NumElements;
}
subscript メソッドはもう少し作業が必要です。それは同じ static_cast で始まります:
AnyReturn subscript(intptr_t i, const char **outName, void (**outFreeFunc)(const char *)) { auto *Tuple = static_cast<const TupleTypeMetadata *>(type);
次に、呼び出し側がこのタプルに含めることができないインデックスを要求していないことを確認するために境界チェックを行います。
if (i < 0 || (size_t)i > Tuple->NumElements)
swift::crash("Swift mirror subscript bounds check failure");
サブスクリプトには 2 つの働きがあります:値と対応する名前を取得します。構造体またはクラスの場合、名前は格納されたプロパティの名前です。タプルの場合、名前はその要素のタプル・ラベルか、ラベルがない場合は .0 のような数値インジケータのいずれかです。
ラベルは、メタデータの labels フィールドに空白で区切られたリストに格納されます。このコードは、リスト内の i 番目の文字列を追跡します。
// Determine whether there is a label.
bool hasLabel = false;
if (const char *labels = Tuple->Labels) {
const char *space = strchr(labels, ' ');
for (intptr_t j = 0; j != i && space; ++j) {
labels = space + 1;
space = strchr(labels, ' ');
}
// If we have a label, create it.
if (labels && space && labels != space) {
*outName = strndup(labels, space - labels);
hasLabel = true;
}
}
ラベルがない場合は、適切な数値名を生成します。
if (!hasLabel) {
// The name is the stringized element number '.0'.
char *str;
asprintf(&str, ".%" PRIdPTR, i);
*outName = str;
}
私たちは Swift と C++ の交差点で作業しているので、自動メモリ管理のような素晴らしいものは得られません。Swift には ARC があり、C++ には RAII がありますが、2 つは一緒にはなりません。outFreeFunc は、C++ コードが、返された名前を解放するために使用する関数を呼び出し元に提供することを可能にします。ラベルは free で自由にする必要があるので、このコードは *outFreeFunc の値をそれに応じて設定します:
*outFreeFunc = [](const char *str) { free(const_cast<char *>(str)); };
それは名前の世話をします。意外にも、この値は検索するのが簡単です。Tuple メタデータには、与えられたインデックスの要素に関する情報を返す関数が含まれています。
auto &elt = Tuple->getElement(i);
elt には、要素値へのポインタを取得するためにタプル値に適用できるオフセットが含まれています。
auto *bytes = reinterpret_cast<const char *>(value);
auto *eltData = reinterpret_cast<const OpaqueValue *>(bytes + elt.Offset);
elt には要素の型も含まれています。型と値へのポインタを使用すると、その値を含む新しい Any を構築することができます。型には、与えられた型の値を含む、記憶装置の割り当てと初期化のための関数ポインタが含まれています。このコードでは、これらの関数を使用して値を Any にコピーし、Any を呼び出し元に返します。
Any result;
result.Type = elt.Type;
auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
result.Type->vw_initializeWithCopy(opaqueValueAddr,
const_cast<OpaqueValue *>(eltData));
return AnyReturn(result);
}
};
全てはタプルのために。
swift_getFieldAt
構造体、クラス、および列挙型の要素を調べることは、現在かなり複雑です。この複雑さの大部分は、これらの型と、型のフィールドに関する情報を含むフィールド記述子の間の直接参照の欠如によるものです。 swift_getFieldAt というヘルパー関数は、与えられた型の適切なフィールド記述子を検索します。この全体関数は、その直接参照を一度追加すると消えてしまいますが、その間に、実行時コードが言語のメタデータを使用して型情報を検索する方法を面白く見ていきます。
関数のプロトタイプは次のようになります。
void swift::_swift_getFieldAt( const Metadata *base, unsigned index, std::function<void(llvm::StringRef name, FieldType fieldInfo)> callback) {
調査する型と参照するフィールドインデックスが必要です。また、コールバックを受け取り、探し出された情報と共に呼び出されます。
最初のタスクは、この型の型コンテキスト記述子を取得することです。これには、後で使用される、型に関する追加情報が含まれています。
auto *baseDesc = base->getTypeContextDescriptor();
if (!baseDesc)
return;
作業は 2 つの部分に分かれています。まず、型のフィールド記述子を探します。フィールド記述子には、その型のフィールドに関するすべての情報が含まれます。フィールド記述子が利用可能になるやいなや、この関数は記述子から必要な情報を探すことができます。
記述子からの情報の検索は、getFieldAt というヘルパーで包み込まれ、getFieldAt は適切なフィールド記述子を検索する際にさまざまな場所から他のコードで呼び出されます。検索から始めましょう。demangler (mangler は「ずたずたに切る」という意味) を取得することから始めます。これは、ずたずたに切られた型名を実際の型参照に変換するために使用されます。
auto dem = getDemanglerForRuntimeTypeResolution();
また、複数の検索を高速化するキャッシュもあります。
auto &cache = FieldCache.get();
キャッシュにすでにフィールド記述子がある場合は、getFieldAt を呼び出します。
if (auto Value = cache.FieldCache.find(base)) {
getFieldAt(*Value->getDescription());
return;
}
検索コードを簡単にするために、FieldDescriptor を取得し、それが検索されているかどうかを確認するヘルパーがあります。記述子が一致する場合は、記述子をキャッシュに入れ、getFieldAt を呼び出し、呼び出し元に成功した事を返します。マッチングは複雑ですが、基本的には、結局マングル化された名前を比較することだけです。
auto isRequestedDescriptor = [&](const FieldDescriptor &descriptor) {
assert(descriptor.hasMangledTypeName());
auto mangledName = descriptor.getMangledTypeName(0);
if (!_contextDescriptorMatchesMangling(baseDesc,
dem.demangleType(mangledName)))
return false;
cache.FieldCache.getOrInsert(base, &descriptor);
getFieldAt(descriptor);
return true;
};
フィールド記述子は、実行時に登録することも、ビルド時にバイナリに焼くこともできます。これらの 2 つのループは、既知のすべてのフィールド記述子を検索して一致するかどうかを調べます。
for (auto §ion : cache.DynamicSections.snapshot()) {
for (const auto *descriptor : section) {
if (isRequestedDescriptor(*descriptor))
return;
}
}
for (const auto §ion : cache.StaticSections.snapshot()) {
for (auto &descriptor : section) {
if (isRequestedDescriptor(descriptor))
return;
}
}
一致するものが見つからない場合は、警告を記録し、空のタプルでコールバックを呼び出して、何かを与えるだけです:
auto typeName = swift_getTypeName(base, /*qualified*/ true);
warning(0, "SWIFT RUNTIME BUG: unable to find field metadata for type '%*s'\n",
(int)typeName.length, typeName.data);
callback("unknown",
FieldType()
.withType(TypeInfo(&METADATA_SYM(EMPTY_TUPLE_MANGLING), {}))
.withIndirect(false)
.withWeak(false));
}
これは、フィールド記述子の検索を担当します。getFieldAt ヘルパーは、フィールド記述子をコールバックに渡される名前とフィールドの型に変換します。要求されたフィールドレコードをフィールド記述子から取得することから始めます。
auto getFieldAt = [&](const FieldDescriptor &descriptor) {
auto &field = descriptor.getFields()[index];
名前はレコードから直接アクセスできます:
auto name = field.getFieldName(0);
フィールドが実際に列挙型 case の場合、型がない可能性があります。それを早期に確認し、それに応じてコールバックを呼び出します。
if (!field.hasMangledTypeName()) {
callback(name, FieldType().withIndirect(field.isIndirectCase()));
return;
}
フィールドレコードには、フィールド型がマングル化された名前として格納されます。コールバックはメタデータへのポインタを期待しているので、マングル化された名前は実際の型に解決されなければなりません。関数 _getTypeByMangledName はその作業の大部分を処理しますが、呼び出し元は型によって使用される汎用引数を解決する必要があります。これを行うには、その型がネストされている汎用コンテキストをすべて取り出す必要があります:
std::vector<const ContextDescriptor *> descriptorPath;
{
const auto *parent = reinterpret_cast<
const ContextDescriptor *>(baseDesc);
while (parent) {
if (parent->isGeneric())
descriptorPath.push_back(parent);
parent = parent->Parent.get();
}
}
今度は、マングル化した名前を取得し、型をフェッチし、汎用引数を解決するラムダを渡します。
auto typeName = field.getMangledTypeName(0);
auto typeInfo = _getTypeByMangledName(
typeName,
[&](unsigned depth, unsigned index) -> const Metadata * {
要求された深度が記述子パスのサイズを超えている場合は、失敗します。
if (depth >= descriptorPath.size())
return nullptr;
それ以外の場合は、フィールドを含む型から汎用引数をフェッチします。これには、インデックスと深度を単一のフラットインデックスに変換する必要があり、これは、記述子パスを上がり、与えられた深度に達するまで各ステージで汎用パラメータの数を追加することによって行われます。
unsigned currentDepth = 0;
unsigned flatIndex = index;
const ContextDescriptor *currentContext = descriptorPath.back();
for (const auto *context : llvm::reverse(descriptorPath)) {
if (currentDepth >= depth)
break;
flatIndex += context->getNumGenericParams();
currentContext = context;
++currentDepth;
}
インデックスが与えられた深度で使用可能な汎用パラメータを超えている場合は、失敗します。
if (index >= currentContext->getNumGenericParams())
return nullptr;
それ以外の場合は、基本の型から適切な汎用引数をフェッチします。
return base->getGenericArgs()[flatIndex];
});
以前と同じように、型が見つからなかった場合は、空のタプルを使用します。
if (typeInfo == nullptr) {
typeInfo = TypeInfo(&METADATA_SYM(EMPTY_TUPLE_MANGLING), {});
warning(0, "SWIFT RUNTIME BUG: unable to demangle type of field '%*s'. "
"mangled type name is '%*s'\n",
(int)name.size(), name.data(),
(int)typeName.size(), typeName.data());
}
次に、見つかったものでコールバックを呼び出します。
callback(name, FieldType()
.withType(typeInfo)
.withIndirect(field.isIndirectCase())
.withWeak(typeInfo.isWeak()));
};
それは swift_getFieldAt です。そのヘルパーを利用できるようにして、他の反映の (reflection) 実装を見てみましょう。
構造体 (Structs)
構造体の実装は似ていますが、もう少し複雑です。反映をまったくサポートしない struct 型がありますし、構造体内の名前とオフセットを調べることがより骨が折れます。構造体には、反映コードが抽出できる必要がある弱い参照を含めることができます。
まず、構造体が完全に反映されるかどうかを調べるヘルパーメソッドです。これは、struct メタデータからアクセス可能なフラグに格納されます。タプルを持つ上記のコードと同様に、この時点で type は実際は StructMetadata * であることがわかっていますので、自由にキャストすることができます:
struct StructImpl : ReflectionMirrorImpl {
bool isReflectable() {
const auto *Struct = static_cast<const StructMetadata *>(type);
const auto &Description = Struct->getDescription();
return Description->getTypeContextDescriptorFlags().isReflectable();
}
構造体 (struct) の表示スタイルは 's' です。
char displayStyle() {
return 's';
}
子供の count は、メタデータによって報告されたフィールドの数です。この型が実際には反映されない場合は 0 です。
intptr_t count() {
if (!isReflectable()) {
return 0;
}
auto *Struct = static_cast<const StructMetadata *>(type);
return Struct->getDescription()->NumFields;
}
以前と同様に、subscript は複雑な部分です。同様に、境界チェックを行い、オフセットを検索して始まります。
AnyReturn subscript(intptr_t i, const char **outName,
void (**outFreeFunc)(const char *)) {
auto *Struct = static_cast<const StructMetadata *>(type);
if (i < 0 || (size_t)i > Struct->getDescription()->NumFields)
swift::crash("Swift mirror subscript bounds check failure");
// Load the offset from its respective vector.
auto fieldOffset = Struct->getFieldOffsets()[i];
struct フィールドの型情報を取得することはもう少し複雑です。その作業は、_swift_getFieldAt ヘルパー関数に渡されます。
Any result;
_swift_getFieldAt(type, i, [&](llvm::StringRef name, FieldType fieldInfo) {
ひとたびフィールド情報を取得すると、タプルコードと同様に処理が進められます。名前を記入し、フィールドの格納場所へのポインタを計算します:
*outName = name.data();
*outFreeFunc = nullptr;
auto *bytes = reinterpret_cast<char*>(value);
auto *fieldData = reinterpret_cast<OpaqueValue *>(bytes + fieldOffset);
弱い参照を処理するには、フィールドの値を Any の戻り値にコピーする追加の手順が必要です。 loadSpecialReferenceStorage 関数がそれらを処理します。値がロードされない場合、値は通常の格納場所を持ち、通常は戻り値に値をコピーできます。
bool didLoad = loadSpecialReferenceStorage(fieldData, fieldInfo, &result);
if (!didLoad) {
result.Type = fieldInfo.getType();
auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
result.Type->vw_initializeWithCopy(opaqueValueAddr,
const_cast<OpaqueValue *>(fieldData));
}
});
return AnyReturn(result);
}
};
それが構造体の世話をします。
クラス
クラスは構造体に似ており、ClassImpl のコードはほとんど同じです。Objective-C 相互運用には 2 つの顕著な違いがあります。1 つは、Objective-C の debugQuickLookObject メソッドを呼び出す quickLookObject の実装があることです。
#if SWIFT_OBJC_INTEROP id quickLookObject() { id object = [*reinterpret_cast<const id *>(value) retain]; if ([object respondsToSelector:@selector(debugQuickLookObject)]) { id quickLookObject = [object debugQuickLookObject]; [quickLookObject retain]; [object release]; return quickLookObject; } return object; } #endif
もう 1 つは、クラスに Objective-C スーパークラスがある場合、Objective-C 実行時環境からフィールドオフセットを取得しなければならないことです。
uintptr_t fieldOffset;
if (usesNativeSwiftReferenceCounting(Clas)) {
fieldOffset = Clas->getFieldOffsets()[i];
} else {
#if SWIFT_OBJC_INTEROP
Ivar *ivars = class_copyIvarList((Class)Clas, nullptr);
fieldOffset = ivar_getOffset(ivars[i]);
free(ivars);
#else
swift::crash("Object appears to be Objective-C, but no runtime.");
#endif
}
列挙型
列挙型は少し異なります。Mirror は、enum インスタンスが最大で 1 つの子供しか持っていないとみなします。この子供は、そのラベルとして enum case 名を持ち、その値として関連した値を持ちます。関連した値のない case には子供がありません。例えば:
enum Foo {
case bar
case baz(Int)
case quux(String, String)
}
Foo の値に Mirror を使用すると、Foo.bar の子供は表示されず、Foo.baz の Int 値を持つ子供が 1 つ、Foo.quux の (String、String) 値の子供が 1 つだけ表示されます。クラスまたは構造体の値は常に同じフィールドを持ち、したがって同じ子供のラベルと型を持ちますが、同じ型の異なる列挙型の case はありません。 関連した値は indirect でもよいですが、特別な処理が必要です。
enum の値を反映するために必要な 4 つの重要な情報があります:case の名、タグ (値が格納される列挙型の case の数値表現)、ペイロード型、およびペイロードが間接的かどうか。 getInfo メソッドはこれらの値をすべてフェッチします。
const char *getInfo(unsigned *tagPtr = nullptr,
const Metadata **payloadTypePtr = nullptr,
bool *indirectPtr = nullptr) {
タグは、メタデータを直接照会することによって取得されます。
unsigned tag = type->vw_getEnumTag(value);
その他の情報は、_swift_getFieldAt を使用して取得されます。タグを"フィールドインデックス" とし、適切な情報を提供します。
const Metadata *payloadType = nullptr;
bool indirect = false;
const char *caseName = nullptr;
_swift_getFieldAt(type, tag, [&](llvm::StringRef name, FieldType info) {
caseName = name.data();
payloadType = info.getType();
indirect = info.isIndirect();
});
これらの値はすべて呼び出し元に返されます。
if (tagPtr)
*tagPtr = tag;
if (payloadTypePtr)
*payloadTypePtr = payloadType;
if (indirectPtr)
*indirectPtr = indirect;
return caseName;
}
(あなたは疑問に思うかもしれません:なぜ、case 名は、他の 3 つがポインタで返されているのに対し、直接返されるのですか?タグやペイロード型を返さないのですか?答えは:私には本当にわかりません。これはその時の良い考えだと思います。)
count メソッドは getInfo を使用してペイロード型を取得し、ペイロード型が null またはそれ以外に応じて 0 または 1 を返します。
intptr_t count() {
if (!isReflectable()) {
return 0;
}
const Metadata *payloadType;
getInfo(nullptr, &payloadType, nullptr);
return (payloadType != nullptr) ? 1 : 0;
}
subscript メソッドは、値に関するすべての情報を取得することから始まります:
AnyReturn subscript(intptr_t i, const char **outName,
void (**outFreeFunc)(const char *)) {
unsigned tag;
const Metadata *payloadType;
bool indirect;
auto *caseName = getInfo(&tag, &payloadType, &indirect);
実際に値をコピーするにはもう少し作業が必要です。間接的な値を処理するために、プロセス全体が余分なボックスを通過します:
const Metadata *boxType = (indirect ? &METADATA_SYM(Bo).base : payloadType); BoxPair pair = swift_allocBox(boxType);
列挙型の抽出の仕方のため、値を完全にコピーする方法はありません。使用可能な唯一の操作は、ペイロード値を 破壊的に 抽出することです。コピーを作成し、元のままにしておくには、破壊的に抽出してから元に戻します。
type->vw_destructiveProjectEnumData(const_cast<OpaqueValue *>(value)); boxType->vw_initializeWithCopy(pair.buffer, const_cast<OpaqueValue *>(value)); type->vw_destructiveInjectEnumTag(const_cast<OpaqueValue *>(value), tag); value = pair.buffer;
間接的な case では、実際のデータをボックスから引き出さなければなりません。
if (indirect) {
const HeapObject *owner = *reinterpret_cast<HeapObject * const *>(value);
value = swift_projectBox(const_cast<HeapObject *>(owner));
}
すべては今やそろいました。子供のラベルは case 名に設定されています:
*outName = caseName; *outFreeFunc = nullptr;
今ではよく知られているパターンは、ペイロードを Any として返すために使用されます:
Any result;
result.Type = payloadType;
auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
result.Type->vw_initializeWithCopy(opaqueValueAddr,
const_cast<OpaqueValue *>(value));
swift_release(pair.object);
return AnyReturn(result);
}
様々な種類
このファイルにはさらに 3 つの実装がありますが、これらはほとんど何もしません。ObjCClassImpl は Objective-C クラスを処理します。Objective-C は、ivars の内容にあまりにも多くの余裕を許すので、これらのために子供を返そうとはしません。Objective-C クラスは、値に触れないように実装に指示するいくつかの別個のロジックで、ぶら下がったポインタを永遠に置いておくようなことを許されます。Mirror の子供のような値を返そうとすると、Swift のメモリ安全性が保証されません。問題の値がこのようなことをしているかどうかを確実に判断する方法はないので、このコードは完全に回避します。
MetatypeImpl はメタタイプを処理します。Mirror(reflecting:String.self) などの実際の型で Mirror を使用する場合は、これが使用されます。おそらくここに提供するのに有益な情報があるかもしれませんが、現時点では試しておらず、何も返しません。同様に、OpaqueImpl は不透明型を処理しますが、何も返しません。
Swift のインターフェース
Swift 側では、Mirror は、C++ で実装されたインタフェース関数を呼び出して必要な情報を取得し、より親しみやすい形で提示します。これは Mirror のイニシャライザで行われます。
internal init(internalReflecting subject: Any,
subjectType: Any.Type? = nil,
customAncestor: Mirror? = nil)
{
subjectType は、subject 値を反映するために使用される型です。これは通常、値の実行時の型ですが、呼び出し元が superclassMirror を使用してクラス階層を上に行く場合、スーパークラスになります。呼び出し元が subjectType を渡さなかった場合、このコードは C++ コードに subject の型を取得するように要求します。
let subjectType = subjectType ?? _getNormalizedType(subject, type: type(of: subject))
次に、子供の数を取得し、個々の子供を遅延してフェッチするコレクションを作成することによって、children を構築します。
let childCount = _getChildCount(subject, type: subjectType)
let children = (0 ..< childCount).lazy.map({
getChild(of: subject, type: subjectType, index: $0)
})
self.children = Children(children)
getChild 関数は、ラベル名を含む C 文字列を Swift の String に変換する C++ の _getChild 関数の周りの小さな包み物です。
Mirror には、クラス階層を上がったクラスのプロパティを検査する Mirror を返す superclassMirror プロパティがあります。内部的には、必要に応じてスーパークラスの Mirror を構築できるクロージャを格納する _makeSuperclassMirror プロパティがあります。このクロージャは、subjectType のスーパークラスを取得することから始まります。スーパークラスを持たないクラス以外の型やクラスは、スーパークラスミラーを持つことができないため、nil を取得します。
self._makeSuperclassMirror = {
guard let subjectClass = subjectType as? AnyClass,
let superclass = _getSuperclass(subjectClass) else {
return nil
}
呼び出し元は、スーパークラスミラーとして直接返すことができる Mirror インスタンスであるカスタムの祖先表現を指定できます。
if let customAncestor = customAncestor {
if superclass == customAncestor.subjectType {
return customAncestor
}
if customAncestor._defaultDescendantRepresentation == .suppressed {
return customAncestor
}
}
それ以外の場合は、superclass を subjectType として使用する事以外は同じ値に対して新しい Mirror を返します。
return Mirror(internalReflecting: subject,
subjectType: superclass,
customAncestor: customAncestor)
}
最後に、表示スタイルをフェッチしてデコードし、Mirror の残りのプロパティを設定します。
let rawDisplayStyle = _getDisplayStyle(subject)
switch UnicodeScalar(Int(rawDisplayStyle)) {
case "c": self.displayStyle = .class
case "e": self.displayStyle = .enum
case "s": self.displayStyle = .struct
case "t": self.displayStyle = .tuple
case "\0": self.displayStyle = nil
default: preconditionFailure("Unknown raw display style '\(rawDisplayStyle)'")
}
self.subjectType = subjectType
self._defaultDescendantRepresentation = .generated
}
結論
Swift の豊富なタイプのメタデータは、ほとんど舞台裏で存在しており、プロトコル準拠のルックアップや汎用の型解決などをサポートしています。その中には Mirror 型でユーザーに公開されているものがあり、任意の値の実行時検査が可能です。Swift の静的な型付きの性質を考えると、それは最初のうちは不思議で神秘的なように見えるかもしれませんが、すでに利用可能な情報の簡単な適用 (application) です。この実装のツアーは、その謎を解消し、Mirror を使用しているときの状況を把握するのに役立ちます。
<-Swift 5.0 のリリースプロセス Swift パッケージ用の REPL サポート->
