Swift Digest
SE-0498 | Swift Evolution

Expose demangle function in Runtime module

Proposal
SE-0498
Authors
Konrad 'ktoso' Malawski, Alejandro Alonso
Review Manager
Steve Canon
Status
Implemented (Swift 6.4)

01 何が問題だったのか

Swift のシンボル名は、型情報などを含めてエンコードするためにネームマングリング(name mangling)が施されます。たとえば String(cString:) のイニシャライザは、マングルされた形だと $sSS7cStringSSSPys4Int8VG_tcfC のような文字列になります。これをデマングル(demangle)すると Swift.String.init(cString: Swift.UnsafePointer<Swift.Int8>) -> Swift.String と、開発者が読めるフォーマットに戻せます。

マングルされたシンボル名はバックトレースやプロファイリングツールの出力などに現れるため、開発者向けに表示する前にデマングルしたいケースが多くあります。ところが Swift にはそのための公式な API がありませんでした。ツール作者は次のいずれかで対応するしかありません。

  • swift-demangle コマンドを別プロセスとして起動する。プロセス生成のコストが高く、バックトレース表示などホットパスでの利用には向きません。
  • Swift ランタイムの非公式な swift_ 名前空間の関数(例: swift_demangle)を直接呼び出す。公式にサポートされていないため将来的に壊れる可能性があり、近年の Swift はこうした関数を直接参照すると警告を出し、将来的にはエラー化する方針を示しています。

つまり「実行中のプロセス内で、公式にサポートされた形で Swift シンボルをデマングルする」手段がないことが問題でした。

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

Runtime モジュールに、Swift ランタイムのデマングラを呼び出す公式な demangle(_:) 関数を追加します。これにより、別プロセスを起動することなく、非公式 API にも頼らずに、実行中のプロセス内でマングルされたシンボルをデマングルできるようになります。

基本の demangle(_:)

もっとも手軽に使えるのは、String を受け取り String を返すオーバーロードです。

public func demangle(_ mangledName: String) throws(DemanglingError) -> String

マングルされた Swift シンボルを渡すと、人間が読めるフォーマットにデマングルした文字列を返します。渡された文字列が有効な Swift シンボルでない場合は DemanglingError を throw します。Swift 5.0 以降の有効なシンボルは $s(プラットフォーム固有のプレフィックスが前に付く場合もあります)で始まります。

import Runtime

let mangled = "$sSS7cStringSSSPys4Int8VG_tcfC"
do {
  let demangled = try demangle(mangled)
  print(demangled)
  // Swift.String.init(cString: Swift.UnsafePointer<Swift.Int8>) -> Swift.String
} catch {
  // 無効なシンボル
}

バッファに書き込むオーバーロード

パフォーマンスが重要な用途向けに、あらかじめ確保したバッファに直接書き込むオーバーロードも用意されます。こちらは UTF8Span を入力に取り、OutputSpan<UTF8.CodeUnit> に書き込みます。

public func demangle(
  _ mangledName: borrowing UTF8Span,
  into output: inout OutputSpan<UTF8.CodeUnit>
) throws(DemanglingError)

public enum DemanglingError: Error {
  /// バッファが足りず結果が切り詰められた。payload は完全にデマングルするのに必要なバイト数。
  case truncated(requiredBufferSize: Int)
  /// 渡されたマングル済みシンボルが無効。
  case invalidSymbol
}

出力バッファが足りない場合は DemanglingError.truncated(requiredBufferSize:) が throw され、完全な結果に必要なバイト数が分かります。このときも OutputSpan にはそこまでに収まった部分結果が入っており、切り詰めは必ず Unicode スカラの境界で行われるため、部分結果は常に有効な UTF-8 になります(途中のコードユニットで切れることはありません)。

OutputSpan から String に変換する典型的なパターンは次のとおりです。UTF8Span(validating:) が UTF-8 として妥当かを検証してくれるので、String への変換経路は安全です。

var demangledOutputSpan: OutputSpan<UTF8.CodeUnit> = /* ... */

do {
  try demangle("$sSG", into: &demangledOutputSpan)
  let utf8 = try UTF8Span(validating: demangledOutputSpan.span)
  let demangledString = String(copying: utf8)
  print(demangledString) // Swift.RandomNumberGenerator
} catch DemanglingError.truncated(let requiredBufferSize) {
  // requiredBufferSize バイト確保してリトライする、など
} catch {
  // 無効なシンボル
}

demangle 自身は UTF-8 の検証を行いません。あえて検証しないことで、返ってきたバイト列をそのまま別処理に回すこともできます。String 化したいタイミングで UTF8Span(validating:) を通すのが推奨の流れです。

デマングル結果の安定性

マングルされた名前は ABI の一部であり、ABI が安定しているプラットフォームでは変わりません。一方、デマングル結果の文字列表現は安定であることを保証しません。Swift のパッチリリースで変わる可能性もあります。したがって、デマングル結果はあくまで「開発者に見せるための読みやすい表現」として扱い、パースして機械的に利用するような使い方は避けるのが前提です。

Future Directions(参考)

将来的な方向性として、今回はスコープ外とされた次の拡張が speculative に挙げられています(実現を約束するものではありません)。

  • デマングルモードのカスタマイズ: Swift のデマングラには本来さまざまなオプションがあり、どの情報をどこまで含めるかを制御できます。将来、options: DemanglingOptions パラメータを追加する形で拡張できる余地が残されています。今回は API として切り出すべきオプションの選定が十分に煮詰まっていないため、最小限の API から始めています。