Swift Digest
SE-0262 | Swift Evolution

Demangle Function

Proposal
SE-0262
Authors
Alejandro Alonso
Review Manager
Joe Groff
Status
Withdrawn

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> に結果を書き込むバージョンも用意されます。入力は StringUnsafeBufferPointer<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 の形(enumOptionSet か、など)を含めて、いずれも speculative な見通しであり、本提案で具体化するものではありません。

public func demangle(_ input: String, flags: DemangleFlags) -> String?