Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic dispatch #3

Open
vhdirk opened this issue Oct 17, 2024 · 2 comments
Open

Dynamic dispatch #3

vhdirk opened this issue Oct 17, 2024 · 2 comments

Comments

@vhdirk
Copy link

vhdirk commented Oct 17, 2024

As mentioned in #2 , I'm working on a pretty elaborate binary protocol. It's been on a bit of a backburner for a while but recentlyI've been able to allocate some time for this again. And I've hit a bit of a roadblock with deku: The protocol I'm implementing is quite extensible. It defines a base set of configuration objects, but most applications will want to add their own configuration objects. I basically look at it as some kind of weird rpc system.

To implement my proof of concept, I was using enums to handle most of the known variations in the protocol. But since enums are by definition closed sets, I am now looking at dynamic dispatch. The goal would by to have something like (pseudo code):

trait Variant: BitRead {}

# this would probably mean implementing 'BitRead' manually
struct SomeData {

   # defines the id of the variant to parse
   variant_id: u8,
   
   variant: Box<dyn Variant>,
}


struct MyParser {
 
  # variant registry should not be static; we'd like to have multiple versions of MyParser, each with a different registry
  variant_registry: Map<u8, Fn( ?? ) -> Box<dyn Variant>>

}

I've been trying to figure this out with deku, but my main issue has thus far been that deku's DekuReader trait exposes the underlying reader type, which makes it very hard to have dynamic dispatch working at all.
How I would pass the registry deep into the parsing is still an unknown.

So this got me looking at bin-proto again. And reading about the tagging system got me excited; it might even be a good match for my specific use-case.

@wojciech-graj
Copy link
Owner

wojciech-graj commented Oct 22, 2024

You can do what you're trying to do as follows

use bin_proto::{BitRead, ByteOrder, ProtocolRead, Result, TaggedRead};
use std::collections::HashMap;
use std::fmt::Debug;

trait Variant {}

struct Container(Box<dyn Variant>);

type F = dyn Fn(&mut dyn BitRead, ByteOrder, &mut Ctx) -> Result<Box<dyn Variant>>;

struct Ctx<'a>(HashMap<u8, &'a F>);

impl<'a, Tag> TaggedRead<Tag, Ctx<'a>> for Container
where
    Tag: TryInto<u8, Error: Debug>,
{
    fn read(
        read: &mut dyn BitRead,
        byte_order: ByteOrder,
        ctx: &mut Ctx<'a>,
        tag: Tag,
    ) -> Result<Self> {
        let tag = tag.try_into().unwrap();
        let constructor = *ctx.0.get(&tag).unwrap();
        let inner = constructor(read, byte_order, ctx)?;
        Ok(Self(inner))
    }
}

You could construct a ctx as follows

#[derive(ProtocolRead)]
struct ConcreteVariant;

impl Variant for ConcreteVariant {}

impl ConcreteVariant {
    fn new<'a>(
        read: &mut dyn BitRead,
        byte_order: ByteOrder,
        ctx: &mut Ctx<'a>,
    ) -> Result<Box<dyn Variant>> {
        Ok(Box::new(<Self as ProtocolRead<_>>::read(
            read, byte_order, ctx,
        )?))
    }
}

let mut ctx = Ctx(HashMap::from([(42, &ConcreteVariant::new as &F)]));

The only issue I'll have to fix is that if you have a parent struct containing Container, like so, you're unable to specify lifetime parameters for the context. I'll fix that soon

#[derive(ProtocolRead)]
#[protocol(ctx = Ctx<'a>, ctx_generics('a))] 
struct Root {
    tag: u8
    #[protocol(tag = tag)]
    container: Container,
}

@wojciech-graj
Copy link
Owner

With release 0.6.0 you can now do what I described in my previous comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants