Rustのvtableの内部構造

trait objectは、型情報を忘れるかわりにvtableへの参照を持ち回すことで動的ディスパッチを実現している。

vtableを生成するコードはrustc_trans::meth 112行目にある。これによると、vtableの構造は以下のとおり。

  • 0番目: Drop Glue をあらわす関数ポインタ (デストラクタ)
  • 1番目: 元の型のバイト数
  • 2番目: 元の型のアラインメント
  • 3番目以降: メソッドへのポインタ (宣言順)
    • Self: Sized 制約のついたメソッドに対応するスロットには0が代入される。

trait objectはSizedではないので、値をコピー渡しすることはなく、fat pointerで渡す。fat pointerの0番目は、もとのデータと同じ先頭番地を指すポインタで、fat pointerの1番目が、vtableの先頭番地を指すポインタである。

実際にvtableを手動で取り出して実験してみたものが以下のコードである。

use std::mem::transmute;

trait Foo {
    fn fn1(&self, u32);
    fn fn2(&self, u32) where Self: Sized;
    fn fn3(&self, u32);
}

#[derive(Debug)]
struct S1(u64, u64, u64);

impl Foo for S1 {
    fn fn1(&self, x: u32) {
        println!("fn1({:?}, {})", self, x);
    }
    fn fn2(&self, x: u32) where Self: Sized {
        println!("fn2({:?}, {})", self, x);
    }
    fn fn3(&self, x: u32) {
        println!("fn3({:?}, {})", self, x);
    }
}

impl std::ops::Drop for S1 {
    fn drop(&mut self) {
        println!("drop({:?})", self);
    }
}

struct FooVtable {
    drop_glue: fn(&mut S1),
    size: usize,
    align: usize,
    fn1_ptr: fn(&S1, u32),
    fn2_ptr: fn(&S1, u32),
    fn3_ptr: fn(&S1, u32),
}

fn recover_S1(foo: &mut Foo) -> (&mut S1, &'static FooVtable) {
    unsafe { transmute(foo) }
}

fn main() {
    let mut x = S1(3, 4, 5);
    let foo : &mut Foo = &mut x;
    let (xx, vtbl) = recover_S1(foo);
    println!("vtbl.drop_glue = {:x}", vtbl.drop_glue as usize);
    println!("vtbl.size = {:x}", vtbl.size);
    println!("vtbl.align = {:x}", vtbl.align);
    println!("vtbl.fn1_ptr = {:x}", vtbl.fn1_ptr as usize);
    println!("vtbl.fn2_ptr = {:x}", vtbl.fn2_ptr as usize);
    println!("vtbl.fn3_ptr = {:x}", vtbl.fn3_ptr as usize);
    println!("S1::fn1 = {:x}", S1::fn1 as usize);
    println!("S1::fn2 = {:x}", S1::fn2 as usize);
    println!("S1::fn3 = {:x}", S1::fn3 as usize);
    (vtbl.drop_glue)(xx);
    (vtbl.fn1_ptr)(xx, 88);
    (vtbl.fn3_ptr)(xx, 188);
}
vtbl.drop_glue = 55c14c8def80
vtbl.size = 18
vtbl.align = 8
vtbl.fn1_ptr = 55c14c8df0c0
vtbl.fn2_ptr = 0
vtbl.fn3_ptr = 55c14c8df340
S1::fn1 = 55c14c8df0c0
S1::fn2 = 55c14c8df200
S1::fn3 = 55c14c8df340
drop(S1(3, 4, 5))
fn1(S1(3, 4, 5), 88)
fn3(S1(3, 4, 5), 188)
drop(S1(3, 4, 5))

drop glueは別として、メソッドテーブルに入っている関数ポインタは元のメソッドの使い回しであることがわかる。

むろん、このような荒技を実験以外に使うのは好ましくない。