kateinoigakukunのブログ

思考垂れ流しObserver

Effective Techniques in Swift Runtime Library

en

Swift言語のランタイムライブラリで使われているテクニックについて解説します。

ランタイムライブラリと実行可能ファイル

Swiftを実行するためにはswiftCoreというランタイムライブラリが必要です。意識することはあまりありませんが、静的もしくは動的に実行可能ファイルにリンクされています。

Swift 5.0からABI安定が達成されたことで、macOS 10.14.4以降は/usr/lib/swift/libswiftCore.dylibにランタイムライブラリがインストールされています。

SwiftのランタイムライブラリにはSwiftの動的な言語仕様をサポートするための関数が含まれています。

言語仕様の2つの側面

プログラミング言語の言語仕様を考えるとき、以下のような2つに分類できます。

  1. 静的な言語仕様
    • 文法
    • 型システム
  2. 動的な言語仕様
    • 実行時型システム
    • 例外ハンドリング
    • バイナリ互換性

ここでは、動的な言語仕様というのは実行時に使われる言語仕様のことを指します。Swiftの言語機能の中では以下のようなものが例として挙げられます。

  • メモリ確保
  • ARC
  • 実行時の型システム
  • 動的キャスト

これらの言語機能は素朴なコンパイル時のコード生成ではコードサイズが肥大化してしまうため、ランタイムライブラリとして切り出されています。

メモリ確保

ランタイムライブラリの提供する言語機能の例としてメモリ確保の動きを見てみましょう。

Swiftの構造体のように、静的に型のサイズが決定できる場合、コンパイラはそのサイズを確保するようなコードを出力します。

この場合、pet変数はコンパイル時にPet型からインスタンスサイズを24byteに決定できます。

// 24 byte
struct Pet {
  let name: String // 16 byte
  let age: Int     // 8 byte
}

let pet = Pet(name: ..., age: ...) // malloc 24 byte

一方で型から静的にサイズを決定できないケースもあります。例えばSwiftのクラス継承による動的な型の決定を想像してみましょう。

// 8 byte
class View {
  var point: (x: Float, y: Float) // 8 byte
  required init(...) { }

  func copy() -> Self {
    return Self(...) // malloc n byte
  } 
}

// 24 byte
class TextView: View {
  var text: String // 16 byte
  required init(...) { }
}

let view: View = TextView(...)

// この時点でviewがTextViewであることを保証できない。
let anotherView: View = view.copy()

この例ではView型はcopyメソッドを実装しており、 Self型からインスタンスを生成します。 Self型はView型またはTextView型になり得ますが、コンパイラはどちらの型が実行時に使われるか静的に決定できません。

このような Selfを使ったコードを正しく実行するためには、実行時にインスタンスの本来の型情報をインスタンス自身が取り回さなければなりません。

Metadata

実行時に取り回される型情報をSwiftではType Metadataと呼びます。基本的にSwiftユーザーからは見えないようになっており、直接使うこともありません。

Type Metadataは型のサイズや動的なメソッド呼び出しのための関数テーブル、ジェネリック型パラメータのメタデータ、後述するType Descriptorへのポインタなどを保持しています。

ランタイムライブラリは主にMetadataを操作することで動的な言語機能を実装しています。メモリ確保の例では、インスタンスに埋め込まれたType Metadataを取り出してランタイム関数に引数として渡すことで、動的にインスタンスのサイズを決定できるようになっています。

Type Descriptorはジェネリックな型パラメータなどに影響されない、特定の型インスタンスから独立した情報のみを保持しています。また、リフレクションのための情報もここに含まれています。

ジェネリックな型のType Metadataは実際にその型が使われた実行時のタイミングで、型パラメータ毎それぞれ動的に作られるケースがあります。 しかしType MetadataとType Descriptorの二段の構造を取ることで、常に同一のType Descriptorデータを使い回すことができ、メモリを節約しています。

テクニックの背景

データ構造について見ていく前に前提となる背景を軽く話します。

TEXTセグメント

まず、メタデータがどのようにバイナリに格納されているかについてです。

ここではmacOSで使われているMach-O形式について書きます。他のバイナリファイルフォーマットでもさほど変わりません。

基本的に実行可能ファイルは機械語を含むTEXTセグメントとグローバル変数を保持するDATAセグメントに分かれます。

これらのセグメントの大きな違いとして書き込み権限の有無が挙げられます。TEXTセグメントは実行中読み出し専用で書き込み不可ですが、DATAセグメントは実行中に書き込みが可能です。

TEXTセグメントの読み出し専用の性質は動的ライブラリを使う際やプロセスをforkする際に大きく役立ちます。同じ動的ライブラリを2つのプロセスでロードする時、素朴な発想では同じ動的ライブラリを2つメモリ空間に展開します。が、TEXTセグメントが読み出し専用であれば読み書き可能なDATAセグメントのみを重複して展開し、TEXTセグメントは同一のメモリ空間を共有できます。

つまり、ライブラリの占めるTEXTセグメントの割合が多いほどメモリ空間の利用効率が向上します。

SwiftはTEXTセグメントにできるだけメタデータを配置するために一工夫しています。

再配置

再配置は主にリンカがオブジェクトファイルをリンクするタイミングと、プログラムローダが実行可能ファイルをロードするタイミングの2回行われます。

リンク時の再配置ではシンボル同士のアドレスの差や、セクションの先頭からのオフセットなど、実行前にわかる情報を反映します。

また、ロード時の再配置ではプログラムの各部分で使われている絶対アドレスを実際のメモリ空間のアドレスに置換します。基本的に、プログラムが展開されるメモリ空間の先頭アドレスを各アドレスに足し合わせるだけです。

データ構造のテクニック

Relative Pointer

Relative Pointerはポインタ自身のアドレスから対象のアドレスまでのオフセットを保持するポインタ形式です。Swiftのメタデータに含まれるポインタは全てこのRelative Pointer形式になっています。

struct RelativePointer<Pointee> {
    var offset: Int32

    mutating func pointee() -> Pointee {
        return withUnsafePointer(to: &self) { [offset] pointer -> UnsafePointer<Pointee> in
            let rawPointer = UnsafeRawPointer(pointer)
            let advanced = rawPointer.advanced(by: Int(offset))
            return advanced.assumingMemoryBound(to: Pointee.self)
        }.pointee
    }
}

通常のポインタの代わりにRelativePointerを使うメリットはいくつかあります。

  1. バイナリサイズの節約
  2. ロード時再配置を減らせる
  3. メタデータをTEXTセグメントに含められるようにできる

上から1つづつ見ていきます。

まず、Relative Pointerは二点間のアドレスの差を計算するリンカの再配置の仕組みを使っています。この再配置の結果は符号付き32bit整数におさまる、という前提で作られているため、通常のポインタが64bit消費するのに対し、Relative Pointerはその半分の32bitで表現できます。

ロード時の再配置についてですが、前述したとおり、通常のポインタはロード時に再配置が発生するため、プログラムの起動までにオーバーヘッドが発生します。一方、二点間のアドレスの差の計算はリンク時に完了する再配置なので、ロード時のオーバーヘッドがありません。

またロード時の再配置を無くすことによって、メタデータはロード時のデータの書き換えを抑制し、位置独立なデータとして扱うことができます。つまり、実行プログラム本体と同じようにTEXTセグメントに配置して、ほかのプロセスとメモリ空間を共有できるようになります。

もちろんデリファレンス時にオフセットを足し合わせるオーバーヘッドが発生しますが、Swiftでは以上のメリットよりこのような手法を採用しています。

Indirect Pointer

ランタイムライブラリが扱うポインタにはRelative Pointerと組み合わせて、いくつかのテクニックが使われています。その1つの例がIndirect Pointerです。

Indirect Pointerはアライメントによるアドレスの使われないbitを利用してGOT経由のポインタと通常のポインタを同一の型で表現するテクニックです。

アライメントはプロセッサが効率的にデータを扱うために用いられるメモリ空間上のデータ配置規則です。例えば32bitの数値型はアドレスが4の倍数になるように配置されます。

つまり、Int32が配置されるアドレスの下位2bitは必ず0になるわけです。また、Relative Pointer自身も32bit符号付き整数なので、差の数値の下位2bitも0で固定されます。そのため、ポインタの型さえ分かっていればこの下位数ビットは無駄になってしまいます。

Swiftのランタイムライブラリはこの下位数ビットを使っていくつかの状態をポインタに保持しています。

Indirect Pointerは下位1bitをGOTを経由するか否かを表現するために使っています。

GOTを経由する場合はデリファレンスの際に2回デリファレンスします。

struct RelativeIndirectablePointer<Pointee> /*  where alignof(Pointee) => 2 */ {
    let offsetWithIndirectFlag: Int32

    mutating func pointee() -> Pointee {
        let offset: Int32
        if isIndirect {
            offset = offsetWithIndirectFlag & ~isIndirectMask
        } else {
            offset = offsetWithIndirectFlag
        }
        return withUnsafePointer(to: &self) { pointer -> UnsafePointer<Pointee> in
            let rawPointer = UnsafeRawPointer(pointer)
            let advanced = rawPointer.advanced(by: Int(offset))
            if isIndirect {
                let got = advanced.assuimgMemoryBound(to: UnsafePointer<Pointee>.self)
                return got.assumingMemoryBound(to: Pointee.self)
            } else {
                return advanced.assumingMemoryBound(to: Pointee.self)
            }
        }.pointee
    }

    var isIndirect: Bool {
        offsetWithIndirectFlag & isIndirectMask != 0
    }

    var isIndirectMask: Int32 { 0x01 }
}

Int Paired Pointer

また、使われない下位ビットをそのまま数値として取り出すポインタもあります。

struct RelativeDirectPointerIntPair<Pointee, IntTy: BinaryInteger>
    /*  where alignof(Pointee) => 2 */
{
    let offsetWithInt: Int32

    mutating func pointee() -> Pointee {
        let offset = offsetWithInt & ~intMask
        return withUnsafePointer(to: &self) { pointer -> UnsafePointer<Pointee> in
            let rawPointer = UnsafeRawPointer(pointer)
            let advanced = rawPointer.advanced(by: Int(offset))
            return advanced.assumingMemoryBound(to: Pointee.self)
        }.pointee
    }

    var value: IntTy {
        IntTy(offsetWithInt & intMask)
    }

    var intMask: Int32 {
        Int32(
            min(
                MemoryLayout<Pointee>.alignment,
                MemoryLayout<Int32>.alignment
            ) - 1
        )
    }
}

IndirectableとInt Pairedを組み合わせた型もあり、この場合、Indirectか否かを1bit、もう1bitを値として取り出せるようになっています。

Symbolic Reference

Symbolic Referenceはマングリングの種類の1つとして使われています。

通常、型名からメタデータを取得する場合、マングルされた文字列をデマングルしてメタデータを探索します。しかし、対象のオブジェクトがモジュール内に存在する場合、マングルされた名前を介することなく直接参照した方が効率的です。

通常のマングリングは対象のオブジェクトを一意に示す文字列表現ですが、Symbolic Referenceは文字列の一部に対象オブジェクトのアドレスを直接埋め込みます。

Symbolic Referenceは通常のマングルされた文字列と区別するために先頭が制御文字で始まっており、0x01~0x0CまではSymbolic Referenceのために確保されています。

制御文字のあとには4byteのRelative Pointerが埋め込まれており、ランタイムライブラリでデコードされます。

この仕組みにより、モジュール内に定義されたメタデータの参照はほぼコスト無しに行えます。

__swift_instantiateConcreteTypeFromMangledName

この関数は型名からメタデータを取得するために使われます。引数にはこの関数の結果をキャッシュするための { i32, i32 }の構造が渡ってきます。

擬似C++で表現するとこんな感じです。

union Input {
  struct {
    RelativePointer<CChar> typeName;               // 4byte
    int32_t                negativeTypeNameLength; // 4byte
  } nonCached;
  
  TypeMetadata *cached; // 8 byte
}

キャッシュオブジェクトの状態はキャッシュなしとキャッシュ済みの2つあり、それぞれ64bitの使い方が変わってきます。エンディアンによってレイアウトが変わるので、ひとまずリトルエンディアンを仮定して話を進めます。

キャッシュなしの状態では、最初の32bitには型名へのRelative Pointer、後ろの32bitには型名の長さが負号をつけて配置されます。型名はSymbolic Referenceになりうるため、アドレスの一部としてnull文字が入り込む場合があります。そのためnull文字をターミネータとして使えないため、型名の長さが必要になります。

キャッシュありの状態では、64bit全体をキャッシュしたメタデータへの絶対ポインタがそのまま入っています。

この2つの状態はキャッシュオブジェクトを符号付き64bit整数として見た時、負数であればキャッシュなし、正数であればキャッシュあり、として区別できます。これはキャッシュなしの状態で後方32bitに型名の長さを負号をつけて保存したために、64bit全体を符号付き整数として見ると必ず負数になることを利用しています。また、単純に後方32bitの符号を比較するより64bit全体を見るほうが効率的な命令数になります。

まとめ

この様にランタイムライブラリではメモリ空間を最大限に活用するためのテクニックが多く詰まっています。一方でRelative Pointerを前提に組まれた構造がほとんどなので、32bit以上のポインタサイズをサポートしようとすると、ランタイム構造体の調整が必要になってきます。この調整をする場合、コンパイラが出力するメタデータのレイアウトとランタイムライブラリの想定するレイアウトを同期する必要がありますが、現状の実装では特に同期のための仕組みがあるわけではなく、全てのレイアウトを手動で調整する必要があります。

これはSwiftの移植性に大きく関わってくるため、メタデータのレイアウトを一元的に管理する何かしらの仕組みが必要です。例えば、コンパイラが出力したLLVM IRからランタイムライブラリ用のCの構造体を生成するようなコードジェネレータがあれば解決できるかもしれません。(SwiftのメタデータはTrailingObjectsによって可変長なレイアウトを持つため難しいかもしれませんが。)

LLVMコンパイラ実装の基盤として大きな役割を果たしていますが、ランタイムライブラリとコンパイラの間のインターフェイス定義をサポートするようなライブラリはあまり見たことがありません。そのようなレイヤーの技術が充実してくればもっと多くの言語がABI安定を達成できるような世界になるかもしれませんね。

19になりました。

こんにちは。

例年のごとく今年もやってきた誕生日ですが、今年は気がつくと謎のアドベントカレンダーにjoinしてました。

誕生日を迎えました(9/16追記)|noppe*|note

誕生日アドベントカレンダー、次回は家庭の医学くんです。(ノンアポ)

ということで当日からしばらく経ってしまいましたが、欲しいものリストからいただいたものリストを公開していこうと思います。

リストには図々しくも、普通に読みたかった技術書たちをドサっと入れました。

@kishikawakatsumiさんから

詳解UNIXプログラミング 第3版

詳解UNIXプログラミング 第3版

どデカいダンボールで届いたので、むむ、これは本では無いな、とエスパーしたのですが、本という名の鈍器が入っていました。

Amazonの商品画像は正面の図しか教えてくれないので、まさかここまでとは思わず…

ドンッ

先人が築いてきた土台の厚みですね。

最近は興味の方向が段々と下に向いてきたので、楽しく読めそうです。

オーデュボンの祈り (新潮文庫)

オーデュボンの祈り (新潮文庫)

もう一つ、伊坂幸太郎の作品をいただきました。友人に勧められて以来この作家さんの作品をいくつか読んでいるのですが、珍しくハマった作家さんです。

ありがとうございます!

@bannzaiさんから

bannzaiさんのnは2つなんですね。

きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~

きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~

わいわいswifrcのイベントページに載っている、もはや検定教科書と言っても過言では無い本です。

じつはまだ読んだことなくて、雰囲気でLLVM触ってるのでありがたいです!

果たして僕はきつねさんになれるのでしょうか。

乞うご期待です。

‪@noppefoxwolf‬さんから

伊藤園 1日分の野菜 (紙パック) 200ml×24本

伊藤園 1日分の野菜 (紙パック) 200ml×24本

野菜ジュースをいただきました!

大学の食堂の混み具合が異常すぎて、入学後2週間で昼のメニューを野菜ジュースとラムネにシフトした僕にとっては非常にありがたいです。

メッセージに

異世界転生の仕方を教えてください

とあったのですが、たまたまそれを見た母親に怪訝な目をされました!今日も我が家は平和です!


9/17 追記

@kishikawakatsumiさんから(その2)

なんと第2弾があったようで、リンカの本をいただきました。

今年の夏の前半でLinkers & Loadersを読んで「リンカ完全に理解した」状態になったのですが、最近また「何も分からん」のフェーズに入ったので最高のタイミングでした!

パラパラめくってみたところ、Linkers & Loadersとはまた毛色が違うようなので、まだまだ学べることがありそうです。ありがとうございます!!

@d_dateさんから

データ構造とアルゴリズム

データ構造とアルゴリズム

大学の図書館で読めそうじゃんこれ というものにしました

というメッセージと共に、まさに図書館にありそうな本をいただきました!

絶対に図書館にあると思いますが、手元にある安心感は何にも代え難いです!

実はアルゴリズムの類の本は全く読んだことが無く、必要に応じてwikipediaで理解していただけなので、脳の引き出しを拡張するために勉強したいと思います。

ありがとうございます!

@takasekさんから

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

これからも無双してください

最近は暇さえあればマイクロサービスのアーキテクチャについて思いを馳せているので、これを読んで無双できるようなアーキテクチャへの道を見出したいと思います!

あと🐝が思ったよりリアルでした!!

ありがとうございます!

@giginetさんから

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

完全にたかりの図ですが、本当にありがたいことにDDD本を頂きました。

正しくドメインを切り出せるようになるのが直近の目標なので、無限に吸収できることが詰まっている予感です。

これでOSSを全部いい感じにしてくれ〜

という雑で壮大なタスクがメッセージとして降ってきたので、全部いい感じにしたいとおもいます。

僕とgiginetさんがメインでメンテナをしてるIBLinterをよろしくお願いします GitHub - IBDecodable/IBLinter: A linter tool for Interface Builder

@crcrpar

森永製菓 大粒ラムネ 41g×10袋

森永製菓 大粒ラムネ 41g×10袋

僕の生活を支える技術の一つである「大粒ラムネ」をデプロイしていただきました! 野菜ジュースとラムネの最強タッグでしばらくお昼ご飯難民にならなくて済みそうです!

ありがとうございます〜!


多分全部読むのに1年くらいかかりそうですが、無限の伸び代を感じることができて非常にワクワクしてます。

やるぞやるぞ〜

みなさんありがとうございました!

この恩はいろんな形で還元していきたいと思います。

これからも仲良くしてください〜〜

Inside SwiftUI (About @State)

SwiftUI was announced at this WWDC and we've tried it for a few days. This WWDC was the moment everything changed for me.

But SwiftUI is still a private source software as well as UIKit and we need to develop applications while speculating the behavior of it.

There is some features which I felt is a mystery in SwiftUI. I investigated them and internal implementation of SwiftUI.

(This post is just a my prediction)

@State

@State var value: Int

You may write this many time while doing tutorials of SwiftUI. Do you understand how State works?

This feature uses Property Delegates introduced from Swift5.1 and it allows to delegate the implementation of getter and setter to another instance (In this case "another instance" means State).

State is a struct defined in SwiftUI using @propertyDelegate. If State is changed, View will be re-rendered.

First, let's implement State by myself!

@propertyDelegate
struct State<Value> {
    var storage: Value
    var value: Value {
        get { storage }
        set {
            storage = newValue
            renderView()
        }
    }
    init(initialValue value: Value) {
        self.storage = value
    }
    
    func renderView() {
        // ???
    }
}

struct ContentView: View {
    @State var text: String
}

It's almost easy to implement but I couldn't figure out how re-render View. SwiftUI seems not re-render the all Views but some particular views whose state is modified. So I need to link ContentView and text: State<String> to update the view.

Internal structure of State

Let's dump the state instance to investigate internal structure of State by using dump.

struct ContentView: View {
    var text = State<String>(initialValue: "Hello")
    
    init() {
        print("Init:")
        dump(text)
    }
    
    var body: some View {
        print("Body:")
        dump(text)
        return Text("Hello, world")
    }
}

I dumped text in init and body but their outputs are different.

Init:
▿ SwiftUI.State<Swift.String>
  - _value: "Hello"
  - _location: nil

Body:
▿ SwiftUI.State<Swift.String>
  - _value: "Hello"
  ▿ _location: Optional(SwiftUI.StoredLocation<Swift.String>)
    ▿ some: SwiftUI.StoredLocation<Swift.String> #0
      - super: SwiftUI.AnyLocation<Swift.String>
        - super: SwiftUI.AnyLocationBase
      ▿ viewGraph: Optional(SwiftUI.ViewGraph)
        ...

This result shows ViewGraph appears after init but before body. It seems there is a way to inject ViewGraph into State outside of View.

Since ViewGraph is an internal type of SwiftUI, I can only speculate it but it seems ViewGraph manages tree structure of View. If this ViewGraph can render View selectively, it's easy to imitate the re-rendering system.

Then, let's fill the body of renderView.

 @propertyDelegate
 struct State<Value> {
     var storage: Value
+    var viewGraph: ViewGraph?
     var value: Value {
         get { storage }
         set {
             storage = newValue
             renderView()
         }
     }
     init(initialValue value: Value) {
         self.storage = value
     }
 
+    func setViewGraph(_ viewGraph: ViewGraph) {
+        self.viewGraph = viewGraph
+    }
 
     func renderView() {
+        viewGraph.render()
     }
 }

It became clear that how re-render view but it's still uncertain that how inject the ViewGraph into ContentView and make the relationship between State and View.

@State filed belongs to View but there is no way to access text: State<String> through View protocol because the field name is not bound by the protocol. For example, it can be easy to inject if State field is bound by the protocol as follows code. But actually, fields can be named freely.

protocol ViewGraphInjectable {
    var state: State<Value> { get }
}

struct ContentView: View, ViewGraphInjectable {
    @State var state: String
}

Reflection API

The only way to get fields without protocol I first thought is Mirror. It's very simple that getting fields of View using Mirror and if there is State, call setViewGraph.

But does SwiftUI really use Mirror? If Mirror is used, CustomReflectable.customMirror should be called and print "Mirror is used!".

struct ContentView: View, CustomReflectable {
    @State var text: String
    var customMirror: Mirror {
        print("Mirror is used!")
        return Mirror(reflecting: self)
    }
}

But customMirror is not called. For checking, I tried to set breakpoint at swift_reflectionMirror_count which is runtime function used by Mirror but it also doesn't break.

This result indicates that SwiftUI get fields without using Mirror.

Reflection without Mirror

There is still a way to get fields without using Mirror. It's using metadata.

If you don't know metadata, you can learn it by my try! Swift presentation.

Metadata has Field Descriptor which contains accessors for fields of the type. It's possible to get fields by using it.

My various experiments result AttributeGraph.framework uses metadata internally. AttributeGraph.framework is a private framework that SwiftUI use internally for constructing ViewGraph.

You can see it by the symbols of the framework.

$ nm /System/Library/PrivateFrameworks/AttributeGraph.framework/AttributeGraph

There is AG::swift::metadata_visitor::visit_field in the list of symbols. i didn't analysis the whole of assembly code but the name implies that AttributeGraph use visitor pattern to parse metadata. If you set break point, it surely hit.

Why not use Mirror

So why is metadata used instead of Mirror? I think it's for performance.

Mirror.children is represented as [(label: String, value: Any)] to hold any type of value. But Any wraps the actual value and when you use it, Any is unwrapped every time. SwiftUI uses View many times and the overheads can be critical problem.

On the other hand, Using raw pointer through Field Descriptor doesn't affect performance seriously.

Flow to update View

  1. Find State of View using Field Descriptor
  2. Inject ViewGraph into State
  3. Render View.body
  4. State is changed
  5. State notify the ViewGraph to update view
  6. Re-render View.body

Only Apple knows the actual implementation. But it's certain that AttributeGraph.framework has its own reflection system.

I said "I'm looking forward to your great libraries using metadata" in try! Swift but I had never thought Apple do it, I think Apple started to use metadata because ABI stability was built since Swift5. In fact, ABI stability brings us great benefits!

I'll write an article about DynamicViewProperty by next week.

Inside SwiftUI @State編

WWDCでSwiftUIが発表されてから数日が経ちました。一気に世界が変わった気がしますね。 ただ、UIKitと同様にSwiftUIはオープンソースでは無いため、我々開発者は依然挙動をエスパーしながら開発する必要があります。

その中でも、SwiftUIのチュートリアルを試している中で不思議な仕組みがいくつかあったので、僕が調べたSwiftUIの内部構造について書き留めておきます。

(あくまで考察なので間違っていても悪しからず)

追記(2022-09-15)

未だにアクセスがあるので追記。

TokamakというSwiftWasmでSwiftUI互換なコードを書くためのUIフレームワークがあり、 ここで考察した内容とほぼ同じようなテクニックを使ってStateを実装しています。 具体的な実装が気になる方は読んでみてください。

github.com

目次

  • @State編(今ココ)
  • DynamicViewProperty編(来週くらいには書きます)
  • 差分更新編(調査中)

@State

@State var value: Int

チュートリアルをこなした方であれば何度も書いたことでしょう。

Swift5.1からのProperty Delegatesを使った記法で、プロパティの値のgetterとsetterを別の型(ここではState)に委譲できます。

StateというのはSwiftUIに定義された@propertyDelegateなstructです。変更されるとStateが定義されているViewが再レンダリングされる、という振る舞いをします。

ではこの振る舞いからStateの実装を予想してみます。

@propertyDelegate
struct State<Value> {
    var storage: Value
    var value: Value {
        get { storage }
        set {
            storage = newValue
            renderView()
        }
    }
    init(initialValue value: Value) {
        self.storage = value
    }
    
    func renderView() {
        // ???
    }
}

struct ContentView: View {
    @State var text: String
}

(本当はvalueのsetterがnonmutatingなのでbox化されてるはずですが説明のため省略してます。)

簡単に実装できましたがViewを再レンダリングする部分だけは想像できません。keyWindow配下のViewを全て再レンダリングしているのか?、と一瞬思いましたが、Stateが変更されてもルートのViewから全て再レンダリングされる訳では無さそうです。 では、どうやってtext: State<String>ContentViewを紐付けてContentViewだけ再レンダリングしているのでしょうか。

Stateの内部構造

とりあえずStateのpublicなフィールドにはそれっぽい物は無いので、内部フィールドをdumpで調べてみます。

struct ContentView: View {
    var text = State<String>(initialValue: "Hello")
    
    init() {
        print("Init:")
        dump(text)
    }
    
    var body: some View {
        print("Body:")
        dump(text)
        return Text("Hello, world")
    }
}

(StatedelegateValueを使っており$textStateを返さないのでProperty Delegateを使わない記法にあえてしています。)

initbodyでそれぞれtext: State<String>をdumpしましたが、実は違った結果が出力されます。

Init:
▿ SwiftUI.State<Swift.String>
  - _value: "Hello"
  - _location: nil

Body:
▿ SwiftUI.State<Swift.String>
  - _value: "Hello"
  ▿ _location: Optional(SwiftUI.StoredLocation<Swift.String>)
    ▿ some: SwiftUI.StoredLocation<Swift.String> #0
      - super: SwiftUI.AnyLocation<Swift.String>
        - super: SwiftUI.AnyLocationBase
      ▿ viewGraph: Optional(SwiftUI.ViewGraph)
        ...

ViewGraphという怪しいオブジェクトが見えるようになりました。initとbodyの間に何かしらの方法でStateにViewGraphが注入されています。

Internalな型なので名前から推測するしかありませんが、どうやらViewGraphはViewのツリー構造を管理するオブジェクトのようです。このViewGraphがContentViewのみを選択的にレンダリングできる、と仮定すると再レンダリングの仕組みは説明できそうです。 では、StateがViewGraphを持つことを考慮して最初のStateの実装の???を埋めてみます。

 @propertyDelegate
 struct State<Value> {
     var storage: Value
+    var viewGraph: ViewGraph?
     var value: Value {
         get { storage }
         set {
             storage = newValue
             renderView()
         }
     }
     init(initialValue value: Value) {
         self.storage = value
     }
 
+    func setViewGraph(_ viewGraph: ViewGraph) {
+        self.viewGraph = viewGraph
+    }
 
     func renderView() {
+        viewGraph.render()
     }
 }

なるほど、Viewを再レンダリングする方法は分かりました。しかし、未だどうやってContentViewStateを紐付けてContentViewViewGraphを注入するのか、は謎のままです。

先ほどの実験からinitbodyの間に呼ばれることは分かっていますが、どこから呼ばれるかは不明です。

また、@StateフィールドはViewに対して生えていますが、protocolでフィールド名が縛られている訳では無いのでtext: State<String>にアクセスする方法がありません。 例えば以下のようにprotocolで縛られていれば注入できると思いますが、実際フィールド名は自由に付けられるので他の方法で実現されているようです。

protocol ViewGraphInjectable {
    var state: State<Value> { get }
}

struct ContentView: View, ViewGraphInjectable {
    @State var state: String
}

Reflection API

protocolを使わずに任意の構造体から動的に値を取り出す、となると真っ先に思いつくのがMirrorです。ViewMirrorにかけて、childrenを取り出して、その中にState型があればsetViewGraphを呼ぶ、という比較的単純な方法です。

それでは本当にMirrorを使っているか実験してみましょう。CustomReflectableが実装されている場合、MirrorにかけるとcustomMirrorが呼ばれるはずなので、"Mirror is used!"が出力されるはずです。

struct ContentView: View, CustomReflectable {
    @State var text: String
    var customMirror: Mirror {
        print("Mirror is used!")
        return Mirror(reflecting: self)
    }
}

しかし、実際はcustomMirrorは呼ばれません。念のため、Mirrorが使っているランタイムAPIswift_reflectionMirror_countブレークポイントを貼ってみましたがヒットしませんでした。

つまり、SwiftUIは内部でMirrorを使わずにViewのフィールドを取得している訳です。

Mirrorを使わないReflection

Mirrorを使わずともフィールドを取得する方法はまだあります。そうですメタデータを利用する方法です。メタデータについては僕のtry! Swiftの発表を見てもらえると雰囲気が掴めると思います。

メタデータにはField Descriptorという、その型のフィールドのアクセサ群が格納されています。それを利用すればフィールドの一覧を取得できるのでMirrorの代わりに使っている可能性は十分にあります。

色々と試行錯誤した結果1、SwiftUIが内部でViewGraphを組み立てるのに使っているAttributeGraph.frameworkというプライベートフレームワークが内部でメタデータを使っている事が分かりました。

では早速AttributeGraph.frameworkのシンボルをnmコマンドで見てみましょう。

$ nm /System/Library/PrivateFrameworks/AttributeGraph.framework/AttributeGraph

AG::swift::metadata_visitor::visit_field というシンボルが見えると思います。アセンブリを全て読んだ訳ではありませんが、名前から察するにメタデータからフィールドをVisitorパターンで回していそうです。ブレークポイントを貼ってみると実際に動いている様子が確認できます。

なぜMirrorを使わないのか

ではなぜMirrorでもできることをメタデータで実装しているのでしょうか。完全に私の考察ですが、パフォーマンスが主な理由だと思っています。

Mirrorchildren[(label: String, value: Any)]で表現します。任意の型を表現するのにAnyを使っていますが、Anyは値をラップしているため、実際の値を使うには毎回取り出す必要があります。Viewが多くなればなるほどこのオーバーヘッドは無視できなくなってくるため、フィールドをポインタで直接操作できるField Descriptorを使っている、というのが予想です。

StateがViewに更新されるまで

ここまでの実験でStateが変更された際にどうやってViewが再レンダリングされるか予想できたのでまとめます。

  1. Viewに対してField DescriptorStateを探してViewGraphを注入する
  2. View.bodyを評価してレンダリングする。
  3. Stateが変更される
  4. StateViewGraphに変更を通知する
  5. View.bodyが再評価されViewが更新される。

実際の実装はAppleのみぞ知るところですが、これ以上のアイディアは思いつきませんでした。ただ、AttributeGraph.frameworkが独自にリフレクションの仕組みを使っているのは確実です。

try! Swiftの発表で「メタデータを使ったライブラリをみんな作ってみてね」と言いましたが、まさかAppleがABI安定化を期に率先して使ってくるとは思いませんでした。今年のWWDCは驚くことばかりですね。

次はDynamicViewPropertyについて書きます。


  1. これは先に調査していたDynamicViewPropertyの仕組みを考えた上でのエスパーです。

try! Swift 2019

毎年期末試験の時期と重なって参加を断念していたので初参加でした。

参加できるだけでも嬉しかったのですが、光栄なことにスピーカーとして登壇することもできました。

大きなカンファレンスでの登壇は昨年のiOSDCが最初で今回はそれ以来です。ただ、今回は規模がさらに大きいことや、英語での登壇だったこともあり余裕は全くなかったです。

どのくらい余裕がなかったかというと、2日目、登壇する直前の午前中のトークをほぼ覚えていないくらいです。絶対にキャッチアップするので許してください。

登壇

Swift Type Metadata (en) - kateinoigakukunのブログ

Swift Type Metadata (ja) - kateinoigakukunのブログ

Swiftのメタデータについて話しました。内容的には年末に書いたアドカレの発展版です。 Swift Type metadata - Qiita

わいわいswiftcで@omochimetaruが楽しそうに話していたのがきっかけで調べ始めたのですが、ここら辺の技術は掘れば掘るほど面白かったです。

ただ、発表は20分しか無い上に900人にウケる内容にする必要があるので、風呂敷を広げすぎないように構成するのが大変でした。シュッと飛ばした部分が結構あるので、今回の発表で興味を持った方はこの辺を見てもらえると色々と繋がってきて楽しいと思います。

後半で紹介した拙作のOSSはメルカリのインターンの中で作ったものです。基本的なアイディアはCodableのデコードの仕組みに乗っかって任意の構造体を組み立てる、という簡単なものです。

最初はenumをサポートしておらず、各自書いてもらう方式をとっていたのですが、今回のスピーカーで僕のメンターでもある@tarunonから、どうにかしてくれ〜と御達しがあったので、なんとか作りました。

初めてコンパイラの勉強が業務の役に立ちました。

英語で話したこともあってか、Ask the speakerの時間に何人か海外の方もいらしてくださり、今回は言及出来なかったstaticなメタプログラミングとの比較や具体的なユースケースの実装相談など、楽しく議論することができました。何より自分の話した内容が伝わっていることが実感できて嬉しかったです。

セッション

個人的に面白かったトークは 「protocol/extensionにジェネリクスを入れたい」です。型パラを静的に分解して詰め直す技がテクニカルでした。カンファレンス中にお話する機会が何度かあったのですが、Swiftに限らない言語の型システムの深い理解から来る興味深い話が聞けました。

ワークショップ

ワークショップはOpen Source Swiftに参加しました。以前自分が報告した SR-7467 に取り組みました。調査を始めた時はTypeCheckerのキャッシュ機構のバグだと思っていたのですが実際の問題は別のところにあり、相当時間を使ってしまいました。最終的にバグの原因は完全にわかったのですが、色々と考慮漏れがあり未だテストが通ってません。

[SR-7467] Skip validation while checking protocol conformance by kateinoigakukun · Pull Request #23514 · apple/swift · GitHub

今後

やりたいこと、勉強したいことは増えるばかりで時間がいくらあっても足りません。正しく取捨選択していくぞ

Swift Type Metadata (en)

www.youtube.com

try! Swift 2019

I'm kateinoigakukun, working at Mercari as an intern. Today, I'll talk about Metadata which is one of the most important things to understand how Swift works.

Swift is well-known for being a statically-typed language, but it actually has many dynamic things on runtime.

let typeName = String(describing: Int.self)

I'm sure all of you have looked at StackOverflow and written code like this to get a type name.

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)

For example, when you call register method for UITableViewCell, you use this to match the xib name and type name. It's useful extension. But have you ever thought about how this code works on runtime? This is your first step towards thinking about memory representation in Swift. Let's dig into the world of metadata!

Agenda

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

First, I'll explain "What is type metadata". Swift type metadata is not something we are familiar with. But we usually get the benefits of it, because Core features of Swift use it for dynamic behavior. I'll explain the example of the String initializer and talk about how type metadata is used inside of Swift. Then I'll introduce how to use metadata in Swift and some examples of hacking 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

Let's start. Type metadata is Swift's internal information about types like instance size, number of cases of enum, and so on. This information is stored statically in binary or generated dynamically in runtime. Metatype is represented as type name plus self keyword and the actual value is a pointer to the metadata.

extension String {

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

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

}

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

Int.self is a metatype object and it is passed to the String initializer. This initializer accepts any type of value, and returns the description property if the type of the value conforms to CustomStringConvertible and returns the type name if a metatype is passed in.

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

It can not be true that metatype implements the description property of CustomStringConvertible because there is no way to extend metatype. And it seems impossible to implement with the pure Swift API because Swift doesn’t have a runtime API like Objective-C. So there should be some magic.

SwiftCore

  • Swift standard library
  • Fundamental types and interfaces

SwiftRuntime

  • Swift runtime library
  • Dynamic behavior

The initializer is implemented in SwiftCore and the magic is implemented in SwiftRuntime. SwiftCore is Swift standard library written in Swift and contains fundamental data types like String and Int and protocols. SwiftRuntime is the Swift runtime library written in C++ and contains runtime behavior. Many dynamic features like dynamic casting and allocating instances are implemented in this library.

stdlib/public/core/Mirror.swift

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

Swift is open source, so we can see this on Github. In this initializer, the print_unlocked which is internal function is called.

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)

Digging into the call stack for the case when type of argument is metatype, the _typeName function is called, and calls another function _getTypeName. Look at the _getTypeName definition. First, you can see that the function has @_silgen_name attribute and doesn't have body code. What’s this? This @_silgen_name attribute specifies the function name that the declaration will have at link time. In this use case, this definition is used to link to a function in SwiftRuntime. I'll skip the details of the linked function, but it simply extracts the type name from the metadata. Then, how is metadata represented in the memory world?

As you can see in the figure, metadata is broken down and represented as - Value witness table which is a group of functions for manipulating instance

  • a kind value which represents the kind of type such as class, struct, protocol, etc,
  • and NominalTypeDescriptor which records detailed information of the type.
  • In the case of a class, VTable is also included,
  • and in the case of a generic type, type parameters are embedded dynamically.

So, in the NominalTypeDescriptor there is the type name we are looking for. We can get the type name from the nominal type descriptor by just advancing the metadata pointer. It seems not difficult to implement, so let's reproduce the String initializer in SwiftRuntime. (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>
}

In the first step, reproduce the memory layout as a struct. Most information about the memory layout is documented but a part of it is already outdated, so we need to read the source code of swift compiler sometimes. To simplify this example, I will only implement for structs. Then, we need to understand RelativePointer to reproduce the memory layout.

(include/swift/Basic/RelativePointer.h)

RelativePointer is not just a pointer. A basic absolute pointer has the address to the referent, but relative pointer has the offset from its own address to the referent's address. It works by just reading the offset and advancing from its own address. Using RelativePointer instead of an absolute pointer reduces relocation.

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)
}

Then, we finished preparing, so let's extract the type name. First, cast the metatype object into the pointer of metadata. But in Swift's type system, metatype doesn't have subtyping relation with metadata pointer, so use unsafeBitCast to cast the metatype. Access the name pointer through the type descriptor and advance the offset to be an absolute CChar pointer. Next convert this to a Swift String. Then, the implementation has been completed!

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

Execute this, You can get the type name. This is the first step of meta programming with metadata!

Use cases inside of Swift

  • Allocate instance
  • Dynamic method dispatch
    • VTable
  • Reflection

Metadata is used for dynamic behavior in Swift. Many people use Swift without realizing this, but there are many use cases. Where? The most common use case is allocating an instance. And if you call a method through protocol or class, the method table stored in metadata is referenced to get the method reference. In other cases, Mirror API uses metadata to reflect properties.

In this way, metadata is very useful inside of Swift, but we can abuse it.

Method swizzling

Next, I'll talk about the Black magic that you used in Objective-C. The black magic is Method swizzling. If you understand the metadata, you can get great power.

Method swizzling

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

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

First, reproduce the memory layout as getting the type name. Class methods are called via VTable that is a table of pointers to functions. So it should work if we replace the pointers.

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

Get the metadata from metatype using unsafeBitCast and get the both pointers of the functions to swizzle. Then, it's easy to replace them. This is very simple but this works well. Like this, using metadata we can achieve what seems impossible.

Use cases

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

Now I will introduce some use cases of metadata in OSS I found. The top two provide a Swifty interface to access metadata information. They are very useful when using metadta. The third one is a JSON serialization library which enables encoding and decoding JSON without mapping configuration.

alibaba/HandyJSON

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

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

This feature has been achieved by Codable with compiler code generation since Swift4, but HandyJSON was created before Codable and uses metadata to make a relationship between value and property name without Objective-C API.

Use cases

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

The last use case is my library 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)

This library enables instantiating stubs without any arguments and makes it easy to instantiate struct with many many fields. Most of this feature is implemented with Codable but some features are implemented using type metadata.

kateinoigakukun/StubKit

Before I introduce the use case of metadata, I'll share with you how this stub function works. First, a basic struct forms a tree structure and you can traverse it using Decoder protocol.

So if we prepare the stub of a leaf and inject it while traversing, we can instantiate any type of stub without arguments.

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()
    }
}

For example, String, Int and enum can be leaf types. It's easy to prepare a stub of basic data types, but enum can be defined by users so we need to prepare all stubs of custom enums manually. I know it's very hard so I implemented generating enum instances using metadata.

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
}

We know that we can cast Int value to enum because they have same memory layout. However the method is only available for enum and it's necessary to check whether the type is enum. Metadata is effective here. The head address of the metadata is which kind the type is class, struct or enum. The kind value of enum is statically 1. So we can check if the type is enum by comparing this value.

Caution

  • ABI stability
  • Responsibility

Swift4.2 has no ABI stability so metadata layout will be broken in Swift5. But we have good news that ABI stability is accomplished in Swift5! So you can make a library easily. But if you want to support both Swift4 and 5, you will need a lot of effort to maintain it. We got great power but please remember that using metadata is not an official way of doing things in Swift. If you use the metadata and particularly if you rewrite it, you must have a deep understanding of metadata. For example, method swizzling may be reverted to the original implementation by optimization. It's necessary to dispatch method through method table to swizzle implementation, but dynamic dispatch can be optimized to static dispatch by "Devirtualize" optimization. You must handle cases like this to use this magic. "With great power comes great responsibility"

Summary

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

Let me wrap up the key points. - First, Swift uses metadata for dynamic method dispatch, reflection API and so on. - Second, we can use it in Swift by reproducing memory layout. It brings big benefits and it's just fun. So, I'm looking forward to your great libraries using metadata. That’s all, thank you all very much.

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でメタデータを使うことができます。とても役に立つだけでなく、単純に楽しいです。 皆さんのメタデータを使った素晴らしいライブラリを待ち望んでいます。 以上で終わります。ありがとうございました。