kateinoigakukunのブログ

思考垂れ流しObserver

Swift Type Metadata (ja)

www.youtube.com

try! Swift 2019

kateinoigakukunです。メルカリでインターンをしています。

今日はSwiftを理解するために重要なメタデータについて話します。Swiftは静的型付けな言語として知られていますが、 実はランタイムにおいては動的な部分が多々あるのです。

let typeName = String(describing: Int.self)

皆さんもStackOverflowを見て、一度はこんなコードを書いて型名を取得したことがあるでしょう。

extension UITableView {
    func register<Cell>(nibWithCellClass: Cell.Type) where Cell: UITableViewCell {
        let typeName = String(describing: Cell.self)
        let nib = UINib(nibName: typeName, bundle: Bundle.main)
        register(nib, forCellReuseIdentifier: typeName)
    }
}

tableView.register(nibWithCellClass: TweetCell.self)

例えばUITableViewCellをregisterするとき、Xibのファイル名をクラス名と合わせるために使っていると思います。よくあるextensionですね。一方で、この一行がランタイムでどのように動いているか考えたことはありますか?これを機にSwiftのランタイムのメモリ表現を考えてみませんか? メタデータの世界に飛び込んでみましょう!

Agenda

  1. What is type metadata?
  2. Explore String(describing: Int.self)
  3. How to use metadata in Swift
  4. Use cases in OSS

まず、そもそもメタデータとはなにか、について話します。メタデータは我々がアプリを開発する上であまり馴染みが無いかもしれません。しかし、アプリを作っている時でさえ、Swiftのコア機能は動的な動作のために内部的にメタデータを使っており、実はいつも恩恵を受けているのです。前のスライドで出した例を紐解きながら、Swiftが内部でどのようにメタデータを使っているかを解説します。そして、Swiftでどのようにメタデータを使うか、それを使ったハックを紹介します。

What is type metadata?

  • Type information in Swift runtime
  • Used in Swift internal dynamic behavior
  • Metatype is pointer to metadata
let metatype: Int.Type = Int.self

メタデータは型についてのSwiftの内部情報です。例えばインスタンスのサイズや、enumのcase数などが含まれています。これらの情報はバイナリ上に静的に格納されていたり、ランタイムで動的に生成される場合もあります。Swift上では型名にselfを付けて表現されるメタタイプがメタデータへのポインタとなっています。

extension String {

  public init<Subject: CustomStringConvertible>(describing instance: Subject) { ... }

  public init<Subject>(describing instance: Subject) { ... }

}

let typeName = String(describing: Int.self) // "Int"

前述したように、 Int.selfはメタタイプのオブジェクトであり、そのメタタイプはStringのイニシャライザに渡されます。 このイニシャライザは任意の型の値を受け付けます。渡されてきた値の型が CustomStringConvertibleに準拠している場合はdescriptionプロパティをそのまま返しますが、メタタイプが渡された場合は型名を返す、という挙動になっています。

extension Int.Type: CustomStringConvertible { // 🚫 Cannot extend a metatype 'Int.Type'
    var description: String {
        return "Int"
    }
}

Swiftではメタタイプは拡張できないので、CustomStringConvertibledescriptionプロパティが実装されているはずがありません。そのため、純粋なSwiftのAPIで実装するのは不可能だと思われます。SwiftにはObjective-CのようなランタイムAPIが無いからです。つまり、どこかに魔法があるはずです。

SwiftCore

  • Swift standard library
  • Fundamental types and interfaces

SwiftRuntime

  • Swift runtime library
  • Dynamic behavior

このイニシャライザはSwiftCoreで実装されており、魔法はSwiftRuntimeに実装されています。SwiftCoreには、Swiftで使われるStringIntなどの基本的なデータ型とSequenceNumericなどのプロトコルが実装されています。 また、SwiftRuntimeはSwiftのC++で書かれたランタイムライブラリでランタイムの振る舞いが実装されています。ダイナミックなキャストやインスタンスアロケーションのような多くの動的な言語機能はこのライブラリに実装されています。

stdlib/public/core/Mirror.swift

struct String {
  public init<Subject>(describing instance: Subject) {
    _print_unlocked(instance, &self)
  }
}

Swiftの実装はオープンソースになっているので、Githubからいつでも見ることが出来ます。このイニシャライザの内部ではprint_unlocked関数が呼ばれます。

stdlib/public/core/Misc.swift

public func _typeName(_ type: Any.Type, qualified: Bool = true) -> String {
  let (stringPtr, count) = _getTypeName(type, qualified: qualified)
  return String._fromUTF8Repairing(
    UnsafeBufferPointer(start: stringPtr, count: count)).0
}

@_silgen_name("swift_getTypeName")
public func _getTypeName(_ type: Any.Type, qualified: Bool) -> (UnsafePointer<UInt8>, Int)

引数の型がメタ型である場合の呼び出しスタックを掘り下げていくと、typeName関数がメタ型を引数として呼び出されています。そして、また別の関数_getTypeNameがさらに呼ばれます。 _getTypeNameの定義を見てみましょう。 まず、 @ _silgen_name属性が追加されていること、関数の実装がないことに気がつくと思います。これは何でしょう?この@_silgen_nameアトリビュートは、リンク時に付けられる関数の名前を指定します。 今回の場合、このシグネチャーは SwiftRuntimeの関数にリンクするために使われます。リンクされた関数の実装については省略しますが、単純にメタデータに含まれている型名を取り出すだけです。 では、メタデータはメモリ上でどのように表現されるでしょうか?

図のようにメタデータは分割されて表現されます。 - インスタンスを操作するための関数の集合であるValueWitnessTable - クラス、構造体、プロトコルなどの種類の種類を表すkind value - 型の詳細な情報を保持するNominalTypeDescriptor。 - クラスの場合はVTableもメタデータに含まれています、 - さらに、genericな型の場合、型パラメータも動的に埋め込まれます。

NominalTypeDescriptorには、我々が今求めている型の名前が入っています。 つまり、メタデータのアドレスを進めるだけで、NominalTypeDescriptorから型の名前を取得できるということです。 実装するのは難しく無さそうです。Swiftのコードでこの SwiftRuntimeのStringのイニシャライザの実装を再現してみましょう!

(docs/ABI/TypeMetadata.rst)

struct StructMetadata {
    let kind: Int
    let typeDescriptor: UnsafePointer<StructTypeDescriptor>
}

struct StructTypeDescriptor {
    let flags: Int32
    let parent: Int32
    let name: RelativePointer<CChar>
}

まず最初にランタイムのメモリレイアウトをSwiftのstructとして再現します。メモリレイアウトに関する情報はほとんどドキュメントに書かれていますが、一部は古くなっているため、時々Swiftコンパイラーのソースコードを直接読まなくてはいけません。簡単のため、今回はstructについてのみ実装します。 ここでRelativePointerというポインタの型が登場します。

(include/swift/Basic/RelativePointer.h)

RelativePointerはただのポインターではありません。通常のポインターが参照先へのアドレスを持っているのに対し、RelativePointerは自身のアドレスから参照先のアドレスへのオフセットを保持しています。参照するときはオフセット分アドレスを進めるだけです。absolute addressの代わりにrelative addressを使うことで無駄なアドレスの再配置を減らせます。

func getTypeName<Subject>(of type: Subject.Type) -> String {
    let metadataPointer = unsafeBitCast(
        type, to: UnsafePointer<StructTypeMetadata>.self
    )
    let namePointer: UnsafePointer<CChar> = metadataPointer.pointee
                        .typeDescriptor.pointee
                        .name.advancedPointer()
    return String(cString: namePointer)
}

下準備が出来たので実際に型名を取り出してみましょう。まず、引数から渡ってきたメタタイプをメタデータのポインタにキャストします。ここではSwift上のサブタイピング関係が無いためunsafeBitCastを使います。取り出したメタデータからtype descriptorを経由してnameのrelative pointerにアクセスし、relative pointerからabsolute pointerに変換するとCCharポインターになります。これをSwiftのString型に変換してreturnします。 これで実装は完了しました。

let typeName = getTypeName(of: Int.self) // "Int"

これを動かすと、無事に型名を取得できます。メタプログラミングの第一歩目ですね!

Use cases inside of Swift

  • Allocate instance
  • Dynamic method dispatch
    • VTable
  • Reflection

メタデータはSwiftの動的な振る舞いのためによく使われます。 皆さんはメタデータを使っていることを意識せずにSwiftを書いていますが、Swiftには多くのユースケースがあります。 それはどこでしょう? 最も一般的なユースケースは、インスタンスをアロケートすることです。さらに、protocolclassを介してメソッドを呼び出すと、メタデータに格納されている関数テーブルが参照され、呼び出したいメソッドの参照が取得されます。 他の例としては、プロパティのリフレクションのためにメタデータを使っているMirrorAPIが挙げれられます。

このように便利に使われているメタデータですが、人間はずる賢いので他の使い道を思いついてしまいます。

Method swizzling

ここからは皆さんがObjective-Cのころ使っていたあの黒魔術をSwiftで再現してみましょう。そうです、Method swizzlingです。メタデータを理解することで、大いなる力を手に入れることができるのです。

class Animal {
    func bar() { print("bar") }
    func foo() { print("foo") }
}

struct ClassMetadata {
    ...
    // VTable
    var barRef: FunctionRef
    var fooRef: FunctionRef
}

型名を取得したときと同じ要領でメタデータのメモリレイアウトをstructで再現します。ClassのメソッドはVTableという関数ポインタのテーブルを通して呼び出されるため、そのポインタを書き換えれば実現できそうです。

let metadata = unsafeBitCast(
    Animal.self, to: UnsafeMutablePointer<ClassMetadata>.self
)

let bar = withUnsafeMutablePointer(to: &metadata.pointee.barRef) { $0 }
let foo = withUnsafeMutablePointer(to: &metadata.pointee.fooRef) { $0 }

bar.pointee = foo.pointee

let animal = Animal()
animal.bar() // foo

メタデータのポインタをメタタイプから取り出して、そこからswizzleする関数のポインタをmutableなポインタとして取り出します。すると関数ポインタの値を入れ替えるのは簡単です。このコードは非常にシンプルですがきちんと動きます。 このように、メタデータの情報によって我々は不可能なように見えるものでも実現できるのです。

Use cases

  • Zewo/Reflection
  • wickwirew/Runtime
  • alibaba/HandyJSON
  • kateinoigakukun/StubKit

私が見つけたメタデータを使っているOSSを紹介します。上の2つはメタデータの情報にアクセスするためのSwiftyなインターフェースを提供してくれるライブラリです。3つ目は、マッピングのための明示的な設定なしでJSONエンコードとデコードができるJSONライブラリです。

alibaba/HandyJSON

struct Item: HandyJSON {
    var name: String = ""
    var price: Double?
    var description: String?
}

if let item = Item.deserialize(from: jsonString) {
    // ...
}

この機能はSwift4.0以降ではCodableコンパイラがコード生成を使って達成できますが、HandyJSONCodableが登場する前から動いており、Objective-CAPIなしで動的にプロパティ名とプロパティの値の紐づけのためにメタデータを使っています。

Use cases

  • Zewo/Reflection
  • wickwirew/Runtime
  • alibaba/HandyJSON
  • kateinoigakukun/StubKit

最後のユースケースは私のライブラリStubKitです。

kateinoigakukun/StubKit

import StubKit

struct User: Codable {
    let name: String
    let age: UInt
}

let user = try Stub.make(User.self)
// User(name: "This is stub string", age: 12345)

このライブラリを使うと、たくさんのフィールドを持つstructでも引数なしで簡単にスタブをインスタンス化できます。この機能の大部分は Codableで実装されていますが、いくつかの機能はmetadataを使って実装されています。

メタデータを使った事例を紹介する前にこのスタブをインスタンスする機能がどのように動いているのかを解説します。まず、基本的に我々の使っている構造体は木構造で成り立っており、Decoderプロトコルを使ってトラバースすることができます。

そのため、この木構造中の葉の部分のスタブを準備してトラバースしながらスタブを注入すれば、引数なしで任意のスタブをインスタンス化できます。

func leafStub<T>(of type: T.Type) -> T {
    guard let stubbable = type as? Stubbable else { return nil }
    return type.stub
}

extension Int: Stubbable {
    var stub: Int { return 12345 }
}

extension enum: Stubbable { // 🚫 Can't extend
    var stub: Self {
        return enumStub()
    }
}

例えば、 StringIntURLおよびenumは葉の型になる可能性があります。 基本データの型のスタブを準備するのは簡単ですが、enumはユーザーが定義できるため、自前で定義した全てのenumのスタブを手動で準備する必要があります。これはできれば避けたいですね。そのため、メタデータを使ってenumインスタンスを動的に生成してみました。

func enumStub<T>(of type: T.Type) -> T? {
    if isEnum(type: type) {
        let rawValue = 0
        let rawPointer = withUnsafePointer(to: rawValue) { UnsafeRawPointer($0) }
        return rawPointer.assumingMemoryBound(to: T.self).pointee
    }
    return nil
}

func isEnum<T>(type: T.Type) -> Bool {
    let metadata = unsafeBitCast(type, to: UnsafePointer<EnumMetadata>.self).pointee
    return metadata.kind == 1 // kind value of enum is 1
}

メモリレイアウトからEnumのスタブはInt型の値をキャストすれば手に入れられることが分かっています。しかし、その方法が使えるのはenumだけなので、スタブする型がenumの型であるか、ということをチェックする必要があります。そこでメタデータの出番です。メタデータの先頭アドレスにはその型がclassなのか、structなのか、enumなのか、といったkindの値が入っています。enumのkind値は1で固定なのでこの値を比較すれば型がenumかどうかを調べることができます。

Caution

  • ABI stability
  • Responsibility

Swift4.2ではABIが安定していないため、Swift5ではメタデータのレイアウトが崩れます。しかし、喜ばしいことにSwift5でABIの安定性が達成されました。今後は気軽にメタデータを使ったライブラリを作ることができます。ただし、Swift 4と5の両方をサポートしたい場合は維持が大変でしょう。 私たちは大いなる力を手に入れましたが、メタデータを使うことはSwiftのオフィシャルな方法ではないことを覚えておいてください。メタデータを使う時、特に書き換える場合は、メタデータについて正しい知識を持っておく必要があります。

Caution

  • ABI stability
  • Responsibility

例えば、前に紹介したMethod swizzlingはSwiftの型による最適化によって元の実装に戻ってしまうことがあります。実装を入れ替えるにはテーブルを参照してメソッドを実行する必要がありますが、Devirtualizeという最適化によって動的呼び出しが静的呼び出しになってしまうのです。 魔法を使うにはこのようなケースを正しくハンドルしなければなりません。 「大いなる力には大いなる責任が伴う」のです。

Summary

  • Swift uses metadata for dynamic behavior
  • We can use metadata in Swift
  • Let's write meta programming libraries!

要点をまとめます。 - まず、Swiftは動的メソッドディスパッチやリフレクションのAPIなどにメタデータを内部的に使用しています。 - また、メモリレイアウトを再現することでSwiftでメタデータを使うことができます。とても役に立つだけでなく、単純に楽しいです。 皆さんのメタデータを使った素晴らしいライブラリを待ち望んでいます。 以上で終わります。ありがとうございました。