IndexStoreを使ってSwiftコードを静的解析する
Swiftで静的解析ツールを作るとなると、基本的にSwiftSyntaxを使うことが多いと思います。 ただ、SwiftSyntaxで得られる情報はコンパイラ内部のパイプラインの序盤で生成されるため、「ある変数がどこから参照されているか」といった意味解析後に解る情報は使えません。
そこで、Xcodeがコードジャンプやリネームなどに使っている(だろう)Index-While-Buildingという仕組みを紹介します。
Index-While-Buildingとはその名の通りコンパイラがコンパイル中に生成する中間情報をIDEのコードインデックスに利用する、という仕組みです。
swiftc
やclang
には-index-store-path
というオプションがありディレクトリを指定することで、そこにインデックスデータが出力されます。
この仕組で生成されたデータは、libIndexStore
を使って読み取ることができます。 libIndexStore
はAppleがフォークするLLVMプロジェクトの一部で、Xcodeのツールチェーンに同梱されています。
詳しい話はこちらのドキュメントを参照してください。
libIndexStoreの仕組みを使えば、SwiftSyntaxの構文レベルでの解析では実現できなかった、よりリッチな解析が可能になります。
libIndexStoreを使った例には以下のようなものがあります。
- apple/sourcekit-lsp: Swift向けLSP実装
- apple/indexstore-db: sourcekit-lsp向けのIndexStoreクライアント + クエリ高速化のためのDB。
- peripheryapp/periphery: 未使用コードを検出するツール
- apple/swift-package-manager: Swiftのパッケージング管理 + ビルドシステム
しかし、いざlibIndexStoreを使ってツールを作ろうとすると、Swift向けlibIndexStore
クライアントが無いことに気が付きます。上で紹介したツール達はそれぞれ内部でlibIndexStoreとのバインディングを自前実装しています。 indexstore-db
は一応IndexStoreクライアントとして使うこともできますが、単にIndexStoreを使うためにはtoo muchかつsourcekit-lspから使われる前提でAPIが公開されているため制約が多いです。
そこで、(比較的)libIndexStoreのインターフェースをそのままSwiftから使うためのクライアントライブラリを書きました。
そして、そのライブラリを使って実装したのがswift-renamerです。雑に紹介するとXcodeのリネーム機能をプログラマブルに制御するようなライブラリです。
こんな感じのコードを書くと
import SwiftRenamer let renamer = SwiftRenamer(storePath: indexStorePath) let replacements = try system.replacements(where: { (occ) -> String? in occ.symbol.usr == "s:16IntegrationTests9ViewModelC4nameSSSgvp" ? "nickname" : nil }) for (filePath, replacements) in replacements { let rewriter = try SourceRewriter(content: String(contentsOfFile: filePath)) replacements.forEach(rewriter.replace) let newContent = rewriter.apply() newContent.write(toFile: filePath, atomically: true, encoding: .utf8) }
こんな感じでリネームできます。これは「ある変数がどこから参照されているか」という情報をIndexStoreが持っているのでサクッと実装できます。
class User { - var name: String? + var nickname: String? } class ViewController: UIViewController { let user = User() override func viewDidLoad() { super.viewDidLoad() - user.name = "Initial Name" + user.nickname = "Initial Name" } }
IndexStoreにどんな情報が入っているのか気になった方は、swift-indexstoreをクローンしてindex-dump-tool
コマンドにXcodeが生成するIndexStoreディレクトリを渡すと中身が見えて楽しいと思います。
$ git clone https://github.com/kateinoigakukun/swift-indexstore $ cd swift-indexstore $ swift run index-dump-tool print-record --index-store-path ~/Library/Developer/Xcode/DerivedData/YourProject-XXXX/Index/DataStore
これを使って便利なツールを作ってくれ〜
WebAssembly向けのデバッガを開発しました
こんにちは。私はSwift言語のWebAssembly向けコンパイルをサポートするプロジェクトを進めている一人です。
その開発を効率的に進めていくためにWebAssembly向けのデバッガwasminspectを開発しました。
Swift for WebAssemblyの進捗
この間フォーラムに詳しい進捗報告をしました。 概要としては
- 約90%のテストケースが通るようになった
- まだ一部の実行時の言語仕様が動かない
- 実行時キャストなど
という感じです。
Our co-maintainer @kateinoigakukun has posted a detailed update on the current status of SwiftWasm and remaining issues that are being worked on or require contributions from the community (please do get involved!):https://t.co/LHMAXy3hwv
— SwiftWasm (@swiftwasm) 2020年2月18日
これをデバッグする必要が出てきたこと、既存のデバッグ手法では太刀打ちできないことから今回のデバッガを開発することにしました。
既存のデバッグ手法
既存のデバッグ手法としては以下のようなものが挙げられます。
WebAssembly以外のアーキテクチャでデバッグ
大抵の場合、自分が元の言語で書いたロジックが間違っているので、わざわざWebAssembly向けバイナリでデバッグする必要はありません。RustでWebAssembly開発をしている場合でもRustをx86_64向けにコンパイルしてLLDBでデバッグすればよいのです。
これは手慣れた手法でデバッグでき、LLDBやGDBなどデバッグする環境が十分に整っていることが大きなメリットです。
一方、実際に動かすバイナリをデバッグできるわけではないので、コンパイラが吐くコードが信用できないときには使えません。あくまでデバッグできるのは手元のマシンのアーキテクチャ向けに吐かれたバイナリであり、WebAssemblyバイナリではありません。今回はWebAssembly向けコンパイルのパイプラインをデバッグしたかったため、この手法は不採用でした。
printデバッグ
古典的なデバッグ手法ですが、デバッガが無い環境では非常に有用な手法です。printデバッグの最大のメリットはWebAssemblyバイナリ自体をデバッグできることです。
しかし、printのためのコードが入り込むため、結局デバッグ対象のバイナリ自身をデバッグできるわけではないです。デバッグ用のコードを入れたらスタックが破壊される、ということがザラに起きるのでこれは致命的です。
また、あくまで中間状態をロギングできるだけなので、プロセスの制御ができません。例えばブレークポイントを貼って任意のタイミングでプロセスを止めたり、ステップ実行しながら条件分岐の進み方を確認することはできません。
Chrome DevTools
最新のChromeではDevToolsのWebAssembly対応が進んできています。最近はDWARFの解釈もできるようになったようです。実際にブラウザ上でデバッグできるのは非常に大きなメリットです。また将来的にWebAssemblyをWeb開発で普及させていくためには必須の機能だと思います。
ただ、現状フレーム上の変数やメモリ空間をダンプできなかったり、機能不足感が否めないです。
OSSとして開発されているので開発に参加することも考えましたが、プロジェクトが非常に大きいこと、v8とのインテグレーション作業に時間がかかりそうなことから、見送りました。
Initial DWARF support has landed in Chrome DevTools!
— Chrome DevTools (@ChromeDevTools) 2019年11月8日
It means that you can resolve stack traces, set breakpoints and step-in/-over source code in C/C++/Rust natively, without generating source maps. pic.twitter.com/s3IwkJV6Tr
LLDB + wasmtime
wasmtimeはMozillaが開発しているWebAssemblyのJITコンパイラです。
LLDBやGDBにはJITコンパイルされたネイティブコードとデバッグ情報からデバッグする機能があります。
Debugging JIT-ed Code With GDB — LLVM 10 documentation
wasmtimeはWASMに埋め込まれたDWARFをJITコンパイル後のアーキテクチャ向けに変換して、LLDBと通信することでソースレベルデバッグを実現しています。
実際に自分でデバッガを作り始めるまではこの手法でデバッグしていました。 ソースレベルのステップ実行やフレームの変数ダンプなどはそこそこ動いていました。 何よりもLLDBの基盤に乗ることで、既存のデバッガ資産をそのまま使えるというのが大きいです。
しかし、この手法でデバッグできるのはあくまでJITコンパイルされたネイティブコードであり、WebAssemblyではありません。なのでWebAssembly命令レベルでのステップ実行などはできないです。また、WebAssemblyをネイティブコードに変換する際に一部の情報が落ちてしまう問題もありました。
例えばWebAssemblyには関数呼び出し時にシグネチャの一致をチェックして、一致しない場合trapする仕様があります。デバッグの必要がない場合、不一致の時abortするだけで十分ですが、デバッグ時には実際にどの型を比較したのか、などの追加の情報が欲しいです。これを実現するためにはデバッグ時のみネイティブコードにデバッグ情報を乗せる、などの作業が必要になります。しかし、それぞれの例外をハンドリングするためにデバッグ情報を伝播するのは非常に大変でした。
本当に欲しかったデバッガの要件
コンパイラ開発において本当に欲しかったデバッガの要件は以下のとおりです。
- 実行時にどのwasm命令を実行しているか把握できる
- プロセスの制御ができる
- メモリダンプ
- DWARFを解釈できる
- 命令とソースコードの対応
- 変数のダンプ
しかしこれを満たすようなデバッガが見つからなかったため、自作する結論に至りました。
wasminspect
GIFでだいたいの雰囲気は伝わると思います。
冬休みの一週間強でデバッガ用のWebAssemblyランタイムを書いて、春休みの最初の一週間でデバッガフロントエンドを書いて、やっとまともに使えるようになりました。今ではバリバリコンパイラ開発で活躍しています。
実装
基本的にLLDBのコードを参考にしながら実装しました。大きな違いはLLDBがOSのシステムコールを使ってプロセスを制御するのに対して、wasminspectでは内蔵したランタイムを直接制御する部分です。
ランタイムはJITコンパイルもAOTコンパイルもせず、愚直にインタプリタとして実装しました。これはWebAssembly命令レベルでのステップ実行を実現するためです。また、wasmtimeとLLDBの組み合わせのときに発生したデバッグ情報の伝播も必要なくなりました。
ランタイムの開発は公式のドキュメントやテストケースが充実していて非常にスムーズに進みました。
DWARF for WebAssembly
正直DWARF対応だけで何本かブログが書けそうなくらいしんどかったです。基本的に仕様を読みながらLLDBのコードを参考にして実装するだけでしたが、単純に量が多かったです。
WebAssemblyに固有の話としてはDWARF上のスタックとLLVMが吐くWebAssemblyのスタックが一致しない、ということがありました。
LLVMが吐くWebAssemblyでは線形メモリ上に擬似スタックのような空間を確保してそこを伸び縮みさせてフレーム変数を管理しています。(WebAssemblyの仕様で定義されているスタックとは別)
rsp
に対応する値がグローバルに保持されていて、x86_64向けの命令のようにプロローグとエピローグが擬似スタック上で実行されます。
DWARFが示すスタックはこの擬似スタックにほぼ対応しているので、LLVMの実装に依存することでローカル変数のダンプができるようになりました。
将来的にはWebAssembly向けの位置情報がDWARFの仕様に追加されるんじゃないかなぁと予想しています。既にドラフトの文書があったりします。
自作デバッガの功績
今回作ったデバッガは既にいくつかのSwiftのバグを修正するのに大きく貢献しています。特にメモリダンプ機能は非常に便利で、正常に動くx86_64向けバイナリと比較することで破壊されたメモリの位置を特定できます。
僕もメモリダンプとにらめっこしてる pic.twitter.com/lwLBWK2rkT
— kateinoigakukun (@kateinoigakukun) 2020年2月21日
まとめ
一応一通り動くデバッガができましたが、基本的に僕自身がSwift for WASMの開発をするための道具として開発しているだけなので、あまり他の環境で使われることを考慮していません。試していないですが、多分EmscriptenではDWARFの解釈がうまく行かないと思います。
ドキュメントを軽く書いたのでもし興味があれば触ってみてください。
0からデバッガを作る機会はなかなか無いので楽しかったです。これからも「無いなら作ればいい」の精神でやっていきたいと思います。
XcodeGenとCocoaPodsをいい感じに使う
背景
XcodeGenはXcodeのプロジェクトファイルをYAMLから生成するためのツールです。生のプロジェクトファイルは記述が冗長であったり、とても人間が手で管理できるものではないため、YAMLで管理しようというのがモチベーションです。 github.com
iOS/macOSアプリの開発においてライブラリ管理のデファクトスタンダードとなっているCocoaPodsというソフトウェアがあります。これはライブラリをビルドするためのプロジェクトファイル Pods.xcodeproj
を生成し、メインのプロジェクトファイルと統合することでアプリにライブラリをインストールします。
さて、この2つのツールを組み合わせて使おうとすると、
- XcodeGenでプロジェクトファイルを生成
pod install
でプロジェクトファイルとPods.xcpdeproj
を統合
という2ステップの流れになります。しかし、pod install
はキャッシュ機構があるとはいえ、ブランチをチェックアウトする度に実行するのはヘビーです。
そこでCocoaPodsとプロジェクトファイルの統合を事前に手動で行う方法を紹介します。この方法では Podfile.lock
と Pods/Manifest.lock
に差分がない場合においてはpod install
を実装せずに済むため、プロジェクトファイルの生成時間を大幅に削減できます。
CocoaPodsの内部実装に依存する形になるのでCocoaPodsの変更に追従していく必要がありますが、やることは単純なので大まかな統合の処理は変わらないはずです。
詳細
実はCocoaPodsがプロジェクトファイルに行う処理はそこまで多くありません。
実際にpod install
を行ったときに発生するxcodeprojへのdiffがこちらです。
- ビルドをトリガーするためのダミーターゲット(Pods_XXX.framework)Dependenciesに追加
- CocoaPodsが生成したxcconfigをプロジェクトのベースconfigに設定(他のxcconfigが設定されていない場合のみ)
- EmbedするDynamic FrameworkをアプリにコピーするBuild Scriptを注入(
[CP] Embed Pods Frameworks
) - ライブラリが使うリソースをアプリにコピーするBuild Scriptを注入 (
[CP] Copy Pods Resources
) pod install
が実行されていない場合にビルドをfailさせる Build Scriptを注入([CP] Check Pods Manifest.lock
)
だけです。ライブラリのビルドをトリガーする部分はXcodeのビルドシステムに乗っているため、特に考えることはないです。 それぞれ順番に何をするか説明します。
0. (下準備)
CocoaPodsがプロジェクトファイルに手を加えないようにPodfileで integrate_targets: false
を指定します。
install! 'cocoapods', integrate_targets: false target 'XcodeGenWithPods' do use_frameworks! pod "Alamofire" end
あと、生成されたPods.xcodeprojとメインのプロジェクトファイルを束ねるxcworkspaceを作っておきます。通常はCocoaPodsが生成しますが自動統合を切ったため自分で用意する必要があります。
1. ビルドをトリガーするためのダミーターゲット(Pods_XXX.framework)Dependenciesに追加
CocoaPodsのビルドシステムは自前で色々やっているので基本的にXcodeのビルドシステムに依存していません。ただ、そのままではXcodeのビルドボタンを押した時に何もビルドされないので、最初のビルドを発火させるためダミーターゲットを用意しています。
これを依存ターゲットとしてプロジェクトファイルに追加する必要があります。プロジェクトファイルを跨いだフレームワークの参照なので、project.ymlにimplicit
オプションを付ける必要があります。
dependencies: - framework: "Pods-${target_name}.framework" implicit: true embed: false
2. CocoaPodsが生成したxcconfigをプロジェクトのベースconfigに設定
これはCocoaPodsを使いつつxcconfigを使うときと同じテクニックです。CocoaPodsはアプリのビルド時に正しくヘッダを探したり、ライブラリをリンクするために様々なフラグをxcconfig経由で渡しています。そのため、XcodeGenからプロジェクトファイルを生成する際にはそれを指定する必要があります。
targets: "XcodeGenWithPods": configFiles: Debug: Pods/Target Support Files/Pods-${target_name}/Pods-${target_name}.debug.xcconfig Release: Pods/Target Support Files/Pods-${target_name}/Pods-${target_name}.release.xcconfig
3, 4, 5. Build Scriptを注入
Dynamicなフレームワークとしてライブラリをビルドした場合、実行時に.frameworkファイルを参照するので成果物をアプリにコピーします。 また、Staticなlibraryとしてビルドするとリソースをアプリに直接コピーする必要があるので、その処理を発火させます。
これら2つのシェルスクリプトはCocoaPodsが必要に応じて生成するので、存在しなければ実行する必要はありません。
preBuildScripts: - name: "[CP] Check Pods Manifest.lock" path: /bin/sh script: | diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null if [ $? != 0 ] ; then # print error to STDERR echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2 exit 1 fi # This output is used by Xcode 'outputs' to avoid re-running this script phase. echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}" inputFiles: - "${PODS_PODFILE_DIR_PATH}/Podfile.lock" - "${PODS_ROOT}/Manifest.lock" outputFiles: - "$(DERIVED_FILE_DIR)/Pods-${target_name}-checkManifestLockResult.txt" postCompileScripts: - name: "[CP] Embed Pods Frameworks" path: /bin/sh script: '"${PODS_ROOT}/Target Support Files/Pods-${target_name}/Pods-${target_name}-frameworks.sh"' - name: "[CP] Copy Pods Resources" path: /bin/sh script: '"${PODS_ROOT}/Target Support Files/Pods-${target_name}/Pods-${target_name}-resources.sh"'
サンプルプロジェクト
targetTemplates
というXcodeGenの素晴らしい機能を使って汎用的なproject.ymlにしているので、これをコピペするだけで大抵の環境では動くはずです。
Staticライブラリにしてる場合やabstract targetを使っている場合、多分動かないので適宜変更してください。
Effective Techniques in Swift Runtime Library
Swift言語のランタイムライブラリで使われているテクニックについて解説します。
ランタイムライブラリと実行可能ファイル
Swiftを実行するためにはswiftCoreというランタイムライブラリが必要です。意識することはあまりありませんが、静的もしくは動的に実行可能ファイルにリンクされています。
Swift 5.0からABI安定が達成されたことで、macOS 10.14.4以降は/usr/lib/swift/libswiftCore.dylib
にランタイムライブラリがインストールされています。
SwiftのランタイムライブラリにはSwiftの動的な言語仕様をサポートするための関数が含まれています。
言語仕様の2つの側面
プログラミング言語の言語仕様を考えるとき、以下のような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を使うメリットはいくつかあります。
- バイナリサイズの節約
- ロード時再配置を減らせる
- メタデータを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してました。
誕生日アドベントカレンダー、次回は家庭の医学くんです。(ノンアポ)
ということで当日からしばらく経ってしまいましたが、欲しいものリストからいただいたものリストを公開していこうと思います。
そういえば実は昨日19歳になりました。
— kateinoigakukun (@kateinoigakukun) 2019年9月13日
抱負は限界突破です。頑張ります。https://t.co/JBiMkdBzER
リストには図々しくも、普通に読みたかった技術書たちをドサっと入れました。
@kishikawakatsumiさんから
- 作者: W. Richard Stevens,Stephen A. Rago,大木敦雄
- 出版社/メーカー: 翔泳社
- 発売日: 2014/04/22
- メディア: 大型本
- この商品を含むブログ (2件) を見る
どデカいダンボールで届いたので、むむ、これは本では無いな、とエスパーしたのですが、本という名の鈍器が入っていました。
Amazonの商品画像は正面の図しか教えてくれないので、まさかここまでとは思わず…
ドンッ
先人が築いてきた土台の厚みですね。
最近は興味の方向が段々と下に向いてきたので、楽しく読めそうです。
- 作者: 伊坂幸太郎
- 出版社/メーカー: 新潮社
- 発売日: 2003/11/28
- メディア: 文庫
- 購入: 21人 クリック: 154回
- この商品を含むブログ (832件) を見る
もう一つ、伊坂幸太郎の作品をいただきました。友人に勧められて以来この作家さんの作品をいくつか読んでいるのですが、珍しくハマった作家さんです。
ありがとうございます!
@bannzaiさんから
bannzaiさんのnは2つなんですね。
きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~
- 作者: 柏木餅子,風薬
- 出版社/メーカー: インプレス
- 発売日: 2013/06/21
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (11件) を見る
わいわいswifrcのイベントページに載っている、もはや検定教科書と言っても過言では無い本です。
じつはまだ読んだことなくて、雰囲気でLLVM触ってるのでありがたいです!
果たして僕はきつねさんになれるのでしょうか。
乞うご期待です。
@noppefoxwolfさんから
- 出版社/メーカー: 伊藤園
- 発売日: 2012/03/05
- メディア: 食品&飲料
- 購入: 11人 クリック: 29回
- この商品を含むブログ (9件) を見る
野菜ジュースをいただきました!
大学の食堂の混み具合が異常すぎて、入学後2週間で昼のメニューを野菜ジュースとラムネにシフトした僕にとっては非常にありがたいです。
メッセージに
異世界転生の仕方を教えてください
とあったのですが、たまたまそれを見た母親に怪訝な目をされました!今日も我が家は平和です!
9/17 追記
@kishikawakatsumiさんから(その2)
リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術 (COMPUTER TECHNOLOGY)
- 作者: 坂井弘亮
- 出版社/メーカー: CQ出版
- 発売日: 2010/08/01
- メディア: 単行本
- 購入: 6人 クリック: 55回
- この商品を含むブログ (16件) を見る
なんと第2弾があったようで、リンカの本をいただきました。
今年の夏の前半でLinkers & Loadersを読んで「リンカ完全に理解した」状態になったのですが、最近また「何も分からん」のフェーズに入ったので最高のタイミングでした!
パラパラめくってみたところ、Linkers & Loadersとはまた毛色が違うようなので、まだまだ学べることがありそうです。ありがとうございます!!
@d_dateさんから
- 作者: 杉原厚吉
- 出版社/メーカー: 共立出版
- 発売日: 2001/12/01
- メディア: 単行本
- 購入: 3人 クリック: 15回
- この商品を含むブログ (3件) を見る
大学の図書館で読めそうじゃんこれ というものにしました
というメッセージと共に、まさに図書館にありそうな本をいただきました!
絶対に図書館にあると思いますが、手元にある安心感は何にも代え難いです!
実はアルゴリズムの類の本は全く読んだことが無く、必要に応じてwikipediaで理解していただけなので、脳の引き出しを拡張するために勉強したいと思います。
ありがとうございます!
@takasekさんから
- 作者: Sam Newman,佐藤直生,木下哲也
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/02/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
これからも無双してください
最近は暇さえあればマイクロサービスのアーキテクチャについて思いを馳せているので、これを読んで無双できるようなアーキテクチャへの道を見出したいと思います!
あと🐝が思ったよりリアルでした!!
ありがとうございます!
@giginetさんから
エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)
- 作者: エリック・エヴァンス,今関剛,和智右桂,牧野祐子
- 出版社/メーカー: 翔泳社
- 発売日: 2011/04/09
- メディア: 大型本
- 購入: 19人 クリック: 1,360回
- この商品を含むブログ (131件) を見る
完全にたかりの図ですが、本当にありがたいことにDDD本を頂きました。
修行の身なのでいつでもウェルカムですよ(非常に図々しい)
— kateinoigakukun (@kateinoigakukun) 2019年9月16日
正しくドメインを切り出せるようになるのが直近の目標なので、無限に吸収できることが詰まっている予感です。
これでOSSを全部いい感じにしてくれ〜
という雑で壮大なタスクがメッセージとして降ってきたので、全部いい感じにしたいとおもいます。
僕とgiginetさんがメインでメンテナをしてるIBLinterをよろしくお願いします GitHub - IBDecodable/IBLinter: A linter tool for Interface Builder
@crcrpar
- 出版社/メーカー: 森永製菓
- 発売日: 2018/03/20
- メディア: 食品&飲料
- この商品を含むブログ (2件) を見る
昼ラムネ買っておきました。
— M.K. (@crcrpar) 2019年9月16日
僕の頭がまともだったら明日届くはずですが何かの間違いで僕の家に届くかもしれない
僕の生活を支える技術の一つである「大粒ラムネ」をデプロイしていただきました! 野菜ジュースとラムネの最強タッグでしばらくお昼ご飯難民にならなくて済みそうです!
ありがとうございます〜!
多分全部読むのに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
- Find
State
ofView
usingField Descriptor
- Inject
ViewGraph
intoState
- Render
View.body
State
is changedState
notify theViewGraph
to update view- 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
を実装しています。
具体的な実装が気になる方は読んでみてください。
目次
@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") } }
(State
がdelegateValue
を使っており$text
がState
を返さないのでProperty Delegateを使わない記法にあえてしています。)
init
とbody
でそれぞれ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を再レンダリングする方法は分かりました。しかし、未だどうやってContentView
とState
を紐付けてContentView
のViewGraph
を注入するのか、は謎のままです。
先ほどの実験からinit
とbody
の間に呼ばれることは分かっていますが、どこから呼ばれるかは不明です。
また、@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
です。View
をMirror
にかけて、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
が使っているランタイムAPI、swift_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
でもできることをメタデータで実装しているのでしょうか。完全に私の考察ですが、パフォーマンスが主な理由だと思っています。
Mirror
はchildren
を[(label: String, value: Any)]
で表現します。任意の型を表現するのにAny
を使っていますが、Any
は値をラップしているため、実際の値を使うには毎回取り出す必要があります。View
が多くなればなるほどこのオーバーヘッドは無視できなくなってくるため、フィールドをポインタで直接操作できるField Descriptor
を使っている、というのが予想です。
StateがViewに更新されるまで
ここまでの実験でState
が変更された際にどうやってView
が再レンダリングされるか予想できたのでまとめます。
View
に対してField Descriptor
でState
を探してViewGraph
を注入するView.body
を評価してレンダリングする。State
が変更されるState
がViewGraph
に変更を通知するView.body
が再評価されView
が更新される。
実際の実装はAppleのみぞ知るところですが、これ以上のアイディアは思いつきませんでした。ただ、AttributeGraph.framework
が独自にリフレクションの仕組みを使っているのは確実です。
try! Swiftの発表で「メタデータを使ったライブラリをみんな作ってみてね」と言いましたが、まさかAppleがABI安定化を期に率先して使ってくるとは思いませんでした。今年のWWDCは驚くことばかりですね。
次はDynamicViewPropertyについて書きます。