寄付窓口はこちら

Swift type metadata | try! Swift Tokyo 2019 2-6

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

f:id:niwatako:20190322120133j:plain

こんにちは、家庭の医学くんという名前なのでそのように読んでください。メルカリでインターンをしています、おととい高校を卒業しました。

f:id:niwatako:20190322120216j:plain

f:id:niwatako:20190322120219j:plain

メタデータはSwiftを理解するのに重要です

runtimeには実は動的な部分が多くあります。

こんなコードを書いたことがあるでしょう

f:id:niwatako:20190322120242j:plain

f:id:niwatako:20190322120247j:plain

CellをClassと合わせるためにこんなコードを書いたことがあるのでは

Swiftのランタイムのメノリ表現を考えてみませんか

メタデータとはそもそもなにか

f:id:niwatako:20190322120323j:plain

アプリ開発では馴染みがないかもしれません。

Swiftコア機能は内部的にmetadataをつかっていて実はいつも恩恵を受けています。

内部でどう使っているか解説します。

どのようにメタデータを使うか、ハックを紹介します

f:id:niwatako:20190322120407j:plain

メタデータは型についてのSwiftの内部データです。enumのケース数など。バイナリ上に静的に格納されていたり、runtimeに動的に生成されたりします。

f:id:niwatako:20190322120444j:plain

メタタイプが渡されたら型名を返します

f:id:niwatako:20190322120528j:plain

CustomStringConvertibleに対応しているはずはない

つまりどこかに魔法があるはず

イニシャライザはSwiftCoreにある。Swift runtimeに魔法が実装されている。

f:id:niwatako:20190322120550j:plain

SwiftCoreにStringやIntなど基本データ型とSequenceなどのプロトコルが実装されている。

SwiftRuntimeはC++で書かれている。ランタイムの振る舞いがそこで実装されている。動的な機能はここに実装されている。

SwiftはOpensourceなのでGithubで見られる

f:id:niwatako:20190322120655j:plain

f:id:niwatako:20190322120701j:plain

引数の型がMetaのとき、typeNameが呼び出され、getTypeNameが呼ばれている

関数の実装がない。

_silgen_nameはリンク時につけられる関数の名前を指定します。実装の解説は省略しますが、単純にメタデータに含まれている型名を取り出すだけです。

メモリ上でどのように表現されるのか。引数の型がメタデータの場合

f:id:niwatako:20190322120832j:plain

ValueWitnessTableで見ていくことができる

Nominal Type Descriptorは型の詳細な情報を所持している。

Generic typeのばあい型パラメータも動的に埋め込まれる

つまりNominalTypeDescriptorから型の名前を取得できる

実装するのは難しくなさそうです。

SwiftコードでSwiftランタイムのイニシャライザの実装を実行してみましょう

f:id:niwatako:20190322120952j:plain

メモリレイアウトをStructで再現します。

メモリレイアウトはドキュメントにありますが、古くなっているため、Swfitコンパイラを直接読む必要があります。

今回はStructに限定します

f:id:niwatako:20190322121034j:plain

RelativePointerは参照先のアドレスを持っているのではなく、自身から参照先までのオフセットを所持しています。参照するときはオフセット分を進めるだけです。

下準備が出来たので型名を取り出してみましょう

f:id:niwatako:20190322121111j:plain

引数から渡ってきたMetadataをMetadataのポインタにキャストします。ここではSwiftのサブタイピング関係がないため、Unsafebitcastを使います

f:id:niwatako:20190322121154j:plain

SwiftのStringに変換して返します。

f:id:niwatako:20190322121221j:plain

メタデータを用いた第一歩

メタデータは動的なふるまいのために使われます。どこにあるのでしょう。インスタンスのアロケート、プロトルやClassを介してメソッドを呼び出されると、メタデータの関数テーブルから呼び出したいメソッドのアドレスがとれる

f:id:niwatako:20190322121235j:plain

ほかに、ミラーAPIがあげられます。

とても利便性が高いことが分かると思います。

今度は黒魔術を再現してみましょう。Metadataを理解することで大きな力を手にできます

f:id:niwatako:20190322121334j:plain

タイプネームを取得したときと同じ要領で

関数ポインタを書き換える

f:id:niwatako:20190322121408j:plain

f:id:niwatako:20190322121426j:plain

Swizzingするメソッドのポインタと入れ替える

ちゃんと動きます。

f:id:niwatako:20190322121452j:plain

OSSメタデータを使っている事例

f:id:niwatako:20190322121456j:plain

上2つは?メタデータにアクセスするSwiftyな方法を提供してくれる

3つ目はJsonエンコード・デコードができる

Codableでできるが、HandyJSONはCodableが登場する前から動いている。

f:id:niwatako:20190322121533j:plain

Objective-Cなしで動的なプロパティと値のヒモ付にMetadataを使う

最後のライブラリは私の

f:id:niwatako:20190322121605j:plain

たくさんのフィールドを持つStructでも引数無しで簡単にスタブをインスタンス化することができる。

この機能の大部分はCodableを使って実装されいてるが、いくつかはMetadataを使って実装されている。

事例を紹介するまえに、スタブインスタンス化機能がどのように動いているか紹介します。

基本的に木構造で成り立っています。デコーダプロトコルを使っている

f:id:niwatako:20190322121705j:plain

葉の部分を使えば引数無しで注入できる

f:id:niwatako:20190322121722j:plain

enumはユーザーが定義できるので自分で定義したenumのすべての値注入を用意する必要がある

Int型の値をキャストすればenumのケースが得られる。

f:id:niwatako:20190322121805j:plain

スタブする型がenumかチェックする。そこでMetadataが使える。

4.2ではABIの安定性がありません。Metadataのレイアウトが5では崩れます。

f:id:niwatako:20190322121856j:plain

しかし良いこともあります。Swfit5でABI安定が達成されました。

ということで今後は気軽にMetadataを使ったライブラリを作ることが出来ます。

4と5をサポートしたければかなり苦労するでしょう。

大いなる力を手に入れました。Metadataを使うことはオフィシャルな方法ではありません。Metadataを使うときは、とくに書き換えを行う場合は、正しい知識を持っておく必要があります。MethodSwizzlingはSwiftの型による最適化でもとの実装に戻ってしまうことがあります。

たとえばRevertualizeで動的呼び出しが静的呼び出しになる場合があり、こうしたケースをきちんとハンドリングする必要がある。

大いなる力には大いなる責任が伴うということです。

f:id:niwatako:20190322122108j:plain

Swiftは動的ディスパッチにMetadataを使っています

メモリレイアウトを再現すればMetadataを使えます

皆さんのメタデータを使った素晴らしいライブラリを楽しみに待っています。