Resolving not-object-safe error with trait having async methods

I have the following MWE:

Which tries to instantiate a Arc<dyn BlobStorage<FileWriter>> and call save_blob on it.
When I remove all async statements, it compiles

but using async I dont get the compiler warnings for the following code:

use std::{
    io,
    path::{Path, PathBuf},
    sync::Arc,
};
pub trait Writer: Send + Sync {
    async fn save(&self, dest: &Path) -> Result<(), io::Error>;
}

/// Simple interface which provides access to blob storage
/// in different components of the application.
pub trait BlobStorage<W: Writer + Sized>: Sync + Send {
    async fn store_blob(&self, writer: W) -> Result<(), io::Error>;
}

/// ======================================================================
/// A simple file writer.
pub struct FileWriter {
    file: PathBuf,
}

/// We implement the Writer trait.
impl Writer for FileWriter {
    async fn save(&self, dest: &Path) -> Result<(), io::Error> {
        std::fs::copy(&self.file, dest);
        Ok(())
    }
}

/// ======================================================================
pub struct Storage;

impl<W> BlobStorage<W> for Storage
where
    W: Writer,
{
    async fn store_blob(&self, storer: W) -> Result<(), io::Error> {
        storer.save(&PathBuf::from("path-where-to-store"));
        Ok(())
    }
}

fn main() {
    let f = FileWriter {
        file: PathBuf::from("myfile.txt"),
    };

    let p = Arc::new(Storage {});
    let interface = p.clone() as Arc<dyn BlobStorage<FileWriter>>;

    let ff = interface.store_blob(f);
    futures::executor::block_on(ff);
}

Output:

error[E0038]: the trait `BlobStorage` cannot be made into an object
  --> components/common/src/bin/test/main.rs:45:34
   |
45 |     let interface = p.clone() as Arc<dyn BlobStorage<FileWriter>>;
   |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `BlobStorage` 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>
  --> components/common/src/bin/test/main.rs:14:14
   |
13 | pub trait BlobStorage<W: Writer + Sized>: Sync + Send {
   |           ----------- this trait cannot be made into an object...
14 |     async fn store_blob(&self, writer: W) -> Result<(), io::Error>;
   |              ^^^^^^^^^^ ...because method `store_blob` is `async`
   = help: consider moving `store_blob` to another trait
   = help: only type `Storage` is seen to implement the trait in this crate, consider using it directly instead
   = note: `BlobStorage` can be implemented in other crates; if you want to support your users passing their own types here, you can't refer to a specific type

The explanation rustc --explain E0038 is very very good but does not contain anything about async.

Would really appreciate if somebody can enligthen my small brain :blush: .

See this recent thread. The generic parameter W adds a wrinkle, but if it's acceptable to limit it to non-borrowing (: 'static satisfying) types, the erased version of the trait could be:

pub trait BlobStorageErased<W: Writer + Sized>: Sync + Send {
    fn store_blob(&self, writer: W) 
    -> 
        Pin<Box<dyn '_ + Future<Output = Result<(), io::Error>> /* + Send + Sync */>>
    ;
}

impl<T: ?Sized + BlobStorage<W>, W: Writer + 'static> BlobStorageErased<W> for T {
    fn store_blob(&self, writer: W) 
    -> 
        Pin<Box<dyn '_ + Future<Output = Result<(), io::Error>> /* + Send + Sync */>>
    {
        Box::pin(self.store_blob(writer))
    }
}
-    let interface = p.clone() as Arc<dyn BlobStorage<FileWriter>>;
+    let interface = p.clone() as Arc<dyn BlobStorageErased<FileWriter>>;

Uch, thats a mouth full. Thanks for the details.

I understand only little of it.

  • Why the Pin returning, and not only Box?
  • Is that essentially what the proc macro #[async_trait] does?.
  • Why is that when I remove the generic W it so far works in current nightly, 1.76?

The goal is to return something implementing Future, but Box<T> only implements Future if T: Future + Unpin + ?Sized. Unpin is not a supertrait of Future, so dyn Future doesn't implement Unpin, so Box<dyn Future> doesn't implement Future.

But Pin<P> implements Future if P: DerefMut<T> where <T as Deref>::Target: Future, so Pin<Box<dyn Future>> implements Future.

It's part of the gnitty-gritty details around futures being self-referential and thus requiring extra promises around when they can and can't be moved in order to be sound.

Pretty much.

Have a link? This still fails.

Sorry I was wrong only compiles when async is removed. jeah but then its a different story.

I was also unsure why the following still compiles even tough 'static is used?
How can the local let f = FileWriter {... be 'static or do I understand 'static wrongly...

Same code but with a dispatch to a function

Probably that. Lifetime bounds on types such as

where W: 'static

are not restrictions on the liveness scope of values. Instead they're conditions on "where" the types are valid. Effectively, a 'static bound means the type has no non-'static lifetimes;[1] in practical terms, this generally means there are no[2] references involved.

So for example, FileWriter: 'static because it involves no lifetimes or generic parameters at all. This doesn't mean you can't drop values of type FileWriter or anything like that.

You can read more about the misconception here, though that page uses the term "lifetime" both for the liveness scope of a value and for those '_ things, which I try to avoid due to confusions such as this.


  1. or type parameters that aren't 'static, etc ↩︎

  2. non-'static ↩︎

1 Like

Thanks a lot. Very good answer!

For interested readers: Here is a follow up: Passing Fn closure to async trait method

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.