Swift Digest
SE-0419 | Swift Evolution

Swift Backtrace API

Proposal
SE-0419
Authors
Alastair Houghton
Review Manager
Steve Canon
Status
Accepted

01 何が問題だったのか

プログラム実行中の任意のタイミングでコールスタックを取得できると、テストフレームワークやロギング、ライブラリやアプリケーション側の診断処理などで役立ちます。しかし、コールスタックを正しく取り出す処理はプラットフォーム依存で実装も難しく、Swiftの標準機能としては提供されていませんでした。

そのため、これまではサードパーティ製のパッケージに頼る必要がありました。ただし既存のパッケージには次のような課題があります。

  • async フレーム(await をまたぐ Task のスタック)を追跡できない、あるいは対応が限定的
  • 対応プラットフォームが限られている(Linux / Windows 中心のものなど)
  • シンボル解決(symbolication)やdemanglingまで含めた一貫した機能が揃っていない
  • 利用するだけで依存関係が増え、コマンドラインツールやサーバーサイドアプリケーションへの組み込みが煩雑になる

バックトレース取得はSwiftのランタイムが本来最も自然に担える機能であり、他の言語では言語/ランタイム組み込みで提供されていることも多い領域です。そこで、プログラム実行中に明示的にバックトレースを取得・整形するための公式APIが求められていました。

02 どのように解決されるのか

新しい Runtime モジュールに Backtrace 型を追加し、現在地点からのバックトレース取得と、その後のシンボル解決(symbolication)をプログラムから行えるようにします。Runtimeimport することで利用できます。

import Runtime

var backtrace = try Backtrace.capture()
print(backtrace)

if let symbolicated = backtrace.symbolicated() {
  print(symbolicated)
}

ここで提供されるAPIは async-signal-safe ではなく、汎用のクラッシュレポータを組み立てる用途には向かない 点に注意が必要です。想定しているのは、通常実行中にプログラムから明示的にバックトレースを取得するユースケースです。

Backtrace

BacktraceFrame の列を保持する struct で、CustomStringConvertible / Codable / Sendable に適合します。

public struct Backtrace: CustomStringConvertible, Codable, Sendable {
  public var architecture: String
  public var frames: some Sequence<Frame> { get }
  public var images: [Image]?
  // ...
}

frames は配列ではなく Sequence として公開されており、将来的に差分圧縮などの省メモリな表現を採用できる余地を残しています。images は、バックトレース取得アルゴリズムがプロセスにマップされたイメージ情報も同時に収集した場合にだけ埋められます。埋まっていない場合は、あとから Backtrace.captureImages() を別途呼び出して取得できます。

アドレスの表現

フレーム内のプログラムカウンタは、生のポインタではなく Backtrace.Address というopaqueな型で持ちます。

public struct Address: Comparable, Hashable, Codable, Sendable,
                       LosslessStringConvertible,
                       ExpressibleByIntegerLiteral {
  var bitWidth: Int { get }
  var isNull: Bool { get }
}

これは、アドレスが別プロセスや別アーキテクチャのものである可能性があるためで、ポインタとしてデリファレンスしてはいけないという設計意図を型で表しています。必要に応じて FixedWidthInteger への変換を経由して整数として扱えます。

extension FixedWidthInteger {
  init?(_ address: Backtrace.Address)
}

フレームの種類

Backtrace.Frame は列挙型で、通常の関数呼び出しのリターンアドレスに加え、シグナルハンドラなどで捕捉した正確なプログラムカウンタ、await に対応する async resume point、フレーム省略や打ち切りを表す不連続点を区別できます。

public enum Frame: CustomStringConvertible, Codable, Sendable {
  case programCounter(Address)     // 正確なPC(シグナルハンドラなど)
  case returnAddress(Address)      // 通常の関数呼び出しのリターンアドレス
  case asyncResumePoint(Address)   // async タスクの await 再開点
  case omittedFrames(Int)          // 省略されたフレーム数が分かっている不連続
  case truncated                   // 末尾で長さ不明に打ち切られた場合

  public var originalProgramCounter: Address { get }
  public var adjustedProgramCounter: Address { get }
}

asyncResumePoint があるため、Task をまたぐ async な呼び出し列も追跡できます。

バックトレースの取得

現在地点からのバックトレースは Backtrace.capture(...) で取得します。

@inline(never)
public static func capture(algorithm: UnwindAlgorithm = .auto,
                           limit: Int? = 64,
                           offset: Int = 0,
                           top: Int = 16) throws -> Backtrace
  • algorithm.auto / .fast(フレームポインタを辿る高速な方法)/ .precise(Darwin・ELFではEH unwind情報、WindowsではWin32 API)から選べます。
  • limit はフレーム数の上限で、nil を渡すと無制限になります。
  • offset は先頭からスキップするフレーム数で、ヘルパ関数経由で capture を呼ぶ場合に不要なフレームを除くのに便利です。capture 自身は結果に含まれません。
  • top は、limit で打ち切る際に必ずスタックトップ側に残すフレーム数です。深い再帰で途中を省略しつつ、呼び出し元の情報はきちんと残すといった使い方を想定しています。

ランタイムにロードされているイメージの一覧は、バックトレースとは独立に取得することもできます。

public static func captureImages() -> [Image]

Backtrace.Image はイメージ名・パス・uniqueID(DarwinではLC_UUID、Linuxではbuild ID)・ベースアドレスなどを保持します。

symbolication

アドレスからシンボル名を解決する処理(symbolication)は一般にコストが高く、フレーム単位よりもバックトレース全体に対してまとめて行う方が効率的です。そのため別の型 SymbolicatedBacktrace を返す設計になっています。

public func symbolicated(with images: [Image]? = nil,
                         options: SymbolicationOptions = .default)
  -> SymbolicatedBacktrace?

SymbolicationOptions はオプションセットで、次の項目を制御します。

  • .showInlineFrames: インライン化された関数呼び出しも仮想フレームとして表示
  • .showSourceLocations: ソース位置(ファイル・行・列)の解決。コストがかかる場合があり、たとえばKubernetes上のPodをクラッシュ時に素早く再起動させたい、といった状況ではオフにしたいことがあります。
  • .useSymbolCache: シンボルキャッシュが利用可能なら使う

デフォルトはこれら3つを有効にした .default です。

SymbolicatedBacktrace

SymbolicatedBacktrace は、もとの Backtrace と各フレームに対するシンボル情報を保持します。

public struct SymbolicatedBacktrace: CustomStringConvertible, Codable, Sendable {
  public var backtrace: Backtrace
  public var frames: some Sequence<Frame> { get }
  public var images: [Backtrace.Image]
  public var isSwiftRuntimeFailure: Bool { get }

  public struct Frame: CustomStringConvertible, Codable, Sendable {
    public var captured: Backtrace.Frame { get }
    public var symbolInfo: SymbolInfo? { get }
    public var isInline: Bool { get }
    public var isSwiftRuntimeFailure: Bool { get }
    public var isSwiftThunk: Bool { get }
    public var isSystem: Bool { get }
  }

  public struct SymbolInfo: CustomStringConvertible, Codable, Sendable {
    public var image: Backtrace.Image { get }
    public var rawName: String { get }    // demangle前
    public var name: String { get }       // demangle後
    public var offset: Int { get }
    public var sourceLocation: SourceLocation? { get }
    public var isSwiftRuntimeFailure: Bool { get }
    public var isSwiftThunk: Bool { get }
    public var isSystem: Bool { get }
  }

  public struct SourceLocation: CustomStringConvertible, Codable, Sendable {
    var path: String { get }
    var line: Int { get }
    var column: Int { get }
  }
}

SymbolInfo.name には demangle 済みの人間可読なシンボル名が入ります。isSwiftRuntimeFailure はゼロ除算や算術オーバーフローなどSwiftランタイムが捕捉した失敗を、isSwiftThunk はSwiftが生成したthunk関数を、isSystem はランタイム初期化コードやSwift Concurrencyの内部サポートなど、ユーザーにとって通常ノイズとなるフレームを表します。表示の整形時にこれらを使ってフィルタできます。

Future Directions

今回のAPIでは意図的にスコープに含められなかった機能もあり、今後の追加候補として言及されています(speculativeであり、実現を約束するものではありません)。

  • 別の手段で集めたアドレス配列から Backtrace を組み立てる機能
  • バックトレース表示の高度な整形
  • 別スレッドや別プロセスからのバックトレース取得

これらは当面SPIとして実装されうる可能性があり、後日APIに昇格するかもしれません。