async fundamentals initiative

initiative status: active

What is this?

This page tracks the work of the async fundamentals initiative, part of the wg-async-foundations vision process! To learn more about what we are trying to do, and to find out the people who are doing it, take a look at the charter.

Current status

This is an umbrella initiative and, as such, it covers a number of subprojects.

See the roadmap for a list of individual milestones and their status.

SubprojectIssueProgressStateStatus
async fn#50547β–°β–°β–°β–°β–°βœ…stable
static async fn in trait#91611β–°β–°β–±β–±β–±πŸ¦€accepted rfc
dyn async fn in traitβ€“β–°β–±β–±β–±β–±πŸ¦€planning rfc
async dropβ€“β–°β–±β–±β–±β–±πŸ¦€not started
async closuresβ€“β–°β–±β–±β–±β–±πŸ’€not started

Key:

  • βœ… – phase complete
  • πŸ¦€ – phase in progress
  • πŸ’€ – phase not started yet

How Can I Get Involved?

  • Check for 'help wanted' issues on this repository!
  • If you would like to help with development, please contact the owner to find out if there are things that need doing.
  • If you would like to help with the design, check the list of active design discussions first.
  • If you have questions about the design, you can file an issue, but be sure to check the FAQ or the design discussions first to see if there is already something that covers your topic.
  • If you are using the feature and would like to provide feedback about your experiences, please open a "experience report" issue.
  • If you are using the feature and would like to report a bug, please open a regular issue.

We also participate on Zulip, feel free to introduce yourself over there and ask us any questions you have.

Building Documentation

This repository is also an mdbook project. You can view and build it using the following command.

mdbook serve

✏️ Updates

Lang-team initiatives give monthly updates. This section collects the updates from this initiative for posterity.

2021-Oct: Lang team update

  • Owner: tmandry
  • Liaison/author: nikomatsakis, currently (looking for a replacement)

Although the async fundamentals initiative hasn't technically formed yet, I'm going to write an update anyhow as "acting liaison". To start, I would like to find another liaison! I think that I am a bit close to the work here and the group would benefit from a liaison who is a bit more distant.

Our overall charter: Make it possible to write async fn in traits, as well as enabling key language features that bring async more into parity with sync:

  • Async functions in traits
    • in both static and dyn contexts
  • Async drop
  • Async closures

This is a key enabler for most of the async vision doc. For example, the various interop traits (e.g., async iteration, async read, async write, etc) all build on async functions in traits.

We have identified an MVP, which aims to support async fn in traits in static contexts by desugaring to an (anonymous) associated GAT plus (on the impl side) a TAIT. We are preparing an RFC describing this MVP and talking to various folks about doing the implementation work.

We are assembling a group of stakeholders that we will talk to in order to get feedback on the MVP and on future design decisions (in addition to the lang team and so forth).

In addition to the MVP, we are drafting an evaluation doc that identifies further challenges along with possible solutions. Once we feel good about the coverage for a particular challenge, we will create targeted RFCs for that specific item.

One specific direction of interest is creating core enablers that can be used to experiment with the most ergonomic syntax or capabilities. As an example, for dyn async traits, there is a need to return some form of "boxed dyn future", but there are many runtiem techniques one might use for this (the most obvious being to return a Box, of course). Supporting those options requires being able to manipulate vtables and the like. It may be an optional to make those kind of "core capabilities" available as simple primitives. This would allow us to experiment with a procedural macro that generates an easy to use wrapper built on these primitives; once we have a clear idea what exactly that should be, we can bring it into the language. (It remains to be seen if this is a better path than trying to build the say thing first and work out the primitives later.)

Niko has also been writing blog posts to walk through the dyn logic in more detail (starting at part 1 and continuing in part 2).

πŸ“œ async fn fundamentals Charter

This initiative is part of the wg-async-foundations vision process.

Proposal

async fn exists today, but does not integrate well with many core language features like traits, closures, and destructors. We would like to make it so that you can write async code just like any other Rust code.

Goals

Able to write async fn in traits and trait impls

The goal in general is that async fn can be used in traits as widely as possible:

  • for foundational traits, like reading, writing, and iteration;
  • for async closures;
  • for async drop, which is built in to the language;
  • in dyn values, which introduce some particular complications;
  • in libraries, for all the usual reasons one uses traits;
  • in ordinary programs, using all manner of executors.

Key outcomes

Support async drop

Users should be able to write "async fn drop" to declare that the destructor may await.

Key outcomes

  • Types can perform async operations on cleanup, like closing database connections
  • There's a way to detect and handle async drop types that are dropped synchronously
  • Await points that result from async cleanup can be identified, if needed

Support async closures

Support async closures and AsyncFn, AsyncFnMut, AsyncFnOnce traits.

Key outcomes

  • Async closures work like ordinary closures but can await values
  • Traits analogous to Fn, FnMut, FnOnce exist for async
  • Reconcile async blocks and async closures

Membership

Stakeholders

Stakeholder representatives

The pitch

The async fundamentals initiative is developing designs to bring async Rust "on par" with synchronous Rust in terms of its core capabilities:

  • async functions in traits (our initial focus)
  • async drop (coming later)
  • async closures (coming later)

We need feedback from people using Rust in production to ensure that our designs will meet their needs, but also to help us get some sense of how easy they will be to understand. One of the challenges with async Rust is that it has a lot of possible variations, and getting a sense for what kinds of capabilities are most important will help us to bias the designs.

We also want people to commit to experimenting with these designs while they are on nightly! This doesn't mean that you have to ship production software based on the nightly compiler. But it does mean that you agree to, perhaps, port your code over to use the nightly compiler on a branch and tell us how it goes. Or experiment with the nightly compiler on other codebases you are working on.

Expected time commitment

  • One 90 minute meeting per month + written feedback
    • Stucture:
      • ~30 minute presentation covering the latest thoughts
      • ~60 minutes open discussion with Tyler, Niko, other stakeholders
    • Written feedback:
      • answer some simple questions, provide overall perspective
      • expected time: ~30 minutes or less
  • Once features become available (likely early next year), creating branch that uses them
    • We will do our best to make this easy
    • For example, I expect us to offer an alternative to async-trait procedural macro that generates code that requires the nightly compiler
    • But this will still take some time! How much depends a bit on you.
    • Let's guess-timate 2-3 hours per month

Benefits

  • Shape the design of async fn in traits
  • Help ensure that it works for you
  • A t-shirt!

Goals of the stakeholder program

The goal of the stakeholder program is to make Rust's design process even more inclusive. We have observed that existing mechanisms like the RFC process or issue threads are often not a very good fit for certain categories of users, such as production users or the maintainers of large libraries, as they are not able to keep up with the discussion. As a result, they don't participate, and we wind up depriving ourselves of valuable feedback. The stakeholder program looks to supplement those mechanisms with direct contact.

Another goal is to get more testing: one problem we have observed is that features are often developed and deployed on nightly, but production users don't really want to try them out until they hit stable! We would like to get some commitment from people to give things a try so that we have a better chance of finding problems before stabilization.

We want to emphasize that we welcome design feedback from all Rust users, regardless of whether you are a named stakeholder or not. If you're using async Rust, or have read through the designs and have a question or idea for improvement, please feel free to open an issue and tell us about it!

Number of stakeholder representatives

We are selecting a small number of stakeholders covering various points in the design space, e.g.

  • Web services author
  • Embedded Rust
  • Web framework author
  • Web framework consumer
  • High-performance computing
  • Operating systems

If you have thoughts or suggestions for good stakeholders, or you think that you yourself might be a good fit, please reach out to tmandry or nikomatsakis!

Meeting notes: Async Foundations Stakeholders Nov 2021

Reading

Phase 1

Phase 1 narrative (optional)

Attending

  • Tyler Mandry (Rust, Google)
  • Niko Matsakis (Rust, AWS)
  • Alice Ryhl (Tokio, Google)
  • Hipployte Barraud (glommio, DataDog)
  • Dario Nieuwenhuis (embassy)
  • Yoshua Wuyts (async-std, Microsoft)
  • Rafael Leite (AWS S3)
  • Fabien Gaud (AWS S3)
  • Marie Janssen (Fuchsia Bluetooth team, Google)

Questions

Put your questions here and we will answer them.

How do I format my questions?

Just like this! Make a ### section and then add some stuff.

Overall impressions

  • Alice: seems like a reasonable start; the dyner thing seems like it should be temporary, but doing things in phases makes sense to me.
  • Hippolyte: the document reads well, was easy to follow.
  • Yosh: I find it hard to reason about dyn safety, so I am not sure I fully understood the ins and outs of that section.
  • Fabien: Why use another macro to support dyn? I would expect it to always be there. Maybe it has to do with no-std?
  • Niko: Problem of "no best choice", so the idea with macro was to let us experiment before committing to what is the "default" choice, but we'll probably need something that lets people make their own versions of traits.
  • Fabien: If this were permanent, I have a vision of my code using dyner everywhere.
  • Alice: It seemed to me that dyner was a way of getting some form of async traits without having to immediately solve the problems that come from dynamic dispatch. Getting async traits would still be very good.
  • Hippolyte: I'm wondering about the forwards compat story for dyner.
  • Niko: I think it should be interoperable, it's standard code, but it's basically "some type that implements the trait X (just via dynamic dispatch)". If then there were a built-in version of dyn, it would presumanbly be applicable to this type too (but you'd get two layers of dynamic dispatch).
  • Alice: Thinking of two use cases: Android Binder (IPC); AsyncRead, Stream, etc. Works great in first use case. But we need to specify that Futures are Send.
  • Niko: yeah, we have to solve that.
  • Hippolyte: for glommio, would prefer if the default were not-send.
  • Alice: what about generic functions?
  • Niko: generic over types works for argument position impl trait; other cases are trickier.

Async Overloading

Yosh: Before we can make forward progress on AsyncRead, AsyncWrite, and AsyncIterator, we should make an explicit decision on how we want to introduce them in the stdlib. More directly, this means making a decision on the topic of async overloading.

Related: who are the portability group? The current link in the docs leads nowhere.

  • Niko:
    • Overloading means having one function that can be either sync or async depending on how it is used.
    • But I think it's kind of orthogonal for async traits.
  • Yosh: Yeah, but before we stabilize async iter and async io traits, we do need to figure it out.
  • Niko:
    • traits are kind of the beginning
    • async read, write, iterator would be rephrased in terms of async fns in traits
  • Tyler:
    • right, we didn't cover the overall roadmap, but I agree we need to be thinking about overloading before we stabilize those traits, and we do want to stabilize them sooner rather than later
  • Hippolyte:
    • so this proposal would be... I write one function and I get both a regular and a poll version?
  • Tyler:
    • we'd like to make it so you don't have to write the poll at all
  • Yosh:
    • There's no concrete proposal.
  • Niko:
    • I'd hope that you can not have poll at all, we'd want to move even without async overloading, and get rid of poll
  • Hippolyte:
    • Sometimes you have to pull a future from a stream, etc

Dynamic dispatch on embedded / no-std

nikomatsakis: I think that we can make the equivalent of &dyn and &mut dyn work, at least for some traits. Right now, they don't because the returned future is boxed, but there are various techniques one could use to avoid that (e.g., I'm experimenting with something I call "inline futures" where the space for the future is pre-allocated). But my question is, is that...good enough? How often do people dyn in no-std like settings and do you ever need to take ownership of the dyn thing? If so, how do people manage it now?

dario: in embedded we rarely use dyn, you're building for a particular chip/hardware/etc so you know the types pretty precisely. I've used it to save code size sometimes. e.g. if you need to do an operation on an async-read/async-write byte stream and you want to do it on two kinds of streams. Useful there. In these cases, usually &mut dyn is enough. Quite rare that you want to own a dyn object, you pretty much have to use an allocator for that. You can of course do that in embedded but it's usually better to be avoided.

fgaud: This has been a problem for us that if the async function takes self, it does not work with Box<dyn>, We solved that with a weird enum wrapping but (a) that's really working around limitations and (b) that's not very good for a library (not extensible)

Futures 2.0 trait

Yosh: How does that fit into the proposed timeline? Is this 'review how async fn works'? If so, that section could use work to be clearer on which aspects we intend to review.

Tyler: Short answer is we consider a Futures 2.0 trait to be orthogonal to this work, and since it's much more ambitious we're not tackling it yet.

IntoFuture

Yosh: When enumerating missing functionality from async/.await, we should not forget IntoFuture. This was merged and then reverted in 2019, with a PR opened once again last week. Can we include landing this as part of the Phase 1 milestones?

At Microsoft we depend on this feature for the design of our Azure SDKs.

Links:

Traits in libraries

nikomatsakis: One of the reasons that I want to see async fn in traits supported is that I think it's blocking various kinds of "framework" and library development. Do you think the design as described would be something people should ship e.g. in stable Tokio or elsewhere? Of particular interest might be the way things are not yet dyn-safe-- though a separate crate of dyner-ified traits could be distributed (they should keep working indefinitely, as they will build on stable Rust).

Hippolyte: One of the things that we really miss in Glommio is async Drop (who doesn't). In a context where blocking in illegal, dropping a file descriptor for instance is a real problem. Either you close the descriptor using a blocking syscall or you leak an fd.

Rafael: not having async-drop is the most painful thing

Niko: Is it a problem if async-drop is kind of best effort? I think the only real problem would be if a future is dropped in a sync function.

Niko: Realistic example: imagine there was a collection that had its own drop (MyVec). If it drops things... it'll be a problem.

Dario: Can we have a 'non-droppable type'?

Tyler: Possible, but hard.

Dario: Better than panicking.

Tyler: would be like Sized, need a default trait bound.

Alice: would be really useful for entirely different purposes.

Dario: Related to the leak trait. Not having guaranteed drop is a huge problem for embedded. Safe APIs over DMA need to be able to guarantee drop. Rust borrow checker doesn't see the DMA writes happening in the background. Same basic issue as io-uring.

Hippolyte: So you would want to disallow mem-forget?

Dario: It's two separate things. One is to make types that can't be dropped, the other is to have a trait (Leak) where anything that may fail to run dtor requires it (e.g., Rc, etc).

Hippolyte: Was just going to ask about reference cycles.

Dario: Right, you couldn't put a non-leakable type inside an arc. Not sure about the ergonomics, but it'd be powerful.

Hippolyte: I'm concerned this will be leaky (no pun intended -ed.) and it would get everywhere.

Niko: Right, I am definitely worried that "undroppable" and "unleakable" would be two orthogonal things, lots of complexity.

Hippolyte: Maybe we could make those the same trait?

Hippolyte: Something where you don't want synchronous drop?

Alice: buffered writes where you want to flush on drop.

Dario: most things that have async drop would benefit, right?

Yosh: Is that a practical issue?

Alice: I've not run into it myself, I've definitely had people ask me for it.

Niko: But Async Drop would work for them right? It's probably that they are in an async function and they want to ensure it flushes when it returns?

Syntax for Naming the future

Dario: There was some talks that I can't find right now on adding syntax to name the types of functions/methods/trait methods. P::request seems like it could conflict with that, as it could be the type of the function returning the future, or the type of the future.

Tyler: We've discussed both of these. No final decision has been made. Seems like you might not need to name the anonymous type of the function. So the question is what's most ergonomic, useful, flexible. If we can avoid having to write e.g. P::request::Output without giving up anything we care about, that's better, right?

Niko: This is kind of the evolution of that proposal.

Dario: Maybe being able to name function types is maybe useful for other things?

Alice: Let's say you defined the trait with explicit associated types, could you still use an async fn in the impl of the trait?

Dario: RFC says no.

Tyler: We'd like to allow it, but it depends on some other features that aren't all setup.

Alice: There are times you do need names, for send bounds etc.

Tyler: Some discussion of whether one could add explicit associated types on the trait side.

Niko: I don't think it gives you any new capability that you dont' have under those proposal.

Alice: Right, I think it is necessary for things to have a name by default. I don't know about you, but having to add Output everywhere seems sketchy.

Tyler: the syntax ::Output?

Niko: It seems unnecessary to me, is that what you mean by sketchy?

Niko: Happy to chat later about it DArio but I actually don't think there IS much of a use case for fn types; the ones I can come up with are I think better solved by const generics.

Dyner and forward-compatibility

Hippolyte: Once dyn is fully supported in trait, what will happen with crates defining dyner traits? Will they continue to work, be interoperable?

(covered earlier)

Where do we see with clauses on the timeline?

Yosh: mostly curious how we see this fit in / who would be responsible for investigating this.

Niko: with traits, it's a scheme for implied parameters. It came from thinking about how to pass.

Dario: Why not thread / task locals?

Alice: Tokio's task local implementation could work with any runtime.

Dario: Seems like a big addition!

Niko: It is. I think it carries its weight but...

Yosh: How can I get involved?

Tyler: Not something under active discussion, I had in mind writing some blog posts, I think we need to have some updated proposal.

Yosh: Feels like it'll become relevant in later stages.

Tyler: Yes. One problem I wanted to solve is having some kind of scoped context that can be threaded around. But we can make a lot of progress without it, e.g. async fn in trait, async drop.

Dario: I'm concerned about scoped clauses requiring alloc, which might be a problem for std. with clauses enables "global things" that you can assume they are there-- maybe code then gets less portable?

Yosh: But because these can be overridden and tweaked, it could go the other way, e.g. you can provide your own impl of the file system. Definitely thinking about no-std is part of the discussion.

Dario: Sure, but that requires a trait that's applicable to all scenarios. The API for a TCP stack for example varies depending on alloc. If stuff starts to rely on global things, it'll be tricky to find the right API.

Alice: With parameters are set by some stack frame above you. They are therefore set without requiring an allocator.

Niko: I think we're mixing up a few things. The with clauses are desugared to just implicit parameters that the compiler adds on your behalf. Scopes and the possibility of general APIs are something else.

Dario: My point is general. e.g. something like a global allocator etc. Even a "runtime" trait that looks innocent may embed assumptions, like that there is an allocator available (has to allocate the task).

Wrapping up

Tyler: Feel free to ping us with more questions / comments in between sessions.

One good place is the wg-async-foundations stream.

Async Stakeholders Sep 2022: Notes

  • Date: Sep 13, 2022
  • Participants:
    • Tyler Mandry
    • Niko Matsakis
    • Marie Janssen
    • Alice Ryhl
    • Russell Cohen
    • Dario Nieuwenhuis
    • Eric Holk
    • Fabien Gaud
    • Zelda Hessler
    • Hippolyte Barraud
    • Yoshua Wuyts

Agenda

Our discussion will be based on this doc: Async fn in trait: A user's guide from the future

Factual questions about the doc will be answered first.

Function return syntax

#![allow(unused)]
fn main() {
async fn fetch_and_process_in_task<F>(
    f: F,
)
where
    F: Fetch,
    F: Send + 'static, // πŸ‘ˆ Added so `F` can be sent to new task
    F::fetch(..): Send // πŸ‘ˆ Added, this syntax is new!
}
#![allow(unused)]
fn main() {
async fn fetch_and_process_in_task(
    f: impl Fetch<fetch(..): Send> + Send + 'static,
)
}
  • Is it confusing?
  • Is it easy to explain to new Rust devs?
  • Is it ergonomic enough to use?

Boxing::new

#![allow(unused)]
fn main() {
let mut f = HttpFetch { .. };
fetch_and_process_dyn(Boxing::new(&mut f));
}
  • Is it confusing?
  • Is it easy to explain to new Rust devs?
  • Is it ergonomic enough to use?
  • Would you rather have implicit boxing? How strong is your preference for or against that?
  • How likely do you think it is that you will use an adapter besides Boxing to use async traits via dyn?
    • If so, what strategy would you want?

Work stealing

Lots of the new syntax introduced is to support work stealing executors that require their tasks to be Send.

  • For executors that use work stealing, why?
  • Do we have benchmarks or data on this?

Other possible topics

  • Other impressions about the doc
  • dyner
  • Mixing async fn with its desugaring
  • Refinement

Open ended

  • What about async Rust has been giving you the most trouble lately?

Notes

Function return syntax

Confusing? Easy to explain? Thoughts on ergonomics?

Confusion about what F::fetch(..) refers to -- does it refer to the type before/after the await? Expectation was that it would be the type after the await, not before!

Is this one new syntax, or is it two? Can you write F::fetch by itself, or is F::fetch(..) the only accepted syntax.

Would have to rule out cases where associated types had the same name as the method, or else limit it to async functions.

We did support that, and could go forward, modulo shadowing. Explaining the idea of a syntax where you can name the zero-sized type for functions did require explaining the zero-sized function types, which was just hard to talk about.

In this we are saying that: F is a function that returns something which is send. Could you also specify that it returns T and then say that T is Send?

#![allow(unused)]
fn main() {
fn foo<T>()
where
    F: Fetch<for<'a> fetch(&'a mut Self) = T>,
    T: Send,


fn foo<T>()
where
    for<'a> {
        exists<T> {
            F: Fetch<fetch(&'a mut Self) = T>,
            T: Send
        }
    }
}

How common is thread-per-core setups? How often will people interact with Send?

Some customers for AWS SDK are trying to build TPC and don't want to have the need for send bounds. Unclear how much this is a theoretical concern.

In Embedded, Send is pretty much never needed, because you rarely have more than one core. It's quite annoying because some libraries require Send. You structure your program in a way that things are not Send because you don't need it (e.g., using RefCell). I think it'd be better if the ecosystem didn't assume Send by default.

Question ultimately is are we getting a lot of value out of work stealing.

The fact that Send and Sync is required in many places is a problem and it doesn't seem like people necessarily benefit from it.

In async-std we didn't really consider the option of not having work-stealing, but now that glommio came along people are talking about it.

In Fuchsia it's very common to have a future that is not send and not sync. Most components are run single threaded. Common answer to "oh this doesn't work because it's asking you to use Send" is "don't do that".

Browser environments are another place where Send/Sync mostly don't make sense, since you usually run on the main thread.

We definitely shouldn't have implement where Send bound on all the futures -- but it's important to consider the ergonomics of where Send bounds, especially as many people are using runtimes that require Send. For them it's important that they're ergonomic, even if maybe they didn't need it. We could talk about whether runtimes should stop requiring send and propose some kind of non-work-stealing alternative, but we do have to make sure they're ergonomic to use.

It's hard to tell if the proposed where Send bound will be an ergonomics problem or not -- if there were a lot of functions, probably.

The syntax was intended also to solve -> impl Trait for existing things.

This issue is tracking our customer's desire to use alternate async runtimes, including those that run on a single thread: https://github.com/awslabs/aws-sdk-rust/issues/52 We currently use tokio by default so our futures are almost all defined as send and sync

Lifetimes of manual desugaring (yosh)

#![allow(unused)]
fn main() {
impl Fetch for HttpFetch {
    #[refine]
    fn fetch(
        &mut self,
        request: ResourceId
    ) -> Box<dyn Future<Output = Result<ResourceData>> {
        Box::pin(async move {
            // `&mut self` would need to be live here somehow, is that supported?
        })
    }
}
}

you'd need Box<dyn Future<> + '_>, but apart from that?

But the '_ is not required. If you can write it without self, for example.

#![allow(unused)]
fn main() {
fn foo<T>()
where
    F: Fetch<for<'a> fetch(&'a mut Self) = T>,
    T: Send,
}

This generic code says that T is independent from 'a, albeit in a subtle way.

Would allow you to have implementations that are independent

Mentioning the return type of fn (alice)

Can we do this now?

#![allow(unused)]
fn main() {
async fn my_async_fn() -> i32 { .. }

type MyFut = my_async_fn(..);
}

All other left-hand-sides of where bounds are usable in all places where types go.

nikomatsakis: Yes, that's my intent, anyway. I'd like the return type syntax to just work for everything.

If there were generic arguments and the type were dependent on them, you couldn't do it, so you'd need something like this...

#![allow(unused)]
fn main() {
async fn my_async_fn(x: impl Display) -> i32 { .. }

type MyFut<T: Display> = my_async_fn(T);
}

Boxing

Summary is: in order to use dyn if there are async functions (or RPITIT), you have to wrap your object in a Boxing::new, which will cause it to allocate the future returned by Box.

#![allow(unused)]
fn main() {
fn foo() {
    let mut x = something;
    bar(Boxing::new(&x))
}

fn bar(x: &dyn AsyncIterator)
}

This is one of the things we changed.

Would be good to show some of the alternatives. Like, would Boxing work for other things? Can you plugin other things here? I like that this keeps future allocation explicit, but seems like it will be used commonly, might get heavy if every time you create a dyn you need the Boxing. I worry that it might be heavy.

In an internal project we use StackFuture that allocates futures on the stack. Makes a big array of u8s and casts the future into that. You get inline storage up to a certain size. Could I write an adapter like that?

This is sort of confusing to me. I see

#![allow(unused)]
fn main() {
async fn caller() {
    let mut f = HttpFetch { .. };
    fetch_and_process_dyn(&mut f); // πŸ‘ˆ ERROR!
}
}

and I see the error that this wants to be boxed, but I'm like, "Why, it's confusing?" I would look at it and say "I don't need to box the thing, I want to output something that needs to be boxed", but even that, it feels weird that I have to do it myself, I wish you could say it somewhere else? (from the chat: Hear hear!)

You want to a way for fn to say "give me something that will box?" Answer: it feels to me like impl Into<> where you're saying, give me something that's convertible.

We tried to make it so that you don't care, as the one who takes the dyn, whether it will box or not.

What I really heard was "it would be nice if I didn't have to say give me Boxing here".

Yeah it'd be nice if you could give people the "option" to do boxing without having to say it as explicitly. But then you'd have the issue where libraries have to interoperate.

My thought when I see Boxing is...I agree that it seems dangerous to implicitly box, but this Boxing thing seems weird. I don't know if there's a better solution, I don't see one, but it seems weird to me. It makes sense to me, but it seems hard to teach.

nikomatsakis: another idea would be have a warning by default that people can opt to silence on their project if they don't care.

#![allow(unused)]
fn main() {
async fn caller() {
    let mut f = HttpFetch { .. };
    fetch_and_process_dyn(&mut f); // πŸ‘ˆ ERROR!
    fetch_and_process_dyn(Boxing::new(&mut f)); // Works
    fetch_and_process_dyn(&mut InlineAdapter::new(f)); // Works, but doesn't box later, instead uses stack allocate
}
}

^ why is that Boxing::new(..) instead of &mut Boxing::new(..)? -- not entirely sure

#![allow(unused)]
fn main() {
async fn caller() {
    let mut f = HttpFetch { .. };
    fetch_and_process_dyn(&mut f); // πŸ‘ˆ works, but lints
    fetch_and_process_dyn(&mut InlineAdapter::new(f)); // works, but doesn't box later, instead uses stack allocate, and never lints
}
}

Yosh: what about the inverse: inline by default, opt-in to boxing?

Problem is that inlining doesn't always work. Each future needs to be pre-allocated, there may be infinite calls to &self methods which means the pre-allocated size is potentially unbounded.

Eric: it feels like boxing is being made at a weird place in the program. At the point where it is converted from static to dynamic, but you really want to make that decision at the call site-- isn't that the place you want to be in control over where it gets stored? I get that this is hard, and that's why were moving towards this boxing thing.

I suspect there's no right answer, but at least some of the time, you want to be able to write code that works equally well in multiple contexts.

You could inline the &mut and box the &.

Other downside: takes up a lot of stack space. (Edit: only the max of all the futures you call.)

What is the language level requirement for something to be convertible to dyn?

Basically: you'd have to have an impl where each method returns a "single pointer", or really something that implements what is required for box* creation.

Interesting language idea: e.g. could we have a way to return a Vec for cases that are not ...

It would work for anything that returns -> impl Trait, so maybe -> Vec<T>, but yes -> impl Sequence<T>.

This is recurring problem in no-std contexts, because returning vecs is a painful thing.

#![allow(unused)]
fn main() {
//

trait GetElements {
    fn elements(&self) -> Vec<Element>;
}

// but this generalized form is potentially no-std compatible

trait GetElements {
    fn elements(&self) -> impl Sequence<Element>;
}
}

Alternatives, reasons to avoid boxing? Maybe you want to cache the same box over and over again?

Needed it in microsoft beacuse allocation was really expensive in this particular context, it happened to be a very bad allocator. But this was a pretty niche scenario, probably not something your typical async programmer runs into all the time.

Common enough perhaps that it shouldn't be disregarded.

Scenario needs to be supported but it will be quite unusual.

Depends on the environment. e.g., drivers for the linux kernel? They probably share a lot of similarities.

Audio path for bluetooth is a similar use case.

Android binder currently allocates for every binder (IPC) call, but they're supposed to be low latency.

How widespread are the coercions etc?

In one case, scsi driver, have a vector of dyn, and stack future is inlining the target to a big future.

Ah, ok, so it's more the case of: hard-coding on the dyn how much space the future requires.

Don't know that we have to solve it now.

stack-future came out of: see problem, bring in calvalry, doing detailed work. How can we make it so we don't need Rust experts to solve that sort of problem of tweaking the sizes? Users can figure it out through diagnostics?

I think it has to be PGO.

Discoverability?

I'd like it to be a tool, but it's kind of science fiction at this time.

what about async has been giving most trouble lately?

AWS SDK would like to support other runtimes but often things are inextricably linked together. Very difficult to take async-std runtime without also using surf, which has its own HTTP types, vs tokio, which uses hyper. You end up with these confusing errors where there are competing things with the same name but slight differences. You runtime into the core abstraction stuff. Biggest change if things for reading bytes asynchronously could solidify around a single thing.

Read trait sufficient or more? Answer: reading from a body is the most important thing. Users have to do that very often.

Libraries often want the ability to spawn tasks or set timers?

Yep! That's a common thing. One of the ways we're trying to enable people to swap out runtimes. We've abstracted around how we ask executor "has this time passed", we have "async sleep impl", but because of all the other stuff, it's not possible. Anytime you're doing some kind of I/O or clock thing, you need some abstraction for how the executor likes to deal with the system. It's kind of meaningless to have abstractions at that level. So even though we're splitting it, it's kind of a false split.

How common is that as a request? Most common request.

Dealing with polling, lifetime across closures. Sometimes you pass a lifetime in, and you know the closure will run here, and not need the lifetime past that, but there's no way to explain that to Rust. Could maybe drop the closure? (nikomatsakis: I'd love to see an example.)

An issue I've seen a few times that arises when porting blocking code to async: no way to write async closures that borrow things from the caller. For example....

#![allow(unused)]
fn main() {
fn transaction(
    &mut self,
    f: impl FnOnce(&mut Foo),
);
}

if I have to port this to async, no way to specify it. It would have to be higher-ranked and lots of strange borrow checker errors. I've never been able to get it to work. There are some wild-hacks, the higher-ranked closure hack, but it only works in narrow cases.

Comes up on the tokio questions forum from time to time. There's a hack where you can make a helper trait, but the type inferences fails if there's a closure, basically requires that you can put the lifetimes in all the right places and I don't know how to do it.

Example where this is very painful:

blocking: https://github.com/rust-embedded/embedded-hal/blob/master/src/spi/blocking.rs#L199

async: https://github.com/rust-embedded/embedded-hal/blob/master/embedded-hal-async/src/spi.rs#L57

Ultimately gave up and used a raw pointer.

#![allow(unused)]
fn main() {
fn transaction(
    &mut self,
    f: impl FnOnce(&mut Foo) -> impl Future<Output = ()> + '_
);
}

Yosh: Oli and I were messing around today with what async closuress could potentially look like using keyword generics:

#![allow(unused)]
fn main() {
async<A> trait Fn<Args> {
    type Output;
    async<A> fn call(&self, args: Args) -> Self::Output;
}
}

which would allow

#![allow(unused)]
fn main() {
fn transaction(
    &mut self,
    f: impl async FnOnce(&mut Foo) // or something
);
}

There's this hack you can do:

#![allow(unused)]
fn main() {
trait MyAsyncFn<'a, Arg, Out>: FnOnce(Arg) -> Self::Fut {
    type Fut: Future<Output = Out>;
}

 + an impl block
}

^ I tried that but then type inference breaks when calling a function taking impl MyAsyncFn, you have to use named functions or manually write the types of args+return in the closure (I think...?) ~Dario

The fact that Waker must be Sync is not great if runtimes are not send/sync by default

https://github.com/rust-lang/rust/issues/66481

Async fn fundamentals

This initiative is part of the overall async vision roadmap.

Impact

  • Able to write async fn in traits and trait impls
    • Able to easily declare that T: Trait + Send where "every async fn in Trait returns a Send future"
    • Traits that use async fn can still be dyn safe though some tuning may be required
    • Async functions in traits desugar to impl Trait in traits
  • Able to write "async fn drop" to declare that the destructor may await
  • Support for async closures

Milestones

MilestoneStateKey participants
Author evaluation doc for static async traitdone πŸŽ‰tmandry
Author evaluation doc for dyn async traitdone πŸŽ‰tmandry
Author evaluation doc for async dropπŸ’€
Author evaluation doc for impl Trait in traitsdonetmandry
Stabilize type alias impl traitin-progressoli-obk
Stabilize generic associated typesdone πŸŽ‰jackh726
Author RFC for async fn in traitsdone πŸŽ‰
Author evaluation doc for async closuresblog posts authored, doc pendingnikomatsakis
Author RFC for impl trait in traitsdone
Feature complete for async fn in traitsdone πŸŽ‰compiler-errors
Feature complete for impl Trait in traitsdone πŸŽ‰compiler-errors
Feature complete for async dropπŸ’€
Feature complete for async closuresπŸ’€
Stabilize async fn in traitsproposedcompiler-errors
Stabilize impl trait in traitsproposedcompiler-errors
Stabilize async dropπŸ’€
Stabilize async closuresπŸ’€

Design discussions

This directory hosts notes on important design discussions along with their resolutions. In the table of contents, you will find the overall status:

  • βœ… -- Settled! Input only needed if you have identified a fresh consideration that is not covered by the write-up.
  • πŸ’¬ -- Under active discussion. Check the write-up, which may contain a list of questions or places where feedback is desired.
  • πŸ’€ -- Paused. Not under active discussion, but we may be updating the write-up from time to time with details.

Static async fn in traits

Impact

  • Able to write async fn in traits and impls and use them in statically dispatched contexts
  • Able to easily declare that T: Trait + Send where "every async fn in Trait returns a Send future"

Design notes

Support async fn syntax in traits.

The core idea is that it desugars into impl trait in traits:

#![allow(unused)]
fn main() {
trait SomeTrait {
    async fn foo(&mut self);
}

// becomes:

trait SomeTrait {
    fn foo<(&mut self) -> impl Future<Output = ()> + '_;
}
}

Naturally it should also work in an impl:

#![allow(unused)]
fn main() {
impl SomeTrait for someType {
    async fn foo(&mut self);
}
}

For async functions in traits to be useful, it is important that traits containing async fn be dyn-safe, which introduces a number of challenges that we have to overcome.

Frequently asked questions

Can users easily bound those GATs with Send, maybe even in the trait definition?

  • People are likely to want to say "I want every future produced by this trait to be Send", and right now that is quite tedious.
  • We need a way to do this.
  • This applies equally to other "-> impl Trait in trait" scenarios.

What about "dyn" traits?

  • See the sections on "inline" and "dyn" async fn in traits below!

MVP: Static async fn in traits

This section defines an initial minimum viable product (MVP). This MVP is meant to be a subset of async fns in traits that can be implemented and stabilized quickly.

In a nutshell

  • In traits, async fn foo(&self) desugars to
    • an anonymous associated type type Foo<'me>: Future<Output = ()> (as this type is anonymous, users cannot actually name it; the name Foo here is for demonstrative purposes only)
    • a function fn foo(&self) -> Self::Foo<'_> that returns this future
  • In impls, async fn foo(&self) desugars to
    • a value for the anonymous associated type type Foo<'me> = impl Future<Output = ()>
    • a function fn foo(&self) -> Self::Foo<'_> { async move { ... } }
  • If the trait used async fn, then the impl must use async fn (and vice versa)
  • Traits that use async fn are not dyn safe
    • In the MVP, traits using async fn can only be used with impl Trait or generics

What this enables

  • The MVP is sufficient for projects like embassy, which already model async fns in traits in this way.
  • TODO: Once we have a list of stakeholders, try to get a sense for how many uses of async-trait could be replaced

Notable limitations and workarounds

  • No support for dyn
    • This is a fundamental limitation; the only workaround is to use async-trait
  • No ability to name the resulting futures:
    • This means that one cannot build non-generic adapters that reference those futures.
    • Workaround: define a function alongside the impl and use a TAIT for its return type
  • No ability to bound the resulting futures (e.g., to require that they are Send)
    • This rules out certain use cases when using work-stealing executor styles, such as the background logging scenario. Note that many other uses of async fn in traits will likely work fine even with a work-stealing executor: the only limitation is that one cannot write generic code that invokes spawn.
    • Workaround: do the desugaring manually when required, which would give a name for the relevant future.

Implementation plan

  • This MVP relies on having generic associated types and type alias impl trait, but they are making good progress.
  • Otherwise, the implementation is a straightforward desugaring, similar to how inherent async fns are implemented
  • We may wish to also ship a variant of the async-trait macro that lets people easily experiment with this feature

Forward compatibility

The MVP sidesteps a number of the more challenging design problems. It should be forwards compatible with:

  • Adding support for dyn traits later
  • Adding a mechanism to bound the resulting futures
  • Adding a mechanism to name the resulting futures
    • The futures added here are anonymous, and we can always add explicit names later.
    • If we were to name the resulting futures after the methods, and users had existing traits that used those same names already, this could present a conflict, but one that could be resolved.
  • Supporting and bounding async drop
    • This trait will not exist yet with the MVP, and supporting async fn doesn't enable anything fundamental that we don't have to solve anyway.

impl Trait in traits

This effort is part of the impl trait initiative. Some notes are kept here as a summary.

Summary

Requires

Design notes

Support -> impl Trait (existential impl trait) in traits. Core idea is to desugar such thing into a (possibly generic) associated type:

#![allow(unused)]
fn main() {
trait SomeTrait {
    fn foo<(&mut self) -> impl Future<Output = ()> + '_;
}

// becomes something like:
//
// Editor's note: The name of the associated type is under debate;
// it may or may not be something user can name, though they should
// have *some* syntax for referring to it.

trait SomeTrait {
    type Foo<'me>: Future<Output = ()> + 'me
    where
        Self: 'me;

    async fn foo(&mut self) -> Self::Foo<'_>;
}
}

We also need to support -> impl Trait in impls, in which case the body desugars to a "type alias impl trait":

#![allow(unused)]
fn main() {
impl SomeTrait for SomeType {
    fn foo<(&mut self) -> impl Future<Output = ()> + '_ {

    }
}

// becomes something using "type alias impl Trait", like this:

trait SomeTrait {
    type Foo<'me> = impl Future<Output = ()> + 'me
    where
        Self: 'me;

    fn foo(&mut self) -> Self::Foo<'_> {
        ...
    }
}
}

Frequently asked questions

What is the name of that GAT we introduce?

  • I called it Bar here, but that's somewhat arbitrary, perhaps we want to have some generic syntax for naming the method?
  • Or for getting the type of the method.
  • This problem applies equally to other "-> impl Trait in trait" scenarios.
  • Exploration doc

Dyn async trait

Impact

  • Traits that contain async fn or impl trait in traits can still be dyn safe
  • Costs like boxing of futures are limited to code that uses dyn Trait and not to all users of the trait
  • Reasonable defaults around things like Send + Sync and what kind of boxing is used
  • Ability to customize those defaults for individual traits or on a crate-wide or module-wide basis

Requires

Design notes

  • Permit a trait TheTrait containing async fn or impl trait in traits to be used with dyn TheTrait, at least if other criteria are met.
  • Do not require annoying annotations.
  • Permit the user to select, for TheTrait, how the futures will be boxed or otherwise represented, which would permit us to use Box or potentially other types like SmallBox etc.
  • User should also be able to control whether the resulting futures are assumed to be send.

Older notes

The most basic desugaring of async fn in traits will make the trait not dyn-safe. "Inline" async fn in traits is one way to circumvent that, but it's not suitable for all traits that must be dyn-safe. There are other efficient options:

  • Return a Box<dyn Async<...>> -- but then we must decide if it will be Send, right? And we'd like to only do that when using the trait as a dyn Trait. Plus it is not compatible with no-std (it is compatible with alloc).
    • This comes down to needing some form of opt-in.

This concern applies equally to other "-> impl Trait in trait" scenarios.

We have looked at revising how "dyn traits" are handled more generally in the lang team on a number of occasions, but this meeting seems particularly relevant. In that meeting we were discussing some soundness challenges with the existing dyn trait setup and discussing how some of the directions we might go enabled folks to write their own impl Trait for dyn Trait impls, thus defining for themselves how the mapping from Trait to dyn Trait. This seems like a key piece of the solution.

One viable route might be:

  • Traits using async fn are not, by default, dyn safe.
  • You can declare how you want it to be dyn safe:
    • #[repr(inline)]
    • or #[derive(dyn_async_boxed)] or some such
      • to take an #[async_trait]-style approach
    • It would be nice if users can declare their own styles. For example, Matthias247 pointed out that the Box used to allocate can be reused in between calls for increased efficiency.
  • It would also be nice if there's an easy, decent default -- maybe you don't even have to opt-in to it if you are not in no_std land.

Frequently asked questions

What are the limitations around allocation and no-std code?

"It's complicated". A lot of no-std code does have an allocator (it depends on alloc), though it may require fallible allocation, or permit allocation of fixed quantities (e.g., only at startup, or so long as it can be known to be O(1)).

Dyn trait

Impact

  • Soundness holes relating to dyn Trait are closed.
  • The semver implication of whether a trait is "dyn or not" are clear.
  • More kinds of traits are dyn-safe.
  • Easily able to have a "dynamically dispatched core" with helper methods.
  • Users are able to the "adaptation" from a statically known type (T: Trait) into a dyn Trait.

Design notes

Soundness holes

FIXME-- list various issues here :)

Semver implications

Today, the compiler automatically determines whether a trait is "dyn-safe". This means that otherwise legal additions to the trait (such as new )

More kinds of traits are dyn-safe

Currently dyn-safe traits exclude a lot of functionality, such as generic methods. We may be able to lift some of those restrictions.

Easily able to have a "dynamically dispatched core" with helper methods

There is a common pattern with e.g. Iterator where there is a dynamically dispatched "core method" (fn next()) and then a variety of combinators and helper methods that use where Self: Sized to side-step dyn-safety checks. These methods often involve generics. We should make this pattern easier and more obvious, and (ideally) make it work better -- e.g., by having those methods also available on dyn Trait receivers (which seems fundamentally possible).

Adaptation

In the case of async Rust, given a trait Foo that contains async fn methods, we wish to be able to have the user write dyn Foo without having to specify the values of the associated types that contain the future types for those methods. Consider the fully desugard example:

#![allow(unused)]
fn main() {
trait Foo {
    type Method<..>: Future;
    fn method() -> Self::Method<..>
}
}

Roughly speaking we wish to be able to supply an impl like

#![allow(unused)]
fn main() {
impl Foo for dyn Foo {
    type Method<..> = Box<dyn Future<..>>;
    fn method() -> Self::Method {
        // call, via vtable, a shim that will create the `Box`
        // (or whichever smart pointer is desired)
    }
}
}

Ideally, this would be a general capability that users can use to control the adaptation of "known types" to dyn types for other traits.

Async drop

Impact

  • Able to create types (database connections etc) that perform async operations on cleanup
  • Able to detect when such types are dropped synchronously
  • Able to identify the await points that result from async cleanup if needed

Design notes

We can create a AsyncDrop variant that contains an async fn:

#![allow(unused)]
fn main() {
impl AsyncDrop for MyType {
    async fn drop(&mut self) {
        ...
    }
}
}

Like Drop, the AsyncDrop trait must be implemented for all values of its self-type.

Async drop glue

Within async functions, when we drop a value, we will invoke "async drop glue" instead of "drop glue". "Async drop glue" works in the same basic way as "drop glue", except that it invokes AsyncDrop where appropriate (and may suspend):

  • The async drop glue for a type T first executes the AsyncDrop method
    • If T has no AsyncDrop impl, then the glue executes the synchronous Drop impl
      • If T has no Drop impl, then this is a no-op
  • The async drop glue then recursively "async drops" all fields of T

Auto traits

Rust presently assumes all types are droppable. Consider a function foo:

#![allow(unused)]
fn main() {
async fn foo<T>(x: T) {}
}

Here, we will drop x when foo returns, but we do not know whether T implements AsyncDrop or not, and we won't know until monomorphization. However, to know whether the resulting future for foo(x) is Send, we have to know whether the code that drops x will be send. So we must come up with a way to know that T: Send implies that the async drop future for T is Send.

Explicit async drop

We should have a std::mem::async_drop analogous to std::mem::drop:

#![allow(unused)]
fn main() {
async fn async_drop<T>(x: T) { }
}

Implicit await points

When you run async drop glue, there is an implicit await point. Consider this example:

#![allow(unused)]
fn main() {
async fn foo(dbc: DatabaseConnection) -> io::Result<()> {
    let data = socket().read().await?;
    dbc.write(data).await?;
}
}

Here, presuming that DatabaseConnection implements AsyncDrop, there are actually a number of async drops occurring:

#![allow(unused)]
fn main() {
async fn foo(dbc: DatabaseConnection) -> io::Result<()> {
    let data = match socket().read().await {
        Ok(v) => v,
        Err(e) => {
            std::mem::async_drop(dbc).await;
            return e;
        }
    };
    let () = match dbc.write(data).await? {
        Ok(()) => (),
        Err(e) => {
            std::mem::async_drop(dbc).await;
            return e;
        }
    };
    std::mem::async_drop(dbc).await;
}
}

As this example shows, there are important ergonomic benefits here to implicit async drop, and it also ensures that async and sync code work in analogous ways. However, implicit await points can be a hazard for some applications, where it is important to identify all await points explicitly (for example, authors of embedded applications use await points to reason about what values will be stored in the resulting future vs the stack of the poll function). To further complicate things, async-drop doesn't only execute at the end of a block or an "abrupt" expression like ?: async-drop can also execute at the end of every statement, given temporary values.

The best solution here is unclear. We could have an "allow-by-default" lint encouraging explicit use of async_drop, but as the code above shows, the result may be highly unergonomic (also, imagine how it looks as the number of variables requiring async-drop grows).

Another option is to target the problem from another angle, for example by adding lints to identify when large values are stored in a future or on the stack, or to allow developers to tag local variables that they expect to be stored on the stack, and have the compiler warn them if this turns out to not be true. Users could then choose how to resolve the problem (for example, by shortening the lifetime of the value so that it is not live across an await).

Running destructors concurrently

It's often the case that at the end of a function or scope, multiple destructors are run. In general the order (which is the reverse order of initialization) matters, since one local could borrow from another, or there is some other logical dependency between them.

However, in some cases the order might not matter at all. In async, it would be possible to run destructors for multiple locals concurrently. As an example, we could mark the destructors like this:

#![allow(unused)]
fn main() {
#[concurrent]
impl AsyncDrop for Foo {
    async fn drop(&mut self) { ... }
}
}

Here, #[concurrent] means that Foo does not take logical dependencies or dependents with other values, and it is safe to drop concurrently. (The compiler would still enforce memory safety, of course.)

In these cases, however, it's usually enough to impl synchronous Drop and spawn a task for the "real" destructor. That keeps the language simple, though it's less convenient to write.

Preventing sync drop

It is easy enough to make async-drop be used, but it is currently not possible to prevent sync drop, even from within an async setting. Consider an example such as the following:

#![allow(unused)]
fn main() {
async fn foo(dbc: DatabaseConnection) -> io::Result<()> {
    drop(dbc);
}
}

The compiler could however lint against invoking (or defining!) synchronous functions that take ownership of values whose types implement AsyncDrop. This would catch code like the case above. We may have to tune the lint to avoid false warnings. Note that it is important to lint both invocation and definition sites because the synchronous function may be generic (like drop, in fact).

The question remains: what should code that implements AsyncDrop do if synchronous Drop is invoked? One option is panic, but that is suboptimal, as panic from within a destructor is considered bad practice. Another option is to simply abort. A final option is to have some form of portable "block-on" that would work, but this is effectively the (as yet unsolved) async-sync-async sandwich problem.

Preventing this 'properly' would require changing fundamental Rust assumptions (e.g., by introducing the ?Drop trait). While such a change would make Rust more expressive, it also carries complexity and composition hazards, and would require thorough exploration. It is also a step that could be taken later (although it would require some form of explicit impl !Drop opt-in by types to avoid semver breakage).

Supporting both sync and async drop

It should perhaps be possible to support both sync and async drop. It is not clear though if there are any real use cases for this.

Async closures

Impact

  • Able to create async closures that work like ordinary closures but which can await values.
  • Analogous traits to Fn, FnMut, FnOnce, etc
  • Reconcile async blocks and async closures

Design notes

The fundamental problem async closures are meant to solve is that normal closures can't return a value that borrows from the closure itself (or, by extension, anything it captures). This is a big problem in async because the execution of all async code is encapsulated in a future returned by our function. Since that asynchronous code often needs to operate on captured values, it must in turn borrow from the closure.

This blog post describes the problem in more detail: https://smallcultfollowing.com/babysteps/blog/2023/03/29/thoughts-on-async-closures/.

One of the assertions made by this post is that async functions need their own traits, analogous to Fn and friends:

#![allow(unused)]
fn main() {
trait AsyncFnMut<A> {
    type Output;
    async fn call(&mut self, args: A) -> Self::Output;
}

// Similarly for AsyncFn, AsyncFnOnce.
}

A generalization of this would be to define "lending function" traits.

#![allow(unused)]
fn main() {
trait LendingFnMut<A> {
    type Output<'this>
    where
        Self: 'this;
    
    fn call(&mut self, args: A) -> Self::Output<'_>;
    //      ^                                  ^^^^
    // Lends data from `self` as part of return value.
}
}

This trait is similar to what AsyncFnMut above desugars to, except without saying anything about async or futures.

The LendingFnMut trait is from a follow up post that explains how we can actually extend the existing Fn traits to support "lending functions" that can do exactly what we want. It takes advantage of the fact that existing Fn trait bounds must use a special Fn(A, B) -> C syntax that always specifies their output type.

https://smallcultfollowing.com/babysteps/blog/2023/05/09/giving-lending-and-async-closures/

Other notes:

AsyncFnOnce is almost the same as Future/Async -- both represent, effectively, a future that can be driven exactly once. The difference is that your type can distinguish statically between the uncalled state and the persistent state after being called, if you wish, by using separate types for each. This can be useful for situations where an async fn is Send up until the point it is called, at which point it creates inner state that is not Send.

The concept of AsyncFn is more clear, but it requires storing the state externally to make sense: how else can there be multiple parallel executions.

πŸ”¬ Evaluation

The evaluation surveys the various design approaches that are under consideration. It is not required for all initiatives, only those that begin with a problem statement but without a clear picture of the best solution. Often the evaluation will refer to topics in the design-discussions for more detailed consideration.

Goals

Write async fn in traits, impls

The goal of the impact is to enable users to write async fn in traits and impls in a natural way. As a simple example, we would like to support the ability to write an async fn in any trait:

#![allow(unused)]
fn main() {
trait Connection {
    async fn open(&mut self);
    async fn send(&mut self);
    async fn close(&mut self);
}
}

Along with the corresponding impl:

#![allow(unused)]
fn main() {
impl Connection for MyConnection {
    async fn open(&mut self) {
        ...
    }

    async fn send(&mut self) {
        ...
    }

    async fn close(&mut self) {
        ...
    }
}
}

The goal in general is that async fn can be used in traits as widely as possible:

  • for foundational traits, like reading, writing, and iteration;
  • for async closures;
  • for async drop, which is built in to the language;
  • in dyn values, which introduce some particular complications;
  • in libraries, for all the usual reasons one uses traits;
  • in ordinary programs, using all manner of executors.

Support async drop

One particular trait worth discussing is the Drop trait. We would like to support "async drop", which means the ability to await things during drop:

#![allow(unused)]
fn main() {
trait AsyncDrop {
    async fn drop(&mut self);
}
}

Like Drop, the AsyncDrop trait would be

Executor styles and Send bounds

One key aspect of async fn in traits has to do with how to communicate Send bounds needed to spawn tasks. The key question is roughly "what send bounds are required to safely spawn a task?"

  • A single threaded executor runs all tasks on a single thread.
  • A thread per core executor selects a thread to run a task when the task is spawned, but never migrates tasks between threads. This can be very efficient because the runtimes never need to communicate across threads except to spawn new tasks.
    • In this scenario, the "initial state" must be Send but not the future once it begins executing.
    • Example: glommio::spawn
  • A work-stealing executor can move tasks between threads even mid-execution.
    • In this scenario, the future must be Send at all times (or we have to rule out the ability to have leaks of data out from the future, which we don't have yet).
    • Example: tokio::spawn

Reference scenarios

Background logging

In this scenario, the start_writing_logs function takes an async iterable and spawns out a new task. This task will pull items from the iterator and send them to some server:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Self::Item;
}

// Starts a task that will write out the logs in the background
async fn start_writing_logs(
    logs: impl AsyncIterator<Item = String> + 'static
) {
    spawn(async move || {
        while let Some(log) = logs.next().await {
            send_to_serve(log).await;
        }
    });
}
}

The precise signature and requirements for the spawn function here will depend on what kind of executor you are using, so let's consider each case but let's consider each case separately.

One note: in [tokio] and other existing executors, the spawn function takes a future, not an async closure. We are using a closure here because that is more analogous to the synchronous signature, but also because it enables a distinction between the initial state and the future that runs.

Thread-local executor

This is the easy case. Nothing has to be Send.

Work-stealing executor

In this case, the spawn function will require both that the initial closure itself is Send and that the future it returns is Send (so that it can be moved from place to place as code executes).

We don't have a good way to express this today! The problem is that there is a future that results from calling logs.next(), let's call it F. The future to be spawned has to be sure that F: Send. There isn't a good way to do this today, and even explaining the problem is surprisingly hard. Here is a "desugared version" of the program that shows what is needed:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    type NextFuture: Future<Output = Self::Item>;

    fn next(&mut self) -> impl Self::NextFuture;
}

// Starts a task that will write out the logs in the background
async fn start_writing_logs<I>(
    logs: I
) 
where
    I: AsyncIterator<Item = String> + 'static + Send,
    I::NextFuture: Send,
{
    spawn(async move || {
        while let Some(log) = logs.next().await {
            send_to_serve(log).await;
        }
    });
}
}

(With RFC 2289, you could write logs: impl AsyncIterator<Item = String, NextFuture: Send> + Send, which is more compact, but still awkward.)

Implementing AsyncRead

AsyncRead is being used here as a "stand-in" for some widely used trait that appears in the standard library. The details of the trait are not important.

Self is send

In this scenario, the Self type being used to implement is sendable, but the actual future that is created is not.

#![allow(unused)]
fn main() {
struct MySocketBuddy {
    x: u32
}

impl AsyncRead for MySocketBuddy {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let ssh_key = Rc::new(vec![....]);
        do_some_stuff(ssh_key.clone());
        something_else(self.x).await;
    }
    // ERROR: `ssh_key` is live over an await;
    //        Self implements Send
    //        therefore resulting future must implement Send
}
}

Self is not send

In this scenario, the Self type being used to implement is not sendable.

#![allow(unused)]
fn main() {
struct MySocketBuddy {
    x: u32,
    ssh_key: Rc<Vec<u8>>,
}

impl AsyncRead for MySocketBuddy {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        do_some_stuff(self.ssh_key.clone());
        something_else(self.x).await;
    }
    // OK
}
}

Dyn

Embedded system consuming general purpose libraries

Summary

  • General purpose library defines a trait Trait that use async fn
  • Embedded library can write a function consume that take &mut dyn Trait or &dyn Trait
  • Embedded library can call consume without requiring an allocator
    • It does have to jump through some "reasonable" hoops to specify the strategy for allocating the future, and it may not work for all possible traits
    • Can choose from:
      • pre-allocating storage space on the caller stack frame for resulting futures
      • creating an enum that chooses from all possible futures
  • The (admittedly vaporware) portability lint can help people discover this

Status quo

Grace is working on an embedded system. She needs to parse data from an input stream that is formatted as a series of packets in the format TLA. She finds a library tla on crates.io with a type that implements the async iterator trait:

#![allow(unused)]
fn main() {
pub struct TlaParser<Source> { ... }

#[async_trait]
impl<Source> AsyncIterator for TlaParser<Source> {
    type Item = TlaPacket;
    async fn next(&mut self) -> Option<Self::Item> {
        ...
    }
}
}

Unfortunately, because async_trait desugars to something which uses Box internally, she can't use it: she's trying to write for a system with no allocator at all!

Note: The actual status quo is that the Stream trait is not available in std, and the one in the futures crate uses a "poll" method which would be usable by embedded code. But we're looking to a future where we use async fn in traits specifically.

Shiny future

Grace is working on an embedded system. She needs to parse data from an input stream that is formatted as a series of packets in the TLA format. She finds a library tla on crates.io with a type that implements the async iterator trait:

#![allow(unused)]
fn main() {
pub struct TlaParser<Source> { ... }

impl<Source> AsyncIterator for TlaParser<Source> {
    type Item = TlaPacket;
    async fn next(&mut self) -> Option<Self::Item> {
        ...
    }
}
}

She has a function that is called from a number of places in the codebase:

#![allow(unused)]
fn main() {
fn example_caller() {
    let mut tla_parser = TlaParser::new(SomeSource);
    process_packets(&mut tla_parser);
}

fn process_packets(parser: &mut impl AsyncIterator<Item = TlaPacket>) {
    while let Some(packet) = parser.next().await {
        process_packet(packet);
    }
}
}

As she is developing, she finds that process_packets is being monomorphized many times and it's becoming a significant code size problem for her. She decides to change to dyn to avoid that:

#![allow(unused)]
fn main() {
fn process_packets(parser: &mut dyn AsyncIterator<Item = TlaPacket>) {
    while let Some(packet) = parser.next().await {
        process_packet(packet);
    }
}
}

Tackling portability by preallocating

However, now her code no longer builds! She's getting an error from the portability lint: it seems that invoking parser.next() is allocating a box to return the future, and she has specified that she wants to be compatible with "no allocator":

warning: converting this type to a `dyn AsyncIterator` requires an allocator
3 |    process_packets(&mut tla_parser);
  |                    ^^^^^^^^^^^^^^^
help: the `dyner` crate offer various a `PreAsyncIterator` wrapper type that can use stack allocation instead

Following the recommendations of the portability lint, she investigates the rust-lang dyner crate. In there she finds a few adapters she can use to avoid allocating a box. She decides to use the "preallocate" adapter, which preallocates stack space for each of the async functions she might call. To use it, she imports the PreAsyncIterator struct (provided by dyner) and wraps the tla_parser in it. Now she can use dyn without a problem:

#![allow(unused)]
fn main() {
use dyner::preallocate::PreAsyncIterator;

fn example_caller() {
    let tla_parser = TlaParser::new(SomeSource);
    let mut tla_parser = PreAsyncIterator::new(tla_parser);
    process_packets(&mut tla_parser);
}

fn process_packets(parser: &mut dyn AsyncIterator<Item = TlaPacket>) {
    while let Some(packet) = parser.next().await {
        process_packet(packet);
    }
}
}

Preallocated versions of her own traits

As Grace continues working, she finds that she also needs to use dyn with a trait of her own devising:

#![allow(unused)]
fn main() {
trait DispatchItem {
    async fn dispatch_item(&mut self) -> Result<(), DispatchError>;
}

struct MyAccumulatingDispatcher { }

impl MyAccumulatingDispatcher {
    fn into_result(self) -> MyAccumulatedResult;
}

fn example_dispatcher() -> String {
    let mut dispatcher = MyAccumulatingDispatcher::new();
    dispatch_things(&mut dispatcher);
    dispatcher.into_result()
}

async fn dispatch_things(context: Context, dispatcher: &mut dyn DispatchItem) {
    for item in context.items() {
        dispatcher.dispatch_item(item).await;
    }
}
}

She uses the dyner::preallocate::create_struct macro to create a PreDispatchItem struct she can use for dynamic dispatch:

#![allow(unused)]
fn main() {
#[dyner::preallocate::for_trait(PreDispatchItem)]
trait DispatchItem {
    async fn dispatch_item(&mut self) -> Result<(), DispatchError>;
}
}

Now she is able to use the same pattern to call dispatch_things. This time she wraps an &mut dispatcher instead of taking ownership of dispatcher. That works just fine since the trait only has an &mut self method. This way she can still call into_result:

#![allow(unused)]
fn main() {
fn example_dispatcher() -> MyAccumulatedResult {
    let mut dispatcher = MyDispatcher::new();
    let mut dispatcher = PreDispatchItem::new(&mut dispatcher);
    dispatch_things(&mut dispatcher);
    dispatcher.into_result()
}
}

Other strategies

Reading the docs, Grace finds a few other strategies available for dynamic dispatch. They all work the same way: a procedural macro generates a custom wrapper type for the trait that handles custom dispatch cases. Some examples:

  • Choosing from one of a fixed number of alternatives; returning an enum as the future and not a Box<impl Future>.

Performance-sensitive inner loop with dynamic dispatch

Taking ownership of the receiver

Summary

  • Support traits that use fn(self) with dynamic dispatch
    • The caller will have to be using a Box<dyn Foo>, but that is not hard-coded into the trait.

Status quo

Grace is working on an embedded system. She needs to parse data from an input stream that is formatted as a series of packets in the format TLA. She finds a library tla on crates.io with a type that implements the async iterator trait:

Async drop

Summary

  • Able to define an async fn drop for types that must release async resources when they go out of scope
  • Manage the interaction with Send

Status quo

Shiny future

Embedded async drop

Async fn in traits usage scenarios

What follows are a list of "usage scenarios" for async fns in traits and some of their characteristics.

Core Rust async traits

Defining core Rust async traits like AsyncRead:

#![allow(unused)]
fn main() {
trait AsyncRead {
    async fn read(&mut self, buffer: &mut [u8]) -> usize;
}
}

And implementing them in client code:

#![allow(unused)]
fn main() {
}

Characteristics:

  • Need static dispatch with zero overhead, so folks can write fn foo<F: AsyncRead>(), have it fully monomorphized, and equally efficient
  • Need to support dyn AsyncRead eventually (people will want to do that)
  • Need ability to

Client: ship library in crates.io that leverages async traits

Example from AWS Rust SDK:

#![allow(unused)]
fn main() {
pub trait ProvideCredentials: Send + Sync + Debug {
    fn provide_credentials<'a>(&'a self) -> ProvideCredentials<'a>;
}
}

People implement this like

#![allow(unused)]
fn main() {
impl ProvideCredentials for MyType {
    fn provide_credentials<'a>(&'a self) -> ProvideCredentials<'a> {
        ProvideCredentials::new(async move { ... })
    }
}
}

where [ProvideCredentials](https://docs.rs/aws-types/0.10.1/aws_types/credentials/future/struct.ProvideCredentials.html) is a struct that takes an impl Future` and creates

Embedded: ship library in crates.io that leverages async traits

Challenges

This section describes the challenges that we need to find solutions for.

Bounding futures

The challenge is to be able to concisely and intuitively bound the futures resulting from async fn calls, as described in the Background Logging scenario.

Naming futures

Status

Seems unlikely to be adopted. Doesn't feel right, and doesn't give any expanded abilities, such as the ability to name the future type.

Summary

The challenge is to be able to name the future that results from a particular async function. This can be used, for example, when naming future types, or perhaps as part of bounding futures.

It is likely that this problem is best solved in the context of the impl trait initiative.

Dyn traits

Supporting dyn Trait when Trait contains an async fn is challenging:

#![allow(unused)]
fn main() {
trait Trait {
    async fn foo(&self);
}

impl Trait for TypeA {
    async fn foo(&self);
}

impl Trait for TypeB { ... }
}

Consider the desugared form of this trait:

#![allow(unused)]
fn main() {
trait Trait {
    type Foo<'s>: Future<Output = ()> + 's;

    fn foo(&self) -> Self::Foo<'_>;
}

impl Trait for TypeA {
    type Foo<'s> = impl Future<Output = ()> + 's;

    fn foo(&self) -> Self::Foo<'_> {
        async move { ... } // has some unique future type F_A
    }
}

impl Trait for TypeB { ... }
}

The primary challenge to using dyn Trait in today's Rust is that dyn Trait today must list the values of all associated types. This means you would have to write dyn for<'s> Trait<Foo<'s> = XXX> where XXX is the future type defined by the impl, such as F_A. This is not only verbose (or impossible), it also uniquely ties the dyn Trait to a particular impl, defeating the whole point of dyn Trait.

For this reason, the async_trait crate models all futures as Box<dyn Future<...>>:

#![allow(unused)]
fn main() {
#[async_trait]
trait Trait {
    async fn foo(&self);
}

// desugars to

trait Trait {
    fn foo(&self) -> Box<dyn Future<Output = ()> + Send + '_>;
}
}

This compiles, but it has downsides:

  • Allocation is required, even when not using dyn Trait.
  • The user must state up front whether Box<dyn Future...> is Send or not.
    • In async_trait, this is declared by writing #[async_future(?Send)] if desired.

Desiderata

Here are some of the general constraints:

  • The ability to use async fn in a trait without allocation
  • When using a dyn Trait, the type of the future must be the same for all impls
    • This implies a Box or other pointer indirection, or something like inline async fn.
  • It would be nice if it were possible to use dyn Trait in an embedded context (without access to Box)
    • This will not be possible "in general", but it could be possible for particular traits, such as AsyncIterator

Bounding async drop

As a special case of the bounding futures problem, we must consider AsyncDrop.

#![allow(unused)]
fn main() {
async fn foo<T>(t: T) {
    runtime::sleep(22).await;
}
}

The type of foo(t) is going to be a future type like FooFuture<T>. This type will also include the types of all futures that get awaited (e.g., the return value of runtime::sleep(22) in this case). But in the case of T, we don't yet know what T is, and if it should happen to implement AsyncDrop, then there is an "implicit await" of that future. We have to ensure that the contents of that future are taken into account when we determine if FooFuture<T>: Send.

Guaranteeing async drop

One challenge with AsyncDrop is that we have no guarantee that it will be used. For any type MyStruct that implements AsyncDrop, it is always possible in Rust today to drop an instance of MyStruct in synchronous code. In that case, we cannot run the async drop. What should we do?

Obvious alternatives:

  • Panic or abort
  • Use some form of "block on" or other default executor to execute the asynchronous await
  • Extend Rust in some way to prevent this condition.

We can also mitigate this danger through lints (e.g., dropping value which implements AsyncDrop).

Some types may implement both synchronous and asynchronous drop.

Implicit await with async drop

Consider this code:

#![allow(unused)]
fn main() {
async fn foo(input: &QueryInput) -> anyhow::Result<()> {
    let db = DatabaseHandle::connect().await;
    let query = assemble_query(&input)?;
    let results = db.perform_query(query).await;
    while let Some(result) = results.next().await? {
        ...
    }
}
}

Now let us assume that DatabaseHandle implements AsyncDrop to close the connection. There are numerous points here where db could be dropped (e.g., each use of ?). At each of those points, there is effectively an implicit await similar to AsyncDrop::async_drop(db).await. It seems clear that users should not be required to manually write those things, but it is also a weakening of the existing .await contract (that all blocking points are visible).

Design documents

This section contains detailed design documents aimed at various challenges.

DocumentChallenges addressedStatus
Implied SendBounding futures❌
Trait multiplicationBounding futuresπŸ€”
Inline async fnBounding futures, Dyn traits (partially)πŸ€”
Custom dyn implsDyn traitsπŸ€”
[Auto traits consider AsyncDrop][Bounding drop]πŸ€”

Implied Send

Status

❌ Rejected. This idea can be quite productive, but it is not versatile (it rules out important use cases) and it is not supportive (it is confusing).

(FIXME: I think the principles aren't quite capturing the constriants here! We should adjust.)

Summary

Targets the bounding futures challenge.

The core idea of "implied Send" is to say that, by default at least, the future that results from an async fn must be Send if the Self type that implements the trait is Send.

In Chalk terms, you can think of this as a bound like

if (Implemented(Self: Send)) { 
    Implemented(Future: Send)
}

Mathematically, this can be read as Implemented(Self: Send) => Implemented(Future: Send). In other words, if you assume that Self: Send, then you can show that Future: Send.

Desugared semantics

If we extended the language with if bounds a la Chalk, then the desugared semantics of "implied send" would be something like this:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    type NextFuture: Future<Output = Self::Item>
                   + if (Self: Send) { Send };

    fn next(&mut self) -> impl Self::NextFuture;
}
}

As a result, when you implement AsyncIterator, the compiler will check that your futures are Send if your input type is assumed to be Send.

What's great about this

The cool thing about this is that if you have a bound like T: AsyncIterator + Send, that automatically implies that any futures that may result from calling AsyncIterator methods will also be Send. Therefore, the background logging scenario works like this, which is perfect for a work stealing executor style.

#![allow(unused)]
fn main() {
async fn start_writing_logs(
    logs: impl AsyncIterator<Item = String> + Send + 'static
) {
    ...
}
}

What's not so great

Negative reasoning: Semver interactions, how to prove

In this proposal, when one implements an async trait for some concrete type, the compiler would presumably have to first check whether that type implements Send. If not, then it is ok if your futures do not implement Send. That kind of negative reasoning is actually quite tricky -- it has potential semver implications, for example -- although auto traits are more amenable to it than other things, since they already interact with semver in complex ways.

In fact, if we use the standard approach for proving implication goals, the setup would not work at all. The typical approach to proving an implication goal like P => Q is to assume P is true and then try to prove Q. But that would mean that we would just wind up assuming that the Self type is Send and trying to use that to prove the resulting Future is Send, not checking whether Self is Send to decide.

Not analogous to async fn outside of traits

With inherent async functions, we don't check whether the resulting future is Send right away. Instead, we remember what state it has access to, and then if there is some part of the code that requires a future to be Send, we check then.

But this "implied send" approach is different: the trait is effectively declaring up front that async functions must be send (if the Self is send, at least), and so you wind up with errors at the impl. This is true regardless of whether the future ever winds up being used in a spawn.

The concern here is not precisely that the result is too strict (that's covered in the next bullet), but rather that it will be surprising behavior for people. They'll have a hard time understanding why they get errors about Send in some cases but not others.

Stricter than is required for non-work-stealing executor styles

Building on the previous point, this approach can be stricter than what is required when not using a work stealing executor style.

As an example, consider a case where you are coding in a thread-local setting, and you have a struct like the following

#![allow(unused)]
fn main() {
struct MyCustomIterator {
    start_index: u32
}
}

Now you try to implement AsyncIterator. You know your code is thread-local, so you decide to use some Rc data in the process:

#![allow(unused)]
fn main() {
impl AsyncIterator for MyCustomIterator {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let ssh_key = Rc::new(vec![....]);
        do_some_stuff(ssh_key.clone());
        something_else(self.start_index).await;
    }
}
}

But now you get a compilation error:

error: `read` must be `Send`, since `MyCustomIterator` is `Send`

Frustrating!

Trait multiplication

Status

Seems unlikely to be adopted, but may be the seed of a better idea

Summary

Introduce a new form of bound, trait multiplication. One can write T: Iterator * Send and it means T: Iterator<Item: Send> + Send (using the notation from RFC 2289). More generally, Foo * Bar means Foo + Bar but also that Foo::Assoc: Bar for each associated type Assoc defined in Foo (including anonymous ones defined by async functions).

What's great about this

The cool thing about this is that it means that the check whether things are Send occurs exactly when it is needed, and not at the definition site. This makes async functions in trait behave more analogously with ordinary impls. (In contrast to implied bounds.)

With this proposal, the background logging scenario would play out differently depending on the executor style being used:

  • Thread-local: logs: impl AsyncIterator<Item = String> + 'static
  • Thread per core: logs: impl AsyncIterator<Item = String> + Send + 'static
  • Work stealing: logs: impl AsyncIterator<Item = String> * Send + 'static

The key observation here is that + Send only tells you whether the initial value (here, logs) is Send. The * Send is needed to say "and the futures resulting from this trait are Send", which is needed in work-stealing sceanrios.

What's not so great about this

Complex

The distinction between + and * is subtle but crucial. It's going to be a new thing to learn and it just makes the trait system feel that much more complex overall.

Reptitive for multiple traits

If you had a number of async traits, you would need * Send for each one:

#![allow(unused)]
fn main() {
trait Trait1 {
    async fn method1(&self);
}

trait Trait2 {
    async fn method2(&self);
}

async fn foo<T>()
where
    T: Send * (Trait1 + Trait2)
{

}
}

Inline async fn

Status

Seems unlikely to be adopted, but may be the seed of a better idea

Status quo

Until now, the only way to make an "async trait" be dyn-safe was to use a manual poll method. The AsyncRead trait in futures, for example, is as follows:

#![allow(unused)]
fn main() {
pub trait AsyncRead {
    fn poll_read(
        self: Pin<&mut Self>, 
        cx: &mut Context<'_>, 
        buf: &mut [u8]
    ) -> Poll<Result<usize, Error>>;

    unsafe fn initializer(&self) -> Initializer { ... }
    
    fn poll_read_vectored(
        self: Pin<&mut Self>, 
        cx: &mut Context<'_>, 
        bufs: &mut [IoSliceMut<'_>]
    ) -> Poll<Result<usize, Error>> { ... }
}
}

Implementing these traits is a significant hurdle, as it requires the use of Pin. It also means that people cannot leverage .await syntax or other niceties that they are accustomed to. (See Alan hates writing a stream for a narrative description.)

It would be nice if we could rework those traits to use async fn. Today that is only possible using the async_trait procedural macro:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait AsyncRead {
    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error>;

    async fn poll_read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize, Error>;

    unsafe fn initializer(&self) -> Initializer { ... }
    
}
}

Unfortunately, using async-trait has some downsides (see Alan needs async in traits). Most notably, the traits are rewritten to return a Box<dyn Future>. For many purposes, this is fine, but in the case of foundational traits like AsyncRead, AsyncDrop, AsyncWrite, it is a significant hurdle:

  • These traits should be included in libcore and available to the no-std ecosystem, like Read and Write.
  • These traits are often on the performance "hot path" and forcing a memory allocation there could be significant for some applications.

There are some other problems with the poll-based design. For example, the buffer supplied to poll_read can change in between invocations (and indeed existing adapters take advantage of this sometimes). This means that the traits cannot be used for zero copy, although this is not the only hurdle.

For drop especially, the state must be embedded within the self type

If we want to have an async version of drop, it is really important that it does not return a separate future, but only makes use of state embedded within the type. This is because we might have a Box<dyn Future> or some other type that implements AsyncDrop, but where we don't know the concrete type. We are going to want to be able to drop those, which implies that they will live on the stack, which implies that we have to know the contents of the resulting future to know if it is Send.

Problem: returning a future

The fundamental problem that makes async fn not dyn-safe (and the reason that allocation is required) is that every implementation of AsyncRead requires different amounts of state. The future that is returned is basically an enumeration with fields for each value that may be live across an await point, and naturally that will vary per implementation. This means that code which doesn't know the precise type that it is working with cannot predict how much space that type will require. One solution is certainly boxing, which sidesteps the problem by returning a pointer to memory in the heap.

Using poll methods, as the existing traits do, sidesteps this in a different way: the poll methods basically require that any state that the AsyncRead impl requires across invocations of poll must be present within the self field itself. This is a perfectly valid solution for many applications, but figuring out that state and tracking it efficiently is tedious for users.

Proposal: "inline" futures

The idea is to allow users to opt-in to "inline futures". Users would write a repr attribute on traits that contain async fn methods (the attribute):

#![allow(unused)]
fn main() {
#[repr(inline_async)]
trait AsyncRead {
    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error>;
    
    ...
}
}

The choice of repr is significant here:

  • Like repr on a struct, this is meant to be used for things that affect how the code is compiled and its efficiency, but which don't affect the "mental model" of how the trait works.
  • Like repr on a struct, using repr may imply some limitations on the things you can do with the trait in order to achieve those benefits.

When a trait is as repr(inline_async), the state for all of its async functions will be added into the type that implements the trait (this attribute could potentially also be used per method). This does imply some key limitations:

  • repr(inline_async) traits can only be implemented on structs or enums defined in the current crate. This allows the compiler to append those fields into the layout of that struct or enum.
  • repr(inline_async) traits can only contain async fn with &mut self methods.

Desugaring

The desugaring for an inline_async function is different. Rather than an async fn becoming a type that returns an impl Future, the async fn always returns a value of a fixed type. This is a kind of variant on Future::PollFn, which will simply invoke the poll_read function each time it is called. What we want is something like this, although this doesn't quite work (and relies on unstable features Niko doesn't love):

#![allow(unused)]
fn main() {
trait AsyncRead {
    // Standard async fn desugaring, with a twist:
    fn read(&mut self, buf: &mut [u8]) -> Future::PollFn<
        typeof(<Self as AsyncRead>::poll_read)
    >;

    // 
    fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<usize, Error>>;
}
}

Basically the read method would

  • initialize the state of the future and then
  • construct a Future::PollFn-like struct that contains a pointer to the poll_read function.

FAQ

What's wrong with that desugaring?

The desugaring is pretty close. It has the nice property that, when invoked with a known type, the Future::PollFn dispatches statically the poll function, so there is no dynamic dispatch or loss of efficiency.

However, it also has a problem. The return type is still dependent on Self, so per our existing dyn Rules, that doesn't work.

It should be possible to extend our dyn Rules, though. All that is needed is a bit of "adaptation glue" in the code that is included in the vtable so that it will convert from a Future::PollFn for some fixed T to one that uses a fn pointer. That seems eminently doable, but I'm not sure if it can be expressed in the language today.

Pursuing this road might lead to a fundamental extension in dyn safety, which would be nice!

What state is added precisely to the struct?

  • An integer recording the await point where the future is blocked
  • Fields for any data that outlives the await

What if I don't want lots of state added to my struct?

We could limit the use of variables live across an await.

Could we extend this to other traits?

e.g., simulacrum mentioned -> impl Iterator in a (dyn-safe) trait. Seems plausible.

Why do you only permit &mut self methods?

Since the state for the future is stored inline in the struct, we can only have one active future at a time. Using &mut self ensures that the poll function is only in use by one future at a time, since that future would be holding an &mut reference to the receiver.

We would like to implement AsyncRead for all &mut impl AsyncRead, how can we enable that?

I think this should be possible. The trick is that the poll function would just dispatch to another poll function. We might be able to support it by detecting the pattern of the async fn directly awaiting something reachable from self and supporting that for arbitrary types:

#![allow(unused)]
fn main() {
impl<T: AsyncRead> AsyncRead for &mut T {
    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        T::read(self, buf).await
    }
}
}

Basically this compiles to a poll_read that just tweaks dispatches to another poll_read with some derefs.

Can you implement both AsyncRead and AsyncWrite for the same type with this technique?

You can, but you can't simultaneously read and write from the same value. You would need a split-like API.

Custom dyn impls

As described in dyn traits, dyn Trait types cannot include the types of each future without defeating their purpose; but outside of a dyn context, we want those associated types to have unique values for each impl. Threading this needle requires extending Rust so that the value of an associated type can be different for a dyn Trait and for the underlying impl.

How it works today

Conceptually, today, there is a kind of "generated impl" for each trait. This impl implements each method by indirecting through the vtable, and it takes the value of associated types from the dyn type:

#![allow(unused)]
fn main() {
trait Foo {
    type Bar;

    fn method(&self);
}

impl<B> Foo for dyn Foo<Bar = B> {
    type Bar = B;

    fn method(&self) {
        let f: fn(&Self) = get_method_from_vtable(self)
        f(self)
    }
}
}

Meanwhile, at the point where a type (say u32) is coerced to a dyn Foo, we generate a vtable based on the impl:

#![allow(unused)]
fn main() {
// Given
impl Foo for u32 {
    fn method(self: &u32) { XXX }
}

// we could a compile for `method`:
// fn `<u32 as Foo>::method`(self: &u32) { XXX }

fn eg() {
    let x: u32 = 22;
    &x as &dyn Foo // <-- this case
}

// generates a vtable with a pointer to that method:
//
// Vtable_Foo = [ ..., `<u32 as Foo>::method` ]
}

Note that there are some known problems here, such as soundness holes in the coherence check.

Rough proposal

What we would like is the ability for this "dyn" impl to diverge more from the underlying impl. For example, given a trait Foo with an async fn method:

#![allow(unused)]
fn main() {
trait Foo {
    async fn method(&self);
}
}

The compiler might generate an impl like the following:

#![allow(unused)]
fn main() {
impl<B> Foo for dyn Foo {
    //          ^^^^^^^ note that this type doesn't include Bar = ...

    type Bar = Box<dyn Future<Output = ()>>;
    //         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ because the result is hardcoded

    fn method(&self) -> Box<dyn Future<Output = ()>> {
        let f: fn(&Self) = get_method_from_vtable(self)
        f(self) 
    }
}
}

The vtable, meanwhile, resembles what we had before, except that it doesn't point directly to <u32 as Foo>::method, but rather to a wrapper function (let's call it methodX) that has the job of coercing from the concrete type into a Box<dyn Future>:

// Vtable_Foo = [ ..., `<u32 as Foo>::methodX`]
// fn `<u32 as Foo>::method`(self: &u32) { XXX  }
// fn `<u32 as Foo>::methodX`(self: &u32) -> Box<dyn> { Box::new(TheFuture)  }

Auto traits

To handle "auto traits", we need multiple impls. For example, assuming we adopted trait multiplication, we would have multiple impls, one for dyn Foo and one for dyn Foo * Send:

#![allow(unused)]
fn main() {
trait Foo {
    async fn method(&self);
}

impl<B> Foo for dyn Foo {
    type Bar = Box<dyn Future<Output = ()>>;

    fn method(&self) -> Box<dyn Future<Output = ()>> {
            
    }
}

impl<B> Foo for dyn Foo * Send {
    //          ^^^^^^^^^^^^^^

    type Bar = Box<dyn Future<Output = ()> + Send>;
    //         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    fn method(&self) -> Box<dyn Future<Output = ()>> {
            ....
    }
}

// compiles to:
//
// Vtable_Foo = [ ..., `<u32 as Foo>::methodX`]
// fn `<u32 as Foo>::method`(self: &u32) { XXX  }
// fn `<u32 as Foo>::methodX`(self: &u32) -> Box<dyn> { Box::new(TheFuture)  }
}

Hard-coding box

One challenge is that we are hard-coding Box in the above impls. We could control this in a number of ways:

  • Annotate the trait with an alternate wrapper type
  • Extend dyn types with some kind of indicator of the wrapper (dyn(Box)) that they use for this case
  • Generate impls for Box<dyn> -- has several shortcomings

Applicable

Everything here is applicable more broadly, for example to types that return Iterator.

It'd be nice if we extended this capability of "writing your own dyn impls" to end-users.

Auto traits consider async drop

One way to solve the bounding async drop challenge is to require that, if a type X implements AsyncDrop, then X: Send only if the type of its async drop future is also Send. The drop trait is already integrated quite deeply into the language, so adding a rule like this would not be particularly challenging.

Simple names

One simple way to give names to async functions is to just generate a name based on the method. For example:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Self::Item;
}
}

could desugar to

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    type Next<'me>: Future<Output = Self::Item> + 'me;
    fn next(&mut self) -> Self::Next<'_>;
}
}

Users could then name the future with <T as AsyncIterator>::Next<'a>.

This is a simple solution, but not a very general one, and perhaps a bit surprising (for example, there is no explicit declaration of Next).

Bound Items

Summary

Targets the bounding futures challenge.

This is a series of smaller changes (see detailed design), with the goal of allowing the end user to name the bound described in the challenge text in this way (i'll let the code speak for itself):

#![allow(unused)]
fn main() {
pub trait AsyncIterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;
}

pub async fn start_writing_logs<F>(
    logs: F
) where IsThreadSafeIterator<F>, F: 'static {  // IsThreadSafeIterator is defined below
    todo!()
}

pub bound IsThreadSafeAsyncIterator<T> {
    T: AsyncIterator + Send,
    // Add a `fn` keyword here to refer to the type of associated function.
    <T as AsyncIterator>::fn next: SimpleFnOnceWithSendOutput,
    // Use `*` to refer to potentially long list of all associated functions.
    // this is useful in certain cases.
    <T as AsyncIterator>::fn *: SimpleFnOnceWithSendOutput,
}

}

Detailed design

I'm not good at naming things, and all names and identifiers are subject to bikeshedding.

  • Bound Item(Language construct). Allow user to name a combination of bounds. No Self allowed here. Could replace the trait_alias language feature. Improve ergonomics of many existing code if used properly.

    I'm hesitant on which of PascalCase, snake_case, or UPPER_CASE should be used for this naming though.

  • Syntax for refering to the type of a associated item in path. Currently using fn keyword. Can expand to const and type keywords if needed. Turbofish might be involved if the assoc item is generic.

  • SimpleFnOnce trait (Language item). A function can only accept one set of arguments, so all functions and closures shall implement this, while user-defined callable types doesn't have to. This is used for reasoning about the output parameters of associated functions.

    #![allow(unused)]
    fn main() {
    pub trait SimpleFnOnce: FnOnce<Self::Arg> {
        type Arg;
    }
    }
  • SimpleFnOnceWithSendOutput trait (Lib construct or user-defined).

    #![allow(unused)]
    fn main() {
    pub trait SimpleFnOnceWithSendOutput : SimpleFnOnce where
        Self::Output: Send
    {}
    
    impl<T> SimpleFnOnceWithSendOutput for T where T:SimpleFnOnce, T::Output: Send {}
    }

What's great about this

  • Absolutely minimal type system changes.
  • Quite easy to learn. (Name it and use the name)
  • Easy to write library documentation, and give examples.

What's not so great

  • New syntax item constructs. Three parsers (rustc, ra, syn) needs to change to support this.
  • Very verbose code writing style. Many more lines of code.
  • Will become part of library API in many cases.

With clauses

Status

Crazy new idea that solves all problems

Summary

  • Introduce a with(x: T) clause that can appear wherever where clauses can appear
    • These variables are in scope in any (non-const) code block that appears within those scopes.
  • Introduce a new with(x = value) { ... } expression
    • Within this block, you can invoke fuctions that have a with(x: T) clause (presuming that value is of type T); you can also invoke code which calls such functions transitively.
    • The values are propagated from the with block to the functions you invoke.

More detailed look

Simple example

Suppose that we have a generic visitor interface in a crate visit:

#![allow(unused)]
fn main() {
trait Visitor {
    fn visit(&self);
}

impl<V> Visitor for Vec<V>
where
    V: Visitor,
{
    fn visit(&self) {
        for e in self {
            e.visit();
        }
    }
}
}

I would like to use this interface in my crate. But I was hoping to increment a counter each time one of my types is visited. Unfortunately, the Visitor trait doesn't offer any way to thread access to this counter into the impl. With with, though, that's no problem.

#![allow(unused)]
fn main() {
struct Context { counter: usize }

struct MyNode {

}

impl Visitor for MyNode 
with(cx: &mut Context)
{
    fn visit(&self) {
        cx.counter += 1;
    }
}
}

Now I can use this visitor trait as normal:

#![allow(unused)]
fn main() {
fn process_item() {
    let cx = Context { counter: 0 };
    let v = vec![MyNode, MyNode, MyNode];
    with(cx: &mut cx) {
        v.visit();
    }
    assert_eq!(cx.counter, 3);
}
}

How it works

We extend the environment with a with(name: Type) clause. When we typecheck a with(name: value) { ... } statement, we enter those clauses into the environment. When we check impls that contain with clauses, they match against those clauses like any other where clause.

After matching an impl, we are left with a "residual" of implicit parameters. When we monomorphize a function applied to some particular types, we will check the where clauses declared on the function against those types and collect the residual parameters. These are added to the function and supplied by the caller (which must have them in scope).

Things to overcome

Dyn value construction: we need some way to permit impls that use with to be made into dyn values. This is very hard, maybe impossible. The problem is that we don't want to "capture" the with values into the dyn -- so what do we do if somebody packages up a Box<dyn> and puts it somewhere?

We could require that context values implement Default but .. that stinks. =)

We could panic. That kind of stinks too!

We could limit to traits that are not dyn safe, particularly if there was a manual impl of dyn safety. The key problem is that, today, for a dyn safe trait, one can make a dyn trait without knowing the source type:

#![allow(unused)]
fn main() {
fn foo<T: Visitor + 'static>(v: T) {
    let x: Box<dyn Visitor> = Box::new(v);
}
}

But, now, what happens if the Box<dyn Visitor> is allowed to escape the with scope, and the methods are invoked?

Conceivably we could leverage lifetimes to prevent this, but I'm not exactly sure how. It would imply a kind of "lifetime view" on the type T that ensures it is not considered to outlive the with scope. That doesn't feel right. What we really want to do is to put a lifetime bound of sorts on the... use of the where clause.

We could also rework this in an edition, so that this capability is made more explicit. Then only traits and impls in the new edition would be able to use with clauses. This would harm edition interop to some extent, we'd have to work that out too.

Dynx

A dynx Trait represents "a pointer to something that implements Trait". It is an implementation detail of our design for "async fn in traits". Best description is currently to be found in the explainer. This area covers various thorny subquestions.

How do you create a dynx?

In the previous section, we showed how a #[dyn(identity)] function must return "something that can be converted into a dynx struct", and we showed that a case of returning a Pin<Box<impl Future, A>> type. But what are the general rules for constructing a dynx struct? You're asking the right question, but that's a part of the design we haven't bottomed out yet.

In short, there are two "basic" approaches we could take. One of them is more conservative, in that it doesn't change much about Rust today, but it's also much more complex, because dyn dealing with all the "corner cases" of dyn is kind of complicated. The other is more radical, but may result in an overall smoother, more coherent design.

Apart from that tantalizing tidbit, we are intentionally not providing the details here, because this document is long enough as it is! The next document dives into this question, along with a related question, which is how dynx and sealed traits interact.

This is actually a complex question with (at least) two possible answers.

Alternative A: P must deref to something that implements Bounds

The pointer type P must implement IntoRawPointer (along with various other criteria) and its referent must implement Bounds.

IntoRawPointer trait

#![allow(unused)]
fn main() {
// Not pseudocode, will be added to the stdlib and implemented
// by various types, including `Box` and `Pin<Box>`.

unsafe trait IntoRawPointer: Deref {
    /// Convert this pointer into a raw pointer to `Self::Target`. 
    ///
    /// This raw pointer must be valid to dereference until `drop_raw` (below) is invoked;
    /// this trait is unsafe because the impl must ensure that to be true.
    fn into_raw(self) -> *mut Self::Target;
    
    /// Drops the smart pointer itself as well as the contents of the pointer.
    /// For example, when `Self = Box<T>`, this will free the box as well as the
    /// `T` value.
    unsafe fn drop_raw(this: *mut Self::Target);
}
}

Other conditions pointer must meet

  • Must be IntoRawPointer which ensures:
    • Deref and DerefMut are stable, side-effect free and all that
    • they deref to the same memory as into_raw
  • If Bounds includes a &mut self method, P must be DerefMut
  • If Bounds includes a &self method, P must be Deref
  • If Bounds includes Pin<&mut Self>, P must be Unpin ... and ... something something DerefMut? how do you get from Pin<P> to Pin<&mut P::Target>?
  • If Bounds includes Pin<&Self>, P must be Unpin ... and ... something something DerefMut? how do you get from Pin<P> to Pin<&mut P::Target>?
  • If Bounds includes an auto trait AutoTrait, P must implement AutoTrait
    • and: dynx Bounds implements the auto trait AutoTrait (in general, dynx Bounds implements all of Bounds)
  • Bounds must be "dyn safe"

Alternative B: P must implement Bounds

Alternatively, we could declare that the pointer type P must implement Bounds. This is much simpler to express, but it has some issues. For example, if you have

#![allow(unused)]
fn main() {
trait Foo { 

}
}

then we could not construct a dynx Foo from a Box<dyn Foo> because there is no impl Foo for Box<dyn Foo>. It would be nice if those impls could be added automatically or at least more easily.

#![allow(unused)]
fn main() {
// Not pseudocode, will be added to the stdlib and implemented
// by various types, including `Box` and `Pin<Box>`.

unsafe trait IntoRawPointer: Deref {
    /// Convert this pointer into a raw pointer to `Self::Target`. 
    ///
    /// This raw pointer must be valid to dereference until `drop_raw` (below) is invoked;
    /// this trait is unsafe because the impl must ensure that to be true.
    fn into_raw(self) -> *mut Self::Target;
    
    /// These methods would be used by compiler to convert back so we can invoke the original
    /// impls.
    unsafe fn from_ref(this: &*mut Self::Target) -> &Self;
    unsafe fn from_mut_ref(this: &mut *mut Self::Target) -> &mut Self;
    ...

    /// Drops the smart pointer itself as well as the contents of the pointer.
    /// For example, when `Self = Box<T>`, this will free the box as well as the
    /// `T` value.
    unsafe fn drop_raw(this: *mut Self::Target);
}
}

With auto traits

We plan to address this in a follow-up RFC. The core idea is to build on the notation that one would use to express that you wish to have an async fn return a Send. As an example, one might write AsyncIterator<next: Send> to indicate that next() returns a Send future; when we generate the vtable for a dyn AsyncIterator<next: Send>, we can ensure that the bounds for next are applied to its return type, so that it would return a dynx Future + Send (and not just a dynx Future). We have also been exploring a more convenient shorthand for declaring "all the futures returned by methods in trait should be Send", but to avoid bikeshedding we'll avoid talking more about that in this document!

Dynx and sealed traits

As described here, every dyn-safe trait Trait gets an "accompanying" dynx Trait struct and an impl Trait for dynx Trait impl for that struct. This can have some surprising interactions with unsafe code -- if you have a trait that can only be safely implemented by types that meet certain criteria, the impl for a dynx type may not meet those criteria. This can lead to undefined behavior. The question then is: whose fault is that? In other words, is it the language's fault, for adding impls you didn't expect, or the code author's fault, for not realizing those impls would be there (or perhaps for not declaring that their trait had additional safety requirements, e.g. by making the trait unsafe).

A dynx Trait type is basically an implicit struct that implements Trait. For some traits, notably "sealed" traits, this could potentially be surprising.

Consider a trait PodTrait that is meant to be implemented only for "plain old data" types, like u8 and u32. By leveraging the "sealed trait" pattern, one can make it so that users cannot provide any impls of this trait:

#![allow(unused)]
fn main() {
pub mod public_api {
    mod sealed {
        pub trait Supertrait { }
    }

    /// **Guarantee.** Every type `T` that implements `PodTrait` meets
    /// this condition:
    ///
    /// * Given a `&T`, there are `byte_len` bytes of data behind the
    ///   pointer that safely be zeroed
    pub trait PodTrait: sealed::Supertrait {
        fn byte_len(&self) -> usize;
    }
    impl PodTrait for u8 {
        fn byte_len(&self) -> usize { 1 }
    }
    impl PodTrait for u32 {
        fn byte_len(&self) -> usize { 4 }
    }
    ...
}
}

This trait could then be used to build safe abstractions that leverage unsafe reasoning:

#![allow(unused)]
fn main() {
pub mod public_api {
    ...
    
    pub fn zero(pod: &mut impl PodTrait) {
        let n = pod.byte_len();

        // OK: We have inspected every impl of `PodTrait`
        // and we know that `byte_len` accurately
        // describes the length of a pointer.
        unsafe {
            <*mut _>::write_bytes(pod, 0, n)
        }
    }
    
    ...
}
}

Unfortunately, this reasoning is not sound if combined with a dynx type:

#![allow(unused)]
fn main() {
pub mod public_api {
    ...

    trait GetPodTrait {
        fn get(&self) -> impl PodTrait;
    }
}

fn method(x: &dyn GetPodTrait) {
    // `y` will be a `dynx GetPodTrait`, which will be
    // a `Box<u8>` or `Box<u32>`
    let mut y = x.get();
    
    // Calling `zero` is trying to zero the memory *referenced* by
    // the box, but it will actually zero the box pointer itself.
    // Bad!
    public_api::zero(&mut y);
}
}

What went wrong here? The problem is implementing PodTrait carries an additional proof obligation beyond those that the Rust type checker is aware of. As the comment says, one must guarantee that byte_len, invoked on an &impl PodTrait, correctly describes the number of bytes in that memory (and that it can safely be zeroed). This invariant was manually verified for the u8 and u32 impls, but it does not hold for the impl PodTrait for dynx PodTrait generated by the compiler.

There are a few ways to resolve this conundrum:

  • Declare that PodTrait should have been declared as unsafe, and that -> impl Foo is not dyn safe if Foo is an unsafe trait.
    • One could argue that the original code was wrong to declare PodTrait as safe; if it were declared as unsafe, and we used that to suppress the dynx impl (after all, we can't know whether it satisfies the extra conditions), the above code would no longer compile.
    • However, many Rust devs consider unafe to be more of a tool for the user -- if you have private functions and types, you are supposed to be able to reason fully about what they do without needing internal unsafe annotations (there hasn't been a formal decision about whether this is a valid principle, but it has many adherents, and it's possible we should make it one).
    • Furthermore, this means that unsafe traits can't be used with dynx, which could in some cases be useful.
      • We could have an opt-in for this case.
  • Some form of opt-in for dyn compatibility (aka, dyn safety).
    • Ignoring backwards compatiblility, we could require traits to "opt-in" to being dyn-compatible (e.g., one might write dyn trait Foo instead of just having a trait Foo that doesn't make use of various prohibited features). In that case, the user is "opting in" to the generation of the dynx impl; e.g., a dyn trait PodTrait declaration would be simply broken, because that dyn trait declaration implies the generation of an impl PodTrait for dynx PodTrait, and the safety criteria are not met (this is subtle, but then the code itself is subtle).
      • We could leverage editions in various ways to resolve the backwards compatibility issues.
    • Arguments in favor of opt-in:
      • The fact that the compiler implicitly decides whether your trait is dyn compatible or not is a common source of user confusion -- it's easy to not realize you are promising people dyn compatibility, and also easy to not realize that you've lost it via some change to the crate.
      • Furthermore, there are some soundness holes (e.g., #57893) that are much harder to fix because there is no declaration of dyn compatibility. In those cases, if we knew the trait was meant to be dyn compatible, we could easily enforce stricter rules that would prevent unsoundness. But detecting whether the trait would meet those stricter rules is difficult, and so it is hard to determine automatically whether the trait should be dyn compatible or not.
      • Finally, some form of explicit "opt-in" to dyn compatibility might allow us to close various ergonomic holes, such as automatically providing an impl of Trait for Box<dyn Trait>, &dyn Trait, and other pointer types (perhaps all pointer types).
    • Arguments against:
      • Backwards compatibility. We currently determine automatically if traits are "dyn compatible" or not. That code has to keep working.
      • Arguably, traits should be dyn compatible by default, and you should "opt out" to make use of the extended set of features that purely static traits provide (we could, of course, do that over an edition).
  • If a trait SomeTrait includes an -> impl Foo method, make the trait dyn safe only if SomeTrait could itself have declared a struct that implements Foo
    • This is a rather wacky rule, but the idea is that if something is "sealed", then either (a) the trait SomeTrait is outside the sealed abstraction, and so won't be able to implement it; or (b) it's your own fault, because it's inside the sealed abstraction.
    • In this case, the offending code is inside the sealed abstraction, so we'd still have the bug.
  • Introduce a "sealed" keyword and declare that code is wrong.
    • We would rather not entangle these designs; also, like the "unsafe"-based solution, combining sealed + dyn compatible would likely make sense.

βš–οΈ Case Studies

Spring 2023: In preparation for stabilization an MVP of async function in traits, we asked a number of teams to integrate the nightly support on an experimental basis and talk through how well it worked for them. The following case studies provide details on their experiences. In short, the conclusions were:

  • Static dispatch AFIT generally meets expectations.
  • Most every project needs dynamic dispatch, but the workaround of deriving an "erased" version of the trait is sufficient for now, and can be hidden from end-users as a private impl detail.
  • Some solution for applying send bounds is required. RTN is fine for most cases but doesn't scale well to traits with many methods.
  • Having some automated way to derive "dynamic compatible" and "all methods with send bound" variants of the trait would be useful.

Async Builder + Provider API Case Study

This case study presents a common API pattern found in builders in the AWS SDK.

Current API

Several builders in the AWS SDK follow a "async provider" model, where the builder takes an implementation of a trait returning a future to customize behavior.

#![allow(unused)]
fn main() {
let credentials = DefaultCredentialsChain::builder()
    // Provide an `impl ProvideCredentials`
    .with_custom_credential_source(MyCredentialsProvider)
    .build()
    .await;
}

In this example, the user is able to add a custom credentials source for a DefaultCredentialsChain. This credentials source is allowed to do async work upon invocation by the credentials chain. The with_custom_credential_source builder method takes an implementation of the ProvideCredentials trait:

#![allow(unused)]
fn main() {
pub trait ProvideCredentials: Send + Sync + Debug {
    fn provide_credentials(&self) -> ProvideCredentials<'_>;
}
}

The current ProvideCredentials trait is a bit awkward. It expects the implementor to return an instance of ProvideCredentials<'_> struct, which acts like a boxed future that yields Result<Credentials, CredentialsError>:

#![allow(unused)]
fn main() {
struct MyCredentialsProvider;
// Implementations return `ProvideCredentials<'_>`, which is basically a boxed
// `impl Future<Output = Result<Credentials, CredentialsError>>`.
impl ProvideCredentials for MyCredentialsProvider {
    fn provide_credentials(&self) -> ProvideCredentials<'_> {
        ProvideCredentials::new(async move {
            /* Make some credentials */
        })
    }
}
}

Under the hood, when the builder's with_custom_credential_source is called, it boxes the impl ProvideCredentials and stores it for use in the DefaultCredentialsChain that will be built.

With AFIT

Since ProvideCredentials basically returns an impl Future already, with AFIT, ProvideCredentials can instead be simplified to:

#![allow(unused)]
fn main() {
trait ProvideCredentials {
    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError>;
}
}

The user can then provide an implementation of the trait without the extra step of wrapping the function body in ProvideCredentials::new(async { ... }).

#![allow(unused)]
fn main() {
impl ProvideCredentials for MyCredentialsProvider {
    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
        let credentials =  query_something().await?;
        // do other things like validation
        Ok(credentials)
    }
}
}

And the builder invocation remains the same...

#![allow(unused)]
fn main() {
let credentials = DefaultCredentialsChain::builder()
    // Provide an `impl ProvideCredentials`
    .with_custom_credential_source(MyCredentialsProvider)
    .build()
    .await;
}

Dynamic Dispatch: Behind the API

To make this change to the builder, we need to take instances of the new ProvideCredentials trait. Without AFIDT1, we can't simply box the impl ProvideCredentials in with_custom_credential_source like we were doing before.

Luckily, we can use a small type erasure hack to get around the lack of AFIDT, introducing a new trait called ProvideCredentialsDyn that has a blanket impl for all implementors of ProvideCredentials:

1

"Async functions in dyn trait", allowing traits with async fn methods to be object safe.

#![allow(unused)]
fn main() {
trait ProvideCredentialsDyn {
    fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + '_>>;
}

impl<T: ProvideCredentials> ProvideCredentialsDyn for T {
    fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + '_>> {
        Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
    }
}
}

This new ProvideCredentialsDyn trait is object-safe, and can be boxed and stored inside the builder instead of ProvideCredentials:

#![allow(unused)]
fn main() {
struct DefaultCredentialsChain {
    credentials_source: Box<dyn ProvideCredentialsDyn>,
    // ...
}

impl DefaultCredentialsChain {
    fn with_custom_credential_source(self, provider: impl ProvideCredentials) {
        // Coerce `impl ProvideCredentials` to `Box<dyn ProvideCredentialsDyn>`
        Self { provider: Box::new(credentials_source), ..self }
    }
}
}

This extra trait is an implementation detail that is not in the public-facing, API so it can be migrated away when support for AFIDT is introduced.

A full builder pattern example is implemented here: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=8daf7b2d5236e581f78d2c09310d09ac

Send bounds

One limitation with the proposed async version of ProvideCredentials is the lack of a Send bound on the future returned by ProvideCredentials. This bound is enforced by the pre-AFIT version of this trait, so any futures using the builder will not be Send after AFIT migration.

To fix this, we could use a return type bound2 on the with_custom_credential_source builder method:

2

https://smallcultfollowing.com/babysteps/blog/2023/02/13/return-type-notation-send-bounds-part-2/

#![allow(unused)]
fn main() {
impl DefaultCredentialsChain {
    fn with_custom_credential_source(
        self, 
        provider: impl ProvideCredentials<provide_credentials(): Send>
    ) {
        // Coerce `impl ProvideCredentials` to `Box<dyn ProvideCredentialsDyn>`
        Self { provider: Box::new(credentials_source), ..self }
    }
}
}

Then the ProvideCredentialsDyn trait could be modified to return Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>>:

#![allow(unused)]
fn main() {
trait ProvideCredentialsDyn {
    fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>>;
}

impl<T: ProvideCredentials<provide_credentials(): Send>> ProvideCredentialsDyn for T {
    fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>> {
        Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
    }
}
}

Alternative and equivalent to this would be something like bounding by T: async(Send) ProvideCredentials, which may look like:

#![allow(unused)]
fn main() {
impl<T: async(Send) ProvideCredentials> ProvideCredentialsDyn for T {
    fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>> {
        Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
    }
}
}

Usages

The SDK uses this same idiom several times:

  • ProvideCredentials: https://docs.rs/aws-credential-types/0.54.1/aws_credential_types/provider/trait.ProvideCredentials.html
  • AsyncSleep: https://docs.rs/aws-smithy-async/0.54.3/aws_smithy_async/rt/sleep/trait.AsyncSleep.html
  • ProvideRegion: https://docs.rs/aws-config/0.54.1/aws_config/meta/region/trait.ProvideRegion.html

Future improvements

With AFIDT, we can drop the ProvideCredentialsDyn trait and just use Box<dyn ProvideCredentials> as is. Refactoring the API to use AFIDT is a totally internal-facing change.

Netstack3 Async Socket Handler Case Study

This case study presents a simplification of a common handler pattern enabled by async_fn_in_trait (AFIT).

Background

Netstack3 is a networking stack written in Rust for the Fuchsia operating system. As a Fuchsia component, Netstack3 communicates with its clients using Fuchsia-native asynchronous IPC via generated bindings from Fuchsia interface definition language (FIDL) specifications. Netstack3 provides, via various FIDL protocols, the ability for other Fuchsia components (e.g. applications) to create and manipulate POSIX-like socket objects.

Per-socket handler implementations

To allocate a socket, a Fuchsia component sends a single message to the netstack indicating the desired type of socket, along with a Zircon channel that the netstack is expected to listen for requests on. The other end of the channel is held by the client and used to send requests for the new socket to the netstack.

When Netstack3 receives a request to create a socket, it spawns a new fuchsia_async::Task to dispatch incoming requests for the socket to the socket's handler. The type of messages for the socket depends on the protocol, so Netstack3 has distinct handler types for each.

Though the handler types are distinct, their functionality is fairly similar:

  • Handlers wait to receive incoming requests on their channel, then process them.
  • Handlers process each received message completely before polling for the next.
  • Handlers support Clone requests by merging a new stream of incoming requests with their existing one.
  • Handlers exit when either their request stream ends or in response to an explicit Close message.
  • On exit, handlers clean up any resources corresponding to the socket.

Request handling for a given socket is done using async/await. Though each individual socket handles requests serially, this allows requests for different sockets to be handled concurrently by the executor.

Individual implementations

Before the introduction of async_fn_in_trait, Netstack3's socket handlers were each implemented independently, with minimal code reuse. This resulted in significant duplication of code for the common behaviors above. The straightforward refactor would define a generic SocketWorker<H> type with the shared behavior, and that would delegate to an implementer of a trait SocketHandler for socket-type-specific request handling:

#![allow(unused)]
fn main() {
pub struct SocketWorker<H> {
    handler: H,
}

pub trait SocketHandler: Default {
    type Request;

    /// Handles a single request.
    async fn handle_request(
        &mut self,
        request: Self::Request,
    ) -> ControlFlow<(), Option<RequestStream<Self::Request>>;

    /// Closes the socket managed by this handler.
    fn close(self);
}

impl<H: SocketHandler> SocketWorker<H> {
    /// Starts servicing events from the provided event stream.
    pub async fn serve_stream(stream: RequestStream<H>) {
        Self { handler: H::default() }.handle_stream(stream)
    }

    async fn handle_stream(mut self, requests: RequestStream<H>) {
      let Self {handler} = self;
      while let Some(request) = requests.next().await {
        // Call `handler.handle_request()` for each request while merging
        // new request streams into `requests`.
      }
      handler.close()
    }
}
}

Because SocketHandler::handle_request is an async fn, this won't compile on the current version of stable Rust (1.68.0 as of writing). There are a couple options for working around the lack of support for AFIT, but they each have significant downsides:

Option 1: Make request handling a hand-rolled Future impl on a custom type

One way to work around the lack of AFIT is to declare a non-async trait function that returns an instance of an associated type that implements Future. This works, but requires explicitly structuring control flow as state within the associated type. For Netstack3, where request handling branches down tens of paths, maintaining this state machine by hand would be impractically difficult.

Option 2: Use dynamic dispatch

This is similar to the above, but instead of an associated type, the non-async function returns a Box<dyn Future>. The trait implementation can then call an async fn, then box up and return the result. This results in more readable code than option 1 at the cost of allocation and dynamic dispatch at run time. For Netstack3, which is on the critical path of every network-connected component in the system, this is not worth the benefits of abstraction.

With AFIT

Using async_fn_in_trait allowed performing the refactoring proposed above without workarounds. The resulting code doesn't use dynamic dispatch, and the implementations of SocketHandler::handle_request are written as regular async fns. The full definition of the abstract worker and handler trait can be found in the Netstack3 source code).

Send bound limitation

One of the current limitations for AFIT is the inability to specify bounds on the type of the future returned from an async trait function. This can cause errors when the caller of the function requires a Send bound so that the future can be passed to a multi-threaded executor.

Netstack3 uses fuchsia_async::Task::spawn to create tasks that can run on Fuchsia's multi-threaded executor, and so initial attempts to use AFIT ran afoul of the limitation. Luckily, the suggested workaround of moving the spawn point out of generic code worked for Netstack3: Task::spawn is called in socket-specific code instead of within generic socket worker code. Since the compiler has access to the concrete Future-implementing type returned by the specific impl of SocketHandler::handle_request, it can verify that it and all its callers implement Send.

Future usages

While AFIT is currently being used in Netstack3 for abstracting over socket behaviors, it's likely that there are other places where it would prove useful, including in some of the existing per-IP-version code with common logic.

Tower AFIT Case Study

This case study presents how Tower would update it's core traits to improve ergonomics and useability with the aid of async_fn_in_trait (AFIT).

Background

Tower is a library of modular and reusable components for building robust networking clients and servers. The core use case of Tower is to enable users to compose "stacks" of middleware in an easy and reusable way. To achieve this currently Tower provide a Service and a Layer trait. The core component is the Service trait as this is where all the async functionality lives. Layers on the other hand are service constructors and allow users to compose middleware stacks. Since, Layer does not do any async work it's trait design is out of scope of this document for now.

Currently, the Service trait requires users to name their futures which does not allow them to easily use async fn without boxing. In addition, borrowing state within handlers is unergonomic due using associated types for the return future type. This is because to allow the returned future to have a lifetime it would require adding a lifetime generic associated type (GAT) which would force an already verbose trait into an extremely verbose trait that is unergonomic for users to implement. This thus requires all returned futures to be 'static which means that it must own all of its data. While this works right now its an extra step that could be avoided by using async fn's ability to produce non-static futures in an ergonomic way.

The current Service trait looks something like this simplified version which omits poll_ready, Response/Error handling:

#![allow(unused)]
fn main() {
trait Service<Request> {
    type Response;
    type Future: Future<Output = Self::Response>;

    fn call(&mut self, req: Request) -> Self::Future;
}
}

Middleware are Service's that wrap another S: Service such that it will internally call the inner service and wrap its behavior. This allows middleware to modify the input type, wrap the output future and modify the output type.

Due to Service::call requiring &mut self some middleware require their inner Service to implement Clone. Unfortunetly, this bound has caused many issues as it makes layering services quite complex due to the nested inference that happens in ServiceBuilder. This has caused many massive and almost famous error messages produced from Tower.

Moving forward

One of the goals of moving to AFIT is to make using Tower easier and less error prone. The current working idea that we have is to have a trait like:

#![allow(unused)]
fn main() {
trait Service<Request> {
    type Response;

    async fn call(&self, request: Request) -> Self::Response;
}
}

So far the experience has been pretty good working with AFIT with only a few well known issues like dynamic dispatch and send bounds. In addition, we have not looked at using async-trait because it forces Send bounds on our core trait which is an anti-goal for Tower as we would like to support both Send and !Send services.

One of the biggest wins we have experienced moving to AFIT has been how ergonomic borrowing is within traits. For example, we have updated our retry middleware to use AFIT. The implementation goes from two 100+ loc files to one 15 line implementation that is simpler to understand and verify. This large change is due to not having to hand roll futures (which could be an artifact of when tower was created) and the ability to immutably borrow the service via &self rather than having to Clone the struct items. This is enabled by the ergonomic borrows that AFIT provides.

Dynamic dispatch and Send

Since, Tower stacks can become massive types and become very complicated for new Rust users to work with we generally recommend boxing the Tower stack (a stack is a group of Tower Service's) after it has been constructed. This allows, for example, clients to remove their need for generics and simplify their public api while still leveraging Tower's composability.

An example client:

#![allow(unused)]
fn main() {
pub struct Client {
    svc: BoxService<Request, Response, Error>,
}
}

Since, currently AFIT does not support dynamic traits we had to use a similar work around to the Microsoft and Async Builder + Provider API case studies. This workaround uses a second DynService trait that is object safe. Using this trait we can then define a BoxService<'a, Req, Res, Err> Tower service that completely erases the service type.

#![allow(unused)]
fn main() {
trait DynService<Req> {
    type Res;
    type Error;

    fn call<'a>(&'a self, req: Req) -> BoxFuture<'a, Result<Self::Res, Self::Error>>
    where
        Req: 'a;
}

pub struct BoxService<'a, Req, Res, Err> {
    b: Box<dyn 'a + DynService<Req, Res = Res, Error = Err>>,
}
}

This workaround works pretty well but falls short on one piece. The trait object within BoxService is !Send. If we try to add Send bounds to the trait object we eventually end up with a compiler error that says something like impl Future doesn't implement Send and we end up at rust-lang/rust#103854

RTN

Our issue above is that we have no way currently in Rust to express that some async fn in a trait is Send. To achieve this, we tried out a branch from @compiler-errors fork of rust. This branch implemented the return notation syntax for expressing a bounds on impl trait based trait functions like async fn. Using the synatx we were able to express this requirement to allow the trait object to be sendable.

This blanket implementation allows us to have any impl Service + Send automatically implement our dyn version of our Service trait.

#![allow(unused)]
fn main() {
impl<T, Req> DynService<Req> for T
where
    T: Service<Req, call(): Send> + Send,
    T::Res: Send,
    T::Error: Send,
    Req: Send,
{
    type Res = <T as Service<Req>>::Res;
    type Error = <T as Service<Req>>::Error;

    fn call<'a>(&'a self, req: Req) -> BoxFuture<'a, Result<Self::Res, Self::Error>>
    where
        Req: 'a,
    {
        Box::pin(self.call(req))
    }
}
}

In addition, one of the core requirements of tower is to be flexible and we would like the core trait to be used in Send and !Send contexts. This means we require that the way we express sendness is bounded at the users call site rather than at the core trait level.

Another note, because async fn tend to eagerly capture items this new approach requires that the Requset also be Send. This differs from how tower currently works because to bound a returned future by Send does not require that the input request type is Send as well since it does not lazily evaluate it. That said, in practice, if you end up using something like tower::buffer the Request type already needs to be Send to be able to send it over a channel. This is likely a non-issue anyways since current consumer code tends to use buffer to go from a impl Service to impl Service + Clone.

Future areas of exploration

  • We have not yet implemented the full stacking implementation that we currently have with Tower. This means we have yet to potentailly run into any inference issues that could happen when using the Layer trait from Tower.

References

  • https://github.com/LucioFranco/tower-playground/tree/lucio/sendable-box
  • https://github.com/tower-rs/tower

Microsoft Async Case Study

Background

Microsoft uses async Rust in several projects both internally and externally. In this case study, we will focus on how async is used in one project in particular.

This project manages and interacts with low level hardware resources. Performance and resource efficiency is key. Async Rust has proven useful not just because of it enables scalability and efficient use of resources, but also because features such as cancel-on-drop semantics simplify the interaction between components.

Due to our constrainted, low-level environment, we use a custom executor but rely heavily on ecosystem crates to reduce the amount of custom code needed in our executor.

Async Trait Usage

The project makes regular use of async traits. Since these are not yet supported by the language, we have instead used the async-trait crate.

For the most part this works well, but sometimes the overhead introduced by boxing the returned fututures is unacceptable. For those cases, we use StackFuture, which allows us to emulate a dyn Future while storing it in space provided by the caller.

Now that there is built in support for async in traits in the nightly compiler, we have tried porting some of our async traits away from the async-trait crate.

For many of these the transformation was simple. We merely had to remove the #[async_trait] attribute on the trait and all of its implementations. For example, we had one trait that looks similar to this:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait BusControl {
    async fn offer(&self) -> Result<()>;
    async fn revoke(&self) -> Result<()>;
}
}

There were several implementations of this trait as well. In this case, all we needed to do was remove the #[async_trait] annotation.

Send Bounds

In about half the cases, we needed methods to return a future that was Send. This happens by default with #[async_trait], but not when using the built-in feature.

In these cases, we instead manually desugared the async fn definition so we could add additional bounds. Although these bounds applied at the trait definition site, and therefore to all implementors, we have not found this to be a deal breaker in practice.

As an example, one trait that required a manual desugaring looked like this:

#![allow(unused)]
fn main() {
pub trait Component: 'static + Send + Sync {
    async fn save<'a>(
        &'a mut self,
        writer: StateWriter<'a>,
    ) -> Result<(), SaveError>;

    async fn restore<'a>(
        &'a mut self,
        reader: StateReader<'a>,
    ) -> Result<(), RestoreError>;
}
}

The desugared version looked like this:

#![allow(unused)]
fn main() {
pub trait Component: 'static + Send + Sync {
    fn save<'a>(
        &'a mut self,
        writer: StateWriter<'a>,
    ) -> impl Future<Output = Result<(), SaveError>> + Send + 'a;

    fn restore<'a>(
        &'a mut self,
        reader: StateReader<'a>,
    ) -> impl Future<Output = Result<(), RestoreError>> + Send + 'a;
}
}

This also required a change to all the implementation sites since we were migrating from async_trait. This was slightly tedious but basically a mechanical change.

Dyn Trait Workaround

We use trait objects in several places to support heterogenous collections of data that implements a certain trait. Rust nightly does not currently have built-in support for this, so we needed to find a workaround.

The workaround that we have used so far is to create a DynTrait version of each Trait that we need to use as a trait object. One example is the Component trait shown above. For the Dyn version, we basically just duplicate the definition but apply #[async_trait] to this one. Then we add a blanket implementation so that we can triviall get a DynTrait for any trait that has a Trait implementation. As an example:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait DynComponent: 'static + Send + Sync {
    async fn save<'a>(
        &'a mut self,
        writer: StateWriter<'a>,
    ) -> Result<(), SaveError>;

    async fn restore<'a>(
        &'a mut self,
        reader: StateReader<'a>,
    ) -> Result<(), RestoreError>;
}

#[async_trait]
impl<T: Component> DynComponent for T {
    async fn save(&mut self, writer: StateWriter<'_>) -> Result<(), SaveError> {
        <Self as Component>::save(self, writer).await
    }

    async fn restore(
        &mut self,
        reader: StateReader<'_>,
    ) -> Result<(), RestoreError> {
        <Self as Component>::restore(self, reader).await
    }
}
}

It is a little annoying to have to duplicate the trait definition and write a blanket implementation, but there are some mitigating factors. First of all, this only needs to be done in once per trait and can conveniently be done next to the non-Dyn version of the trait. Outside of that crate or module, the user just has to remember to use dyn DynTrait instead of just DynTrait.

The second mitigating factor is that this is a mechanical change that could easily be automated using a proc macro (although we have not done so).

Return Type Notation

Since there is an active PR implementing Return Type Notation (RTN), we gave that a try as well. The most obvious place this was applicable was on the Component trait we have already looked at. This turned out to be a little tricky. Because async_trait did not know how to parse RTN bounds, we had to forego the use of #[async_trait] and use a manually expanded version where each async function returned Pin<Box<dyn Future<Output = ...> + Send + '_>>. Once we did this, we needed to add save(..): Send and restore(..): Send bounds to the places where the Component trait was used as a DynComponent. There were only two methods we needed to bound, but this was still mildly annoying.

The #[async_trait] limitation will likely go away almost immediately once the RTN PR merges, since it can be updated to support the new syntax. Still, this experience does highlight one thing, which is that the DynTrait workaround requires us to commit up front to whether that trait will guarantee Send futures or not. There does not seem to be an obvious way to push this decision to the use site like there is with Return Type Notation.

This is something we will want to keep in mind with any macros we create to help automate these transformations as well as with built in language support for async functions in trait objects.

Conclusion

We have not seen any insurmountable problems with async functions in traits as they are currently implemented in the compiler. That said, it would be significantly more ergonomic with a few more improvements, such as:

  • A way to specify Send bounds without manually desugaring a function. Return type notation looks like it would work, but even in our limited experience with it we have run into ergonomics issues.
  • A way to simplify dyn Trait support. Language-provided support would be ideal, but a macro that automates something like our DynTrait workaround would be acceptable too.

That said, we are eager to be able to use this feature in production!

Use of AFIT in Embaassy

The following are rough notes on the usage of Async Function in Traits from the Embassy runtime. They are derived from a conversation between dirbaio and nikomatsakis.

Links to uses of async functions in traits within Embassy:

most of these are "abstract over hardware", and when you build a firmware for some product/board you know which actual hardware you have, so you use static generics, no need for dyn

the few instances I've wished for dyn is:

  • with embedded-io it does sometimes happen. For example, running the same terminal ui over a physical serial port and over telnet at the same time. Without dyn that code gets monomorphized two times, which is somewhat wasteful.
  • this trait https://github.com/embassy-rs/embassy/blob/master/embassy-usb/src/lib.rs#L89 . That one MUST use dyn because you want to register multiple handlers that might be different types. Sometimes it'd have been handy to be able to do async things within these callbacks. Workaround is to fire off a notification to some other async task, it's not been that bad.
    • niko: how is this used?
    • handlers are added here, passed into UsbDevice here, and then called when handling some bus-related stuff, for example here.
    • the tldr of what it's used for is you might have a "composite" usb device, which can have multiple "classes" at the same time (say, an Ethernet adapter and a serial port). Each class gets its own "endpoints" for data, so each launches its own independent async tasks reading/writing to these endpoints.
    • But there's also a "control pipe" endpoint that carries "control" requests that can be for any class for example for ethernet there's control requests for "bring the ethernet interface up/down", so each class registers a handler with callbacks to handle their own control requests, there's a "control pipe" task that dispatches them.
    • Sometimes when handling them, you want to do async stuff. For example for "bring the ethernet interface up" you might want to do some async SPI transfer to the ethernet chip, but currently you can't.
    • niko: would all methods be async if you could?
    • not sure if all methods, but probably control_in/control_out yes. and about where to store the future for the dyn... not sure. That crate is no-alloc so Box is out it'd probably be inline in the stack, like with StackFuture. Would need configuring the max size, probably some compile-time setting, or a const-generic in UsbDevice.

πŸ“š Explainer

The "explainer" is "end-user readable" documentation that explains how to use the feature being deveoped by this initiative. If you want to experiment with the feature, you've come to the right place. Until the feature enters "feature complete" form, the explainer should be considered a work-in-progress.

For an overview, start with the user guide from the future.

More detailed explanations of each feature follow.

Async fns in traits

A user's guide from the future.

Updated: Sep 12, 2022

This document explains the proposed design for async functions in traits, from the point of view of a "knowledgeable Rust user". It is meant to cover all aspects of the design we expect people to have to understand to leverage this feature. It doesn't go into every detail of how it will be implemented.

Intro

This guide explains how traits work when they contain async functions. For the most part, a trait that contains an async functions works like any other trait -- but there are a few interesting questions and interactions that come up with async functions that don't arise with ordinary functions. We'll use a series of examples to explain how everything works.

Traits and impls with async fn syntax

Defining a trait with an async function is straightforward ("just do it"):

#![allow(unused)]
fn main() {
use std::io::Result;

struct ResourceId { }
struct ResourceData { }

trait Fetch {
    async fn fetch(&mut self, request: ResourceId) -> Result<ResourceData>;
}
}

Similarly, implementing such a trait is straightforward ("just do it"):

#![allow(unused)]
fn main() {
struct HttpFetch { }

impl Fetch for HttpFetch {
    async fn fetch(&mut self, request: ResourceId) -> Result<ResourceData> {
        ... /* something that may await */ ...
    }
}
}

Writing generic functions using async traits

Generic functions, types, etc that reference async traits work just like you would expect from other traits:

#![allow(unused)]
fn main() {
async fn fetch_and_process(f: impl Fetch, r: ResourceId) -> Result<()> {
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

That example used impl Trait, but of course one could expand it to the (mostly equivalent) version with an explicit generic type:

#![allow(unused)]
fn main() {
async fn fetch_and_process<F>(f: F, r: ResourceId) -> Result<()>
where
    F: Fetch,
{
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

Bounding with Send, Sync, and other bounds

Suppose that we wanted to create a version of fetch_and_process in which the "fetching and processing" took place in another task? We can write a variant of fetch_and_process, fetch_and_process_in_task, that does this, but we have to add new where-clauses:

#![allow(unused)]
fn main() {
async fn fetch_and_process_in_task<F>(
    f: F,
    r: ResourceId,
)
where
    F: Fetch,
    F: Send + 'static, // πŸ‘ˆ Added so `F` can be sent to new task
    F::fetch(..): Send // πŸ‘ˆ Added, this syntax is new!
                       //    We will be explaining it shortly.
{
    tokio::spawn(async move {
        let data: ResourceData = f.fetch(r).await?;
        process(data)?;
        Ok(())
    });
}
}

If you prefer, you could write it with impl Trait syntax like so:

#![allow(unused)]
fn main() {
async fn fetch_and_process_in_task(
    f: impl Fetch<fetch(..): Send> + Send + 'static,
    //            ~~~~~~~~~~~~~~~    ~~~~~~~~~~~~~~
    //            Added! Leverages the new `fetch(..)`
    //            syntax we are about to explain, but
    //            also associated type bounds (RFC 2289).
    r: ResourceId,
) {
    tokio::spawn(async move {
        let data: ResourceData = f.fetch(r).await?;
        process(data)?;
        Ok(())
    });
}
}

Let's walk through those where-clauses in detail:

  • F: Send and F: 'static -- together, these indicate that it is safe to move F over to another task. The Send bound means that F doesn't contain any thread-local types (e.g., Rc), and the 'static means that F doesn't contain any borrowed data from the current stack.
  • F::fetch(..): Send -- this is new syntax. It says that "the type returned by a call to fetch() is Send". In this case, the type being returned is a future, so this is saying "the future returned by F::fetch() will be Send".

Function call bounds in more detail

The F::fetch(..): Send bound is actually an example of a much more general feature, return type notation. This notation lets you refer to the type that will be returned by a function call in all kinds of places.

As a simple example, you could now write a type like identity_fn(u32) to mean "the type returned by identity_fn when invoked with a u32":

#![allow(unused)]
fn main() {
let f: identity_fn(u32) = identity_fn(22_u32);
}

Knowing the types of arguments can be important for figuring out the return type! For example, if identity_fn is defined like so...

#![allow(unused)]
fn main() {
fn identity_fn<T>(t: T) -> T {
    t
}
}

...then identity_fn(u32) is equivalent to u32, identity_fn(i32) is equivalent to i32, and so on.

When you are using return type notation in a bound, you can use .. to mean "for all the types accepted by the function". So when we write F::fetch(..): Send, that is in fact equivalent to this much more explicit bound:

#![allow(unused)]
fn main() {
where
    for<'a> F::fetch(&'a mut F, ResourceData): Send
    // --> equivalent to `F::fetch(..): Send`
}

The more explicit version spells out explicitly that we want "the type returned by Fetch when given some &'a mut F and a ResourceData".

The .. notation can only be used in bounds because it refers to a whole range of types. If you were to write let f: F::fetch(..) = .., that would mean that f has multiple types, and that is not currently permitted.

Turbofish and return type notation

In some cases, specifying the types of the arguments to the function are not enough to uniquely specify its behavior:

#![allow(unused)]
fn main() {
fn make_default<T: Default>() -> Option<T> {
    Some(T::default())
}
}

Just as in an expression, you can use turbofish to handle scenarios like this. The following function references make_default::<T>(), for example, as a (rather convoluted) synonym for Option<T>:

#![allow(unused)]
fn main() {
fn foo<T>(t: T)
where
    make_default::<T>(): Send,
}

Dynamic dispatch and async functions

The other major way to use traits is with dyn Trait notation. This mostly works normally with async functions, but there are a few differences to be aware of.

Let's start with a variant of the fetch_and_process function that uses dyn Trait dispatch:

#![allow(unused)]
fn main() {
async fn fetch_and_process_dyn(f: &mut dyn Fetch, r: ResourceId) -> Result<()> {
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

Looks simple! But what, what about the code that calls fetch_and_process_dyn, how does that look? This is where things with async functions get a bit more complicated. The obvious code to call fetch_and_process_dyn, for example, will not compile:

#![allow(unused)]
fn main() {
async fn caller() {
    let mut f = HttpFetch { .. };
    fetch_and_process_dyn(&mut f); // πŸ‘ˆ ERROR!
}
}

Compiling this we get the following message:

error[E0277]: the type `HttpFetch` cannot be converted to a
              `dyn Fetch` without an adapter
 --> src/lib.rs:3:23
  |
3 |     fetch_and_process_dyn(&mut f);
  |     --------------------- ^^^^^^ the trait `Foo` is not implemented for `HttpFetch`
  |     |
  |     required by a bound introduced by this call
  |
  = help: consider introducing the `Boxing` adapter,
    which will box the futures returned by each async fn
3 |     fetch_and_process_dyn(Boxing::new(&mut f));
  |                           ++++++++++++      +

The Boxing adapter

What is going on here? The Boxing adapter indicates that, each time you invoke an async fn through the dyn Fetch, the future that is returned will be boxed.

The reasons that boxing are required here are rather subtle. The problem is that the amount of memory a future requires depends on the details of the function it results from (i.e., it is based on how much state is preserved across each await call). Normally, we store the data for futures on the stack, but this requires knowing precisely which function is called so that we can know exactly how much space to allocate. But when invoking an async function through a dyn value, we can't know what type is behind the dyn, so we can't know how much stack space to allocate.

To solve this, Rust only permits you to create a dyn value if the size of each future that is returned is the same size as a pointer. The Boxing adapter ensures this is the case by boxing each of the futures into the heap instead of storing their data on the stack.

:::info What if I don't want to box my futures?

For the vast majority of applications, Boxing works great. In fact, the #[async_trait] procedural macro that most folks are using today boxes every future (the built-in design only boxes when using dynamic dispatch).

However, there are times when boxing isn't the right choice. Particularly tight loops, for example, or in #[no_std] scenarios like kernel modules or IoT devices that lack an operating system. For those cases, there are other adapters available that allocate futures on the stack or in other places. See Appendix B for the details. :::

Specifying that methods return Send

Circling back to our fetch_and_process example, there is another potential problem. When we write this...

#![allow(unused)]
fn main() {
async fn fetch_and_process(f: &mut dyn Fetch, r: ResourceId) -> Result<()> {
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

...this code will compile just fine, but if you are using a "work stealing runtime" (e.g., tokio by default), you may find that it doesn't work for you. The problem is that f.fetch(r) returns "some kind of future", but we don't know that the future is Send, and hence the fetch_and_process() future is itself not Send.

When using a F: Fetch argument, this worked out ok because we knew the type F exactly, and the future we returned would be Send as long as F was. With dyn Fetch, we don't know exactly what kind of type we will have, and we can only go based on what the type says -- and the type doesn't promise Send futures.

There are a few ways to write fetch_and_process such that the returned future is known to be Send. One of them is to make use of the return type bounds. Instead of writing dyn Fetch, we can write dyn Fetch<fetch(..): Send>, indicating that we need more than just "some Fetch instance" but "some Fetch instance that returns Send futures":

#![allow(unused)]
fn main() {
async fn fetch_and_process(
    f: &mut dyn Fetch<fetch(..): Send>, // πŸ‘ˆ New!
    r: ResourceId,
) -> Result<()> {
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

Writing dyn Fetch<fetch(..): Send> is ok if you don't have to do it a lot, but if you do, you'll probably find yourself wishing for a shorter version. One option is to define a type alias:

#![allow(unused)]
fn main() {
type DynFetch<'bound> = dyn Fetch<fetch(..): Send> + 'bound;

async fn fetch_and_process(
    f: &mut DynFetch<'_>, // πŸ‘ˆ Shorter!
    r: ResourceId,
) -> Result<()> {
    let data: ResourceData = f.fetch(r).await?;
    process(data)?;
    Ok(())
}
}

Appendix A: manual desugaring

In general, an async function in Rust can be desugared into a sync function that returns an impl Future. The same is true of traits. We could substitute the following definitions for the traits or impls and they would still compile:

#![allow(unused)]
fn main() {
trait Fetch {
    fn fetch(
        &mut self,
        request: ResourceId
    ) -> impl Future<Output = Result<ResourceData>> + '_;
}

impl Fetch for HttpFetch {
    fn fetch(
        &mut self,
        request: ResourceId
    ) -> impl Future<Output = Result<ResourceData>> + '_ {
        async move { ... /* something that may await */ ... }
    }
}
}

In fact, we can even "mix and match", using an async function in the trait and an impl Trait in the impl or vice versa.

It is also possible to write an impl with a specific future type:

#![allow(unused)]
fn main() {
impl Fetch for HttpFetch {
    #[refine]
    fn fetch(
        &mut self,
        request: ResourceId
    ) -> Box<dyn Future<Output = Result<ResourceData>> {
        Box::pin(async move {  /* something that may await */ })
    }
}
}

This last case represents a refinement on the trait -- the trait promises only that you will return "some future type". The impl is then promising to return a very specific future type, Box<dyn Future>. The #[refine] attribute indicates that detail is important, and that when people are referencing the return type of the fetch() method for the type HttpFetch, they can rely on the fact that it is Box<dyn Future> and not some other future type. (See RFC 3245 for more information.)

Appendix B: Other adapters

Inline adapter

The "inline" adapter allocates stack space in advance for each possible async fn in the trait. Unlike Boxing, it has a few sharp edges, and doesn't work with every trait. For this reason, it is not built-in to the compiler but rather shipped through a library.

To use the inline adapter, you first add the dyner crate to your package.

... TBW ...

Appendix Z: Internal notes

These are some notes and design questions that arose in reading and thinking through this document. They may be addressed in future revisions.

Are there semver considerations in terms of when a dyn can be created? We should only permit creating a dyn when the impl has a #[refine] and the type implements IntoDyn or whatever trait we decide to use for the dyn* implementation.

Async fn in traits

accepted rfc

See the RFC.

Async fn in dyn trait

planning rfc

Welcome! This document explores how to combine dyn and impl Trait in return position. This is crucial pre-requisite for async functions in traits. As a motivating example, consider the trait AsyncIterator:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    
    async fn next(&mut self) -> Option<Self::Item>;
}
}

The async fn here is, of course, short for a function that returns impl Future:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    
    fn next(&mut self) -> impl Future<Output = Option<Self::Item>>;
}
}

The focus of this document is on how we can support dyn AsyncIterator. For an examination of why this is difficult, see this blog post.

Key details

Here is a high-level summary of the key details of our approach:

  • Natural usage:
    • To use dynamic dispatch, just write &mut dyn AsyncIterator, same as any other trait.
    • Similarly, on the impl side, just write impl AsyncIterator for MyType, same as any other trait.
  • Allocation by default, but not required:
    • By default, trait functions that return -> impl Trait will allocate a Box to store the trait, but only when invoked through a dyn Trait (static dispatch is unchanged).
    • To support no-std or high performance scenarios, types can customize how an -> impl Trait function is dispatch through dyn. We show how to implement an InlineAsyncIterator type, for example, that wraps another AsyncIterator and stores the resulting futures on pre-allocated stack space.
      • rust-lang will publish a crate, dyner, that provides several common strategies.
  • Separation of concerns:
    • Users of a dyn AsyncIterator do not need to know (or care) whether the impl allocates a box or uses some other allocation strategy.
    • Similarly, authors of a type that implements AsyncIterator can just write an impl. That code can be used with any number of allocation adapters.

How it feels to use

planning rfc

Let's start with how we expect to use dyn AsyncIterator. This section will also elaborate some of our desiderata1, such as the ability to use dyn AsyncIterator conveniently in both std and no-std scenarios.

1

Ever since I once saw Dave Herman use this bizarre latin plural, I've been in love with it. --nikomatsakis

How you write a function with a dyn argument

We expect people to be able to write functions that take a dyn AsyncIterator trait as argument in the usual way:

#![allow(unused)]
fn main() {
async fn count(i: &mut dyn AsyncIterator) -> usize {
    let mut count = 0;
    while let Some(_) = i.next().await {
        count += 1;
    }
    count
}
}

One key part of this is that we want count to be invokable from both a std and a no-std environment.

How you implement a trait with async fns

This, too, looks like you would expect.

#![allow(unused)]
fn main() {
struct YieldingRangeIterator {
    start: u32,
    stop: u32,
}

impl AsyncIterator for YieldingRangeIterator {
    type Item = u32;

    async fn next(&mut self) {
        if self.start < self.stop {
            let i = self.start;
            self.start += 1;
            tokio::thread::yield_now().await;
            Some(i)
        } else {
            None
        }
    }
}
}

How you invoke count in std

You invoke it as you normally would, by performing an unsize coercion. Invoking the method requires an allocator by default.

#![allow(unused)]
fn main() {
let x = YieldingRangeIterator::new(...);
let c = count(&mut x /* as &mut dyn AsyncIterator */).await;
}

Using dyn without allocation

planning rfc

In the previous chapter, we showed how you can invoke async methods from a dyn Trait value in a natural fashion. In those examples, though, we assume that it was ok to allocate a Box for every call to an async function. For most applications, this is true, but for some applications, it is not. This could be because they intend to run in a kernel or embedded context, where no allocator is available, or it could be because of a very tight loop in which allocation introduces too much overhead. The good news is that our design allows you to avoid using Box, though it does take a bit of work on your part.

In general, functions that accept a &dyn Trait as argument don't control how memory is allocated. So the count function that we saw before can be used equally well on a no-std or kernel platform:

#![allow(unused)]
fn main() {
async fn count(iter: &mut dyn AsyncIterator) {
    // Whether or not `iter.next()` will allocate a box
    // depends on the underlying type; this `count` fn
    // doesn't have to know, and so it works equally
    // well in a no-std or std environment.
    ...
}
}

The decision about whether to use box or some other way of returning a future is made by the type implementing the async trait. In the previous example, the type was YieldingRangeIterator, and its impl didn't make any kind of explicit choice, and thus the default is that it will allocate a Box:

#![allow(unused)]
fn main() {
impl AsyncIterator for YieldingRangeIterator {
    type Item = u32;

    async fn next(&mut self) {
        // The default behavior here is to allocate a `Box`
        // when `next` is called through a `dyn AsyncIterator`
        // (no `Box` is allocated when `next` is called through
        // static dispatch, in that case the future itself is
        // returned.)
        ...
    }
}
}

If you want to use YieldingRangeIterator in a context without Box, you can do that by wrapping it in an adapter type. This adapter type will implement an alternative memory allocation strategy, such as using pre-allocated stack storage.

For the most part, there is no need to implement your own adapter type, beacuse the dyner crate (to be published by rust-lang) includes a number of useful ones. For example, to pre-allocate the next future on the stack, which is useful both for performance or no-std scenarios, you could use an "inline" adapter type, like the InlineAsyncIterator type provided by the dyner crate:

#![allow(unused)]
fn main() {
use dyner::InlineAsyncIterator;
//         ^^^^^^^^^^^^^^^^^^^
//         Inline adapter type

async fn count_range(mut x: YieldingRangeIterator) -> usize {
    // allocates stack space for the `next` future:
    let inline_x = InlineAsyncIterator::new(x); 
    
    // invoke the `count` fn, which will no use that stack space
    // when it runs
    count(&mut inline_x).await
}
}

Dyner provides some other strategies, such as the CachedAsyncIterator (which caches the returned Box and re-uses the memory in between calls) and the BoxInAllocatorAsyncIterator (which uses a Box, but with a custom allocator).

How you apply an existing "adapter" strategy to your own traits

The InlineAsyncIterator adapts an AsyncIterator to pre-allocate stack space for the returned futures, but what if you want to apply that inline stategy to one of your traits? You can do that by using the #[inline_adapter] attribute macro applied to your trait definition:

#![allow(unused)]
fn main() {
#[inline_adapter(InlineMyTrait)]
trait MyTrait {
    async fn some_function(&mut self);
}
}

This will create an adapter type called InlineMyTrait (the name is given as an argument to the attribute macro). You would then use it by invoking new:

#![allow(unused)]
fn main() {
fn foo(x: impl MyTrait) {
    let mut w = InlineMyTrait::new(x);
    bar(&mut w);
}

fn bar(x: &mut dyn MyTrait) {
    x.some_function();
}
}

If the trait is not defined in your crate, and hence you cannot use an attribute macro, you can use this alternate form, but it requires copying the trait definition:

#![allow(unused)]
fn main() {
dyner::inline::adapter_struct! {
    struct InlineAsyncIterator for trait MyTrait {
        async fn foo(&mut self);
    }
}
}

How it works

planning rfc

This section is going to dive into the details of how this proposal works within the compiler. As a user, you should not generally have to know these details unless you intend to implement your own adapter shims.

Return to our running example

Let's repeat our running example trait, AsyncIterator:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    
    async fn next(&mut self) -> Option<Self::Item>;
}

async fn count(i: &mut dyn AsyncIterator) -> usize {
    let mut count = 0;
    while let Some(_) = i.next().await {
        count += 1;
    }
    count
}
}

Recall that async fn next desugars to a regular function that returns an impl Trait:

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    
    fn next(&mut self) -> impl Future<Output = Option<Self::Item>>;
}
}

Which in turn desugars to a trait with an associated type (note: precise details here are still being debated, but this desugaring is "close enough" for our purposes here):

#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    
    type next<'me>: Future<Output = Option<Self::Item>>
    fn next(&mut self) -> Self::next<'_>;
}
}

Challenges of invoking an async fn through dyn

When count is compiled, it needs to invoke i.next(). But the AsyncIterator trait simply declares that next returns some impl Future (i.e., "some kind of future"). Each impl will define its own return type, which will be some kind of special struct that is "sufficiently large" to store all the state that needed by that particular impl.

When an async function is invoked through static dispatch, the caller knows the exact type of iterator it has, and hence knows exactly how much stack space is needed to store the resulting future. When next is invoked through a dyn AsyncIterator, however, we can't know the specific impl and hence can't use that strategy. Instead, what happens is that invoking next on a dyn AsyncIterator always yields a pointer to a future. Pointers are always the same size no matter how much memory they refer to, so this strategy doesn't require knowing the "underlying type" beneath the dyn AsyncIterator.

Returning a pointer, of course, requires having some memory to point at, and that's where things get tricky. The easiest (and default) solution is to allocate and return a Pin<Box<dyn Future>>, but we wish to support other kinds of pointers as well (e.g., pointers into pre-allocated stack space). We'll explain how our system works in stages, first exploring a version that hardcodes the use of Box, and then showing how that can be generalized.

How the running example could work with Box

planning rfc

Before we get into the full system, we're going to start by just explaining how a system that hardcodes Pin<Box<dyn Future>> would work. In that case, if we had a dyn AsyncIterator, the vtable for that async-iterator would be a struct sort of like this:

#![allow(unused)]
fn main() {
struct AsyncIteratorVtable<I> {
    type_tags: usize,
    drop_in_place_fn: fn(*mut ()), // function that frees the memory for this trait
    next_fn: fn(&mut ()) -> Pin<Box<dyn Future<Output = Option<I> + '_>>
}
}

This struct has three fields:

  • type_tags, which stores type information used for Any
  • drop_in_place_fn, a funcdtion that drops the memory of the underlying value. This is used when the a dyn AsyncIterator is dropped; e.g., when a Box<dyn AsyncIterator> is dropped, it calls drop_in_place on its contents.
  • next_fn, which stores the function to call when the user invokes next. You can see that this function is declared to return a Pin<Box<dyn Future>>.

(This struct is just for explanatory purposes; if you'd like to read more details about vtable layout, see this description.)

Invoking i.next() (where i: &mut dyn AsynIterator) ultimately invokes the next_fn from the vtable and hence gets back a Pin<Box<dyn Future>>:

#![allow(unused)]
fn main() {
i.next().await

// becomes

let f: Pin<Box<dyn Future<Output = Option<I>>>> = i.next();
f.await
}

How to build a vtable that returns a boxed future

We've seen how count calls a method on a dyn AsyncIterator by loading next_fn from the vtable, but how do we construct that vtable in the first place? Let's consider the struct YieldingRangeIterator and its impl of AsyncIterator that we saw before in an earlier section:

#![allow(unused)]
fn main() {
struct YieldingRangeIterator {
    start: u32,
    stop: u32,
}

impl AsyncIterator for YieldingRangeIterator {
    type Item = u32;

    async fn next(&mut self) {...}
}
}

There's a bit of a trick here. Normally, when we build the vtable for a trait, it points directly at the functions from the impl. But in this case, the function in the impl has a different return type: instead of returning a Pin<Box<dyn Future>>, it returns some impl Future type that could have any size. This is a problem.

To solve it, the vtable doesn't directly reference the next fn from the impl, instead it references a "shim" function that allocates the box:

#![allow(unused)]
fn main() {
fn yielding_range_shim(
    this: &mut YieldingRangeIterator,
) -> Pin<Box<dyn Future<Output = Option<u32>>>> {
    Box::pin(<YieldingRangeIterator as AsyncIterator>::next(this))
}
}

This shim serves as an "adaptive layer" on the callee's side, converting from the impl Future type to the Box. More generally, we can consider the process of invoking a method through a dyn as having adaptation on both sides, like shown in this diagram:

diagram

(This diagram shows adaptation happening to the arguments too; but for this part of the design, we only need the adaptation on the return value.)

Generalizing from box with dynx structs

planning rfc

So far our design has hardcoded the use of Box in the vtable. We can generalize this by introducing a new concept into the compiler; we currently call this a "dynx type"1. Like closure types, dynx types are anonymous structs introduced by the compiler. Instead of returning a Pin<Box<dyn Future>>, the next function will return a dynx Trait, which represents "some kind of pointer to a dyn Trait". Note that the dynx Trait types are anonymous and that the dynx syntax we are using here is for explanatory purposes only. Users still work with pointer types like &dyn Trait , &mut dyn Trait, etc.2

1

Obviously the name "dynx type" is not great. We were considering "object type" (with e.g. obj Trait as the explanatory syntax) but we're not sure what to use here. 2: It may make sense to use dynx as the basis for a user-facing feature at some point, but we are not proposing that here.

At runtime, a dynx Trait struct has the same size as a Box<dyn Trait> (two machine words). If a dynx Trait were an ordinary struct, it might look like this:

#![allow(unused)]
fn main() {
struct dynx Trait {
    data: *mut (),
    vtable: *mut (),
}
}

Like a Box<dyn Trait>, it carries a vtable that lets us invoke the methods from Trait dynamically. Unlike a Box<dyn Trait>, it does not hardcode the memory allocation strategy. Instead, a dynx vtable repurposes the "drop" function slot to mean "drop this pointer and its contents". This allows dynx Trait types to be created from any kind of pointer, such as a Box, &, &mut, Rc, or Arc. When a dynx Trait struct is dropped, it invokes this drop function from its destructor:

#![allow(unused)]
fn main() {
impl Drop for dynx Trait {
    fn drop(&mut self) {
       let drop_fn: fn(*mut ()) = self.vtable.drop_fn;
       drop_fn(self.data);
    }
}
}

Using dynx in the vtable

Now that we have dynx, we can define the vtable for an AsyncIterator almost exactly like we saw before, but using dynx Future instead of a pinned box:

#![allow(unused)]
fn main() {
struct AsyncIteratorVtable<I> {
    ..., /* some stuff */
    next: fn(&mut ()) -> dynx Future<Output = Option<I>>
    //                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                   Look ma, no box!
}
}

Calling i.next() for some i: &mut dyn AsyncIterator will now give back a dynx result:

#![allow(unused)]
fn main() {
i.next().await

// becomes

let f: dynx Future<Output = Option<I>> = i.next();
f.await
}

When the dynx f is dropped, its destructor will call the destructor from the vtable, freeing the backing memory in whatever way is appropriate for the kind of pointer that the dynx was constructed from.

What have we achieved? By using dynx in the vtable, we have made it so that the caller doesn't know (or have to know) what memory management strategy is in use for the resulting trait object. It knows that it has an instance of some struct (dynx Future, specifically) that implements Future, is 2-words in size, and can be dropped in the usual way.

The shim function builds the dynx

In the previous section, we saw that using dynx in the vtable means that the caller no longer knows (or cares) what memory management strategy is in use. This section explains how the callee actually constructs the dynx that gets returned (and how impls can tweak that construction to choose the memory management strategy they want).

Absent any other changes, the impl of AsyncIter contains an async fn next() that returns an impl Future which could have any size. Therefore, to construct a dynx Future, we are going to need an adaptive shim, just like we used in the Pin<Box> example. In fact, the default shim is almost exactly the same as we saw in that case. It allocates a pinned box to store the future and returns it. The main difference is that it converts this Pin<Box<T>> into a dynx Future before returning, rather than coercing to a Pin<Box<dyn Future>> return type (the next section will cover shim functions that don't allocate a box):

#![allow(unused)]
fn main() {
// "pseudocode", you couldn't actually write this because there is no
// user-facing syntax for the `dynx Future` type

fn yielding_range_shim(
    this: &mut YieldingRangeIterator,
) -> dynx Future<Output = Option<u32>> {
    let boxed = Box::pin(<YieldingRangeIterator as AsyncIterator>::next(this));
    
    // invoke the (inherent) `new` method that converts to a `dynx Future`
    <dynx Future>::new(boxed)
}
}

The most interesting part of this function is the last line, which construct the dynx Future from its new function. Intuitively, the new function takes one argument, which must implement the trait IntoRawPointer (added by this design). The IntoRawPointer trait is implemented for smart pointer types int the standard library, like Box, Pin<Box>, and Rc, and represents "some kind of pointer" as well as "how to drop that pointer":

#![allow(unused)]
fn main() {
// Not pseudocode, will be added to the stdlib and implemented
// by various types, including `Box` and `Pin<Box>`.

unsafe trait IntoRawPointer: Deref {
    /// Convert this pointer into a raw pointer to `Self::Target`. 
    ///
    /// This raw pointer must be valid to dereference until `drop_raw` (below) is invoked;
    /// this trait is unsafe because the impl must ensure that to be true.
    fn into_raw(self) -> *mut Self::Target;
    
    /// Drops the smart pointer itself as well as the contents of the pointer.
    /// For example, when `Self = Box<T>`, this will free the box as well as the
    /// `T` value.
    unsafe fn drop_raw(this: *mut Self::Target);
}
}

The <dynx Future>::new method just takes a parameter of type impl IntoRawPointer and invokes into_raw to convert it into a raw pointer. This raw pointer is then packaged up, together with a modified vtable for Future, into the dynx structure. The modified vtable is the same as a normal Future vtable, except that the "drop" slot is modified so that its drop function points to IntoRawPointer::drop_raw, which will be invoked on the data pointer when the dynx is dropped. In pseudocode, it looks like this:

#![allow(unused)]
fn main() {
// "pseudocode", you couldn't actually write this because there is no
// user-facing syntax for the `dynx Future` type; but conceptually this
// inherent function exists (in the actual implementation, it may be inlined
// by the compiler, since you could never name it

struct dynx Future<O> {
    data: *mut (),   // underlying data pointer
    vtable: &'static FutureVtable<O>, // "modified" vtable for `Future<Output = O>` for the underlying type
}

struct FutureVtable<O> {
    /// Invokes `Future::poll` on the underlying data.
    ///
    /// Unsafe condition: Expects the output from `IntoRawPointer::into_raw`
    /// which must not have already been freed.
    poll_fn: unsafe fn(*mut (), cx: &mut Context<'_>) -> Ready<()>,
    
    /// Frees the memory for the pointer to future.
    ///
    /// Unsafe condition: Expects the output from `IntoRawPointer::into_raw`
    /// which must not have already been freed.
    drop_fn: unsafe fn(*mut ()),
}

impl<O> dynx Future<Output = O> {
    fn new<RP>(from: RP)
    where
        RP: IntoRawPointer,
        RP::Target: Sized,               // This must be sized so that we know we have a thin pointer.
        RP::Target: Future<Output = O>,  // The target must implement the future trait.
        RP: Unpin,                       // Required because `Future` has a `Pin<&mut Self>` method, see discussion later.
    {
        let data = IntoRawPointer::into_raw(from);
        let vtable = FutureVtable<O> {
            poll_fn: <RP::Target as Future>::poll,
            drop_fn: |ptr| RP::drop_raw(ptr),
        }; // construct vtable
        dynx Future {
            data, vtable
        }
    }
}

impl<O> Future for dynx Future<Output = O> {
    type Output = O;
    
    fn poll(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Ready<()> {
        // Unsafety condition is met since...
        // 1. self.data was initialized in new and is otherwise never changed.
        // 2. drop must not yet have run or else self would not exist.
        unsafe {
            // Conceptually...
            let pin: Pin<RP> = Pin::new(self); // requires `RP: Unpin`.
            let pin_mut_self: Pin<&mut RP::Target> = pin.as_mut();
            self.vtable.poll_fn(pin_mut_self, cx);
            self = pin.into_inner(); // XXX is this quite right?
            
            self.vtable.poll_fn(self.data, cx)
        }
    }
}


impl<O> Drop for dynx Future<Output = O> {
    fn drop(&mut self) {
        // Unsafety condition is met since...
        // 1. self.data was initialized in new and is otherwise never changed.
        // 2. drop must not yet have run or else self would not exist.
        unsafe {
            self.vtable.drop_fn(self.data);
        }
    }
}
}

Identity shim functions: avoiding the box allocation

planning rfc

In the previous section, we explained how the default "shim" created for an async fn allocates Box to store the future; this Box is then converted to a dynx Future when it is returned. Using Box is a convenient default, but of course it's not always the right choice: for this reason, you can customize what kind of shim using an attribute, #[dyn], attached to the method in the impl:

  • #[dyn(box)] -- requests the default strategy, allocating a box
  • #[dyn(identity)] -- requests a shim that just converts the returned future into a dynx. The returned future must be of a suitable pointer type (more on that in the next section).

An impl of AsyncIterator that uses the default boxing strategy explicitly would look like this:

#![allow(unused)]
fn main() {
impl AsyncIterator for YieldingRangeIterator {
    type Item = u32;

    #[dyn(box)]
    async fn next(&mut self) { /* same as above */ }
}
}

If we want to avoid the box, we can instead write an impl for AsyncIterator that uses dyn(identity). In this case, the impl is responsible for converting the impl Future return value into a an appropriate pointer from which a dynx can be constructed. For example, suppose that we are ok with allocating a Box, but we want to do it from a custom allocator. What we would like is an adapter InAllocator<I> which adapts some I: AsyncIterator so that its futures are boxed in a particular allocator. You would use it like this:

#![allow(unused)]
fn main() {
fn example<A: Allocator>(allocator: A) {
    let mut iter = InAllocator::new(allocator, YieldingRangeIterator::new();
    fn_that_takes_dyn(&mut iter);
}

fn fn_that_takes_dyn(x: &mut dyn AsyncIterator) {
    // This call will go into the `InAllocator<YieldingRangeIterator>` and
    // hence will allocate a box using the custom allocator `A`:
    let value = x.next().await;
}
}

To implement InAllocator<I>, we first define the struct itself:

#![allow(unused)]
fn main() {
struct InAllocator<A: Allocator, I: AsyncIterator> {
    allocator: A,
    iterator: I,
}

impl<A: Allocator, I: AsyncIterator> InAllocator<A, I> {
    pub fn new(
        allocator: A,
        iterator: I,
    ) -> Self {
        Self { allocator, iterator }
    }
}
}

and then we implement AsyncIterator for InAllocator<..>, annotating the next method with #[dyn(identity)]. The next method

#![allow(unused)]
fn main() {
impl<A, I> AsyncIterator for InAllocator<A, I>
where
    A: Allocator + Clone, 
    I: AsyncIterator,
{
    type Item = u32;

    #[dyn(identity)]
    fn next(&mut self) -> Pin<Box<I::next, A>> {
        let future = self.iterator.next();
        Pin::from(Box::new_in(future, self.allocator.clone()))
    }
}
}

Nested impl Trait and dyn adaptation

We've seen that async fn desugars to a regular function returning impl Future. But what happens when we have another impl Trait inside the return value of the async fn?

#![allow(unused)]
fn main() {
trait DebugStream {
    async fn next(&mut self) -> Option<impl Debug + '_>;
}

impl DebugStream for Factory {
    async fn next(&mut self) -> Option<impl Debug + '_> {
        if self.done {
            None
        } else {
            Some(&self.debug_state)
        }
    }
}
}

Trait desugaring

How does something like this desugar? Let's start with the basics...

The trait first desugars to:

#![allow(unused)]
fn main() {
trait DebugStream {
    fn next(&mut self) ->
        impl Future<Output = impl Debug + '_> + '_;
}
}

which further desugars to:

#![allow(unused)]
fn main() {
trait DebugStream {
    type next<'me>: Future<Output = impl Debug + 'me>
    //                                         ^^^^^
    // This lifetime wouldn't be here if not for
    // the `'_` in `impl Debug + '_`
    where
        Self: 'me;
    fn next(&mut self) -> Self::next<'_>;
}
}

which further desugars to:

#![allow(unused)]
fn main() {
trait DebugStream {
    type next<'me>: Future<Output = Self::next_0<'me>>
    where
        Self: 'me;
    type next_0<'a>: Debug
    //         ^^^^
    // This lifetime wouldn't be here if not for
    // the `'_` in `impl Debug + '_`
    where
        Self: 'a; // TODO is this correct?
    fn next(&mut self) -> Self::next<'_>;
}
}

As we can see, this problem is more general than async fn. We'd like a solution to work for any case of nested impl Trait, including on associated types.

Impl desugaring

Pretty much the same as the trait.

The impl desugars to:

#![allow(unused)]
fn main() {
impl DebugStream for Factory {
    fn next(&mut self) ->
        impl Future<Output = Option<impl Debug + '_>> + '_
    {...}
}
}

which further desugars to:

#![allow(unused)]
fn main() {
impl DebugStream for Factory {
    type next<'me> =
        impl Future<Output = Option<impl Debug + 'me>> + 'me
    //                                         ^^^^^
    // This lifetime wouldn't be here if not for
    // the `'_` in `impl Debug + '_`
    where
        Self: 'me; // TODO is this correct?
    fn next(&mut self) -> Self::next<'me>
    {...}
}
}

which further desugars to:

#![allow(unused)]
fn main() {
impl DebugStream for Factory {
    type next<'me> =
        impl Future<Output = Option<Self::next_0<'me>>> + 'me
    where
        Self: 'me;
    type next_0<'a> = impl Debug + 'a
    //         ^^^^
    // This lifetime wouldn't be here if not for
    // the `'_` in `impl Debug + '_`
    where
        Self: 'a;
    fn next(&mut self) -> Self::next<'me>
    {...}
}
}

Dyn adaptation

Let's start by revisiting out the "basic" case: returning regular old impl Future.

#![allow(unused)]
fn main() {
trait BasicStream {
    async fn next(&mut self) -> Option<i32>;
    // Desugars to:
    fn next(&mut self) -> impl Future<Output = Option<i32>>;
}

struct Counter(i32);
impl BasicStream for Counter {
    async fn next(&mut self) -> Option<i32> {...}
}
}

As we saw before, the compiler generates a shim for our type's next function:

#![allow(unused)]
fn main() {
// Pseudocode for the compiler-generated shim that goes in the vtable.
fn counter_next_shim(
    this: &mut Counter,
) -> dynx Future<Output = Option<i32>> {
    // We would skip boxing for #[dyn(identity)]
    let boxed = Box::pin(<Counter as BasicStream>::next(this));
    <dynx Future>::new(boxed)
}
}

Now let's attempt to do the same thing for our original example. Here it is from above:

#![allow(unused)]
fn main() {
trait DebugStream {
    async fn next(&mut self) -> Option<impl Debug + '_>;
}

impl DebugStream for Factory {
    fn next(&mut self) ->
        impl Future<Output = Option<impl Debug + '_>> + '_
    {...}
}
}

Generating the shim here is more complicated, because now it must do two layers of wrapping.

#![allow(unused)]
fn main() {
// Pseudocode for the compiler-generated shim that goes in the vtable.
fn factory_next_shim(
    this: &mut Counter,
) -> dynx Future<Output = Option<i32>> {
    let fut: impl Future<Output = Factory::next_0> =
        <Factory as DebugStream>::next(this);

    // We need to turn the above fut into:
    //     impl Future<Output = dynx Debug>
    // To do this, we need *another* shim...
    struct FactoryNextShim<'a>(Factory::next<'a>);
    impl<'a> Future for FactoryNextShim<'a> {
        type Output = Option<dynx Debug>;
        fn next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            let ret = <Factory::next<'a> as Future>::poll(
                // This is always sound, probably
                pin_project!(self).0,
                cx,
            );
            match ret {
                Poll::Ready(Some(output)) => {
                    // We would skip boxing for #[dyn(identity)] on
                    // impl Future for Factory::next.. which means
                    // #[dyn(identity)] on the impl async fn?
                    // Or do we provide a way to annotate the
                    // future and `impl Debug` separately? TODO
                    //
                    // Why Box::new and not Box::pin like below?
                    // Because `Debug` has no `self: Pin` methods.
                    let boxed = Box::new(output);
                    Poll::Ready(<dynx Debug>::new(boxed))
                }
                Poll::Ready(None) | Poll::Pending => {
                    // No occurrences of `Output` in these variants.
                    Poll::Pending
                }
            }
        }
    }
    let wrapped = FactoryNextShim(fut);

    // We would skip boxing for #[dyn(identity)]
    // Why Box::pin? Because `Future` has a `self: Pin` method.
    let boxed = Box::pin(wrapped);
    <dynx Future>::new(boxed)
}
}

This looks to be a lot of code, but here's what it boils down to:

For some impl Foo<A = impl Bar>,

We generate a wrapper type of our outer impl Foo and implement the Foo trait on it. Our implementation forwards the methods to the actual type, takes the return value, and maps any occurrence of the associated type A in the return type to dynx Bar.

This mapping can be done structurally on the return type, and it benefits from all the flexibility of dynx that we saw before. That means it works for references to A, provided the lifetime bounds on the trait's impl Bar allow for this.

There are probably cases that can't work. We should think more about what those are.

Unresolved questions

planning rfc

There are some interesting details that are yet to be resolved, and they become important indeed in the "feel" of the overall design. Covering those details however would make this document too long, so we're going to split it into parts. Nonetheless, for completeness, this section lists out some of those "questions yet to come".

Which values can be converted into a dynx struct?

In the previous section, we showed how a #[dyn(identity)] function must return "something that can be converted into a dynx struct", and we showed that a case of returning a Pin<Box<impl Future, A>> type. But what are the general rules for constructing a dynx struct? You're asking the right question, but that's a part of the design we haven't bottomed out yet. See this design document for more details.

What about dyn with sendable future, how does that work?

We plan to address this in a follow-up RFC. The core idea is to build on the notation that one would use to express that you wish to have an async fn return a Send. As an example, one might write AsyncIterator<next: Send> to indicate that next() returns a Send future; when we generate the vtable for a dyn AsyncIterator<next: Send>, we can ensure that the bounds for next are applied to its return type, so that it would return a dynx Future + Send (and not just a dynx Future). We have also been exploring a more convenient shorthand for declaring "all the futures returned by methods in trait should be Send", but to avoid bikeshedding we'll avoid talking more about that in this document! See this design document for more details.

How do dynx Trait structs and "sealed traits" interact?

As described here, every dyn-safe trait Trait gets an "accompanying" dynx Trait struct and an impl Trait for dynx Trait impl for that struct. This can have some surprising interactions with unsafe code -- if you have a trait that can only be safely implemented by types that meet certain criteria, the impl for a dynx type may not meet those criteria. This can lead to undefined behavior. The question then is: whose fault is that? In other words, is it the language's fault, for adding impls you didn't expect, or the code author's fault, for not realizing those impls would be there (or perhaps for not declaring that their trait had additional safety requirements, e.g. by making the trait unsafe). This question turns out to be fairly complex, so we'll defer a detailed discussion beyond this summary. See this design document for more details.

Future possibilities

planning rfc

Caching across calls

It has been observed that, for many async fn, the function takes a &mut self "lock" on the self type. In that case, you can only ever have one active future at a time. Even if you are happy to box, you might get a big speedup by caching that box across calls (This is not clear, we'd have to measure, the allocator might do a better job than you can).

We believe that it would be possible to 'upgrade' the ABI to perform this optimization without any change to user code: effectively the compiler would, when calling a function that returns -> impl Trait via dyn, allocate some "scratch space" on the stack and pass it to that function. The default generated shim, which today just boxes, would make use of this scratch space to stash the box in between calls (this would apply to any &mut self function, essentially).

It's also possible to get this behavior today using an adapter.

User-facing summary

planning rfc

This is a brief summary of the user-facing changes.

  • Extend the definition of dyn Trait to include:
    • Async functions
    • Functions that return -> impl Trait (note that -> (impl Trait, impl Trait) or other such constructions are not supported)
      • So long as Trait is dyn safe
  • Extend the definition of impl to permit
    • #[dyn(box)] and #[dyn(identity)] annotations
  • TBD

Appendix: Implementation plan

planning rfc

Inline async iter adapter

planning rfc

This Appendix demonstrates how the inline async iterator adapter works.

#![allow(unused)]
fn main() {
pub struct InlineAsyncIterator<'me, T: AsyncIterator> {
    iterator: T,
    future: MaybeUninit<T::next<'me>>,
}

impl<T> AsyncIterator for InlineAsyncIterator<T>
where
    T: AsyncIterator,
{
    type Item = T::Item;

    #[dyn(identity)] // default is "identity"
    fn next<'a>(&'a mut self) -> InlineFuture<'me, T::next<'me>> {
        let future: T::next<'a> = MaybeUninit::new(self.iterator.next());
        // Why do we need this transmute? Is 'a not sufficient?
        let future: T::next<'me> = transmute(future);
        self.future = future;
        InlineFuture::new(self.future.assume_init_mut())
    }
}

pub struct InlineFuture<'me, F>
where
    F: Future
{
    future: *mut F,
    phantom &'me mut F
}

impl<'me, F> InlineFuture<'me, F> {
    /// Unsafety condition:
    ///
    /// `future` must remain valid for all of `'me`
    pub unsafe fn new(future: *mut F) -> Self {
        Self { future }
    }
}

impl<'me, F> Future for InlineFuture<'me, F>
where
    F: Future,
{
    fn poll(self: Pin<&mut Self>, context: &mut Context) {
        // Justified by the condition on `new`
        unsafe { ... }
    }
}

impl<F> IntoRawPointer for InlineFuture<F> {
    type Target = F;

    // This return value is the part that has to be thin.
    fn into_raw(self) -> *mut Self::Target {
        self.future
    }

    // This will be the drop function in the vtable.
    unsafe fn drop_raw(this: *mut Self::Target) {
        unsafe { drop_in_place(self.future) }
    }
}

impl<'me> Drop for InlineFuture<'me, F> {
    fn drop(&mut self) {
        unsafe { <Self as IntoRawPointer>::drop_raw(self.future); }
    }
}
}

✨ RFC

The RFC exists here in draft form. It will be edited and amended over the course of this initiative. Note that some initiatives produce multiple RFCs.

Until there is an accepted RFC, any feature gates must be labeled as experimental.

When you're ready to start drafting, copy in the template text from the rfcs repository.

Approved RFC: Static async fn in traits

This RFC was opened on the Rust RFCs repository as RFC#3185 and was merged. The tracking issue is #91611.

Refined trait implementations

Summary

This RFC generalizes the safe_unsafe_trait_methods RFC, allowing implementations of traits to add type information about the API of their methods and constants which then become part of the API for that type. Specifically, lifetimes and where clauses are allowed to extend beyond what the trait provides.

Motivation

RFC 2316 introduced the notion of safe implementations of unsafe trait methods. This allows code that knows it is calling a safe implementation of an unsafe trait method to do so without using an unsafe block. In other words, this works today:

trait Foo {
    unsafe fn foo(&self);
}

struct Bar;
impl Foo for Bar {
    fn foo(&self) {
        println!("No unsafe in this impl!")
    }
}

fn main() {
    // Call Bar::foo without using an unsafe block.
    let bar = Bar;
    bar.foo();
}

Unsafe is not the only area where we allow impl signatures to be "more specific" than the trait they're implementing. Unfortunately, we do not handle these cases consistently today:

Associated types

Associated types are a case where an impl is required to be "more specific" by specifying a concrete type.

#![allow(unused)]
fn main() {
struct OnlyZero;

impl Iterator for OnlyZero {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> {
        Some(0)
    }
}
}

This concrete type is fully transparent to any code that can use the impl. Calling code is allowed to rely on the fact that <OnlyZero as Iterator>::Item = usize.

#![allow(unused)]
fn main() {
let mut iter = OnlyZero;
assert_eq!(0usize, iter.next().unwrap());
}

Types in method signatures

We also allow method signatures to differ from the trait they implement.

#![allow(unused)]
fn main() {
trait Log {
    fn log_all(iter: impl ExactSizeIterator);
}

struct OrderedLogger;

impl Log for OrderedLogger {
    // Don't need the exact size here; any iterator will do.
    fn log_all(iter: impl Iterator) { ... }
}
}

Unlike with unsafe and associated types, however, calling code cannot rely on the relaxed requirements on the log_all method implementation.

fn main() {
    let odds = (1..50).filter(|n| *n % 2 == 1);
    OrderedLogger::log_all(odds)
    // ERROR:              ^^^^ the trait `ExactSizeIterator` is not implemented
}

This is a papercut: In order to make this API available to users the OrderedLogger type would have to bypass the Log trait entirely and provide an inherent method instead. Simply changing impl Log for OrderedLogger to impl OrderedLogger in the example above is enough to make this code compile, but it would no longer implement the trait.

The purpose of this RFC is to fix the inconsistency in the language and add flexibility by removing this papercut. Finally, it establishes a policy to prevent such inconsistencies in the future.

Guide-level explanation

When implementing a trait, you can use function signatures that refine those in the trait by being more specific. For example,

#![allow(unused)]
fn main() {
trait Error {
    fn description(&self) -> &str;
}

impl Error for MyError {
    fn description(&self) -> &'static str {
        "My Error Message"
    }
}
}

Here, the error description for MyError does not depend on the value of MyError. The impl includes this information by adding a 'static lifetime to the return type.

Code that knows it is dealing with a MyError can then make use of this information. For example,

#![allow(unused)]
fn main() {
fn attempt_with_status() -> &'static str {
    match do_something() {
        Ok(_) => "Success!",
        Err(e @ MyError) => e.description(),
    }
}
}

This can be useful when using impl Trait in argument or return position.1

#![allow(unused)]
fn main() {
trait Iterable {
    fn iter(&self) -> impl Iterator;
}

impl<T> Iterable for MyVec<T> {
    fn iter(&self) -> impl Iterator + ExactSizeIterator { ... }
}
}

Note that when using impl Trait in argument position, the function signature is refined as bounds are removed, meaning this specific impl can accept a wider range of inputs than the general case. Where clauses work the same way: since where clauses always must be proven by the caller, it is okay to remove them in an impl and permit a wider range of use cases for your API.

#![allow(unused)]
fn main() {
trait Sink {
    fn consume(&mut self, input: impl Iterator + ExactSizeIterator);
}

impl Sink for SimpleSink {
    fn consume(&mut self, input: impl Iterator) { ... }
}
}

Finally, methods marked unsafe in traits can be implemented as safe APIs, allowing code to call them without using unsafe blocks.

1

At the time of writing, return position impl Trait is not allowed in traits. The guide text written here is only for the purpose of illustrating how we would document this feature if it were allowed.

Reference-level explanation

Trait implementations

The following text should be added after this paragraph from the Rust reference:

A trait implementation must define all non-default associated items declared by the implemented trait, may redefine default associated items defined by the implemented trait, and cannot define any other items.

Each associated item defined in the implementation meet the following conditions.

Associated consts

  • Must be a subtype of the type in the corresponding trait item.

Associated types

  • Associated type values must satisfy all bounds on the trait item.
  • Each where clause must be implied by the where clauses on the trait itself and/or the associated type in the trait definition, where "implied" is limited to supertrait and outlives relations. This would be expanded to all implied bounds when that feature is enabled.

Associated functions

  • Must return any subtype of the return type in the trait definition.
  • Each argument must accept any supertype of the corresponding argument type in the trait definition.
  • Each where clause must be implied by the where clauses on the trait itself and/or the associated function in the trait definition, where "implied" is limited to supertrait and outlives relations. This would be expanded to all implied bounds when that feature is enabled.
  • Must not be marked unsafe unless the trait definition is also marked unsafe.

When an item in an impl meets these conditions, we say it is a valid refinement of the trait item.

Using refined implementations

Refined APIs are available anywhere knowledge of the impl being used is available. If the compiler can deduce a particular impl is being used, its API is available for use by the caller. This includes UFCS calls like <MyType as Trait>::foo().

Transitioning away from the current behavior

Because we allow writing impls that look refined, but are not usable as such, landing this feature means we are auto-stabilizing new ecosystem API surface. There are two ways of dealing with this:

Do nothing

Assume that public types want to expose the APIs they actually wrote in their implementations, and allow using those APIs immediately.

Soft transition

Be conservative and require library authors to opt in to refined APIs. This can be done in two parts.

Lint against unmarked refined impls

After this RFC is merged, we should warn when a user writes an impl that looks refined and suggest that they copy the exact API of the trait they are implementing. Once this feature stabilizes, we can should add and suggest using #[refine] attribute to mark that an impl is intentionally refined.

Automatic migration for the next edition

Because refinement will be the default behavior for the next edition, we should rewrite users' code to preserve its semantics over edition migrations. That means we will replace trait implementations that look refined with the original API of the trait items being implemented.

Documentation

The following text should be added to document the difference in editions.

For historical reasons, not all kinds of refinement are automatically supported in older editions.

Item kindFeatureEdition
Type-All editions
MethodUnsafeAll editions
MethodConst2All editions
Methodimpl Trait in return position2All editions
MethodLifetimes2024 and newer
MethodWhere clauses2024 and newer
Methodimpl Trait in argument position2024 and newer
ConstLifetimes2024 and newer
ConstWhere clauses2024 and newer
2

This feature is not accepted at the time of writing the RFC; it is included here for demonstration purposes.

You can opt in to the new behavior in older editions with a #[refine] attribute on the associated item.

#![allow(unused)]
fn main() {
impl Error for MyError {
    #[refine]
    fn description(&self) -> &'static str {
        "My Error Message"
    }
}
}

This enables refining all features in the table above.

Preventing future ambiguity

This RFC establishes a policy that anytime the signature of an associated item in a trait implementation is allowed to differ from the signature in the trait, the information in that signature should be usable by code that uses the implementation.

This RFC specifically does not specify that new language features involving traits should allow refined impls wherever possible. The language could choose not to accept refined implementation signatures for that feature. This should be decided on a case-by-case basis for each feature.

Interaction with other features

Implied bounds

When implied bounds is stabilized, the rules for valid refinements will be modified according to the italicized text above.

Specialization

Specialization allows trait impls to overlap. Whenever two trait impls overlap, one must take precedence according to the rules laid out in the specialization RFC. Each item in the impl taking precedence must be a valid refinement of the corresponding item in the overlapping impl.

Generic associated types

These features mostly don't interact. However, it's worth noting that currently generic associated types require extra bounds on the trait definition if it is likely they will be needed by implementations. This feature would allow implementations that don't need those bounds to elide them and remove that requirement on their types' interface.

const polymorphism

We may want to allow implementations to add const to their methods. This raises the question of whether we want provided methods of the trait to also become const. For example:

#![allow(unused)]
fn main() {
impl Iterator for Foo {
    const fn next(&mut self) -> ...
}
}

Should the nth method also be considered const fn?

Drawbacks

Why should we not do this?

Accidental stabilization

For library authors, it is possible for this feature to create situations where a more refined API is accidentally stabilized. Before stabilizing, we will need to gain some experience with the feature to determine if it is a good idea to allow refined impls without annotations.

Complexity

Overall, we argue that this RFC reduces complexity by improving the consistency and flexibility of the language. However, this RFC proposes several things that can be considered added complexity to the language:

Adding text to the Rust reference

Part of the reason that text is being added to the reference is that the reference doesn't specify what makes an item in a trait implementation valid. The current behavior of allowing certain kinds of divergence and "ignoring" some of them is not specified anywhere, and would probably be just as verbose to describe.

Types are allowed to have different APIs for the same trait

It is possible for a user to form an impression of a trait API by seeing its use in one type, then be surprised to find that that usage does not generalize to all implementations of the trait.

It's rarely obvious, however, that a trait API is being used at a call site as opposed to an inherent API (which can be completely different from one type to the next). The one place it is obvious is in generic functions, which will typically only have access to the original trait API.

Rationale and alternatives

This RFC attempts to be minimal in terms of its scope while accomplishing its stated goal to improve the consistency of Rust. It aims to do so in a way that makes Rust easier to learn and easier to use.

Do nothing

Doing nothing preserves the status quo, which as shown in the Motivation section, is confusing and inconsistent. Allowing users to write function signatures that aren't actually visible to calling code violates the principle of least surprise. It would be better to begin a transition out of this state sooner than later to make future edition migrations less disruptive.

Require implementations to use exactly the same API as the trait

We could reduce the potential for confusion by disallowing "dormant refinements" with a warning in the current edition, as this RFC proposes, and an error in future editions. This approach is more conservative than the one in this RFC. However, it leaves Rust in a state of allowing some kinds of refinement (like safe impls of unsafe methods) but not others, without a clear reason for doing so.

While we could postpone the question of whether to allow this indefinitely, we argue that allowing such refinements will make Rust easier to learn and easier to use.

Allow #[refine] at levels other than impl items

We could allow #[refine] on individual aspects of a function signature like the return type, where clauses, or argument types. This would allow users to scope refinement more narrowly and make sure that they aren't refining other aspects of that function signature. However, it seems unlikely that API refinement would be such a footgun that such narrowly scoping is needed.

Going in the other direction, we could allow #[refine] on the impl itself. This would remove repetition in cases where an impl refines many items at once. It seems unlikely that this would be desired frequently enough to justify it.

Prior art

Java covariant return types

If you override a method in Java, the return type can be any subtype of the original type. When invoking the method on that type, you see the subtype.

Auto traits

One piece of related prior art here is the leakage of auto traits for return position impl Trait. Today it is possible for library authors to stabilize the auto traits of their return types without realizing it. Unlike in this proposal, there is no syntax corresponding to the stabilized API surface.

Unresolved questions

Do we need a soft transition?

In "Transitioning away from the current behavior" we describe two possible paths: immediate stabilization of any API the compiler accepts that happens to look refined today, and doing a soft transition.

While a soft transition is the more conservative approach, it also isn't obvious that it's necessary.

It would help to do an analysis of how frequently "dormant refinements" occur on crates.io today, and of a sample of those, how many look accidental versus an extended API that a crate author might have meant to expose.

Should #[refine] be required in future editions?

As discussed in Drawbacks, this feature could lead to library authors accidentally publishing refined APIs that they did not mean to stabilize. We could prevent that by requiring the #[refine] attribute on any refined item inside an implementation.

If we decide to require the #[refine] annotation in future editions for all refinements, the only edition change would be that the lint in earlier editions becomes a hard error in future editions.

Alternatively, we may even want to require annotations for more subtle features, like lifetimes, while not requiring them for "louder" things like impl Trait in return position.

This question would also benefit from the analysis described in the previous section.

Future possibilities

Return position impl Trait in traits

One motivating use case for refined impls is return position impl trait in traits, which is not yet an accepted Rust feature. You can find more details about this feature in an earlier RFC. Its use is demonstrated in an example at the beginning of this RFC.

This RFC is intended to stand alone, but it also works well with that proposal.

Equivalence to associated types

One of the appealing aspects of this feature is that it can be desugared to a function returning an associated type.

#![allow(unused)]
fn main() {
trait Foo {
    fn get_state(&self) -> impl Debug;
}

// Desugars to something like this:
trait Foo {
    type Foo = impl Debug;
    fn get_state(&self) -> Self::Foo;
}
}

If a trait used associated types, implementers would be able to specify concrete values for those types and let their users depend on it.

#![allow(unused)]
fn main() {
impl Foo for () {
    type Foo = String;
    fn get_state(&self) -> Self::Foo { "empty state".to_string() }
}

let _: String = ().foo();
}

With refinement impls, we can say that this desugaring is equivalent because return position impl trait would give the exact same flexibility as associated types.

Adding generic parameters

This RFC allows implementers to replace return-position impl Trait with a concrete type. Conversely, sometimes it is desirable to generalize an argument from a concrete type to impl Trait or a new generic parameter.

#![allow(unused)]
fn main() {
fn one_a(input: String) {}
fn one_b(input: impl Display) {}
}

More generally, one way to refine an interface is to generalize it by introducing new generics. For instance, here are some more pairs of "unrefined" APIs a and refined versions of them b.

#![allow(unused)]
fn main() {
fn two_a(input: String) {}
fn two_b<T: Debug = String>(input: T) {}

fn three_a<'a>(&'a i32, &'a i32) {}
fn three_b<'a, 'b>(&'a i32, &'b i32) {}
}

It might also be desirable to turn an elided lifetime into a lifetime parameter so it can be named:

#![allow(unused)]
fn main() {
fn four_a(&self) -> &str {}
fn four_b<'a>(&'a self) -> &'a str {}
}

Adding generic parameters to a trait function is not allowed by this proposal, whether the parameters are named or created implicitly via argument-position impl Trait. In principle it could work for both cases, as long as named parameters are defaulted. Implementing this may introduce complexity to the compiler, however. We leave the question of whether this should be allowed out of scope for this RFC.

hackmd-github-sync-badge

Summary

  • Permit impl Trait in fn return position within traits and trait impls.
  • Allow async fn in traits and trait impls to be used interchangeably with its equivalent impl Trait desugaring.
  • Allow trait impls to #[refine] an impl Trait return type with added bounds or a concrete type.1

Motivation

The impl Trait syntax is currently accepted in a variety of places within the Rust language to mean "some type that implements Trait" (for an overview, see the explainer from the impl trait initiative). For function arguments, impl Trait is equivalent to a generic parameter and it is accepted in all kinds of functions (free functions, inherent impls, traits, and trait impls).

In return position, impl Trait corresponds to an opaque type whose value is inferred. In that role, it is currently accepted only in free functions and inherent impls. This RFC extends the support for return position impl Trait in functions in traits and trait impls.

Example use case

The use case for -> impl Trait in trait functions is similar to its use in other contexts: traits often wish to return "some type" without specifying the exact type. As a simple example that we will use through the RFC, consider the NewIntoIterator trait, which is a variant of the existing IntoIterator that uses impl Iterator as the return type:

#![allow(unused)]
fn main() {
trait NewIntoIterator {
    type Item;
    fn into_iter(self) -> impl Iterator<Item = Self::Item>;
}
}

Guide-level explanation

This section assumes familiarity with the basic semantics of impl trait in return position.

When you use impl Trait as the return type for a function within a trait definition or trait impl, the intent is the same: impls that implement this trait return "some type that implements Trait", and users of the trait can only rely on that.

Consider the following trait:

#![allow(unused)]
fn main() {
trait IntoIntIterator {
    fn into_int_iter(self) -> impl Iterator<Item = u32>;
}
}

The semantics of this are analogous to introducing a new associated type within the surrounding trait;

#![allow(unused)]
fn main() {
trait IntoIntIterator { // desugared
    type IntoIntIter: Iterator<Item = u32>;
    fn into_int_iter(self) -> Self::IntoIntIter;
}
}

When using -> impl Trait, however, there is no associated type that users can name.

By default, the impl for a trait like IntoIntIterator must also use impl Trait in return position.

#![allow(unused)]
fn main() {
impl IntoIntIterator for Vec<u32> {
    fn into_int_iter(self) -> impl Iterator<Item = u32> {
        self.into_iter()
    }
}
}

It can, however, give a more specific type with #[refine]:1

#![allow(unused)]
fn main() {
impl IntoIntIterator for Vec<u32> {
    #[refine]
    fn into_int_iter(self) -> impl Iterator<Item = u32> + ExactSizeIterator {
        self.into_iter()
    }
    
    // ..or even..

    #[refine]
    fn into_int_iter(self) -> std::vec::IntoIter<u32> {
        self.into_iter()
    }
}
}

Users of this impl are then able to rely on the refined return type, as long as the compiler can prove this impl specifically is being used. Conversely, in this example, code that is generic over the trait can only rely on the fact that the return type implements Iterator<Item = u32>.

async fn desugaring

async fn always desugars to a regular function returning -> impl Future. When used in a trait, the async fn syntax can be used interchangeably with the equivalent desugaring in the trait and trait impl:

#![allow(unused)]
fn main() {
trait UsesAsyncFn {
    // Equivalent to:
    // fn do_something(&self) -> impl Future<Output = ()> + '_;
    async fn do_something(&self);
}

// OK!
impl UsesAsyncFn for MyType {
    fn do_something(&self) -> impl Future<Output = ()> + '_ {
        async {}
    }
}
}
#![allow(unused)]
fn main() {
trait UsesDesugaredFn {
    // Equivalent to:
    // async fn do_something(&self);
    fn do_something(&self) -> impl Future<Output = ()> + '_;
}

// Also OK!
impl UsesDesugaredFn for MyType {
    async fn do_something(&self) {}
}
}

Reference-level explanation

Equivalent desugaring for traits

Each -> impl Trait notation appearing in a trait fn return type is effectively desugared to an anonymous associated type. In this RFC, we will use the placeholder name $ when illustrating desugarings and the like.

As a simple example, consider the following (more complex examples follow):

#![allow(unused)]
fn main() {
trait NewIntoIterator {
    type Item;
    fn into_iter(self) -> impl Iterator<Item = Self::Item>;
}

// becomes

trait NewIntoIterator {
    type Item;

    type $: Iterator<Item = Self::Item>;

    fn into_iter(self) -> <Self as NewIntoIterator>::$;
}
}
#![allow(unused)]
fn main() {
trait SomeTrait {
    fn method<P0, ..., Pm>()
}
}

Equivalent desugaring for trait impls

Each impl Trait notation appearing in a trait impl fn return type is desugared to the same anonymous associated type $ defined in the trait along with a function that returns it. The value of this associated type $ is an impl Trait.

#![allow(unused)]
fn main() {
impl NewIntoIterator for Vec<u32> {
    type Item = u32;

    fn into_iter(self) -> impl Iterator<Item = Self::Item> {
        self.into_iter()
    }
}

// becomes

impl NewIntoIterator for Vec<u32> {
    type Item = u32;
    
    type $ = impl Iterator<Item = Self::Item>;

    fn into_iter(self) -> <Self as NewIntoIterator>::$ {
        self.into_iter()
    }
}
}

Generic parameter capture and GATs

Given a trait method with a return type like -> impl A + ... + Z and an implementation of that trait, the hidden type for that implementation is allowed to reference:

  • Concrete types, constant expressions, and 'static
  • Self
  • Generics on the impl
  • Certain generics on the method
    • Explicit type parameters
    • Argument-position impl Trait types
    • Explicit const parameters
    • Lifetime parameters that appear anywhere in A + ... + Z, including elided lifetimes

We say that a generic parameter is captured if it may appear in the hidden type. These rules are the same as those for -> impl Trait in inherent impls.

When desugaring, captured parameters from the method are reflected as generic parameters on the $ associated type. Furthermore, the $ associated type brings whatever where clauses are declared on the method into scope, excepting those which reference parameters that are not captured.

This transformation is precisely the same as the one which is applied to other forms of -> impl Trait, except that it applies to an associated type and not a top-level type alias.

Example:

#![allow(unused)]
fn main() {
trait RefIterator for Vec<u32> {
    type Item<'me>
    where 
        Self: 'me;

    fn iter<'a>(&'a self) -> impl Iterator<Item = Self:Item<'a>>;
}

// Since 'a is named in the bounds, it is captured.
// `RefIterator` thus becomes:

trait RefIterator for Vec<u32> {
    type Item<'me>
    where 
        Self: 'me;

    type $<'a>: impl Iterator<Item = Self::Item<'a>>
    where 
        Self: 'a; // Implied bound from fn

    fn iter<'a>(&'a self) -> Self::$<'a>;
}
}

Validity constraint on impls

Given a trait method where impl Trait appears in return position,

#![allow(unused)]
fn main() {
trait Trait {
    fn method() -> impl T_0 + ... + T_m;
}
}

where T_0 + ... + T_m are bounds, for any impl of that trait to be valid, the following conditions must hold:

  • The return type named in the corresponding impl method must implement all bounds T_0 + ... + T_m specified in the trait.
  • Either the impl method must have #[refine],1 OR
    • The impl must use impl Trait syntax to name the corresponding type, and
    • The return type in the trait must implement all bounds I_0 + ... + I_n specified in the impl return type. (Taken with the first outer bullet point, we can say that the bounds in the trait and the bounds in the impl imply each other.)
1

#[refine] was added in RFC 3245: Refined trait implementations. This feature is not yet stable.

Additionally, using -> impl Trait notation in an impl is only legal if the trait also uses that notation.

#![allow(unused)]
fn main() {
trait NewIntoIterator {
    type Item;
    fn into_iter(self) -> impl Iterator<Item = Self::Item>;
}

// OK:
impl NewIntoIterator for Vec<u32> {
    type Item = u32;
    fn into_iter(self) -> impl Iterator<Item = u32> {
        self.into_iter()
    }
}

// OK:
impl NewIntoIterator for Vec<u32> {
    type Item = u32;
    #[refine]
    fn into_iter(self) -> impl Iterator<Item = u32> + DoubleEndedIterator {
        self.into_iter()
    }
}

// OK:
impl NewIntoIterator for Vec<u32> {
    type Item = u32;
    #[refine]
    fn into_iter(self) -> std::vec::IntoIter<u32> {
        self.into_iter()
    }
}

// Not OK:
impl NewIntoIterator for Vec<u32> {
    type Item = u32;
    fn into_iter(self) -> std::vec::IntoIter<u32> {
        self.into_iter()
    }
}
}

Interaction with async fn in trait

This RFC modifies the β€œStatic async fn in traits” RFC so that async fn in traits may be satisfied by implementations that return impl Future<Output = ...> as long as the return-position impl trait type matches the async fn's desugared impl trait with the same rules as above.

#![allow(unused)]
fn main() {
trait Trait {
  async fn async_fn();
  
  async fn async_fn_refined();
}

impl Trait for MyType {
  fn async_fn() -> impl Future<Output = ()> + '_ { .. }
  
  #[refine]
  fn async_fn_refined() -> BoxFuture<'_, ()> { .. }
}
}

Similarly, the equivalent -> impl Future signature in a trait can be satisfied by using async fn in an impl of that trait.

Nested impl traits

Similarly to return-position impl trait in free functions, return position impl trait in traits may be nested in associated types bounds.

Example:

#![allow(unused)]
fn main() {
trait Nested {
    fn deref(&self) -> impl Deref<Target = impl Display> + '_;
}

// This desugars into:

trait Nested {
    type $1<'a>: Deref<Target = Self::$2> + 'a;
    
    type $2: Display;
    
    fn deref(&self) -> Self::$1<'_>;
}
}

But following the same rules as the allowed positions for return-position impl trait, they are not allowed to be nested in trait generics, such as:

#![allow(unused)]
fn main() {
trait Nested {
    fn deref(&self) -> impl AsRef<impl Sized>; // ❌
}
}

Dyn safety

To start, traits that use -> impl Trait will not be considered dyn safe, unless the method has a where Self: Sized bound. This is because dyn types currently require that all associated types are named, and the $ type cannot be named. The other reason is that the value of impl Trait is often a type that is unique to a specific impl, so even if the $ type could be named, specifying its value would defeat the purpose of the dyn type, since it would effectively identify the dynamic type.

On the other hand, if the method has a where Self: Sized bound, the method will not exist on dyn Trait and therefore there will be no type to name.

Dyn safety for async fn in trait

This RFC modifies the "Static async fn in traits" RFC to allow traits with async fn to be dyn-safe if the method has a where Self: Sized bound. This is consistent with how async fn foo() desugars to fn foo() -> impl Future.

Drawbacks

This section discusses known drawbacks of the proposal as presently designed and (where applicable) plans for mitigating them in the future.

Cannot migrate off of impl Trait

In this RFC, if you use -> impl Trait in a trait definition, you cannot "migrate away" from that without changing all impls. In other words, we cannot evolve:

#![allow(unused)]
fn main() {
trait NewIntoIterator {
    type Item;
    fn into_iter(self) -> impl Iterator<Item = Self::Item>;
}
}

into

#![allow(unused)]
fn main() {
trait NewIntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}
}

without breaking semver compatibility for your trait. The future possibilities section discusses one way to resolve this, by permitting impls to elide the definition of associated types whose values can be inferred from a function return type.

Clients of the trait cannot name the resulting associated type, limiting extensibility

As @Gankra highlighted in a comment on a previous RFC, the traditional IntoIterator trait permits clients of the trait to name the resulting iterator type and apply additional bounds:

#![allow(unused)]
fn main() {
fn is_palindrome<Iter, T>(iterable: Iter) -> bool
where
    Iter: IntoIterator<Item = T>,
    Iter::IntoIter: DoubleEndedIterator,
    T: Eq;
}

The NewIntoIterator trait used as an example in this RFC, however, doesn't support this kind of usage, because there is no way for users to name the IntoIter type (and, as discussed in the previous section, there is no way for users to migrate to a named associated type, either!). The same problem applies to async functions in traits, which sometimes wish to be able to add Send bounds to the resulting futures.

The future possibilities section discusses a planned extension to support naming the type returned by an impl trait, which could work to overcome this limitation for clients.

Rationale and alternatives

Does auto trait leakage still occur for -> impl Trait in traits?

Yes, so long as the compiler has enough type information to figure out which impl you are using. In other words, given a trait function SomeTrait::foo, if you invoke a function <T as SomeTrait>::foo() where the self type is some generic parameter T, then the compiler doesn't really know what impl is being used, so no auto trait leakage can occur. But if you were to invoke <u32 as SomeTrait>::foo(), then the compiler could resolve to a specific impl, and hence a specific impl trait type alias, and auto trait leakage would occur as normal.

Can traits migrate from a named associated type to impl Trait?

Not compatibly, no, because they would no longer have a named associated type.

Can traits migrate from impl Trait to a named associated type?

Generally yes, but all impls would have to be rewritten.

Would there be any way to make it possible to migrate from impl Trait to a named associated type compatibly?

Potentially! There have been proposals to allow the values of associated types that appear in function return types to be inferred from the function declaration. So the trait has fn method(&self) -> Self::Iter and the impl has fn method(&self) -> impl Iterator, then the impl would also be inferred to have type Iter = impl Iterator (and the return type rewritten to reference it). This may be a good idea, but it is not proposed as part of this RFC.

What about using a named associated type?

One alternative under consideration was to use a named associated type instead of the anonymous $ type. The name could be derived by converting "snake case" methods to "camel case", for example. This has the advantage that users of the trait can refer to the return type by name.

We decided against this proposal:

  • Introducing a name by converting to camel-case feels surprising and inelegant.
  • Return position impl Trait in other kinds of functions doesn't introduce any sort of name for the return type, so it is not analogous.
  • We would like to allow -> impl Trait methods to work with dynamic dispatch (see Future possibilities). dyn types typically require naming all associated types of a trait. That would not be desirable for this feature, and these associated types would therefore not be consistent with other named associated types.

There is a need to introduce a mechanism for naming the return type for functions that use -> impl Trait; we plan to introduce a second RFC addressing this need uniformly across all kinds of functions.

As a backwards compatibility note, named associated types could likely be introduced later, although there is always the possibility of users having introduced associated types with the same name.

What about using an explicit associated type?

Giving users the ability to write an explicit type Foo = impl Bar; is already covered as part of the type_alias_impl_trait feature, which is not yet stable at the time of writing, and which represents an extension to the Rust language both inside and outside of traits. This RFC is about making trait methods consistent with normal free functions and inherent methods.

There are different situations where you would want to use an explicit associated type:

  1. The type is central to the trait and deserves to be named.
  2. You want to give users the ability to use concrete types without #[refine].
  3. You want to give generic users of your trait the ability specify a particular type, instead of just bounding it.
  4. You want to give users the ability to easily name and bound the type without using (to-be-RFC'd) special syntax to name the type.
  5. You want the trait to work with dynamic dispatch today.
  6. In the future, you want the associated type to be specified as part of dyn Trait, instead of using dynamic dispatch itself.

Using our hypothetical NewIntoIterator example, most of these are not met for the IntoIter type:

  1. While the Item type is pretty central to users of the trait, the specific iterator type IntoIter is usually not.
  2. The concrete type of an impl may or may not be useful, but usually what's important is the specific extra bounds like ExactSizeIterator that a user can use. Using #[refine] to explicitly choose to expose this (or a fully concrete type) is not overly burdensome.
  3. Rarely does a function taking impl IntoIterator specify a particular iterator type; it would be rare to see a function like this, for example:
    #![allow(unused)]
    fn main() {
    fn iterate_over_anything_as_long_as_it_is_vec<T>(
        vec: impl IntoIterator<IntoIter = std::vec::IntoIter<T>, Item = T>
    )
    }
  4. Bounding the iterator by adding extra bounds like DoubleEndedIterator is useful, but not the common case for IntoIterator. It therefore shouldn't be overly burdensome to use a (reasonably ergonomic) special syntax in the cases where it's needed.
  5. Using IntoIterator with dynamic dispatch would be surprising; more common would be to call .into_iter() using static dispatch and then pass the resulting iterator to a function that uses dynamic dispatch.
  6. If we did use IntoIterator with dynamic dispatch, the resulting iterator being dynamically dispatched would make the most sense.

Therefore, if we were writing IntoIterator today, it would probably use -> impl Trait in return position instead of having an explicit IntoIter type.

The same is not true for Iterator::Item: because Item is so central to what an Iterator is, and because it rarely makes sense to use an opaque type for the item, it would remain an explicit associated type.

Prior art

There are a number of crates that do desugaring like this manually or with procedural macros. One notable example is real-async-trait.

Unresolved questions

  • None.

Future possibilities

Naming return types

We expect to introduce a mechanism for naming the result of -> impl Trait return types in a follow-up RFC.

Dynamic dispatch

Similarly, we expect to introduce language extensions to address the inability to use -> impl Trait types with dynamic dispatch. These mechanisms are needed for async fn as well. A good writeup of the challenges can be found on the "challenges" page of the async fundamentals initiative.

Migration to associated type

It would be possible to introduce a mechanism that allows users to migrate from an impl Trait to a named associated type.

Existing users of the trait won't specify an associated type bound for the new associated type, nor will existing implementers of the trait specify the type. This can be fixed with associated type defaults. So given a trait like NewIntoIterator, we could choose to introduce an associated type for the iterator like so:

#![allow(unused)]
fn main() {
// Now old again!
trait NewIntoIterator {
    type Item;
    type IntoIter = impl Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}
}

The only problem remaining is with #[refine]. If an existing implementation refined its return value of an RPITIT method, we would need the existing #[refine] attribute to stand in for an overriding of the associated type default.

Whatever rules we decide to make this work, they will interact with some ongoing discussions of proposals for #[defines] or #[defined_by] attributes on type_alias_impl_trait. We therefore leave the details of this to a future RFC.

πŸ˜• Frequently asked questions

This page lists frequently asked questions about the design. It often redirects to the other pages on the site.

What is the goal of this initiative?

See the Charter.

Who is working on it!

See the Charter.

The "archive" stores older documents that are preserved for historical purposes but which have been updated with newer designs etc.

Async Fundamentals Stage 1 Explainer

WARNING: This is an archived document, preserved for historical purposes. Go to the explainer to see the most up-to-date material.

This document describes Stage 1 of the "Async Fundamentals" plans. Our eventual goal is to make it so that, in short, wherever you write fn, you can also write async fn and have things work as transparently as possible. This includes in traits, even special traits like Drop.

Now that I've got you all excited, let me bring you back down to earth: That is our goal, but Stage 1 does not achieve it. However, we do believe that Stage 1 does enable async functions in traits in such a way that everything is possible, though it may not be easy.

The hope is that by having a stage 1 where stable Rust exposes the core functionality needed for async traits, we can get more data about how async traits will be used in practice. That can guide us as we try to resolve some of the (rather sticky) design questions that block making things more ergonomic. =)

Review: how async fn works

When you write an async function in Rust:

#![allow(unused)]
fn main() {
async fn test(x: &u32) -> u32 {
    *x
}
}

This is actually shorthand for a function that returns an impl Future and which captures all of its arguments. The body of this function is an async move block, which simply creates and returns a future:

#![allow(unused)]
fn main() {
fn test<'x>(x: &'x u32) -> impl Future<Output = u32> + 'x {
    async move {
        *x
    }
}
}

The await operation can be performed on any value that implements Future. It causes the async fn to execute the future's "poll" function to see if it's value is ready. If not, then it suspends the current frame until it is re-invoked.

Async fn in traits

Writing an async function in a trait desugars in just the same way as an async function elsewhere. Hence this trait:

#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> Response;
}
}

becomes:

#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    fn request<'a>(&'a mut self, request: Request) -> impl Future<Output = Response> + 'a;
}
}

On stable Rust today, impl Trait is not permitted in "return position" within a trait, but we plan to allow it. It will desugar to a function that returns an associated type with the same name as the method itself:

#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    type request<'a>: Future<Output = Response> + 'a;
    fn request<'a>(&'a mut self, request: Request) -> Self::request<'a>;
}
}

Calling t.request(...) thus returns a value of type T::request<'a>. We will reference this request variable later.

Using async fn in traits

When you have async fn in a trait, you can use it with generic types in the usual way. For example, you could write a function that uses the above trait like so:

#![allow(unused)]
fn main() {
async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) {
    let response = provider.request(request).await;
    ...
}
}

Naturally you could also write the above example using explicit generics as well (just as with any impl trait):

#![allow(unused)]
fn main() {
async fn process_request<P>(
    mut provider: P,
    request: Request,
) where
    P: HttpRequestProvider,
{
    let response = provider.request(request).await;
    ...
}
}

Naming the future that is returned

Like any function that returns an impl Trait, the return type from an async fn in a trait is anonymous. However, there are times when it can be very useful to be able to talk about it. One particular place where this comes up is when spawning tasks. Consider a function that invokes process_request (above) many times in parallel:

#![allow(unused)]
fn main() {
fn process_all_requests<P>(
    provider: P,
    requests: Vec<RequestData>,
) where
    P: HttpRequestProvider + Clone + Send,
{
    let handles =
        requests
            .into_iter()
            .map(|r| {
                let provider = provider.clone();
                tokio::spawn(async move {
                    process_request(provider, r).await;
                })
            })
            .collect();
    join_all(handles).await;
}
}

As is, compiling this function gives the following error, even though P is marked as Send:

error[E0277]: the future returned by `HttpRequestProvider::request` (invoked on `P`) cannot be sent between threads safely
   --> src/lib.rs:21:17
    |
21  |                 tokio::spawn(async move {
    |                 ^^^^^^^^^^^^ `<P as HttpRequestProvider>::request` cannot be sent between threads safely
    |
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/lib.rs:35:5
    |
35  |     let response = provider.request(request).await?;
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^ this future is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.13.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`

The problem here is that, while P: Send, the future returned by request is not necessarily Send (it could, for example, create Rc state and store it in local variables). To fix this, one can add the bound on the P::request type:

#![allow(unused)]
fn main() {
fn process_all_requests<P>(
    provider: P,
    requests: Vec<RequestData>,
)
where
    P: HttpRequestProvider + Clone + Send,
    for<'a> P::request<'a>: Send,
{
    let handles =
        requests
            .into_iter()
            .map(|r| {
                let provider = provider.clone();
                your_runtime::spawn(async move {
                    process_request(provider, r).await;
                })
            })
            .collect();
    your_runtime::join_all(handles).await
}
}

The new bound is here:

#![allow(unused)]
fn main() {
    for<'a> P::request<'a>: Send,
}

The "higher-ranked" requirement says that "no matter what lifetime provider has, the result is Send". Specifying these higher-ranked lifetimes can be a bit cumbersome, and sometimes the GATs accumulate quite a large number of parameters. Therefore, the compiler also supports a shorthand form; if you leave off the GAT parameters, the for<'a> is assumed:

#![allow(unused)]
fn main() {
where
    P: HttpRequestProvider + Clone + Send,
    P::request: Send,
}

In fact, using a nightly feature, we can write this more compactly:

#![allow(unused)]
fn main() {
where
    P: HttpRequestProvider<request: Send> + Clone + Send,
}

When would send bounds be required?

One thing we are not sure of is how often send bounds would be required in practice. The main point where such bounds are required are for generic async functions that will be "spawned". For example, the process_request function itself didn't require any kind of bounds on request:

#![allow(unused)]
fn main() {
async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) {
    let response = provider.request(request).await;
    ...
}
}

This is because nothing in this function was required to be Send. The problems only arise when you have a call to spawn or some other function that imposes a Send bound -- and even then, there may be no issue. For example, invoking process_request on a known type doesn't require any sort of where clauses:

#![allow(unused)]
fn main() {
your_runtime::spawn(async move {
    process_request(MyProvider::new(), some_request);
})
}

This works because the compiler is able to see that process_request is being called with a MyProvider, and hence it can determine exactly what future process_request will call when it invokes provider.request, and it can see that this future is Send.

The problems only arise when you invoke spawn on a value of a generic type, like P in our example above. In that case, the compiler doesn't know exactly what future will run, so it needs some bounds on P::request.

Caveat: Async fn in trait are not dyn safe

There is one key limitation in stage 1: traits that contain async fn are not dyn safe. Instead, support for dynamic dispatch is provided via a procedural macro called #[dyner]. In future stages, we expect to support dynamic dispatch "natively", but we still need more experimentation and feedback on the requirements to find the best design.

The #[dyner] macro works as follows. You attach it to a trait:

#![allow(unused)]
fn main() {
#[dyner]
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> Response;
}
}

It will generate, alongside the trait, a type DynHttpRequestProvider (Dyn + the name of the trait, in general):

#![allow(unused)]
fn main() {
// Your trait, unchanged:
trait HttpRequestProvider { .. }

// A struct to represent a trait object:
struct DynHttpRequestProvider<'me> { .. }

impl<'me> HttpRequestProvider for DynHttpRequestProvider<'me>
{ .. }
}

This type is a replacement for Box<dyn HttpRequestProvider>. You can create an instance of it by writing DynHttpRequestProvider::from(x), where x is of some type that implements HttpRequestProvider. The DynHttpRequestProvider implements HttpRequestProvider but it forwards each method through using dynamic dispatch.

Example usage

Suppose we had a Context type that was generic over an HttpRequestProvider:

#![allow(unused)]
fn main() {
struct Context<T: HttpRequestProvider> {
   provider: T,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider,
    };
    // ...
}
}

Now suppose that we wanted to remove the T type parameter, so that Context included a trait object. You could do this with dyner by altering the code as follows:

#![allow(unused)]
fn main() {
struct Context<'me> {
   provider: DynHttpRequestProvider<'me>,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::new(provider),
    };
    // ...
}
}

You might be surprised to see the 'me parameter -- this is needed because we don't whether the provider given to us includes borrowed data. If we knew that it had no references, we might also write:

#![allow(unused)]
fn main() {
struct Context {
   provider: DynHttpRequestProvider<'static>,
}

async fn process_request(
    provider: impl HttpRequestProvider + 'static,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from(provider),
    };
    // ...
}
}

Dyner with references

Dyner currently requires an allocator. The DynHttpRequestProvider::new method, for example, allocates a Box internally, and invoking an async function allocates a box to store the resulting future (the #[async_trait] crate does the same; the difference with this approach is that you only use the boxing when you are using dynamic dispatch).

Sometimes though we would like to construct our dynamic objects without using Box. This might be because we only have a &T access or simply because we don't need to allocate and would prefer to avoid the runtime overhead. In cases like that, you can use the from_ref and from_mut methods. from_ref takes a &impl Trait and gives you a Ref<DynTrait>; Ref is a special type that ensures you only use &self methods. from_mut works the same way but for &mut Trait types. Since the provider type never escapes process_request, we could rework our previous example to use mutable references instead of boxing:

#![allow(unused)]
fn main() {
struct Context<'me> {
   provider: &'me mut DynHttpRequestProvider<'me>,
}

async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from_mut(&mut provider),
    };
    // ...
}
}

The rest of the code would work just the same... you can still invoke methods in the usual way (e.g., cx.provider.request(r).await).

Dyner and no-std

Because dyner requires an allocator, it is not currently suitable for no-std settings. We are exploring alternatives here: it's not clear if there is a suitable general purpose mechanism that could be used in settings like that. But part of the beauty of the dyner approach is that, in principle, there might be "dyner-like" crates that can be used in a no-std environment.

Dyner all the things

Dyner is meant to be a "general purpose" replacement for dyn Trait. It expands the scope of what kinds of traits are dyn safe to include traits that use impl Trait in argument- and return-position. For example, the following trait is not dyn safe, but it is dyner safe:

#![allow(unused)]
fn main() {
#[dyner]
trait Print {
    fn print(&self, screen: &mut impl Screen);
    //                           ^^^^^^^^^^^
    //                    impl trait is not dyn safe;
    //                    it is dyner-safe if the trait
    //                    is also procssed with dyner
}

#[dyner]
trait Screen {
    fn output(&mut self, x: usize, y: usize, c: char);
}
}

Dyner for external traits

For dyner to work well, all the traits that you reference from your dyner trait must be processed with dyner. But sometimes those traits may be out of your control! What can you do then? To support this, dyner permits you to apply dyner to an external trait definition. For example, support you had this trait, referencing the Clear trait from cc_traits:

#![allow(unused)]
fn main() {
#[dyner]
trait MyOp {
    fn apply_op(x: &mut impl cc_traits::Clear);
}
}

Applying dyner to MyOp will yield an error that "the type cc_traits::DynClear is not found". This is because the code that dyner generates expects that, for each Foo trait, there will be a DynFoo type available at the same location, and in this case there is not. You can fix this by using the dyner::external_trait! macro, but unfortunately doing so requires that you copy and paste the trait definition:

#![allow(unused)]
fn main() {
mod ext {
    dyner::external_trait! {
        pub trait cc_traits::Clear {
    	    fn clear(&mut self);
        }
    }
}
}

The dyner::external_trait! macro will generate two things:

  • A re-export of cc_traits::Clear
  • a type DynClear

Now we can rewrite our MyOp trait to reference Clear from this new location and everything works:

#![allow(unused)]
fn main() {
mod ext { ... }

#[dyner]
trait MyOp {
    fn apply_op(x: &mut impl ext::Clear);
    //                       ^^^ Changed this.
}
}

The ext::Clear path is just a re-exported version of cc_traits::Clear, so there is no change there, but the type ext::DynClear is well-defined.

Dyner for things in the stdlib

The dyner crate already includes dyner-ified versions of things from the stdlib. However, to take advantage of them, you have to import the traits from dyner::std. For example, instead of referencing std::iter::Iterator, try dyner::std::iter::Iterator:

#![allow(unused)]
fn main() {
use dyner::std::iter::{Iterator, DynIterator};
//  ^^^^^^^                      ^^^^^^^^^^^

#[dyner]
trait WidgetStream {
    fn request(&mut self, r: impl Iterator<Item = String>);
    //                            ^^^^^^^^ the macro will look for DynIterator
}
}

You can use use dyner::std::prelude::* to get all the same traits as are present in the std prelude, along with their Dyn equivalents.

Feedback requested

We'd love to hear what you think. Nothing here is set in stone, quite far from it!

Some questions we are specifically interested in getting answers to:

  • What about this document was confusing to you? We want you to understand, of course, but we also want to improve the way we explain things.
  • How often do you think you would use static vs dynamic dispatch in traits?
  • How are you managing async fn in trait today? Do you think the dyner crate would work for you?
  • How often do you think you use functions that require Send. Do you anticipate any problems from the bound scheme described above?

Async Fundamentals Mini Vision Doc

WARNING: This is an archived document, preserved for historical purposes. Go to the explainer to see the most up-to-date material.

Grace and Alan are working at BoggleyCorp, developing a network service using async I/O in Rust. Internally, they use a trait to manage http requests, which allows them to easily change to different sorts of providers:

#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> anyhow::Result<Response>;
}
}

They start out using impl HttpRequest and things seem to be working very well:

#![allow(unused)]
fn main() {
async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let response = provider.request(request).await?;
    process_response(response).await;
}
}

As they refactor their system, though, they decide they would like to store the provider in a Context type. When they create the struct, though, they realize that impl Trait syntax doesn't work in that context:

#![allow(unused)]
fn main() {
struct Context {
   provider: impl HttpRequestProvider,
}

async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context { 
        provider
    };
    // ...
}
}

Alan looks to Grace, "What do we do now?" Grace says, "Well, we could make an explicit type parameter, but I think a dyn might be easier for us here." Alan says, "Oh, ok, that makes sense." He alters the struct to use a Box<dyn HttpRequestProvider>:

#![allow(unused)]
fn main() {
struct Context {
   provider: Box<dyn HttpRequestProvider>,
}
}

However, this gets a compilation error: "traits that contain async fn are not dyn safe". The compiler does, however, suggest that they check out the experimental dyner crate. The README for the crate advises them to decorate their trait with dyner::make_dyn, and to give a name to use for the "dynamic dispatch" type:

#![allow(unused)]
fn main() {
#[dyner::make_dyn(DynHttpRequestProvider)]
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> anyhow::Result<Response>;
}
}

Following the readme, they also modify their Context struct like so:

#![allow(unused)]
fn main() {
struct Context {
   provider: DynHttpRequestProvider<'static>,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from(provider),
    };
    // ...
}
}

However, the code doesn't compile just as is. They realize that the Context is only being used inside process_request and so they follow the "time-limited" pattern of adding a lifetime parameter 'me to the context. This represents the period of time in which the context is in use:

#![allow(unused)]
fn main() {
struct Context<'me> {
   provider: DynHttpRequestProvider<'me>,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from(provider),
    };
    // ...
}
}

At this point, they are able to invoke provider.request() as normal.

Spawning tasks

As their project expands, Alan and Grace realize that they are going to have process a number of requests in parallel. They insert some code to spawn off tasks:

#![allow(unused)]
fn main() {
fn process_all_requests(
    provider: impl HttpRequestProvider + Clone,
    requests: Vec<RequestData>,
) -> anyhow::Result<()> {
    let handles =
        requests
            .into_iter()
            .map(|r| {
                let provider = provider.clone();
                tokio::spawn(async move {
                    process_request(provider, r).await;
                })
            })
            .collect();
    tokio::join_all(handles).await
}
}

However, when they write this, they get an error:

error: future cannot be sent between threads safely
   --> src/lib.rs:21:17
    |
21  |                 tokio::spawn(async move {
    |                 ^^^^^^^^^^^^ future created by async block is not `Send`
    |
note: captured value is not `Send`
   --> src/lib.rs:22:37
    |
22  |                     process_request(provider, r).await;
    |                                     ^^^^^^^^ has type `impl HttpRequestProvider + Clone` which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.13.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`
help: consider further restricting this bound
    |
13  |     provider: impl HttpRequestProvider + Clone + std::marker::Send,
    |                                                +++++++++++++++++++

"Ah, right," thinks Alan. "I need to add Send to show that the provider is something we can send to another thread."

#![allow(unused)]
fn main() {
async fn process_all_requests(
    provider: impl HttpRequestProvider + Clone + Send,
    //                                           ^^^^ added this
    requests: Vec<Request>,
) {
    ...
}
}

But when he adds that, he gets another error. This one is a bit more complex:

error[E0277]: the future returned by `HttpRequestProvider::request` (invoked on `impl HttpRequestProvider + Clone + Send`) cannot be sent between threads safely
   --> src/lib.rs:21:17
    |
21  |                 tokio::spawn(async move {
    |                 ^^^^^^^^^^^^ `<impl HttpRequestProvider + Clone + Send as HttpRequestProvider>::Request` cannot be sent between threads safely
    |
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/lib.rs:35:5
    |
35  |     let response = provider.request(request).await?;
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^ this future is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.13.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`
help: introduce a `provider: Send` bound to require this future to be send:
    |
13  ~     provider: impl HttpRequestProvider<request: Send> + Clone + Send,,
    |

Alan and Grace are a bit puzzled. They decide to to their friend Barbara, who knows Rust pretty well.

Barbara looks over the error. She explains to them that when they call an async function -- even in a trait -- that results in a future. "Yeah, yeah", they say, "we know that." Barbara explains that this means that saying that provider is Send does not necessarily imply that the future resulting from provider.request() is Send. "Ah, that makes sense," says Alan. "But what is this suggestion at the bottom?"

"Ah," says Barbara, "the notation T: Foo<Bar: Send> generally means the same as T: Foo, T::Bar: Send. In other words, it says that T implements Foo and that the associated type Bar is Send."

"Oh, I see," says Alan, "I remember reading now that when we use an async fn, the compiler introduces an associated type with the same name as the function that represents the future that gets returned."

"Right", answers Barbara. "So when you say impl HttpRequestProvider<request: Send> that means that the request method returns a Send value (a future, in this case)."

Alan and Grace change their project as the compiler suggested, and things start to work:

#![allow(unused)]
fn main() {
fn process_all_requests(
    provider: impl HttpRequestProvider<request: Send> + Clone,
    requests: Vec<RequestData>,
) -> anyhow::Result<()> {
    ...
}
}

Dyner all the things (even non-async things)

As Alan and Grace get used to dyner, they start using it for all kinds of dynamic dispatch, including some code which is not async. It takes getting used to, but dyner has some definite advantages over the builtin Rust functionality, particularly if you use it everywhere. For example, you can use dyner with traits that have self methods and even methods which take and return impl Trait values (so long as those traits also use dyner, and they use the standard naming convention of prefixing the name of the trait with Dyn):

#![allow(unused)]
fn main() {
// The dyner crate re-exports standard library traits, along
// with Dyn versions of them (e.g., DynIterator). You do have
// to ensure that the `Iterator` and `DynIterator` are reachable
// from the same path though:
use dyner::iter::{Iterator, DynIterator};

#[dyner::make_dyn(DynWidgetStream)]
trait WidgetTransform {
    fn transform(
        mut self, 
        //  ^^^^ not otherwise dyn safe
        w: impl Iterator<Item = Widget>,
        // ^^^^^^^^^^^^^ this would not ordinarily be dyn-safe
    ) -> impl Iterator<Item = Widget>;
    //   ^^^^^^^^^^^^^ this would not ordinarily be dyn-safe
}
}

Dyn trait objects for third-party traits

Using dyner, Alan and Barbara are basically unblocked. After a while, though, they hit a problem. One of their dependencies defines a trait that has no dyn equivalent (XXX realistic example?):

#![allow(unused)]
fn main() {
// In crate parser
trait Parser {
    fn parse(&mut self, input: &str);
}
}

They are able to manage this by declaring the "dyn" type themselves. To do so, however, they have to copy and paste the trait definition, which is kind of annoying:

#![allow(unused)]
fn main() {
mod parser {
    dyner::make_dyn_extern {
        trait parser::Parser {
            fn parse(&mut self, input: &str);
        }
    }
}
}

They can now use crate::parser::{Parser, DynParser} to get the trait and its Dyn equivalent; the crate::parser::Parser is a re-export. "Ah, so that's how dyner re-exports the traits from libstd", Grace realizes.

What's missing?

  • Spawning and needing Send bounds
  • Appendix: how does dyner work?

XXX

  • replacement for where Self: Sized?
    • a lot of those problems go away but
    • we can also offer an explicit #[optimization] annotation:
      • causes the default version to be reproduced for dyn types and exempts the function from all limitations
  • inherent async fn
  • impl A + B
    • DynerAB-- we could even do that, right?
  • https://docs.rs/hyper/0.14.14/hyper/body/trait.HttpBody.html