DEV Community

ProgramCrafter
ProgramCrafter

Posted on

Dynamic typing in Rust - inheritance and by-hand transmutation

In the previous article, I've shown that some variables can have type that is determined (beyond that it's trait object) only at runtime. Let me remind how it went!

trait Animal {
  fn say(&self) -> &'static str;
  fn name(&self) -> &'static str;
}

struct Cat;
impl Animal for Cat {
  fn say(&self) -> &'static str {"meow"}
  fn name(&self) -> &'static str {"cat"}
}

struct Lion;
impl Animal for Lion {
  fn say(&self) -> &'static str {"roar"}
  fn name(&self) -> &'static str {"lion"}
}

fn main() {
  let cat = Cat;
  let animal: &dyn Animal = &cat;

  println!("{}", animal.say());  //  meow
}
Enter fullscreen mode Exit fullscreen mode

Does Rust have inheritance mechanism?

It doesn't. Let's make our trait Animal subtrait of std::any::Any and try casting reference &dyn Animal to &dyn Any.

use std::any::Any;

trait Animal : Any {
  fn say(&self) -> &'static str;
  fn name(&self) -> &'static str;
}

struct Cat;
impl Animal for Cat {
  fn say(&self) -> &'static str {"meow"}
  fn name(&self) -> &'static str {"cat"}
}

struct Lion;
impl Animal for Lion {
  fn say(&self) -> &'static str {"roar"}
  fn name(&self) -> &'static str {"lion"}
}

fn main() {
  let cat = Cat;
  let animal: &dyn Animal = &cat;

  let r: &dyn Any = animal;
    //              ^^^^^^
    // error[E0658]: cannot cast `dyn Animal` to `dyn Any`, trait
    //   upcasting coercion is experimental
}
Enter fullscreen mode Exit fullscreen mode

Our code doesn't compile: we cannot use subinterfaces interchangeably with their base traits. So, if we have arrays [&dyn Animal] and [&usize], we cannot merge them into [&dyn Any].

Is that a design decision or simply not-yet-implemented feature?

If you actually compile the code above, you see that error message links to Rust issue #65991 for more information: in short, upcasting is a feature not yet complete. Then, what's so hard there?

Reference of kind &dyn SomeTrait or &[ArrayValue] is actually two pointers long, and called "wide reference" (sometimes also "fat"). The first pointer there is the object's address, as is normal, and the second one contains some metadata. In case of trait objects, that's pointer to vtable.

What is a vtable?

It contains methods that can be invoked on the object. For instance, for trait Animal : Any the methods are Any::type_id, Animal::name and Animal::say. There are also three additional values, namely function drop_in_place (used to destroy object that has unknown size without moving it), type size and alignment.

To look at these, though, we'll need two things: "function labeller", converting function pointer to string, and a bit of unsafe code.

fn inspect_fn_ptr(f: usize) -> &'static str {
  if f == Cat::type_id  as usize {return "Cat::type_id";}
  if f == Lion::type_id  as usize {return "Lion::type_id";}

  if f == Cat::say  as usize {return "Cat::say";}
  if f == Cat::name as usize {return "Cat::name";}
  if f == Lion::say  as usize {return "Lion::say";}
  if f == Lion::name as usize {return "Lion::name";}
  "?"
}

use std::ptr::{read, write};
use std::mem::size_of;
unsafe fn inspect_vtable(reference: &dyn Animal) -> usize {
  let pr = &reference as *const &dyn Animal as *const usize;

  println!();
  println!("Inspecting metadata of animal {}", reference.name());
  println!("Size of reference: {}", size_of::<&dyn Animal>());

  const PTR_SIZE: usize = size_of::<usize>();
  let meta = read(pr.offset(1));

  println!("First  {PTR_SIZE} bytes are {}", read(pr));
  println!("Vtable {PTR_SIZE} bytes are {}", meta);

  let pm = meta as *const usize;
  println!("Vtable[0] = {} [{}::drop_in_place?]", read(pm), reference.name());
  println!("Vtable[1] = {} [size?]", read(pm.offset(1)));
  println!("Vtable[2] = {} [alignment?]", read(pm.offset(2)));
  println!("Vtable[3] = {} [{}]", read(pm.offset(3)), inspect_fn_ptr(read(pm.offset(3))));
  println!("Vtable[4] = {} [{}]", read(pm.offset(4)), inspect_fn_ptr(read(pm.offset(4))));
  println!("Vtable[5] = {} [{}]", read(pm.offset(5)), inspect_fn_ptr(read(pm.offset(5))));
  println!();

  meta
}
Enter fullscreen mode Exit fullscreen mode

Let's also create function that can replace pointer to vtable. Obviously it is unsafe since type we've got metadata from can be incompatible.

unsafe fn change_vtable(reference: &mut &dyn Animal, meta: usize) {
  let pr = reference as *mut &dyn Animal as *mut usize;
  write(pr.offset(1), meta);
}
Enter fullscreen mode Exit fullscreen mode

And it's time for the main function!

use std::hint::black_box;
fn main() {
  let cat = Cat;

  // black boxing just because reference becomes invalid later
  // and I don't need optimizations inlining Cat everywhere
  let mut animal: &dyn Animal = black_box(&cat);

  unsafe {inspect_vtable(animal)};
  println!("{}", animal.say());

  unsafe {change_vtable(&mut animal, inspect_vtable(&Lion))};
  println!("{}", animal.say());
}
Enter fullscreen mode Exit fullscreen mode

The output I have got is here:

Inspecting metadata of animal cat
Size of reference: 16
First  8 bytes are 140725137267895
Vtable 8 bytes are 93885456622352
Vtable[0] = 93885456308624 [cat::drop_in_place?]
Vtable[1] = 0 [size?]
Vtable[2] = 1 [alignment?]
Vtable[3] = 93885456308512 [Cat::type_id]
Vtable[4] = 93885456308656 [Cat::say]
Vtable[5] = 93885456308672 [Cat::name]

meow

Inspecting metadata of animal lion
Size of reference: 16
First  8 bytes are 93885456560128
Vtable 8 bytes are 93885456622432
Vtable[0] = 93885456308624 [lion::drop_in_place?]
Vtable[1] = 0 [size?]
Vtable[2] = 1 [alignment?]
Vtable[3] = 93885456308480 [Lion::type_id]
Vtable[4] = 93885456308688 [Lion::say]
Vtable[5] = 93885456308704 [Lion::name]

roar
Enter fullscreen mode Exit fullscreen mode

So, we've successfully "transmuted" cat to lion. Now it's quite clear why trait upcasting is not that easy: it requires to rearrange the whole vtable.

Top comments (0)