4

Type Erasure in Rust

 8 months ago
source link: https://belkadan.com/blog/2023/10/Type-Erasure-in-Rust/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.)

This entry was posted on October 23, 2023 and is filed under Technical. Tags: Rust


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK