wojciech-graj / bin-proto

Simple bit-level protocol definitions in Rust.
MIT License
17 stars 1 forks source link

Dynamic dispatch #3

Open vhdirk opened 1 month ago

vhdirk commented 1 month ago

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 commented 1 month ago

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 commented 1 week ago

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