Swift Type Metadata (ja)
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
- What is type metadata?
- Explore
String(describing: Int.self)
- How to use metadata in Swift
- 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ではメタタイプは拡張できないので、CustomStringConvertible
のdescription
プロパティが実装されているはずがありません。そのため、純粋なSwiftのAPIで実装するのは不可能だと思われます。SwiftにはObjective-CのようなランタイムAPIが無いからです。つまり、どこかに魔法があるはずです。
SwiftCore
- Swift standard library
- Fundamental types and interfaces
SwiftRuntime
- Swift runtime library
- Dynamic behavior
このイニシャライザはSwiftCore
で実装されており、魔法はSwiftRuntime
に実装されています。SwiftCore
には、Swiftで使われるString
やInt
などの基本的なデータ型とSequence
やNumeric
などのプロトコルが実装されています。 また、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
メタデータはSwiftの動的な振る舞いのためによく使われます。 皆さんはメタデータを使っていることを意識せずにSwiftを書いていますが、Swiftには多くのユースケースがあります。 それはどこでしょう? 最も一般的なユースケースは、インスタンスをアロケートすることです。さらに、protocol
やclass
を介してメソッドを呼び出すと、メタデータに格納されている関数テーブルが参照され、呼び出したいメソッドの参照が取得されます。
他の例としては、プロパティのリフレクションのためにメタデータを使っているMirror
APIが挙げれられます。
このように便利に使われているメタデータですが、人間はずる賢いので他の使い道を思いついてしまいます。
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
でコンパイラがコード生成を使って達成できますが、HandyJSON
は Codable
が登場する前から動いており、Objective-CのAPIなしで動的にプロパティ名とプロパティの値の紐づけのためにメタデータを使っています。
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() } }
例えば、 String
、Int
、URL
および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でメタデータを使うことができます。とても役に立つだけでなく、単純に楽しいです。 皆さんのメタデータを使った素晴らしいライブラリを待ち望んでいます。 以上で終わります。ありがとうございました。