Swift Digest
Blog | Swift.org Blog↗

Swift のデバッグ情報におけるモジュール追跡

Module Tracking in Swift Debug Info

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。

この記事の要点

背景: デバッガはどうやって Swift モジュールを使うのか

LLDB の強力な機能のひとつが式評価器です。LLDB は Clang と Swift のコンパイラを内部に組み込んでいるため、ブレークポイントで停止した状態で、任意の有効なソースコードを JIT コンパイルしてアプリケーションの文脈で実行できます。単に既存のコードを呼び出すだけでなく、新しい型・関数・クロージャをその場で定義することもできます。

ローカル変数を表示するだけなら、コンパイラの助けは要りません。

(lldb) p myObj

この場合、LLDB は myObj の位置をデバッグ情報から見つけ、その型をリフレクションメタデータから解決できます。しかし、次のように computed property へアクセスすると話が変わります。

(lldb) p myObj.myComputedProperty

myComputedProperty は実際には関数呼び出しなので、評価にはコンパイラが必要です。LLDB は式評価器を初期化するために、現在の関数が属する Swift モジュールの名前をデバッグ情報から探し、概念的には次のように式を関数で包んでコンパイルします。

import Foo

func lldb_expr(_ $__lldb_arg : UnsafeMutablePointer<Any>) {
  let myObj: MyObject = /* some LLDB magic */
  // Expression begins here:
  myObj.myComputedProperty
}

問題は import Foo が曖昧なことです。Swift 言語では同名のモジュールを複数持てませんが、実際のアプリケーションには同じモジュールのコピーが複数存在しえます。たとえば非公開の宣言まで含んだ版と、公開インターフェイスだけを含む module interface が別々に存在したり、macOS 版と Mac Catalyst 版が同じプロセス内に共存したりします。名前だけでは、LLDB が正しいモジュールを選び当てられる保証がありません。

これまでのモジュール受け渡しと課題

LLDB がモジュールの場所を知るには、リンカの協力が必要でした。Darwin ではシステムリンカの -add_ast_path オプションで、リンク時にすべてのバイナリ Swift モジュールを列挙します。

# Linker invocation on macOS
ld -add_ast_path /path/to/Foo.swiftmodule Foo.o -o MyApplication

リンカはこれをシンボルテーブルのエントリに変換し、dsymutil がそれらを集めて dSYM バンドル内の __swift_ast セクションに格納します。リンカが Swift を認識しない Windows・Linux・FreeBSD などでは、コンパイラの -modulewrap アクションでバイナリ Swift モジュールをオブジェクトファイルに変換し、リンカでバイナリに埋め込みます。

# Modulewrap and linker invocation on Linux
swift-frontend -modulewrap Foo.swiftmodule -o Foo.swiftmodule.o
lld Foo.o Foo.swiftmodule.o -o MyApplication

この方式には、特に大規模アプリケーションで次のような課題がありました。

Swift 6.3 からの精密なモジュール追跡

Swift 6.3 から、コンパイラ・Swift ドライバ・LLDB にまたがる改善が段階的に導入されています。いずれも明示的にビルドされたモジュールを土台にしています。

明示的にビルドされたモジュールが Swift の依存関係も追跡する: 明示的にビルドされたバイナリ Swift モジュールは、以前から明示的な Clang モジュールの依存関係を記録していました。Swift 6.3 では、これに加えて Swift モジュールの依存関係も記録します。これにより、モジュールを名前で探す必要がなくなり、インポートが高速かつ曖昧さなく行えます。この動作は自動で、利用者側の変更は不要です。

オブジェクトファイル自身のモジュールへのパスをデバッグ情報に格納する: トップレベルのモジュールさえ見つかれば、LLDB はそれとすべての依存関係を正確にインポートできます。では、ブレークポイント地点の Swift ファイルが属するモジュールをどう特定するのか。Swift 6.3 では、コンパイラがそのパスをデバッグ情報に格納できます。オブジェクトファイル自身のモジュールはコンパイルの入力ではないため、各コンパイルにパスを伝える新しい -debug-module-path オプションが追加されました。

Swift ドライバがコンパイルジョブにモジュールパスを渡す: SwiftPM や Xcode の利用者はこれを意識する必要はありません。Swift ドライバが -debug-module-path を認識し、オブジェクトファイル自身のモジュールへのパスを自動でコンパイラに渡すからです。一方、Swift ドライバを介さずに Swift フロントエンドを直接呼び出す独自ビルドシステムでは、各コンパイルジョブにトップレベルモジュールへのパスを自分で伝える必要があります。

これらのパスは、分散ビルドシステム向けの標準的なパスのリマッピングオプションの対象にもなります。また、キャッシュを有効にしてコンパイルする場合、Swift モジュールやモジュールのデバッグ情報を指すパスはすべて content-addressable storage への参照(場所ではなく内容で識別される参照)になるため、コンパイルキャッシュとも透過的に連携します。

Swift 6.4 で不要になるビルドステップ

次の変更は Swift 6.4 から安全に行えます。

モジュールのパスがデバッグ情報とモジュールヘッダ自身を通じて伝わるようになったため、明示的なモジュールビルドを行う独自ビルドシステムでは、次を取り除けます。

その結果、dsymutil はバイナリ Swift モジュールを処理しなくなり、dSYM バンドルは小さくなります。バイナリ Swift モジュールは生成元のツールチェーンでしか解析できないため、もともと長期保存フォーマットである dSYM バンドルとは相性が良くありませんでした。

デバッグへの影響を心配する必要はありません。ローカル変数の型解決は、Swift 5.6 以降 LLDB がバイナリ中のリフレクションメタデータから行えます。したがって、dSYM バンドルにバイナリ Swift モジュールがなくても、変数の中身を調べたり po でオブジェクトを表示したりする能力は損なわれません。関数呼び出しや computed getter のような複雑な式の評価にはバイナリ Swift モジュールが引き続き必要ですが、LLDB がそれらを元の場所(またはリマップ先)で見つけられる限り、式評価はこれまでどおり動作します。ビルドしたばかりのバイナリを同じマシンでデバッグする場合は常にこの条件を満たします。

Swift 6.4 で速くなる bridging header のインポート

Swift 6.3 までは、LLDB は bridging header を常にソースからコンパイルしており、bridging header を使うデバッグセッションでは無視できない時間がかかることがありました。Swift 6.4 では、LLDB が精密な明示的モジュール情報を使って、プリコンパイル済みの bridging header とその明示的な依存モジュールを直接インポートできるようになります。これにより、bridging header を使うプロジェクトのデバッグが、完全にモジュール化されたプロジェクトと同じくらい速く信頼できるものになります。

まとめ

明示的にビルドされたモジュールに対するこれらの変更により、次が実現されます。

要するに、-modulewrap と -add_ast_path が -debug-module-path に置き換わり、デバッグ情報はより小さく、より正確になります。

関連リンク