As I've written about before, one of the major features we're working on adding to Rust is to allow async functions in traits. Today we have support in nightly for async methods in traits in static contexts. This lets you write code like the following:

trait AsyncCounter {
    async fn get_value(&self) -> usize;
    async fn increment_by(&mut self, amount: usize);
}

async fn use_counter(mut counter: impl AsyncCounter) -> usize {
    counter.increment_by(42).await;
    counter.get_value().await;
}

This empowers a lot of use cases, but we also want to support this feature in dynamic dispatch contexts. In other words, we'd like to be able to write use_counter like this:

async fn use_counter(mut counter: &mut dyn AsyncCounter) -> usize {
    counter.increment_by(42).await;
    counter.get_value().await;
}

While this looks like a straightforward extension on what we already have, I've been surprised by the amount of additional functionality needed to support this change.

It turns out there are also a lot of design questions that in my mind do not have an obviously right answer. There are cases to be made for many different points in the design space, but ultimately the right one will depend on what people use in practice.

In this post, I'd like to explore the space for one of these questions.

The Problem🔗

One of the things that's tricky about supporting async fn in dyn traits (AFIDT) is that we need a standard way to talk about the return types that will work for any implementation of the trait. The plan is to use dyn* to do this. So when using something as a trait object and it has async methods, we'll change the return types of all of the async methods to be dyn* Future instead of impl Future. The nice thing about dyn* Future is that it can refer to any Future, like dyn Future can, but that it doesn't care what kind of pointer it's using, or if it is even a pointer. This is in contrast to dyn Future, which is hard to use by itself since dyn Future is unsized and is instead almost always used as a pointer like Pin<Box<dyn Future>>. This gives us a lot more options for where to store the future returned by an async method and means we can use async trait objects even in contexts where heap allocation is not available.

But, most of the time when you implement a trait your async methods won't return dyn* or even something that is immediately coercible to dyn*. Instead, your async method will usually return what is essentially a closure and the size of that closure will depend on what parts of your stack frame need to be live across await points.

So for AFIDT to actually be useful, we need an ergonomic way to make the futures returned by a function into a dyn* Future.

There are a lot of ways we could do this. In the rest of this post I'd like to enumerate some possibilities.

Make the Trait Do It🔗

We already have a notion of object-safe traits, so already if you want to be able to use a trait as an object then you have to be a little careful with what you do with the trait. This version just makes you be more careful if you want a dyn-safe trait with async methods.

The object-safe version of AsyncCounter would look like:

async trait AsyncCounter {
    fn get_value(&self) -> dyn* Future<Output = usize> + '_;
    fn increment_by(&mut self, amount: usize) -> dyn* Future<Output = ()> + '_;
}

Then anyone who implements this would have to write out the same type signatures and they could pick what kind of dyn* they'd like to return.

This doesn't really solve the problem though, since it's not so much "async fn in dyn traits" as it is "arrow dyn star future fn in dyn trait." ADSFIDT would make a good entry in the most ridiculous Rust acronym contest though!

This approach also means that we'd lose all the benefits of static dispatch when using the trait in a static context.

Make the Implementor Do It🔗

In this version, instead of decided at the trait level whether it can be used as a trait object, we'd decide whether a given impl can be used as a trait object. In other words, we'd write AsyncCounter in the straightforward way using async fn but then we'd do the impl differently. This works because impls are allowed to specify a more specific return type than the trait requires.

One of these dyn-safe impls would look something like:

impl AsyncCounter for MyType {
    fn get_value(&self) -> dyn* Future<Output = usize> + '_ {
        Box::pin(async { ... })
    }

    fn increment_by(&mut self, amount: usize) -> dyn* Future<Output = usize> + '_ {
        Box::pin(async { ... })
    }
}

The Box:pin wouldn't be necessary; the impl could use anything it wanted as long as the compiler could coerce it to dyn*.

To me this seems to be an improvement over the previous option. You decide on a per-type basis whether to support dyn Trait, and if you chose not to then in static contexts you get the benefits of static dispatch.

Use an Artisanal Wrapper🔗

We can generalize our previous solution a little more. We could provide a wrapper that takes something that implements AsyncCounter and provides a new implementation that returns dyn* from the async methods.

The result looks like this:

struct AsyncCounterObject<T>(T);

impl<T: AsyncCounter> AsyncCounter for AsyncCounterObject {
    fn get_value(&self) -> dyn* Future<Output = usize> + '_ {
        Box::pin(self.0.get_value())
    }

    fn increment_by(&mut self, amount: usize) -> dyn* Future<Output = ()> + '_ {
        Box::pin(self.0.increment_by(amount))
    }
}

So then if I have some x that implements AsyncCounter and I want to pass it to a function foo that takes a &mut dyn AsyncCounter, I can call it like this:

let x = AsyncCounterObject(x);
foo(&mut x);

I've called these "artisanal wrappers" because you have to write one of these by hand for each trait you want to use in a dynamic context. We can relax this requirement with some of the upcoming options.

The Compiler Implicitly Boxes For You🔗

This version might be the easiest for users of all. You write your trait with async fns, the impls can use async fn, and you don't have to use any wrappers. You just pass your object into a function that takes a dyn Trait object and everything just works. Behind the scenes, when you convert a value into a dyn Trait, the compiler generates a wrapper like the one we wrote in the last section.

From an ease of use standpoint, this isn't too bad. From a performance and control standpoint, it's not so good. Also, folks like Niko Matsakis and Josh Triplett have argued that this approach goes against the soul of Rust because would be the first time the language implicitly introduced heap allocation.

Universal Wrappers🔗

This approach seems to combine the strengths of the previous two options. We stay true to the soul of Rust by explicitly opting in to the behavior we want, but this approach also doesn't require writing a special wrapper for each trait that you might use as a trait object. With this option, Rust would provide adapters like Boxing that would do the boxing transformation described in the previous section for you. For more about what this approach looks like, see the User's Guide from the Future's section on this adapter.

This has the be provided by Rust, because there's not a way to write a generic adapter like this in Rust today. I hope that we would have that some day, but it will probably require esoteric features like generic trait arguments, so I think it's a ways off. In the mean time, I think it makes sense to get the benefit from having a version provided by compiler magic and if we can implement it in the language in the future then migrating makes sense.

Decide at the Call Site🔗

So far all the approaches have decided what to do with the futures returned by async methods at the point where an value is made into a trait object or earlier. What if we wanted to push this back later? Maybe some code wants to store a trait object's return futures on the stack in some places and in the heap in others? Can we decide later?

It's technically possible, but I think it's mostly infeasible. We'd have to adjust the calling convention so that the caller and callee can coordinate to decide where the return value should go. This could be something like the caller first asks "what's the size and alignment of your return value?" Or the caller could pass an allocator in to the callee. I'm not sure what this looks like in practice, although I suppose it could provide some more power around unsized return types.

I don't know, maybe this idea is worth exploring more, but that's for later.

Conclusion🔗

We've just seen six different ways we could adapt the futures returned by an async method into a dyn* object. These all have different tradeoffs, including ergonomics, flexibility, performance and control, and feasibility to implement at all. I don't see a clear best answer, although I think versions that rely on some kind of wrapper are probably the best given the tools we have available to us. Were it possible to decide at the call site, that seems even better, but this doesn't seem feasible at the moment.

One thing that stands out to me is that many, if not all, of these potential solutions could coexist, and many of them arise naturally from features the language already has. This suggests that maybe the solution isn't a question of language design, but of idioms and culture. For example, boxing at the trait definition site, trait implementation site, or using an artisanal wrapper can all be used in the same language and selected based on the needs of the particular program. If we add compiler generated wrappers like Boxing<T>, then these would also fit into this framework. So maybe the right solution is to provide a few tools and examples and see what the Rust ecosystem settles on.

Ultimately though, the most important thing is to experiment, to write programs using async traits, and to see which options work best in the real world!