Error agnostic use of embedded_io_async::Read (and ::Write)

Hi all,

First time poster here. I’ve been working as an embedded software developer for about 25 years, mostly C but also some C++. Harbouring a wish to work with a modern programming language before my career ends in another fifteen years (give or take), I'm now exploring Rust.

Started a first project on a Raspberry Pi Pico, going with embassy_rp, no_std, and async goodness.

In the project, there is a protocol stack that encodes/decodes messages (enums with payload) to/from datagrams (arrays of u8). These datagrams can be carried over all sorts of data links (serial, ethernet, etc.) so it made sense to make the stack keep references to some implementation of one or more traits that provide the actual functions to read from/write to the link. Instead of rolling my own traits, I figured I'd go with existing ones, and embedded_io_async::{Read, Write} looked like they'd fit the bill.

To illustrate what I'm trying to do, here is a somewhat reasonably sized example.

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp as _;
use embassy_time as _;

use defmt_rtt as _;
use panic_probe as _;

use embedded_io_async::{Error, ErrorKind, ErrorType, Read, Write};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let data_link = MyDataLink::default();
    let protocol_stack = MyProtocolStack::new(&data_link, &data_link);
}

struct MyProtocolStack<'a> {
    reader: &'a dyn Read,
    writer: &'a dyn Write,
}

impl<'a> MyProtocolStack<'a> {
    fn new(reader: &'a impl Read, writer: &'a impl Write) -> Self {
        MyProtocolStack { reader, writer }
    }

    async fn send(&self) {
        self.writer.write([0x01]).await
    }
}

#[derive(Default)]
struct MyDataLink {}

impl Read for MyDataLink {
    async fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Self::Error> {
        Err(MyDataLinkError::NotImplemented)
    }
}

impl Write for MyDataLink {
    async fn write(&mut self, _buf: &[u8]) -> Result<usize, Self::Error> {
        Err(MyDataLinkError::NotImplemented)
    }
}

impl ErrorType for MyDataLink {
    type Error = MyDataLinkError;
}

#[derive(Debug)]
enum MyDataLinkError {
    NotImplemented,
}

impl Error for MyDataLinkError {
    fn kind(&self) -> ErrorKind {
        match self {
            Self::NotImplemented => ErrorKind::Unsupported,
        }
    }
}

As the name indicates, MyProtocolStack is the protocol stack. MyDataLink represents some data link, and it implements the Read and Write traits. (Oh, and please forgive the slightly condescending My... naming, In examples like this it just gives a better picture of what I've made up vs. the work of others.

When I compile, I get this:

error[E0191]: the value of the associated type `Error` in `ErrorType` must be specified
  --> src/main.rs:20:21
   |
20 |     reader: &'a dyn Read,
   |                     ^^^^ help: specify the associated type: `Read<Error = Type>`

error[E0191]: the value of the associated type `Error` in `ErrorType` must be specified
  --> src/main.rs:21:21
   |
21 |     writer: &'a dyn Write,
   |                     ^^^^^ help: specify the associated type: `Write<Error = Type>`

... so clearly there is something about the usage/implementation of Error and ErrorType that I don't get. If I follow the advice from the compiler and redefine the protocol stack thus:

struct MyProtocolStack<'a> {
    reader: &'a dyn Read<Error = MyDataLinkError>,
    writer: &'a dyn Write<Error = MyDataLinkError>,
}

the compiler gets even more upset and tells me:

error[E0038]: the trait `embedded_io_async::Read` cannot be made into an object
  --> src/main.rs:20:17
   |
20 |     reader: &'a dyn Read<Error = MyDataLinkError>,
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `embedded_io_async::Read` cannot be made into an object
   |

... but I don't really want to go this way anyway, because putting implementation details about the trait where it is being referenced sort of defeats the purpose of using a trait in the first place.

So ... there is obviously something here that I'm not "getting" and I would very much appreciate if you could help set me straight. What am I missing? What would proper use of these traits look like?

Also, given my background, it's quite possible that I'm trying to do this in a way that's totally against Rust canon. If so, an indication of how I should go about it instead would be much appreciated.

Thanks in advance, and all the best,

Fredrik

This is because traits like Read with async methods cannot currently be made into objects, meaning they can't be used with the dyn keyword. So regardless of your other questions, your first next step is probably to find a way to avoid using &'a dyn Read and &'a dyn Write.

Here is another answer that has more details on that feature:

You probably need some no_std way of having owned type-erased types. (The std/alloc way is Box<dyn Trait>, but that allocates.) Unfortunately I don't have a recommendation.

Or something else that emulates Box<dyn Trait>. (I have zero experience with that crate too, just an example of something that exists.)

Hi all,

Thanks for your suggestions. Summer holiday is here and my time available to faff with computers is limited, so it's probably going to be a while before I get to do any further experiments with this. I'll let you know how things develop when I do.

Best regards,

Fredrik