Swift Type metadata


Swiftには実行時に型情報を保持するためのType metadataという仕組みがあります。我々が頻繁に使うことはありませんが、Swiftのランタイムの動作を理解するための重要な要素です。

この記事では Type metadata についてコンパイラのコードとドキュメントから調べたことを簡単に解説します。

Type metadata とは

Swiftが実行時に保持している型情報です。ジェネリックでないclassstructenumはコンパイル時に静的に作られますが、ジェネリックな型と関数型、タプルなどの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になってしまう原因を探るために、メタタイプを生成しているコンパイラのコードを読んでみましょう。

swift/MetadataRequest.cpp

/// 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とは何でしょう。

swift/Types.h

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
    • structenumなどの静的に挙動が決まる型
  • 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と戯れましょう。

参考