Trait + async function that returns another trait

Let's say I have a trait that corresponds to a DB/ORM and Factory trait (that knows which DB to create and how). This is how it could look like:

pub trait Factory {
   async fn create_db_from(&mut self, data: bytes::Bytes) -> Result<Box<dyn Db>>;
}

pub trait Db {
   // things like, select, insert, etc...
}


// Some class that needs to (re-)create Db's via Factory:
struct SomeClass {}
impl SomeClass {
   pub fn init(_factories: Box<dyn Factory>) -> Self { todo!() }
}

The problem is that I am getting this error:

error[E0038]: the trait `Factory` cannot be made into an object
  --> ripdup/src/meta.rs:56:32
   |
56 |    pub fn init(_factories: Box<dyn Factory>) -> Self { todo!() }
   |                                ^^^^^^^^^^^ `Factory` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> ripdup/src/meta.rs:46:13
   |
45 | pub trait Factory {
   |           ------- this trait cannot be made into an object...
46 |    async fn create_db_from(&mut self, data: bytes::Bytes) -> Result<Box<dyn Db>>;
   |             ^^^^^^^^^^^^^^ ...because method `create_db_from` is `async`
   = help: consider moving `create_db_from` to another trait

What is the correct way to achieve this?
(I am fine to use unstable features, if there is something relevant)

Looks like traits with async methods aren't object safe. There's probably a reason for it, or maybe another object safety RFC is needed first.

Either way, you can create trait objects from async traits that use the async_trait attribute. Example.

Edit: I found the roadmap issue for dynamic dispatch for traits with async methods here. According to the milestone, the async working group is targeting version 1.82 (October 17th 2024) to release it.

2 Likes

Async methods are impl-Trait-returning methods in disguise, and impl-Trait-returning trait methods are unnameable-associated-type-returning methods in disguise, and methods returning associated types cannot be object-safe, so supporting object-safe async methods is not quite trivial. It however does appear to be in the pipeline.

Niko recently wrote:

Dyn async trait: Besides send bounds, the other major limitation for async fn in trait is that traits using them do not yet support dynamic dispatch. We should absolutely lift this, but to me it’s lower in priority because there is an existing workaround of using a proc-macro to create a DynAsyncTrait type. It’s not ideal, but it’s not as fundamental a limitation as send bounds or the lack of async closures and async drop. (That said, the design work for this is largely done, so it is entirely possible that we land it this year as a drive-by piece of work.)

5 Likes

(Somewhat an inlined version of what @jdahlstrom said.)

Associated items of traits become generic parameters of dyn Trait. For example, if you want a type erased iterator, you have a dyn Iterator<Item = SomeType>. The population of implementors gets partitioned by the associated items.

On top of that, only non-generic (and non-opaque) associated types are supported so far.[1] At a minimum we need a way to name GATs, but probably also a way to name opaque types (TAIT in trait or such).

But if you think about it, due to the partitioning, it's not actually useful on its own for opaque-returning methods (like async fn), because every implementor has a different output type.

If you want it to be useful for that use case, you have to type erase the opaque thing as well:

pub trait ErasedFactory {
    fn create_db_from(
        &mut self,
        data: bytes::Bytes,
    ) -> Pin<Box<dyn '_ + Future<Output = Result<Box<dyn Db>>>>>;
}

impl<T: ?Sized + Factory> ErasedFactory for T {
    fn create_db_from(
        &mut self,
        data: bytes::Bytes,
    ) -> Pin<Box<dyn '_ + Future<Output = Result<Box<dyn Db>>>>> {
        Box::pin(<Self as Factory>::create_db_from(self, data))
    }
}

See also.


  1. Aside from GATs, the other main non-supported items are associated consts. ↩︎

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.