Dynamic typing in Rust - inheritance and by-hand transmutation
Tags: rust, tutorial
Published: 2024-03-03 08:55:29

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!

```rust
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
}
```

## 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`.

```rust
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
}
```

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](https://github.com/rust-lang/rust/issues/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.

```rust
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
}
```

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.

```rust
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);
}
```

And it's time for the `main` function!

```rust
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());
}
```

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
```

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.