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

RoleGithub
Ownertmandry
Liaisonnikomatsakis

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 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 trait🦀tmandry
Author evaluation doc for dyn async trait🦀tmandry
Author evaluation doc for async drop🦀tmandry
Author evaluation doc for impl Trait in traits💤
Stabilize type alias impl trait💤
Stabilize generic associated types💤
Author RFC for async fn in traits💤
Author evaluation doc for async closures💤
Author RFC for async fn in traits💤
Feature complete for async fn in traits💤
Feature complete for impl Trait in traits💤
Feature complete for async drop💤
Feature complete for 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

Async functions need their own traits, analogous to Fn and friends:


#![allow(unused)]
fn main() {
#[repr(async_inline)]
trait AsyncFnOnce<A> {
    type Output;

    // Uh-oh! You can't encode these as `async fn` using inline async functions!
    async fn call(mut self, args: A) -> Self::Output;
}

#[repr(async_inline)]
trait AsyncFnMut: AsyncFnOnce {
    type Output;

    async fn call_mut(&mut self, args: A) -> Self::Output;
}

#[repr(async_inline)]
trait AsyncFn: AsyncFnMut {
    // Uh-oh! You can't encode these as `async fn` using inline async functions!
    async fn call(&self, args: A) -> Self::Output;
}
}

Some 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.

📚 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.

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.

😕 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