Rustのマングリング(名前修飾)

Rustの生成したネイティブコードを見ると、 _ZN6thread5sleep20h87eee61de4645181cAbE のようなシンボル名が見える。この例は std::thread::sleep に対応している。このようにRustの(単相化された)アイテム(関数や変数など)の名前をリンカが認識できる文字列にエンコードする処理はマングリングと呼ばれている。

Rustのマングリングは一見してわかるようにC++互換になっている。実際に上の関数をデマングルすると

$ c++filt _ZN6thread5sleep20h87eee61de4645181cAbE
thread::sleep::h87eee61de4645181cAb

と、それらしき名前が出てくる。

しかし、これは表層上C++互換になっているというだけで、Rustのマングリングの本質はこのハッシュ部分 h87eee61de4645181cAb に依拠している。

以下、Rust 1.15.1の rustc_trans::back::symbol_names に基づいて、Rustのマングリングについて説明する。これはコンパイラのバージョンアップにより情報が古くなる可能性がある。

DefPathを計算する

まず、Rustのアイテムの単相化前の部分を一定の形にエンコードする必要があるが、これには rustc::hir::map::definitions::DefPath が使われる。

DefPathは簡単に言うと、 std::result::Result::map のように、単相化前のアイテムの位置を、crateのルートからの相対パスとして保持したものである。ただしそれだけでは関数内関数など、同じ名前で複数のアイテムを定義できる場合がある。そのため、 foo[1]::bar[0], foo[1]::bar[1] といった感じで番号 (disambiguator) を与えて区別する。disambiguatorは0から順に振られるため、通常は 0 である。

DefPathの定義を抜粋すると以下のようになる。

pub struct DefPath {
    pub data: Vec<DisambiguatedDefPathData>,
    pub krate: CrateNum,
}

pub struct DisambiguatedDefPathData {
    pub data: DefPathData,
    pub disambiguator: u32
}

pub enum DefPathData {
    CrateRoot,
    InlinedRoot(Box<InlinedRootPath>),
    Misc,
    Impl,
    TypeNs(InternedString),
    ValueNs(InternedString),
    Module(InternedString),
    MacroDef(InternedString),
    ClosureExpr,
    TypeParam(InternedString),
    LifetimeDef(InternedString),
    EnumVariant(InternedString),
    Field(InternedString),
    StructCtor,
    Initializer,
    Binding(InternedString),
    ImplTrait
}

DefPathDisambiguatedDefPathDataHash をderiveしているが、今回のハッシュ化に DefPath:Hash の実装は使わない。というのも krate: CrateNum は内部的なu32のidで表現されており、これをそのまま使うとコンパイラの実行ごとにハッシュが安定しないからである。

安定ハッシュを用意する

Rustにはオブジェクトをハッシュ化するための Hash というトレイトが用意されているが、通常のハッシュ実装ではアーキテクチャ依存性が発生するためにクロスコンパイルで問題が生じる可能性がある。というのも、 Hash が利用する Hasher は以下のようなインターフェースを持ち、……

pub trait Hasher {
    fn finish(&self) -> u64;
    fn write(&mut self, bytes: &[u8]);

    fn write_u8(&mut self, i: u8) { ... }
    fn write_u16(&mut self, i: u16) { ... }
    fn write_u32(&mut self, i: u32) { ... }
    fn write_u64(&mut self, i: u64) { ... }
    fn write_usize(&mut self, i: usize) { ... }
    fn write_i8(&mut self, i: i8) { ... }
    fn write_i16(&mut self, i: i16) { ... }
    fn write_i32(&mut self, i: i32) { ... }
    fn write_i64(&mut self, i: i64) { ... }
    fn write_isize(&mut self, i: isize) { ... }
}

……しかもこれらのデフォルト実装は、「現在のアーキテクチャエンディアンでバイト列に変換して write に突っ込む」となっているからである。

Rustのコンパイラはこれに対処するため、 isizeusize を使っても常に同じようにハッシュされるHasherである rustc_data_structures::stable_hasher::StableHasher を用意している。これはblake2bのラッパーだが以下のように動作する:

  • i8u8 は1バイトとして書き込む。
  • 残りの全ての整数型は、リトルエンディアンの128bit整数に変換して書き込む。

型パラメーターとあわせてハッシュ化する

64bitの安定ハッシャーを用意して、以下の順に突っ込む。

  • DefPath (ただしcrateが安定するように工夫してある)
  • アイテム自身の型 (単相化する前)
  • 多相ならば、その型代入

ハッシュを64bitで取り出し、16進表記して接頭辞 h をつける。 (例: h87eee61de4645181cAb)

飾り付けをする

再び、アイテムの絶対パスを計算して、先ほど生成したハッシュと連結する。 (例: thread/sleep/h87eee61de4645181cAb)

サニタイズする

変な文字が入っているかもしれないので、簡単な文字だけからなるように変換する。

  • @$SP$
  • *$BP$
  • &$RF$
  • <$LT$
  • >$GT$
  • ($LP$
  • )$RP$
  • ,$C$
  • :.
  • -.
  • aからz, AからZ, 0から9, _, ., $ → そのまま
  • それ以外のUnicode文字 → $u2764$ のようにコードポイントを16進数で

C++互換でマングリングする

_ZN + (文字数を10進数で + 文字列) ×任意個 + E

できあがり

以上の処理は rustc_trans::trans_item::TransItem::compute_symbol_name で使われている。

まとめ

Rustはアイテムの位置と型引数をできるだけ安定性の高い方法でハッシュ化し、それをシンボル名として使っている。読みやすさのために、C++互換の情報も付与されている。

ハッシュは h + 64bit で17文字のはずなのに実例をみると20文字になっている原因がまだ把握できていない。