Box<dyn T> where T has associated types that are traits

Foreword: I asked this (or a very similar question) on one of the Rust discord servers a few days ago, and after some discussion I ended up going with static dispatch via an enum that also implements T. This works well. Many thanks to the people over there for their assistance. But I also never figured out how to solve my original problem of how to box T correctly when T has associated types that are also traits. I'd like to know how to solve this to further my understanding of Rust. I'm asking again here instead of Discord so I can post a long-form question with a bit more background.


I'm working on a tool that communicates with various i2c devices (temp sensors, etc). The i2c devices are connected to USB->i2c adapters. Many such adapters exist, such as the FT232H and MCP2221. Crates exist for these adapters that work within the embedded_hal ecosystem, such as ftdi_embedded_hal and mcp2221 for the aforementioned adapters, respectively.

I'd like to support more than one adapter type and multiple of each at a time. Which adapters to use and what i2c devices are connected to them are defined in a config file that's loaded at startup.

It's worth nothing that even though I mention i2c and embedded_hal, this tool targets standard Linux (either x86_64 or aarch64) and has full access to the standard library.

Each adapter crate provides an embedded_hal::i2c::I2c implementation. As I'd like to support multiple adapter types, I have to support multiple different implementations of I2c. The I2c trait is also used by all the downstream i2c device driver structs as a generic parameter (for example, see the BME280 driver)

I want to put all the adapter I2c implementations into a HashMap.

let i2c_busses: HashMap<String, Box<dyn I2c<???>>> = HashMap::new()

However, I'm stuck trying to Box I2c...

I2c is defined in embedded_hal (v1.0) as:

pub trait I2c<A: AddressMode = SevenBitAddress>: ErrorType {
    // trait fns
}

pub trait ErrorType {
    type Error: Error;
}

pub trait Error: core::fmt::Debug { .. }

As the associated type Error is a trait, each adapter crate brings along its own implementation. I think I need this to be dyn too?

I'm not sure what type signature I should use for Box.

  • Box<dyn eh1::i2c::I2c> doesn't work:
    |     let b: Box<dyn eh1::i2c::I2c> = Box::new(init_ft232h()?);
    |                    ^^^^^^^^^^^^^ help: specify the associated type: `eh1::i2c::I2c<Error = Type>`
    
  • Box<dyn eh1::i2c::I2c<Error = dyn eh1::i2c::Error>> doesn't work:
    |     let b: Box<dyn eh1::i2c::I2c<Error = dyn eh1::i2c::Error>> = Box::new(init_ft232h()?);
    |                                                                  ^^^^^^^^^^^^^^^^^^^^^^^^ expected `dyn Error`, found `Error<TimeoutError>`
    |
    = note: expected trait object `(dyn embedded_hal::i2c::Error + 'static)`
                      found enum `ftdi_embedded_hal::Error<TimeoutError>`
    = note: required for the cast from `Box<ftdi_embedded_hal::I2c<Ft232h>>` to `Box<dyn embedded_hal::i2c::I2c<Error = (dyn embedded_hal::i2c::Error + 'static)>>`
    

(signature for new_ft232h is: fn init_ft232h() -> Result<ftdi_embedded_hal::I2c<libftd2xx::Ft232h>>)

I think that I need to somehow cast, Box, or wrap the error type. But I'm not sure how to do this.

I also need the Box to also impl the I2c trait so that I can pass it into the downstream i2c driver structs. I think this is done with a new type wrapper that impls I2c and delegates fns it to the Box.

Appreciate any help on this.

Thanks for the reply. And sorry for not using the right terminology. But your reply comes across as very unforgiving.

This makes no sense, no type is ever a trait. Traits are not types, and types are not traits.
An associated type is always a type.

type Error: Error;

pub trait Error

Hence me using the phrase "the associated type Error is a trait". Maybe the wrong phrasing, I dunno...

The compiler tells you exactly what the type is:

Right. But I don't know how to convert, box, cast (or whatever the right term is) Error<TimeoutError> to dyn Error. From what I can tell, ftdi_embedded_hal::Error does impl embedded_hal::i2c::Error (see the docs).

Often to make a trait object safe, you have to make another trait that wraps all the Self instances with Box<dyn Trait>. You can do a similar thing with the associated types.

use std::error::Error;

pub trait I2c {
    type Err: Error;
    fn f(&self) -> Self::Err;
}

pub trait I2cDyn: I2c<Err: 'static> {
    fn f(&self) -> Box<dyn Error + 'static> {
        Box::new(I2c::f(self))
    }
}

impl<T: I2c<Err: 'static>> I2cDyn for T {}

You can do a similar thing with enums, if you choose to use those instead.

2 Likes

"The associated type Error has an eponymous trait bound" or "The associated type Error is bound by an eponymous trait".

But does it also implement your custom trait Error to which the eponymous associated type is bound?

This looks promising and I didn't realise you could do this. I'll play with this. Thanks.

I don't follow, sorry. I don't define my own custom Error trait. That comes from embedded_hal.

Well, obviously the type TimeoutError whatever it is, does not implement the trait Error, wherever it comes from.

You can't. dyn Trait<Assoc = Assoc1> and dyn Trait<Assoc = Assoc2> are unrelated types, and there's no straightforward conversion between them, even if there is between Assoc1 and Assoc2.

Ignoring all the details of this post, I have a couple examples of doing this:


Looking at that definition (and clicking around), I can see that

  • Parameter A has a AddressMode bound and an implicit Sized bound
  • AddressMode is a sealed trait[1] and has two concrete implementors
  • So there are no Sized and type-erased implementors, e.g. Box<dyn AddressMode>
    • And you can't add your own since it's sealed

So type erasing that parameter isn't an option, you're always going to have SevenBitAddress or TenBitAddress (unless the library situation changes).

The supertrait ErrorType is not sealed, and neither is the trait Error. So there's the possibility of type erasure here. Ideally could use Box<dyn Error>, but Box<dyn Error> doesn't implement Error, so you'll need your own newtype instead:

struct AnyError(Box<dyn Error>);

impl Error for AnyError {
    fn kind(&self) -> ErrorKind {
        (*self.0).kind()
    }
}

Now other types can use AnyError in their ErrorType implementations.

Next create some error-erased trait that otherwise mirrors I2c<Addr>, and implement it for all applicable types by mapping their errors into AnyError.

trait AnyI2c<Addr> {
    fn e_write(&mut self, address: Addr, write: &[u8]) -> Result<(), AnyError>;
    // etc
}
impl<Addr, I> AnyI2c<Addr> for I
    where
        Addr: AddressMode,
        I: I2c<Addr, Error: 'static>
{
    fn e_write(&mut self, address: Addr, write: &[u8]) -> Result<(), AnyError> {
        self.write(address, write).map_err(|e| AnyError(Box::new(e)))
    }
}

fn convert<I, Addr>(i2c: I) -> Box<dyn AnyI2c<Addr>>
where
    Addr: AddressMode,
    I: I2c<Addr> + 'static,
{
    Box::new(i2c)
}

Finally, implement the original trait for your erased dyn _ type.

impl<Addr> ErrorType for dyn AnyI2c<Addr> {
    type Error = AnyError;
}

impl<Addr> I2c<Addr> for dyn AnyI2c<Addr>
where
    Addr: AddressMode,
{
    fn write(&mut self, address: Addr, write: &[u8]) -> Result<(), Self::Error> {
        self.e_write(address, write)
    }
}

// impl I2c<Addr> for Box<dyn AnyI2c<Addr>> ...

Now you can use dyn AnyI2c<Addr> and not have to specify ErrorType::Error.


Naming could be better and so on.

I just hammered that out and ignored most of the confusion about overlapping names and general dyn Trait mechanics and so on. Feel free to ask questions.


  1. no one outside of embedded_hal can implement it ↩ī¸Ž

5 Likes

Thank you so much for taking to time to put together such detailed reply. Between this and @drewtato's reply, I was able to figure it out. I had a chance to experiment more over the weekend and now have it working.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.