DEV Community

Cover image for Type system in Rust - subtypes without inheritance? pwned?
ProgramCrafter
ProgramCrafter

Posted on

Type system in Rust - subtypes without inheritance? pwned?

Are there features in Rust type system beyond runtime-determined types of trait objects and ability to transmute them into others? Maybe some types have subtyping relation, which allows to safely cast them to super-s?

A fair warning: we're delving into The Rustonomicon, called also The Dark Arts of Unsafe Rust.

THE KNOWLEDGE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF UNLEASHING INDESCRIBABLE HORRORS THAT SHATTER YOUR PSYCHE AND SET YOUR MIND ADRIFT IN THE UNKNOWABLY INFINITE COSMOS.

Also, we will be assuming considerable prior knowledge, mostly on topic of references (wide and thin) and their lifetimes.

🦀 There are most definitely sub- and supertypes.

Cases of subtyping in Rust

trait Vehicle {}
trait Car : Vehicle {}

struct Chevle;
impl Vehicle for Chevle {}
impl Car for Chevle {}
Enter fullscreen mode Exit fullscreen mode

Trait objects

  1. &Chevle (read-only reference) may be casted either to &dyn Car or to &dyn Vehicle. After all, if we have read access to our car, then it's also access to an unspecified kind of car or a vehicle.

    Why mutable references can't be casted so?
    Let's suppose we have garage-variable that holds Chevle. If we were able to create &mut dyn Car reference to it, we'd be able to put Kia there, and after ref lifetime expiry Kia would stand in garage for Chevle!

  2. &dyn Car to &dyn Vehicle, according to the same rationale (planned, since that requires changing vtables in wide pointers).

Lifetimes

fn bar<'a>() -> &'a str {
    let s: &'static str = "dev.to";
    s
}
Enter fullscreen mode Exit fullscreen mode
  1. References have lifetimes; a reference may have its lifetime shortened.

    Why mutable references can change here?
    I will demonstrate this with a sample code.
    fn foo() {
      /* 'x */ {
        let mut i = 1;
        let p: &mut usize = &mut i;  // &'x mut usize
        /* 'y */ {
          let mut j = 4;
          p = &mut j;    // cannot assign twice to immutable variable `p`
        }
        *p += 1;
      }
    }
    fn bar() {
      /* 'x */ {
        let mut i = 1;
        let mut p: &mut usize = &mut i;  // &'x mut usize
        /* 'y */ {
          let mut j = 4;
          p = &mut j;
        }  // `j` does not live long enough, since its borrow `p` is used below 
        *p += 1;
      }
    }
    

Generic types

That is a complex subject perhaps best explained by its page in The Rustonomicon.

  • F is covariant over T if T being a subtype of U implies that F is a subtype of F (subtyping "passes through")
  • F is contravariant over T if T being a subtype of U implies that F is a subtype of F
  • F is invariant over T otherwise (no subtyping relation can be derived)

A Rust safety bug

static UNIT: &'static &'static () = &&();

fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }

fn bad<'a, T>(x: &'a T) -> &'static T {
    let f: fn(_, &'a T) -> &'static T = foo;
    f(UNIT, x)
}
Enter fullscreen mode Exit fullscreen mode

There's a code that promotes reference having any lifetime to 'static - one outliving the program, thus valid for its entire run - without memory allocation. You may notice that there's an ignored parameter creating unchecked assertion that 'b outlives 'a, which is the root of this issue. There are quite a few variants how to do this, so please remain aware (or don't use double references)!

Top comments (0)