This RFC proposes adding a (defaulted) generic parameter to the core::future::Future trait to allow more flexibility in Future::poll's second argument (context).
Motivation
With the introduction of the async/await syntax (and one could argue, before that), futures have become a core aspect of Rust. However, the current signature poses a few issues:
- The context (and the types it's built from) is not ABI-stable: this is a big problem for plugin systems that wish to expose asynchronous methods, as the futures need to be wrapped in ABI-safe adapters that often impose allocating a new waker for every call to
poll. - Asynchronous frameworks such as
tokiomust resort to side-channels to allow futures to access the executor to perform certain tasks, such as spawning new tasks (the use-case is picked fromfeature(waker_getters)which considers using the access to raw vtables to downcast the waker into a specialised waker that can accomplish such tasks). - The
core::task::Wakertype is the common denominator to all interactions with futures, making adjusting its API and implementation especially trying, as it affects the whole ecosystem indiscriminately. It also imposes its vtable/pointer pair layout to every executor that would wish to construct wakers, regardless of whether this structure is desirable to them.
Guide-level explanation
Futures could be re-defined as follows (at this stage of the RFC, I'll prefix newly introduced symbol with _, and am highly flexible on renaming these symbols):
This means that when implementing futures by hand with impl core::future::Future for MyFuture, the implementation would likely be over-specialized, and a lint offering to generalize the implementation to impl<_W: _WakerTrait> core::future::Future<_W> for MyFuture could help boost the adoption of this new feature.
Note also that the generic parameter should be spread to IntoFuture and any other section of the stdlib that currently uses Future.
Reference-level explanation
The core of the proposal is the API change, which is explained in the guide-level explaination.
The current mechanics of constructing the state machine for async blocks would still be applicable. Care should be taken to generate implementations of future for the entire intersection of the sets of wakers supported by each awaited future.
Note that while async blocks can generate AnonymousType to handle an open set of wakers, there is no existing way to expose its full capabilities. The community has gotten into the habbit of modeling
async
as the following Return Position Impl Trait (RPIT) form
To the best of my knowledge, this model is an oversimplification, and the correct desuggaring would be
[
which could be converted into the following to allow foo to be usable with arbitrary wakers:
The issue of naming that anonymous type through RPIT remains. The most accurate RPIT naming would be the hypothetical impl for<_W> core::future::Future<_W> where {bounds...}, but RPIT doesn't support generics other than lifetimes, nor bounds on said generics.
With the coming support of async fn in traits which (from my external viewpoint) seems to be orthogonal to that of RPIT in traits, this issue could be bypassed entirely, by simply dropping "async functions are just functions that return an async blocks named through RPIT" model in favor of the original "they return an anonymous state machine" model.
Alternatively, users could define pub trait PolyFuture: Future + Future<tokio::Waker> {} and blanket-implement it, and then use that trait for their RPIT and boxed futures. This would only cover closed sets of wakers, but may be an acceptable compromise.
While this awkwardness in interactions with RPIT is somewhat frustrating, Generic Futures are still a net gain in flexibility, and even RPIT code could gain in flexibility by extending their supported closed set of wakers.
Drawbacks
- Risk of API break: great care should be taken to ensure that there isn't some weird corner case where type inference would start failing on existing code once the defaulted generic is added. I've seen type inference failures in the wild when dealing with default generics in the wild, but I don't think this would happen for traits.
- Risk of fragmentation and/or added complexity of executor APIs: since executors will be able to specialise their futures for their executors, this may cause additional fragmentation in the async ecosystem. Maintaining support for traditional futures and specialised futures could have an impact on executor APIs and implementations, as these futures would need to be kept distinct.
- Note that this risk also exists with the runtime specialisation discussed in the
waker_gettersRFC, with the clear advantage for this RFC that fragmentation could be detected and handled at compile time.
- Note that this risk also exists with the runtime specialisation discussed in the
- Due to
std::task::WakeandWakeralready exisiting, naming the new trait for wakers may be awkward, and the three symbols may become confusing to newcomers. - The RFC breaks the RPIT model of teaching
async fn(and assumes the current implementation ofasync fndoesn't rely on RPIT).
Rationale and alternatives
- This design opens up new flexibility in the asynchronous ecosystem, at no extra cost to users that do not care about it.
- The generic parameter could be deeper down the waker rabbit hole (
RawWakerfor privacy, for example), butWakerlevel seems like a good point for the generic to be located. Any deeper thanRawWakerwould over-constrain the design of alternative wakers, as they would be forced into a pointer-vtable design regardless of their need. - This new flexibility holds especially great value to users that wish to pass futures accross the FFI boundary, as this enables desgins that don't need to allocate to clone wakers coming from objects that aren't trusted to have the same ABI.
- Due to the fact that
Futureis a lang-item, this is both a compiler and library proposal. - This proposal should have very little effect on legibility, as this additional generic will generally be infered.
Prior art
RFC #1398 proposed adding the defaulted generics for allocators to Vec and Box.
Unresolved questions
- Is defaulting the added generic truly sufficient to guarantee that no existing code would break?
- I've fooled around in test files to check for edge cases, but haven't found any example of code breaking when simulating the change.
- I've previously attempted to implement this RFC, and test it on a large repo with extensive use of futures, but the repo failed to compile on nightly to begin with.
- I've encountered cases where default generics would break existing code on structures in the past, typically where constructors are considered ambiguous despite all ambiguities being defaulted. This issue shouldn't arise with traits, but is the source of my concern here.
Future possibilities
This RFC could become a precedent in extending existing traits through defaulted generics.