4

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?

Josh Lee
  • 171,072
  • 38
  • 269
  • 275
Chase Walden
  • 1,252
  • 1
  • 14
  • 31

1 Answers1

3

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);
    }
}
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • That is an approach I initially attempted to use. Do you know if it is possible to determine the offsets of the functions by name? So I could theoretically override functions at runtime? i.e. `override(object, ::fn_1, impl_1)`? – Chase Walden Sep 27 '16 at 00:40
  • I don't think it's possible. In C++, I think you can sort of do this with member function pointers, but Rust doesn't have those. – Francis Gagné Sep 27 '16 at 00:48
  • Damn. Well I thought I'd at least see how far I could push the bounds with TraitObjects and Dynamic Dispatch. This is a pretty good start though. – Chase Walden Sep 27 '16 at 00:52