TypeIdの中身を取り出す
Rustの標準ライブラリにはstd::any::TypeId
という型があり、個別の型に対応する一意なIDとして利用できます。
std::cmp::Eq
やstd::cmp::Ord
、std::hash::Hash
などが実装されているため、そのまま辞書のキーにするなどは可能なのですが、この記事ではこれの中身を(無理やり、ただし安全に)取り出すことを考えます。
注意点として、この記事で書かれていることはTypeId
の内部実装に依存しており、将来TypeId
の内部構造が変更になった時に挙動がおかしくなる可能性があります。
なぜ内部状態が必要なのか
標準ライブラリのTypeId
を見ると、このような構造になっています。
pub struct TypeId {
t: u64,
}
ここでt
にpub
などが付いていれば[1]話が早いのですが、残念ながらこのt
は隠蔽されています。
隠蔽されているからにはRust開発メンバーは内部状態が直接利用されることを考えていないでしょうし、こんな記事を書いておいてなんですが私も基本的には直接利用すべきではないと考えています。
それでも内部状態が欲しくなるケースがあって、
- トライ木のキーとして使う
- FFIやシリアライズして通信する時に使う
というパターンが考えられます。
トライ木は一種の連想配列として使うことができるデータ構造ですが、キーをバイト列として扱うことができる必要があります。
例えば、qp_trie
というクレートは、トライ木の一種であるQP-trieの実装ですが、キーがBorrow<[u8]>
であることを要求します。
したがってTypeId
のままではキーとして扱えませんが、内部状態を取り出せばそれをキーにすることができます。
FFIや通信に使う場合は、例えば(Vec<TypeId>, std::collections::HashMap<TypeId, usize>)
のような構造を用意して、出入り口で変換を噛ますという手も無くはないです。
ですが、単なる一意なIDとして、短期間の(保存して後から読み込む、などを行わない)利用であれば、内部状態をそのまま使っても問題はないはずだし、変換コストもかからずに済みます。
改めて言うまでもないかもしれませんが、「トライ木を使わずにHashMapを使えばいい」「大したコストじゃないのだから変換を噛ます方でいい」という判断も悪い訳ではありません。多くの場合はそれで事足りるでしょう。筆者がパフォーマンス厨なだけと言われればそうです。
std::hash::Hasher
を実装する
実際に何をやるかというと、TypeId
がHash
を実装していることを悪用します。
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()
}
こんな感じです。Hasher
はwrite
とfinish
を実装すれば良いのですが、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 }
}
これ以上いけない。
Discussion
Rust好きなのですが、コア部位の名付けが独自過ぎて、
vscodeなどエディターのsuggestionで分からない事が多い...
この記事、助かりました。