"Dynamic" dispatch and associated types

(Sorry for the long post; not totally sure how to simplify further).

I'm trying to implement a couple of different versions of the same abstract "protocol" against each other.
Imagine this interface:

trait Protocol {
    type Message;
    fn generate(&self) -> Self::Message;
    fn verify(&self, m: Self::Message) -> bool;
}

We want to use it as (in practice, this complicated enough to be an entire program, so I really need to reuse this bit):

fn run_protocol<P: Protocol>(protocol: P) -> bool {
    let message: P::Message = protocol.generate();
    protocol.verify(message)
}

We can give a few trivial implementations (note that in some cases there are protocol parameters, so we do actually need the protocol methods to be methods):

struct Foo;

impl Protocol for Foo {
    type Message = bool;

    fn generate(&self) -> bool {
        true
    }

    fn verify(&self, m: bool) -> bool {
        m
    }
}

struct Bar {
    password: u32,
}

impl Protocol for Bar {
    type Message = u32;

    fn generate(&self) -> u32 {
        self.password
    }

    fn verify(&self, m: u32) -> bool {
        m == self.password
    }
}

And then running is easy:

fn main() {
    let foo = Foo {};
    assert!(run_protocol(foo));

    let bar = Bar { magic: 42 };
    assert!(run_protocol(bar));
}

So far, so good.

Now, I want to have my main() pick a protocol based on a config file, so I try to make a little factory method:

fn make_protocol(is_foo: bool) -> Box<dyn Protocol> {
    if is_foo {
        Box::new(Foo {})
    } else {
        Box::new(Bar { magic: 42})
    }
}

This makes rustc unhappy, as "the value of the associated type Message (from the trait Protocol) must be specified." Makes sense, especially since it'd be impossible to stack-allocate space for Message if you called the boxed protocol's generate() method without knowing what it is.

But what's the right way to this? I can imagine a few ways:

  1. (dynamic dispatch) Make the protocol method return heap-allocated values using Box::new.
    This is a little distracting while implementing Protocol, and doesn't actually solve the underlying problem: I still don't know how to specify the return type of make_protocol.
  2. (simulated dynamic dispatch) Make the protocol return an Enum of all the allowed message types, so it doesn't actually need an associated type. This seems to remove the benefit of having an associated type at all.
  3. (single-dispatch) I could do away with make_protocol altogether, and just have a big match over the contents of the config file that looks a bit like the main() above. This seems really hacky to me but would work and maybe be faster.

In practice, I think I'd want to maybe stick in a little shim struct that uses the above trick we can leave our protocol unmodified. I'm open to other ideas, of course; I'm not enthralled with any of the above.

The problem is that to generate code that deals with an unknown Message type, the compiler must know the size of the message, so if you throw away the knowledge of the specific message, the compiler is not able to generate the necessary to assembly to handle the unknown-size value.

In some cases you can create a helper trait that hides the associated type, e.g.:

trait ProtocolWrapper {
    fn generate_and_verify(&self) -> bool;
}

impl<P: Protocol> ProtocolWrapper for P {
    fn generate_and_verify(&self) -> bool {
        // Type of msg is known here, as we are inside a generic impl block.
        let msg = self.generate();
        self.verify(msg)
    }
}

playground

Thanks for the super-quick reply! I think that's perfect, especially in my use case (we're ultimately going to serialize the various Messages to a single GRPC enum using tonic and prost, so I can make the ProtocolWrapper interface these messages.

I think I had all of the elements floating in my head to do this, but was hazy on the big picture and you put it all together for me; much appreciated.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.