Async trait implementation for concrete type

Hello, I am writing a simple TCP game server using tokio and have a small question. Is it possible to make the following code work with async and not have to use a fully qualified path?

trait ReadPacket<P> {
    fn read_packet(&mut self) -> P;
}

#[derive(Clone)]
struct Caller;

#[derive(Debug)]
struct Packet1;

#[derive(Debug)]
struct Packet2;

// normally AsyncReadExt + Unpin instead of Clone, changed for demonstration purpose
impl<T> ReadPacket<Packet1> for T
where T: Clone {
    fn read_packet(&mut self) -> Packet1 {
        // read data from socket
        Packet1 {}
    }
}

impl<T> ReadPacket<Packet2> for T
where T: Clone {
    fn read_packet(&mut self) -> Packet2 {
        Packet2 {}
    }
}

#[derive(Debug)]
enum Test {
    Packet1(Packet1),
    Packet2(Packet2)
}

fn main() {
    let mut caller = Caller {};
    let test1 = Test::Packet1(caller.read_packet());
    let test2 = Test::Packet2(caller.read_packet());
    
    println!("{:?}", test1);
    println!("{:?}", test2);
}

This code works fine with "normal" version, but as soon as I make fn read(&mut self) -> P async I need to change simple Test::Packet1(caller.read()) into Test::Packet1(<Caller as ReadPacket<Packet1>>::read(&mut caller).await), which works fine, but is extremely unreadable.

My current recv loop looks something like that:

while let Ok(packet_id) = reader.read_u8().await {
    match packet_id {
        // here I need to do the weird cast as this doesn't compile
        1 => Test::Packet1(reader.read_packet().await),
        ...
    }
}

This seems to me like the compiler’s trait solving not being able to handle the more complex situation with an intermediate Future. However, I think there’s a way to make your code better and solve this problem. Presuming that no ReadPacket implementation ever requires specific properties of T beyond the general ones, it would be better to move the type parameter from the trait to the associated function:

trait ReadPacket {
    async fn read_packet<R: Clone>(stream: &mut R) -> Self;
}
impl ReadPacket for Packet1 {
    async fn read_packet<R: Clone>(_stream: &mut R) -> Self {
        Packet1 {}
    }
}
// ...

Then to recover use of method-call syntax and the inference you want, define an extension trait:

trait ReadExt {
    async fn read_packet<P: ReadPacket>(&mut self) -> P;
}
impl<T: Clone> ReadExt for T {
    async fn read_packet<P: ReadPacket>(&mut self) -> P {
        P::read_packet(self).await
    }
}

Then this main() compiles:

#[tokio::main]
async fn main() {
    let mut caller = Caller {};
    let test1 = Test::Packet1(caller.read_packet().await);
    let test2 = Test::Packet2(caller.read_packet().await);
    
    println!("{:?}", test1);
    println!("{:?}", test2);
}
2 Likes

Thank you, this makes sense. Didn't even occur to me that I could use Ext traits in this way. At first I had read/write implemented on the Test enum instead and had some issues with getting it to work there so I tried over-complicating it :smiley: . Too bad the compiler didn't "understand" whatever I was trying to do, but this is cleaner either way.