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.