Type Erasure in Rust

Rust traits have the neat property where you can use them either as generic bounds or as dynamic dispatch, with the &dyn MyTrait syntax. The latter is necessary in heterogeneous scenarios, where you want to use multiple concrete types together that all implement a common trait. However, that requires that you have an instance, so that the reference actually “refers” to something. What if you have a trait with “static” requirements, like consts or methods without &self?

If the implementing types have instances, or are just marker types, you can wrap the base trait in a new trait, like in this stripped-down example from my work:

trait Parameters {
  const SECRET_KEY_LENGTH: usize;
  fn generate() -> KeyMaterial<Secret>;
}

trait DynParameters {
  fn secret_key_length(&self) -> usize;
  fn generate(&self) -> KeyMaterial<Secret>;
}

impl<T: Parameters> DynParameters for T {
  fn secret_key_length(&self) -> usize {
    Self::SECRET_KEY_LENGTH
  }

  fn generate(&self) -> KeyMaterial<Secret> {
    Self::generate()
  }
}

Now &dyn DynParameters will work.

However, sometimes the types that conform to the trait don’t have instances to tie a new trait to. If so, take a step back and think about what dynamic dispatch is: a table of trait members that get looked up at run time. You can build a little wrapper type to do this manually:

trait Parameters {
  const SECRET_KEY_LENGTH: usize;
  fn generate() -> KeyMaterial<Secret>;
}

struct AnyParameters {
  secret_key_length: usize;
  generate: fn() -> KeyMaterial<Secret>;
}

impl AnyParameters {
  fn new<T: Parameters>() -> Self {
    Self {
      secret_key_length: T::SECRET_KEY_LENGTH,
      generate: T::generate, // we're not calling it, just referring to it
    }
  }
}

Generics

This is all well and good, but what if the type you need to abstract over isn’t a trait at all, but a generic struct? If the generic is just a marker type, we may be able to get away with substituting our own marker type:

struct KeyMaterial<T> {
  data: Box<[u8]>,
  kind: PhantomData<T>,
}

impl KeyMaterial<()> {
  fn erasing_kind_of<T>(other: KeyMaterial<T>) -> Self {
    Self {
      data: other.data,
      kind: PhantomData,
    }
  }
}

But usually there are some additional operations on the type that we might need. In that case we’ll combine the two techniques: we’ll copy over data from the input struct, and also include a manual dispatch table.

struct KeyMaterial<T> {
  data: Box<[u8]>,
  kind: PhantomData<T>,
}

trait KeyKind {
  fn key_length(key_type: KeyType) -> usize;
}

struct AnyKeyMaterial {
  // Data from KeyMaterial<T>
  data: Box<[u8]>,
  // Operations from T: KeyKind
  key_length: fn(KeyType) -> usize;
}

impl AnyKeyMaterial {
  fn erasing_kind_of<T: KeyKind>(other: KeyMaterial<T>) -> Self {
    Self {
      data: other.data,
      key_length: T::key_length,
    }
  }
}

And now you have a struct that can be used to represent heterogeneous KeyMaterials. (Never mind that the example no longer makes sense.)

This pattern isn’t very complicated, but I didn’t see it written down in one place for Rust, hence this post. (Swift folks who’ve been around for a few years are pretty familiar with similar patterns, though they’re rarer now that Swift’s any types—the equivalent of dyn—aren’t as limited as they were in the past.)