How to Box a trait that has associated types?
Asked Answered
S

1

12

I'm very new to Rust so I may have terminology confused.

I want to use the hashes crates to do some hashing and I want to dynamically pick which algorithm (sha256, sha512, etc.) to use at runtime.

I'd like to write something like this:

let hasher = match "one of the algorithms" {
    "sha256" => Box::new(Sha256::new()) as Box<Digest>,
    "sha512" => Box::new(Sha512::new()) as Box<Digest>
    // etc...
};

I sort of get that that doesn't work because the associated types required by Digest aren't specified. If I attempt to fill them in:

"sha256" => Box::new(Sha256::new()) as Box<Digest<<OutputSize = U32, BlockSize = U64>>>,

I'm left with an error: the trait 'digest::Digest' cannot be made into an object. I think this approach will fail anyway because match will be returning slightly different types in cases where different algorithms have different associated types.

Am I missing something obvious? How can I dynamically create an instance of something that implements a trait and then hold on to that thing and use it through the trait interface?

Scirrhous answered 29/12, 2017 at 20:0 Comment(3)
You could make a wrapper type and a trait of your own that implements the interface you actually need without associated types. Boxing such a trait will work.Scarletscarlett
Related: How can I have a vector of objects that differ by their associated type?Thrifty
@Scarletscarlett I think that's worthy of an answer.Thrifty
G
7

The message refers to object safety (longer article). The Digest trait has two incompatibilities:

  1. It uses associated types (this can be worked around by explicitly setting all type parameters to values compatible for all Digest objects).
  2. It has a method (fn result(self) -> …) taking self by value. You won't be able to call it, which ruins usability of this trait.

Once a trait object is created, information about its subtype-specific features such as memory layout or associated types is erased. All calls to the trait object's methods are done via a vtable pointer. This means they all must be compatible, and Rust can't allow you to call any methods that could vary in these aspects.

A workaround is to create your custom wrapper trait/adapter that is object-compatible. I'm not sure if that's the best implementation, but it does work:

trait Digest {
    type Assoc;
    fn result(self);
}

struct Sha;

impl Digest for Sha {
    type Assoc = u8;
    fn result(self) {}
}

///////////////////////////////////////////

trait MyWrapper {
    fn result(&mut self); // can't be self/Sized
}

impl<T: Digest> MyWrapper for Option<T> {
    fn result(&mut self) {
        // Option::take() gives owned from non-owned
        self.take().unwrap().result() 
    }
}

fn main() {
    let mut digest: Box<MyWrapper> = Box::new(Some(Sha));
    digest.result();
}
Game answered 2/1, 2018 at 19:2 Comment(1)
I wonder if one could implement result as accepting self: Box<Self> as in this answer. That way the Option would no longer be necessary, and compile-time prevention of calling result twice would be restored.Scarletscarlett

© 2022 - 2024 — McMap. All rights reserved.