🦀

TypeIdの中身を取り出す

2 min read

Rustの標準ライブラリにはstd::any::TypeIdという型があり、個別の型に対応する一意なIDとして利用できます。
std::cmp::Eqstd::cmp::Ordstd::hash::Hashなどが実装されているため、そのまま辞書のキーにするなどは可能なのですが、この記事ではこれの中身を(無理やり、ただし安全に)取り出すことを考えます。

注意点として、この記事で書かれていることはTypeIdの内部実装に依存しており、将来TypeIdの内部構造が変更になった時に挙動がおかしくなる可能性があります。

なぜ内部状態が必要なのか

標準ライブラリのTypeIdを見ると、このような構造になっています。

pub struct TypeId {
    t: u64,
}

ここでtpubなどが付いていれば[1]話が早いのですが、残念ながらこのtは隠蔽されています。

隠蔽されているからにはRust開発メンバーは内部状態が直接利用されることを考えていないでしょうし、こんな記事を書いておいてなんですが私も基本的には直接利用すべきではないと考えています。
それでも内部状態が欲しくなるケースがあって、

  1. トライ木のキーとして使う
  2. FFIやシリアライズして通信する時に使う

というパターンが考えられます。

トライ木は一種の連想配列として使うことができるデータ構造ですが、キーをバイト列として扱うことができる必要があります。
例えば、qp_trieというクレートは、トライ木の一種であるQP-trieの実装ですが、キーがBorrow<[u8]>であることを要求します。
したがってTypeIdのままではキーとして扱えませんが、内部状態を取り出せばそれをキーにすることができます。

FFIや通信に使う場合は、例えば(Vec<TypeId>, std::collections::HashMap<TypeId, usize>)のような構造を用意して、出入り口で変換を噛ますという手も無くはないです。
ですが、単なる一意なIDとして、短期間の(保存して後から読み込む、などを行わない)利用であれば、内部状態をそのまま使っても問題はないはずだし、変換コストもかからずに済みます。

改めて言うまでもないかもしれませんが、「トライ木を使わずにHashMapを使えばいい」「大したコストじゃないのだから変換を噛ます方でいい」という判断も悪い訳ではありません。多くの場合はそれで事足りるでしょう。筆者がパフォーマンス厨なだけと言われればそうです。

std::hash::Hasherを実装する

実際に何をやるかというと、TypeIdHashを実装していることを悪用します。
Hasherを実装した型を用意して、Hash::hash()を呼んでやると、Hasher::write()などで内部状態由来の値を受け取ることができる、という仕組みです。

#[derive(Default)]
struct TypeIdHasher(u64, u32);
impl Hasher for TypeIdHasher {
    fn write(&mut self, bytes: &[u8]) {
        for &b in bytes {
            self.0 ^= (b as u64).rotate_left(self.1);
            self.1 = (self.1 + 8) % 64;
        }
    }
    fn write_u64(&mut self, i: u64) {
        self.0 ^= i.rotate_left(self.1);
    }
    fn finish(&self) -> u64 {
        self.0
    }
}

fn type_id_inner(type_id: &TypeId) -> u64 {
    let mut hasher = TypeIdHasher::default();
    type_id.hash(&mut hasher);
    hasher.finish()
}

こんな感じです。Hasherwritefinishを実装すれば良いのですが、TypeIdの内部状態がu64なので、write_u64が呼ばれることを期待してこうしています。
なお、この実装だと、例えばTypeIdの内部状態がu128になると、TypeIdHasherの内部状態が足りないので衝突が生じたりしますし、write_u64ではなくwriteが呼ばれた時にビッグエンディアンの環境だと元の内部状態と異なる値が取れるかもしれません[2]

同じような手段で、内部が非公開だけどHashが実装されている型から内部状態を取り出すことができます。やるべき、とは言いませんが。

おまけ:安全じゃない方法

fn type_id_inner(type_id: &TypeId) -> u64 {
    let ptr = type_id as *const TypeId as *const u64;
    unsafe { *ptr }
}

これ以上いけない。

脚注
  1. まあpubが付いていたら内部状態を書き換えられてしまうので付くことはないと思いますが。例えばBorrow<u64>でも実装されていればそれでも良い ↩︎

  2. これに関しては、少なくとも情報量は同じなので、単なる一意なIDとしての利用するだけなら問題にはなり得ません ↩︎