Search code examples
rustlow-leveltrait-objects

Is there a way to determine the offsets of each of the trait methods in the VTable?


I thought I could try more or less build a trait object from scratch without using the impl blocks. To elaborate:

trait SomeTrait {
    fn fn_1(&self);
    fn fn_2(&self, a: i64);
    fn fn_3(&self, a: i64, b: i64);
}

struct TraitObject {
    data: *mut (),
    vtable: *mut (),
}

fn dtor(this: *mut ()) {
    // ...
}

fn imp_1(this: *mut ()) {
    // ...
}

fn imp_2(this: *mut (), a: i64) {
    // ...
}

fn imp_3(this: *mut (), a: i64, b: i64) {
    // ...
}

fn main() {
    let data = &... as *mut (); // something to be the object
    let vtable = [dtor as *mut (),
                  8 as *mut (),
                  8 as *mut (),
                  imp_1 as *mut (),
                  imp_2 as *mut (),
                  imp_3 as *mut ()]; // ignore any errors in typecasting,
        //this is not what I am worried about getting right

    let to = TraitObject {
        data: data,
        vtable: vtable.as_ptr() as *mut (),
    };
    // again, ignore any typecast errors,

    let obj: &SomeTrait = unsafe { mem::transmute(to) };

    // ...

    obj.fn_1();
    obj.fn_2(123);
    obj.fn_3(123, 456);
}

From what I understand, the order in which the member functions appear in the trait definition is not always the same as the function pointers appear in the VTable. Is there a way to determine the offsets of each of the trait methods in the VTable?


Solution

  • If you don't mind detecting the layout at runtime, then you can compare the function addresses at specific offsets and compare them to the addresses of a known, dummy implementation to match them up. This assumes that you know how many methods there are in the trait, since you may need to read all of them.

    use std::mem;
    
    trait SomeTrait {
        fn fn_1(&self);
        fn fn_2(&self, a: i64);
        fn fn_3(&self, a: i64, b: i64);
    }
    
    struct Dummy;
    
    impl SomeTrait for Dummy {
        fn fn_1(&self) { unimplemented!() }
        fn fn_2(&self, _a: i64) { unimplemented!() }
        fn fn_3(&self, _a: i64, _b: i64) { unimplemented!() }
    }
    
    struct TraitObject {
        data: *mut (),
        vtable: *mut (),
    }
    
    fn main() {
        unsafe {
            let fn_1 = Dummy::fn_1 as *const ();
            let fn_2 = Dummy::fn_2 as *const ();
            let fn_3 = Dummy::fn_3 as *const ();
    
            let dummy = &mut Dummy as &mut SomeTrait;
            let dummy: TraitObject = mem::transmute(dummy);
            let vtable = dummy.vtable as *const *const ();
            let vtable_0 = *vtable.offset(3);
            let vtable_1 = *vtable.offset(4);
            let vtable_2 = *vtable.offset(5);
    
            // Mapping vtable offsets to methods is left as an exercise to the reader. ;)
            println!("{:p} {:p} {:p}", fn_1, fn_2, fn_3);
            println!("{:p} {:p} {:p}", vtable_0, vtable_1, vtable_2);
        }
    }