Swiftには実行時に型情報を保持するためのType metadata
という仕組みがあります。我々が頻繁に使うことはありませんが、Swiftのランタイムの動作を理解するための重要な要素です。
この記事では Type metadata
についてコンパイラのコードとドキュメントから調べたことを簡単に解説します。
Type metadata
とは
Swiftが実行時に保持している型情報です。ジェネリックでないclass
、struct
、enum
はコンパイル時に静的に作られますが、ジェネリックな型と関数型、タプルなどのnon-nominalな型については実行時に動的に作られます。
このType metadata
には
- その型の種類(
enum
なのかstruct
なのか、それともclass
なのか) -
ValueWitnessTable
(型を操作するための関数群) - 型パラメータ
- タプルのラベル
などの情報が含まれています。型の種類によってレイアウトが大幅に違うので逐一ドキュメントを読んでいくのがオススメです。
Type metadata
をSwiftから扱う
では、Type metadata
を使ってSwiftからクラスのインスタンスサイズを取得してみましょう。
Type metadata
から目的の情報を取得するには、その情報がメタデータのメモリレイアウト上のどの位置に存在するかを知る必要があります。swift/TypeMetadata.rst を参考にメモリレイアウトを再現していくと以下のようになります。
struct ClassMetadata {
let isaPointer: Int
let superClass: Any.Type
let objcRuntimeReserved1: Int
let objcRuntimeReserved2: Int
let rodataPointer: Int
let classFlags: Int32
let instanceAddressPoint: Int32
let instanceSize: Int32
}
さて、メモリ上の表現が分かったので実際にType metadata
を取得してみましょう。
SwiftにおけるMetatype
はその型のインスタンスのType metadata
へのポインタになっています。とりあえずunsafeBitCast
でポインタ型にキャストして試してみます。
class Cat {}
let catType: Cat.Type = Cat.self
MemoryLayout.size(ofValue: Cat.self) // 8
let metadataPointer = unsafeBitCast(catType, to: UnsafePointer<ClassMetadata>.self)
let metadata = metadataPointer.pointee
print(metadata.instanceSize) // 16
目的のインスタンスサイズが取得できました
しかし、この方法でstruct
のメタデータを取得しようとしてもうまくいきません。
struct Stone {}
let stoneType: Stone.Type = Stone.self
MemoryLayout.size(ofValue: Stone.self) // 0
let metadataPointer = unsafeBitCast(stoneType, to: UnsafePointer<StructMetadata>.self)
let metadata = metadataPointer.pointee // Crash! :bomb:
どうやらType metadata
を正しく使うにはもう少し知るべきことがあるようです。
Thin Metatype
struct
のメタタイプのサイズが0になってしまう原因を探るために、メタタイプを生成しているコンパイラのコードを読んでみましょう。
/// Emit a metatype value for a known type.
void irgen::emitMetatypeRef(IRGenFunction &IGF, CanMetatypeType type,
Explosion &explosion) {
switch (type->getRepresentation()) {
case MetatypeRepresentation::Thin:
// Thin types have a trivial representation.
break;
case MetatypeRepresentation::Thick:
explosion.add(IGF.emitTypeMetadataRef(type.getInstanceType()));
break;
case MetatypeRepresentation::ObjC:
explosion.add(emitClassHeapMetadataRef(IGF, type.getInstanceType(),
MetadataValueType::ObjCClass,
MetadataState::Complete));
break;
}
}
型の表現方法がMetatypeRepresentation::Thin
になっているとランタイム情報が出力されていません。では、MetatypeRepresentation
とは何でしょう。
enum class MetatypeRepresentation : char {
/// A thin metatype requires no runtime information, because the
/// type itself provides no dynamic behavior.
///
/// Struct and enum metatypes are thin, because dispatch to static
/// struct and enum members is completely static.
Thin,
/// A thick metatype refers to a complete metatype representation
/// that allows introspection and dynamic dispatch.
///
/// Thick metatypes are used for class and existential metatypes,
/// which permit dynamic behavior.
Thick,
/// An Objective-C metatype refers to an Objective-C class object.
ObjC
};
SwiftのメタタイプはThick、Thin、ObjCの3種類に分類されています。
- Thin
-
struct
やenum
などの静的に挙動が決まる型
-
- Thick
-
class
などの動的な挙動をする型
-
- ObjC
- Objective-C由来の型
Thinな型は静的に振る舞いが決まるためランタイム情報が必要にならず、メタタイプのサイズが0になります。これがstruct
のメタタイプの取得が失敗した理由です。
protocol Animal {
static func kind() -> String
}
struct Cat {
static func kind() -> String {
return "猫"
}
}
let catType: Cat.Type = Cat.self // Thin
let animalType: Animal.Type = Cat.self // Thick
animalType.kind() // 猫
一方で上記の例のようにメタタイプのサブタイピングによってThin型をThick型に代入することができます。
またanimalType.kind()
が正しく動作することから、Existentialのメタタイプには実際のメタタイプが保持されていることも分かります。つまり、Existential Metatypeを経由すればThin型のType metadata
も取得できそうです。
Existential Metatype container
Existential Metatype containerはExistential Metatypeの実行時表現で、Existential containerのメタタイプ版のようなものです。内部に実際の型のインスタンスのType metadata
とwitness tableへのポインタ保持しています。
swift/GenExistential.cpp
struct ExistentialMetatypeContainer<Metadata> {
let metadata: UnsafePointer<Metadata>
let witnessTable: UnsafePointer<WitnessTable>
}
これを使って先程失敗したstruct
のメタデータを取得してみましょう。
struct StructMetadata {
let kind: Int
}
protocol Animal {}
struct Cat: Animal {}
let animalType: Animal.Type = Cat.self
let metatypeContainer = unsafeBitCast(animalType, to: ExistentialMetatypeContainer<StructMetadata>.self)
metatypeContainer.metadataPointer.pointee.kind // 1
struct metadataのkindは1であるため、無事目的のType metadata
が取得できたことが分かります。
しかし、この方法には一つ問題点があります。メタデータを取得したいThin型を何かしらのプロトコルに準拠させなければExistential Metatype containerに詰めることができないのです。
Any.Type
しかし、我々は全ての型のsupertypeとして振る舞うAny
を持っています。加えてAnyはnon-nominalであるためThickな型として振る舞い、ランタイム情報を保持しています。
struct Stone {}
let stoneType: Any.Type = Stone.self
let metadataPointer = unsafeBitCast(stoneType, to: UnsafePointer<StructMetadata>.self)
let metadata = metadataPointer.pointee
metadata.kind // 1
こうして安全(?)なType metadata
の取得方法が確立しました
まとめ
Type metadata
は実際にServer Side SwiftのフレームワークZewoなどで使われており、Swiftの言語機能を拡張できる夢の情報源です。一方で、ドキュメント化されていない部分が多く、変更が加えられる可能性もあります。使う際は入念にテストを書くなどの対策が必要です。
楽しく適切にType metadata
と戯れましょう。