Demangle Function
01 何が問題だったのか
Swift コンパイラは、シンボル名を一意に表すために mangled name(例: $sSS7cStringSSSPys4Int8VG_tcfC)にエンコードします。クラッシュログやバックトレース、リンカ出力などで目にするのはこの mangled 形式ですが、人間が読むには demangle した形(例: Swift.String.init(cString: Swift.UnsafePointer<Swift.Int8>) -> Swift.String)に変換する必要があります。
コマンドラインには swift-demangle ツールがあり、ターミナル上での変換は容易です。しかし、実行中の Swift プロセス内で mangled name を demangle したい場合、標準ライブラリには直接のAPIがありません。そのため、Foundation の Process を使ってサブプロセスとして swift-demangle を起動する、といった回りくどい方法を取らざるを得ませんでした。
Swift ランタイム自体には swift_demangle という C関数が存在し、_T / _T0 / $S / $s で始まるシンボルを受け付けます。標準ライブラリからこの機能を素直に公開すれば、プロセス内での demangle は本来もっと簡単にできるはずだ、というのがこの提案の出発点です。
本提案のステータス
本提案はレビューの結果 Returned for revision となり、最終的に Withdrawn となりました。同じ目的を扱う後続提案として SE-0498: Runtime demangle function が提出されています。以下では、当時 SE-0262 として提案されていた API 形を紹介します。
02 どのように解決されるのか
標準ライブラリにトップレベルの demangle 関数を追加し、ランタイムの swift_demangle を通じて mangled name を Swift の文字列に変換できるようにします。用途に応じて3つのオーバーロードが用意されます。
String を返す基本形
最もシンプルな形は、String を受け取って String? を返すバージョンです。入力が有効な mangled Swift シンボルでなければ nil を返します。
public func demangle(_ input: String) -> String?
print(demangle("$s8Demangle3FooV")!) // Demangle.Foo
新しい文字列をアロケートすることを気にしない場面では、これが最も扱いやすい形です。
事前確保したバッファに書き込む形
大量に demangle する、あるいは新しいヒープアロケーションを避けたいといった場面のために、事前に確保した UnsafeMutableBufferPointer<Int8> に結果を書き込むバージョンも用意されます。入力は String と UnsafeBufferPointer<Int8> の2通りから選べます。
public func demangle(
_ mangledNameBuffer: UnsafeBufferPointer<Int8>,
into buffer: UnsafeMutableBufferPointer<Int8>
) -> DemangleResult
public func demangle(
_ input: String,
into buffer: UnsafeMutableBufferPointer<Int8>
) -> DemangleResult
結果は専用の DemangleResult 列挙型で返されます。
public enum DemangleResult: Equatable {
// demangle に成功した
case success
// バッファが足りず途中までしか書けなかった。
// ペイロードは完全な結果を格納するのに必要なバイト数。
case truncated(Int)
// 入力が有効な Swift mangled シンボルではなかった
case invalidSymbol
}
invalidSymbol の場合、バッファには何も書き込まれません。truncated(required) の場合は、required が「完全な結果を書くのに必要な総バイト数(NUL終端含む)」を表します。途中までは書き込まれているので、部分結果をそのまま使うこともできますし、差分(required - buffer.count)だけ追加確保して再実行することもできます。
// Swift.Int requires 10 bytes = 9 characters + 1 null terminator
// Give this 9 to exercise truncation
let buffer = UnsafeMutableBufferPointer<Int8>.allocate(capacity: 9)
defer { buffer.deallocate() }
if case let .truncated(required) = demangle("$sSi", into: buffer) {
print(required) // 10
print(required - buffer.count) // 1
}
print(String(cString: buffer.baseAddress!)) // Swift.In (T が切れている)
成功した場合のバッファの使い方は次のとおりです。
// Demangle.Foo は 13 文字 + NUL終端 1 バイト
let buffer = UnsafeMutableBufferPointer<Int8>.allocate(capacity: 14)
defer { buffer.deallocate() }
let result = demangle("$s8Demangle3BarV", into: buffer)
guard result == .success else {
switch result {
case let .truncated(required):
print("We need \(required - buffer.count) more bytes!")
case .invalidSymbol:
print("I was given a faulty symbol?!")
default:
break
}
return
}
print(String(cString: buffer.baseAddress!)) // Demangle.Foo
受け付けられる入力
実装はランタイムの swift_demangle に委譲されるため、受け付けられる mangled シンボルは _T / _T0 / $S / $s のいずれかで始まるものに限られます。それ以外は nil または .invalidSymbol となります。
Future Directions
swift_demangle には現状未使用の flags パラメータが存在します。将来このフラグが意味を持つようになった場合には、flags: 引数を取るオーバーロードを追加して公開する、という拡張方向が示唆されています。DemangleFlags の形(enum か OptionSet か、など)を含めて、いずれも speculative な見通しであり、本提案で具体化するものではありません。
public func demangle(_ input: String, flags: DemangleFlags) -> String?