2020年のオープンソース活動
2020年はOSS開発に多く時間を使ってきたなぁと思ったのと、GitHub Sponsorsからの振り込みがあったので、報告を兼ねて軽く今年何をしてきたか振り返ってみようと思います。
SwiftWasm
年間を通してずっと開発していたプロジェクトの一つです。このプロジェクト関連でコンパイラやリンカ、デバッガなどいろいろなものを作ってきました。 必要に迫られてLLVMやICUなどの有名どころのプロジェクトにもコントリビューションする機会がありました。
ツールチェーンがおおよそ動くようになってからは、コントリビュータを増やすための発信をしたりディスカッションの場を用意したり、といった活動もしています。 最近は来るSwift 6に導入予定のasync/awaitのWasm上でのプロトタイプを実装しています。
進捗報告
つくったもの
- WebAssembly向けのデバッガを開発しました - kateinoigakukunのブログ
- github.com
- github.com
- Wasmリンカのつくりかた - Speaker Deck
- github.com
- github.com
GSoC
4月の時点でSwiftWasmはおおよそ動くようになっていたので、あとはバイナリサイズの縮小のための最適化をやりたいなと思っていたところ、運の良いことにちょうど良いプロジェクトがあったので応募してみました。
実際には5月から8月までGoogle Summer of Codeの学生としてSwiftのリンク時最適化を実装していました。詳しい話はSwiftコンパイラの勉強会「わいわいswiftc」で話したので興味がある方はスライドか動画をみてください。
実証実験としてある程度の成果はあったもののメインストリームにはまだ入っていません。2021年内には入れたいです。
IBLinter
かれこれ作ってから3年目になりました。SwiftUIが登場してお役御免になるかと思いきや、まだ暫くは使っていただけている様子で、ちょこちょこ新しい機能が追加されています。 今年面白かったのは@r_plusさんのPRで実装されたUIStackViewのbackgroundColor検知機能です。
IBLinterの最新バージョンをリリースしました。
— kateinoigakukun (@kateinoigakukun) 2020年9月26日
xibとstoryboardにおいて、UIStackViewのbackgroundColor設定していると警告を出すルールが追加されました。これでiOS14以上で意図せず背景色がついてしまう問題を機械的に対処できます. Thanks @r_plus !
https://t.co/VarZdcefNP
Periphery
使われていないコードを検出してくれるツールなのですが、暫く開発が止まっており最新のXcodeで動かなくなっていたのでPRを投げていたところメンテナになりました。その後はSourceKitベースの解析をIndexStoreベースの解析に切り替えたり、Xcodeのアップデートに追従したりといったコントリビューションをしていました。最近オリジナル作者の @ileitchさんが開発を再開していますが、スター数の割にコントリビュータが少ないのが少々不安です。興味がある方はぜひ。
まとめ
ここに書いたほぼすべての活動は、インターン先であるメルペイの業務時間でコミュニティ活動として行われています。会社の支援がなければ学業と両立してここまでやってこれていないので本当に良い社だと思います。
また、ありがたいことにGitHub Sponsorsで支援してくださる方もいらっしゃり、活動のモチベーションの維持に大きくプラスになっています。いつもありがとうございます。
来年も継続して活動していけるよう頑張っていきます。
Swiftに搭載予定のC++相互運用機能
Swift Advent Calendar 2020 の1日目の記事です。
今年の頭頃からGoogleのSwift for Tensorflowチームが主体となって、SwiftとC++の相互運用機能の議論と実装が進んでいます。
まだ未実装な部分も多く今すぐに試せるわけではありませんが、マニフェストの内容や現在の実装状況から興味深い部分をピックアップして紹介したいと思います。
大まかな実装方針
Swiftは既にCとObjective-Cとの相互運用をサポートしています。ヘッダをClang Modulesとして読み込むことでSwiftから自然な形で外部言語のAPIを呼び出すことができます。
この機能はSwiftコンパイラにリンクされているlibclangToolingを通じて変換されたClangのASTをSwiftのASTに変換することで実現されています。主なコードはClangImporterモジュールに実装されています。
C++との相互運用も同様のアーキテクチャで実装が進んでいます。
API設計の指針
Ideally, users working in Swift should not feel any difference between native Swift APIs and imported C++ APIs.
https://github.com/apple/swift/blob/main/docs/CppInteroperabilityManifesto.md#goals
SwiftとC++からインポートされたAPIに違いを感じさせないような自然なやり取りを目指しているようです。
また、Rustのrust-bindgenのようなユーザーライブラリとして提供するのではなく、コンパイラに結合された形で提供する選択をすることで、エディタ連携やツーリングの面で滑らかな体験を実現したい、という意図が感じられます。
さらに、"Interop must not be a burden for API vendors."と掲げており、JNIやCLIFのようなグルーレイヤーをできるだけ作らないことを目指しているようです。
関数シグネチャの変換
関数のインポートは比較的直感的な変換になりそうです。
// C++側定義 void increment(int *value); // Swift側から見たとき func increment(_ value: inout Int) // Swiftのユースケース func useIncrement() { var i = 0 increment(&i) }
C++側のポインタ型はnullポインタを許容しますが、Swift側のinoutは決してnullにになることはなく、呼び出し側のSwiftの方が制約が強いため、安全に呼び出すことができます。
ただ、Swift から C++の関数を呼び出す場合は問題になりませんが、逆の場合、つまりC++からSwiftの関数を呼び出す場合問題になることがありそうです。例えば、次のようにC++側のvirtualメソッドをSwift側で実装することを考えてみましょう。
// C++ header. class Incrementer { public: virtual void increment(int *value) = 0; }; // Swift side (compiler synthesized) protocol Incrementer { func increment(_ value: inout Int) } // Swift side struct MyIncrementer: Incrementer { func increment(_ value: inout Int) { } }
この場合、仮想関数テーブルを経由してSwiftのincrementメソッドをC++から呼び出されることがありますが、C++のポインタ型はnullポインタを許容してしまうため、呼び出されたincrementはSwiftとしてありえない状態になります。マニフェストではこの問題に対して明確な対応は書かれていませんが、妥当な線として inout Int
または UnsafePointer<Int>
として変換できるようにする、という案が考えられます。
他にもいろいろなバリエーションがありますが、基本的にSwift側の制約の方が強いため、Swift → C++の呼び出しであれば安全に呼び出せそうです。
ムーブセマンティクス
提案自体は長らくOwnershipManifestにあるのですが、Swiftにはムーブセマンティクスが未だ実装されていません。そのため、RAIIパターンでよく使われるmove-onlyなC++の型を綺麗にSwiftの世界にマッピングできません。
moveonly型がSwiftでサポートされるまでの対応として、UnsafePointerに特別な制約を課し、コピーできないようにしつつ、 moveInitialize
メソッドでムーブする、という方法が挙げられています。
// C++ header. // `File` is a move-only C++ class. class File { private: int file_descriptor_; public: File(std::string_view filename); File(const File &) = delete; File(File &&) = default; ~File(); File& operator=(const File &) = delete; File& operator=(File &&) = default; std::string ReadAll(); }; // Swift side (compiler synthesized) struct File { public init(_ filename: std.string_view) public func ReadAll() -> std.string } // Swift side func useFile(_ f: UnsafeMutablePointer<File>) { // var f2 = f.pointee // compile-time error: can only call a method on 'pointee'. print(f.pointee.ReadAll()) // OK // Move `f` to a different memory location. var f2 = UnsafeMutablePointer<File>.allocate(capacity: 1) f2.moveInitialize(from: f, count: 1) // `f` is left uninitialized now. print(f2.pointee.ReadAll()) // OK f2.deallocate() // OK // The file is closed now. }
Swiftにムーブセマンティクスが完全に実装されると次のような形になりそうです。
// Swift side (compiler synthesized) moveonly struct File { public init(_ filename: std.string_view) public deinit public func ReadAll() -> std.string } // Swift side func useOneFile(_ f: File) { print(f.ReadAll()) // OK // Move `f` to a different memory location. var f2 = move(f) // print(f.ReadAll()) // compile-time error: can't access `f`, its value was moved print(f2.ReadAll()) // OK endScope(f2) // OK // The file is closed now. }
StructとClassのインポート
C++のstructとclassは基本的に同じ機能を提供するためclassについてのみ話します。
C++のclassはSwift的に見ると基本的に値型と同じ振る舞いをするのでSwiftのstructとしてインポートするのが妥当だろうとされています。
ただ、このインポートでは表現できないC++の機能がいくつかあります。
例えば、C++のclassは継承をサポートしていますが、Swiftのstructは継承ができず値型同士のサブタイプ関係は存在しません。そのため、C++のclass型を値レベルで自然にキャストすることはできません。
しかし、そもそも値型同士のサブタイプ関係にはObject slicingの問題があり、ミスが起こりやすいとされています。そのため、安全な操作でないことを示すためにUnsafePointer経由で表現することになりそうです。
// C++ header. class Base { int value1; }; class Derived : public Base { int value2; }; // Swift side (compiler synthesized) struct Base { var value1: Int32 } struct Derived { var value1: Int32 var value2: Int32 } // Swift side var derived: Derived = ... var base: Base = UnsafeRawPointer(&derived).load(as: Base.self)
値レベルでのキャストはunsafeな操作ですが、ポインタ同士のキャストはどうでしょうか?
例えば UnsafePointer<Derived>
と UnsafePointer<Base>
の変換はSwiftとしてはサブタイプ関係がないので違法ですが、実体であるC++としては合法なキャストです。
これを実装するプランとして
as
演算子を拡張してC++型のUnsafePointer
のキャストを特別扱いする- キャスト用のメソッドをC++の型に生成する
- C++型のキャストのためのcompiler intrinsicsな関数を提供する
が挙げられています。
1番のプランは、すでにキャストロジックが複雑になりすぎていることから見送られそうです。
2番のプランはC++からインポートした型に以下のよなAPIを自動生成する、というものです。
// Swift side (compiler synthesized) public struct Derived { @returnsInnerPointer public var asBase: UnsafePointer<Base> { get } public static func downcast(from basePointer: UnsafePointer<Base>) -> UnsafePointer<Derived>? } public struct Base { public static func upcast(from basePointer: UnsafePointer<Derived>) -> UnsafePointer<Base> } // Usage example. func useBasePtr(_ basePtr: UnsafePointer<Base>) { ... } func useDerivedPtr(_ derivedPtr: UnsafePointer<Derived>) { ... } func testDowncast() { var d = Derived() useBasePtr(d.asBase) // or: useBasePtr(Base.upcast(from: &d)) } func testUpcast(_ basePtr: UnsafePointer<Base>) { if let derivedPtr: UnsafePointer<Derived> = Derived.downcast(from: basePtr) { useDerivedPtr(derivedPtr) } }
3番目のプランは以下のような特別なキャスト用の関数を提供するという方法です。2番目のプランではキャストメソッドの自動生成時にキャスト可能な型のペアを制限する形になっていますが、このプランでは型チェック時にC++側の定義を元にSourceとDestinationに派生関係があるか否かを調べるようです。
public func cxxUpcast<Source, Destination>( _ source: UnsafePointer<Source>, to type: Destination.Type ) -> UnsafePointer<Destination> public func cxxDowncast<Source, Destination>( _ source: UnsafePointer<Source>, to type: Destination.Type ) -> UnsafePointer<Destination> public func cxxDynamicDowncast<Source, Destination>( _ source: UnsafePointer<Source>, to type: Destination.Type ) -> UnsafePointer<Destination>?
テンプレート関数のインポート
C++のtypenameテンプレートをSwiftのジェネリック関数としてインポートする機能が提案されており、実際に部分的に実装されています。
// C++ header. template<typename T> void functionTemplate(T t) { ... } // Swift side (compiler synthesized) func functionTemplate<T>(_ t: T)
単純な変換に見えるかもしれませんが、C++のテンプレートとSwiftのジェネリック関数には実行モデルに大きな違いがあります。
C++のテンプレートはコンパイル時に静的に型パラメータが決定され展開されますが、対してSwiftのジェネリック関数は型パラメータは実行時に差し替えることが可能です。スペシャライズはあくまで最適化の1つであり、C++のように常に展開されるわけではありません。
つまり、Swiftネイティブなジェネリック関数はpublicな関数としてモジュール外部から型パラメータを注入して使うことができますが、C++由来のジェネリック関数は必ずモジュール内でスペシャライズされる必要があります。
これらを区別するために @_must_specialize
アトリビュートを導入し、C++由来のジェネリック関数に付ける提案がされています。
// C++ header. template<typename T> void functionTemplate(T t) { ... } // Swift side (compiler synthesized) @_must_specialize func functionTemplate<T>(_ t: T)
テンプレートの展開はClangImporterが型パラメータをSwift型からC++型に変換した上で、Clangの意味解析モジュールでテンプレートをインスタンス化することで実装されています。
例外
-fno-exceptions
オプションが付いていない場合、全てのC++関数が例外を投げる可能性があるため、素直にSwiftとして表現するとthrowsだらけになってしまいます。C++にはnoexcept
というキーワードもありますが、マニフェストによると慣用的なC++ではないそうです。nothrowが付いてないとしても実態は例外を投げない、といったことが多いので、デフォルトをthrowsにするのは使い勝手が悪そうです。
そこで提案されているのが throws!
というキーワードをSwift側に追加するプランです。 throws!
はdo-catchで例外をキャッチすることも可能だが、キャッチしないことも可能、を示します。例外が投げられたがキャッチされなかった場合、プログラムは終了するようです。
標準ライブラリ型のブリッジ
Objective-Cとの相互運用ではNSArrayやNSDictionaryなどのNSプレフィックスの付いたObjective-Cの型とSwiftの型の変換がサポートされています。このブリッジングはSwift側の型がObjective-Cの型の所有権を奪うことでほぼノーコストで行われています。
しかし、同じような仕組みを提供するためにはABIの変更が必要となってしまうようです。
まとめ
Interoperabilityの仕様には細かい決めの問題が大量にあって言語デザインのセンスが問われそうだなと思いました。
今回紹介した機能は実装が予定されている機能の一部だけなので、興味がある方は実装やManifestを見ると面白いと思います。
iOSDC 2020に参加/登壇 しました
kateinoigakukunです。4回目のiOSDCに参加しました。3年連続スピーカーとして参加です。
発表内容
プロポーザル
スライド
デモ
Our co-maintainer @kateinoigakukun developed an amazing demo that allows you to experiment with SwiftWasm and @TokamakUI right in your browser, with syntax highlighting and previews! It even ships with a custom Wasm linker written in pure Swift 😲https://t.co/V9guF8OCFy pic.twitter.com/KkmZt7hDc9
— SwiftWasm (@swiftwasm) 2020年9月21日
ということで、去年iOSDCでも発表したWebAssemblyネタの最新版をお届けしました。いかがでしたでしょうか。ここ1年間の成果発表のような形になったと思います。40分枠にすればよかったと少々後悔してます 😅
登壇中にDiscordのパブリックビューイングチャンネルでオーディエンスとわいわいできたのは事前収録ならではの新鮮な体験でした。Ask the Speakerでは沢山の方に興味を持ってもらえたようでとても嬉しかったです。早速SwiftWasmを試してくださった方も 👀
さっそくSwiftWasmでHello, world!やってみた
— keygx (@keygx) 2020年9月21日
ソースを見るとjsファイルがリンクされてて、バイナリじゃないの?って思ったが、公開用のビルドじゃないからかもと気づいて、--releaseオプションを付けてみると、ビルドに失敗。難しいな
@SwiftWasm でビルドしたバイナリ、ちゃんと @a_Shell_iOS 上の wasm コマンドでも動く。これで、Swift で書いたコマンドラインツールを iPad 上のターミナルで動かせるということに!
— ケーボ (@_kebo) 2020年9月21日
(右はリモートの macOS 上でビルドして、左で iPad ローカルで実行している様子) pic.twitter.com/ABnQH1MA7s
swiftwasmで遊んでいる pic.twitter.com/dLp05XSUJG
— える (@el_metal_) 2020年9月21日
他のスピーカーの発表
最近はコンパイラ周りしか触っていなかったので色々な分野の話を聞けて刺激になりました。まだまだ知らないことや理解が浅いものがあるので勉強していきたいです。
今年もこうして参加できて本当に良かったです。
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安定を達成できるような世界になるかもしれませんね。