- Feature Name: (fill me in with a unique ident, my_awesome_feature)
- Start Date: 2018-04-04
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
This RFC provides the library component for the first-class async
/await
syntax proposed in a companion RFC. It is intentionally minimal, including the
smallest set of mechanisms needed to support async/await with borrowing and
interoperation with the futures crate. Those mechanisms are:
- The task system of the futures crate, which will be moved into
libcore
- A new
Future
trait, which integrates thePinMut
APIs with the task system to provide async values that can interoperate with futures.
The RFC also covers the intended ecosystem migration path.
The basic motivation for this RFC is to provide a supporting mechanism for
async
/await
syntax:
async fn function(argument: &str) -> usize {
// ...
}
The syntax itself is motivated in the companion RFC, and there is a blog post that goes through its importance in greater detail. As with closures, the syntax involves producing an anonymous type, so that the above declaration is equivalent to:
fn function<'a>(argument: &'a str) -> _Anonymous<'a, usize> {
// ...
}
Again like a closure the anonymous type is only usable through the trait it
implements: Future
. The goal of this RFC is to provide a concrete proposal for
this Future
trait, based on the work pioneered by the futures crate.
A secondary benefit of this RFC is that it enshrines the task system currently
defined by the futures crate into libcore
, thereby standardizing and
ultimately stabilizing the async ecosystem around a single lightweight task
mechanism.
The Future
trait represents an asynchronous and lazy computation that may
eventually produce a final value, but doesn't have to block the current thread
to do so.
Futures can be constructed through async
blocks or async
functions, e.g.,
async fn read_frame(socket: &TcpStream) -> Result<Frame, io::Error> { ... }
This async
function, when invoked, produces a future that represents the
completion of reading a frame from the given socket. The function signature
is equivalent to:
fn read_frame<'sock>(socket: &'sock TcpStream)
-> impl Future<Output = Result<Frame, io::Error>> + 'sock;
Other async functions can await this future; see the companion RFC for full details.
In addition to async fn
definitions, futures can be built using adapters, much
like with Iterator
s. Initially these adapters will be provided entirely "out
of tree", but eventually they will make their way into the standard library.
Ultimately asynchronous computations are executed by tasks, which are
lightweight threads. In particular, an executor is able to "spawn" a
()
-producing Future
as an independent task; these tasks are then
cooperatively scheduled onto one or more operating system threads. The
Executor
trait defines this interface, and the task
module provides a host
of related definitions needed when manually implementing Future
s or
executors.
The fundamental mechanism for asynchronous computation in Rust is tasks, which are lightweight threads of execution; many tasks can be cooperatively scheduled onto a single operating system thread.
To perform this cooperative scheduling we use a technique sometimes referred to as a "trampoline". When a task would otherwise need to block waiting for some event, instead it schedules itself for later wakeup and returns to the executor running it, which can then run another task. Subsequent wakeups place the task back on the executors queue of ready tasks, much like a thread scheduler in an operating system.
Attempting to complete a task (or async value within it) is called polling,
and always yields a Poll
value back:
/// Indicates whether a value is available, or if the current task has been
/// scheduled for later wake-up instead.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Poll<T> {
/// Represents that a value is immediately ready.
Ready(T),
/// Represents that a value is not ready yet.
///
/// When a function returns `Pending`, the function *must* also
/// ensure that the current task is scheduled to be awoken when
/// progress can be made.
Pending,
}
When a task returns Poll::Ready
, the executor knows the task has completed and
can be dropped.
Each task executor provides its own scheduling facilities, and hence needs to
customize the way task wakeups are handled. As such, there is a
std::task::Wake
trait defining wakeup behavior:
/// A way of waking up a specific task.
///
/// Any task executor must provide a way of signaling that a task it owns
/// is ready to be `poll`ed again. Executors do so by providing a wakeup handle
/// type that implements this trait.
pub trait Wake: Send + Sync {
/// Indicates that the associated task is ready to make progress and should
/// be `poll`ed.
///
/// Executors generally maintain a queue of "ready" tasks; `wake` should place
/// the associated task onto this queue.
fn wake(&self);
}
In general async values are not coupled to any particular executor, so we use a trait object to handle waking:
/// A `Waker` is a handle for waking up a task by notifying its executor that it
/// is ready to be run.
///
/// This handle contains a trait object pointing to an instance of the `UnsafeWake`
/// trait, allowing notifications to get routed through it.
///
/// Implements `Clone`, `Send`, and `Sync`.
pub struct Waker { ... }
impl Waker {
/// Wake up the task associated with this `Waker`.
pub fn wake(&self);
}
// We will see how to handle the no_std case later in the RFC...
impl<T> From<T> for Waker where Arc<T>: Wake + 'static { ... }
Task execution always happens in the context of a Waker
that can be used to
wake the task up; we'll see the full core::task::Context
structure below.
An executor is responsible for polling tasks to completion. We represent this
with the core::task::Executor
trait:
/// A task executor.
///
/// A *task* is a `()`-producing async value that runs at the top level, and will
/// be `poll`ed until completion. It's also the unit at which wake-up
/// notifications occur. Executors, such as thread pools, allow tasks to be
/// spawned and are responsible for putting tasks onto ready queues when
/// they are woken up, and polling them when they are ready.
pub trait Executor {
/// Spawn the given task, polling it until completion.
///
/// # Errors
///
/// The executor may be unable to spawn tasks, either because it has
/// been shut down or is resource-constrained.
fn spawn_obj(&mut self, task: TaskObj) -> Result<(), SpawnObjError>;
/// Determine whether the executor is able to spawn new tasks.
///
/// # Returns
///
/// An `Ok` return means the executor is *likely* (but not guaranteed)
/// to accept a subsequent spawn attempt. Likewise, an `Err` return
/// means that `spawn` is likely, but not guaranteed, to yield an error.
fn status(&self) -> Result<(), SpawnErrorKind> {
Ok(())
}
}
pub struct TaskObj { .. }
impl TaskObj {
/// Create a new `TaskObj` by boxing the given future.
pub fn new<A: Future<Output = ()> + Send + 'static>(f: A) -> TaskObj;
}
/// Provides the reason that an executor was unable to spawn.
pub struct SpawnErrorKind { .. }
impl SpawnErrorKind {
/// Spawning is failing because the executor has been shut down.
pub fn shutdown() -> SpawnErrorKind;
/// Check whether this error is the `shutdown` error.
pub fn is_shutdown(&self) -> bool;
// additional error variants added over time...
}
/// The result of a failed spawn
pub struct SpawnObjError {
/// The kind of error
pub kind: SpawnErrorKind,
/// The task for which spawning was attempted
pub task: TaskObj,
}
We need the executor trait to be usable as a trait object, which is why TaskObj
is constructed here from a boxed future. (In the no_std section, we'll see
another constructor). In the long run, though, once we can take dyn
by value,
we would deprecate spawn_obj
and add a default spawn
method:
trait Executor {
fn spawn(&mut self, task: Future<Output = ()> + Send) -> Result<(), SpawnErrorKind> {
self.spawn_obj(TaskObj::new(task))
}
// ...
}
At that point we would also deprecate TaskObj
, which is the reason for using
the Obj
suffix -- we want to keep the name Task
available for potential
usage down the line.
In addition to the above, the core::task
module will include the following API
for helping detect bugs:
/// Marks the current thread as being within the dynamic extent of an
/// executor.
///
/// Executor implementations should call this function before beginning to
/// execute a tasks, and drop the returned `Enter` value after completing
/// task execution:
///
/// ```rust
/// let enter = enter().expect("...");
/// /* run task */
/// drop(enter);
/// ```
///
/// Doing so ensures that executors aren't accidentally invoked in a nested fashion.
/// When that happens, the inner executor can block waiting for an event that can
/// only be triggered by the outer executor, leading to a deadlock.
///
/// # Error
///
/// Returns an error if the current thread is already marked, in which case the
/// caller should panic with a tailored error message.
pub fn enter() -> Result<Enter, EnterError>
As stated in the doc comment, the expectation is that all executors will wrap
their task execution within an enter
to detect inadvertent nesting.
All tasks are executed with two pieces of contextual information:
- A
Waker
for waking the task up later on. - An executor, which is the "default" place to spawn further tasks.
Notably, this list does not include task-local data; that can be addressed externally, as we'll see in a later section.
The core::task::Context
type gathers the (stack-rooted) contextual information
together, and is passed by mutable reference to all polling functions:
/// Information about the currently-running task.
///
/// Contexts are always tied to the stack, since they are set up specifically
/// when performing a single `poll` step on a task.
pub struct Context<'a> { .. }
impl<'a> Context<'a> {
pub fn new(waker: &'a Waker, executor: &'a mut Executor) -> Context<'a>
/// Get the `Waker` associated with the current task.
pub fn waker(&self) -> &Waker;
/// Run an asynchronous computation to completion on the default executor.
///
/// # Panics
///
/// This method will panic if the default executor is unable to spawn.
/// To handle executor errors, use the `executor` method instead.
pub fn spawn(&mut self, f: impl Future<Output = ()> + 'static + Send);
/// Get the default executor associated with this task.
///
/// This method is useful primarily if you want to explicitly handle
/// spawn failures.
///
/// NB: this will remain unstable until the final `Executor` trait is ready.
pub fn executor(&mut self) -> &mut BoxExecutor;
}
Note that the spawn
method here will box until Executor
is added.
With all of the above task infrastructure in place, defining Future
is
straightforward:
pub trait Future {
/// The type of value produced on completion.
type Output;
/// Attempt to resolve the computation to a final value, registering
/// the current task for wakeup if the value is not yet available.
///
/// # Return value
///
/// This function returns:
///
/// - `Poll::Pending` if the value is not ready yet.
/// - `Poll::Ready(val)` with the result `val` upon completion.
///
/// Once a future has completed, clients should not `poll` it again.
///
/// When a future is not ready yet, `poll` returns `Poll::Pending`.
/// The future will *also* register the interest of the current task in the
/// value being produced. For example, if the future represents the availability
/// of data on a socket, then the task is recorded so that when data arrives,
/// it is woken up (via `cx.waker()`). Once a task has been woken up,
/// it should attempt to `poll` the future again, which may or may not
/// produce a final value at that time.
///
/// Note that if `Pending` is returned it only means that the *current* task
/// (represented by the argument `cx`) will receive a notification. Tasks
/// from previous calls to `poll` will *not* receive notifications.
///
/// # Runtime characteristics
///
/// `Future` values are *inert*; they must be *actively* `poll`ed to make
/// progress, meaning that each time the current task is woken up, it should
/// actively re-`poll` pending computations that it still has an interest in.
/// Usually this is handled automatically by `async`/`await` notation or
/// via adapter methods. Executors ensure that each task is `poll`ed every
/// time a future internal to that task is ready to make progress.
///
/// The `poll` function is not called repeatedly in a tight loop, but only
/// whenever the computation itself is ready to make progress, as signaled via
/// `cx.waker()`. If you're familiar with the `poll(2)` or `select(2)`
/// syscalls on Unix it's worth noting that async values typically do *not*
/// suffer the same problems of "all wakeups must poll all events"; they
/// are more like `epoll(4)`.
///
/// An implementation of `poll` should strive to return quickly, and should
/// *never* block. Returning quickly prevents unnecessarily clogging up
/// threads or event loops. If it is known ahead of time that a call to
/// `poll` may end up taking awhile, the work should be offloaded to a
/// thread pool (or something similar) to ensure that `poll` can return
/// quickly.
///
/// # Panics
///
/// Once a future has completed (returned `Ready` from `poll`), subsequent
/// calls to `poll` may panic, block forever, or otherwise cause bad behavior.
/// The `Future` trait itself provides no guarantees about the behavior of
/// `poll` after a future has completed.
fn poll(self: PinMut<Self>, cx: &mut task::Context) -> Poll<Self::Output>;
}
Most of the explanation here follows what we've already said about the task
system. The one twist is the use of PinMut
, which makes it possible to keep data
borrowed across separate calls to poll
(i.e., "borrowing over yield
points"). The mechanics of pinning are explained
in the RFC that introduced it,
and the interoperation with traditional futures is described next.
In many respects this RFC simply imports a minimal slice of the futures 0.2 API into libcore, and indeed an earlier iteration was explicitly just that. This incarnation, however, focuses squarely on the absolute minimal footprint needed to provide async/await support.
It's thus out of scope for this RFC to say precisely what the futures 0.3 APIs
will look like, but there are a few key aspects worth calling out. (Note that
over time, many of these APIs will be stabilized into std
.)
It's very common to work with Result
-producing futures, so the crate will
provide the following alias:
/// A convenience for futures that return `Result` values that includes
/// a variety of adapters tailored to such futures.
pub trait TryFuture {
/// The type of successful values yielded by this future
type Item;
/// The type of failures yielded by this future
type Error;
/// Poll this `TryFuture` as if it were a `Future`.
///
/// This method is a stopgap for a compiler limitation that prevents us from
/// directly inheriting from the `Future` trait; in the future it won't be
/// needed.
fn try_poll(self: PinMut<Self>, cx: &mut task::Context) -> Poll<Result<Self::Item, Self::Error>>;
}
impl<F, T, E> TryFuture for F
where F: Future<Output = Result<T, E>>
{
type Item = T;
type Error = E;
fn try_poll(self: PinMut<Self>, cx: &mut task::Context) -> Poll<F::Output> {
self.poll(cx)
}
}
This alias makes it easy to require that a future return a Result
(by bounding
by TryFuture
), and to obtain the success/error types (by using the Item
and
Error
associated types).
Similarly, PollResult<T, E>
is a type alias for Poll<Result<T, E>>
.
The crate will provide extension traits, FutureExt
and TryFutureExt
, with a
full complement of combinators like those provided in futures 0.2.
The FutureExt
trait will, in particular, provide a convenience for working
with Unpin
types without having to use the pin APIs:
trait FutureExt: Future {
fn poll_unpin(&mut self, cx: &mut task::Context) -> Poll<Self::Output>
where Self: Unpin { ... }
// ...
}
Not all async code will be written using async
/await
, and it's important to
retain a solid experience for manually implementing futures.
Compared to the futures 0.2 experience, the main changes in this RFC are (1) the
use of PinMut
and (2) no longer baking in an error type. In both cases, it's
straightforward, if a bit tedious to recover the previous programming model.
Here's an example drawn from tower-grpc:
// code written in futures 0.2 style
impl<T, U, B> Future for ResponseFuture<T, U>
where T: Message + Default,
U: Future<Item = Response<B>>,
B: Body<Data = Data>,
{
type Item = ::Response<Streaming<T, B>>;
type Error = ::Error<U::Error>;
fn poll(&mut self, cx: &mut task::Context) -> Poll<Self::Item, Self::Error> {
self.inner.poll(cx)
}
}
To port this code, we systematically employ a few steps:
- Implementing
Unpin
for the manual future, which opts out of interior borrowing. - Use
TryFuture
for all bounds over futures, and returnPollResult
inpoll
. - Call
poll_unpin
rather thanpoll
when polling inner futures.
All told, the ported code looks as follows:
// now ported to futures 0.3
impl<T, U> Unpin for ResponseFuture<T, U> {}
impl<T, U, B> Future for ResponseFuture<T, U>
where T: Message + Default,
U: TryFuture<Item = Response<B>> + Unpin,
B: Body<Data = Data>,
{
type Output = Result<::Response<Streaming<T, B>>, ::Error<U::Error>>;
fn poll(self: PinMut<Self>, cx: &mut task::Context) -> PollResult<Self::Item, Self::Error> {
self.inner.poll_unpin(cx)
}
}
These changes are mechanical enough that it's likely possible to write a script to perform them with high accuracy.
To be clear, however, there is a definite ergonomic hit here, which is discussed further in the drawbacks section.
The ultimate goal is to ship async/await as part of Rust 2018 (roughly by mid-September).
Much of the design has been vetted over quite a long period (futures 0.1), and the 0.2 changes have gotten substantial use in Google's Fuchsia OS. The task system in particular has existed in roughly the proposed shape for quite a long time in the futures crate, and has thus already been quite thoroughly vetted.
Thus the major new elements are, once more, the removal of built-in errors and
the use of PinMut
.
-
For built-in errors, the situation is straightforward: we can provide equivalent functionality at mild ergonomic cost (an extra import).
-
For
PinMut
, there's a clear way to "opt out" and recover the previous semantics (again at some ergonomic cost), so the primary questions come down to thePinMut
API itself. The core types have already received significant vetting within the RustBelt formal model. In addition to the opt-out, there's ongoing work in makingPinMut
ergonomic and safe to use directly (rather than only throughasync
notation).
Tactically, the proposal is to produce a 0.3-beta release, which will be nightly-only, and to work through support in as large a chunk of the ecosystem as we can manage, by adding feature flags to crates to opt in to the new version.
The APIs proposed above are almost entirely compatible with core
, except for a
couple of constructors that require std
objects:
- Constructing a
Waker
from anArc<T>: Wake
- Constructing a
TaskObj
from a future
These both have a similar shape: we have a concrete but opaque type (Waker
,
TaskObj
) that represents a trait object, but does not force a particular
representation for the trait object. In std
environments, you can largely
gloss over this point and just use Arc
or Box
respectively. But internally,
the Waker
and TaskObj
types are more abstract.
We'll look at the Waker
case in detail. The idea is to provide an UnsafeWake
trait which represents "an arbitrary Wake
-like trait object":
/// An unsafe trait for implementing custom memory management for a
/// `Waker`.
///
/// A `Waker` conceptually is a cloneable trait object for `Wake`, and is
/// most often essentially just `Arc<T>: Wake`. However, in some contexts
/// (particularly `no_std`), it's desirable to avoid `Arc` in favor of some
/// custom memory management strategy. This trait is designed to allow for such
/// customization.
///
/// A default implementation of the `UnsafeWake` trait is provided for the
/// `Arc` type in the standard library.
pub unsafe trait UnsafeWake {
/// Creates a new `Waker` from this instance of `UnsafeWake`.
///
/// This function will create a new uniquely owned handle that under the
/// hood references the same notification instance. In other words calls
/// to `wake` on the returned handle should be equivalent to calls to
/// `wake` on this handle.
///
/// # Unsafety
///
/// This function is unsafe to call because it's asserting the `UnsafeWake`
/// value is in a consistent state, i.e. hasn't been dropped.
unsafe fn clone_raw(self: *mut Self) -> Waker;
/// Drops this instance of `UnsafeWake`, deallocating resources
/// associated with it.
///
/// # Unsafety
///
/// This function is unsafe to call because it's asserting the `UnsafeWake`
/// value is in a consistent state, i.e. hasn't been dropped
unsafe fn drop_raw(self: *mut Self);
/// Indicates that the associated task is ready to make progress and should
/// be `poll`ed.
///
/// Executors generally maintain a queue of "ready" tasks; `wake` should place
/// the associated task onto this queue.
///
/// # Panics
///
/// Implementations should avoid panicking, but clients should also be prepared
/// for panics.
///
/// # Unsafety
///
/// This function is unsafe to call because it's asserting the `UnsafeWake`
/// value is in a consistent state, i.e. hasn't been dropped
unsafe fn wake(self: *mut self);
}
We then provide the following constructor for Waker
:
impl Waker {
pub unsafe fn new(inner: *const dyn UnsafeWake) -> Waker;
}
and a From<Arc<T>>
(where Arc<T>: Wake
) impl that uses it.
This RFC does not propose any implicit, built-in task-local storage. (Explicit storage is always possible).
Task-local storage is implementable on top of the proposed APIs by wrapping a task in a scoped use of thread-local storage. When polling the task, a thread-local value is established and hence usable implicitly within the call chain. But when returning -- which also happens when the task is blocked -- the thread-local is moved back out and stored with the task.
In the future, we anticipate adding "spawn hooks" for the Context::spawn
method, essentially allowing you to guarantee that tasks spawned within some
scope are wrapped in some way. That's a separately useful feature, but it can in
particular be used to implement inheritance of task-local data.
It may be that eventually we do want to build in some task-local data scheme, but:
- The no_std story is unclear.
- There are a lot of possible designs around things like typemaps and inheritance, and so it seems best for this work to begin in the ecosystem first.
This RFC is one of the most substantial additions to std
proposed since
1.0. It commits us to including a particular task and polling model in the
standard library, and ties us to PinMut
.
So far we've been able to push the task/polling model into virtually every niche Rust wishes to occupy, and the main downside has been, in essence, the lack of async/await syntax (and the borrowing it supports).
This RFC does not attempt to provide a complete introduction to the task model that originated with the futures crate. A fuller account of the design rationale and alternatives can be found in the following two blog posts:
To summarize, the main alternative model for futures is a callback-based approach, which was attempted for several months before the current approach was discovered. In our experience, the callback approach suffered from several drawbacks in Rust:
- It forced allocation almost everywhere, and hence was not compatible with no_std.
- It made cancellation extremely difficult to get right, whereas with the proposed model it's just "drop".
- Subjectively, the combinator code was quite hairy, while with the task-based model things fell into place quickly and easily.
Some additional context and rationale for the overall async/await project is available in the companion RFC.
For the remainder of this section, we'll dive into specific API design questions where this RFC differs from futures 0.2.
There are an assortment of reasons to drop the built-in error type in the main trait:
-
Improved type checking and inference. The error type is one of the biggest pain points when working with futures combinators today, both in trying to get different types to match up, and in inference failures that result when a piece of code cannot produce an error. To be clear, many of these problems will become less pronounced when
async
syntax is available. -
Async functions. If we retain a built-in error type, it's much less clear how
async fn
should work: should it always require the return type to be aResult
? If not, what happens when a non-Result
type is returned? -
Combinator clarity. Splitting up the combinators by whether they rely on errors or not clarifies the semantics. This is especially true for streams, where error handling is a common source of confusion.
-
Orthogonality. In general, producing and handling errors is separable from the core polling mechanism, so all things being equal, it seems good to follow Rust's general design principles and treat errors by composing with
Result
.
All of that said, there are real downsides for error-heavy code, even with
TryFuture
:
-
An extra import is needed (obviated if code imports the futures prelude, which we could perhaps more vocally encourage).
-
It can be confusing for code to bound by one trait but implement another.
The error handling piece of this RFC is separable from the other pieces, so the main alternative would be to retain the built-in error type.
Putting aside error handling, which is orthogonal and discussed above, the
primary other big item in this RFC is the move to PinMut
for the core polling
method, and how it relates to Unpin
/manually-written futures. Over the course
of RFC discussions, we've identified essentially three main approaches to this
question:
-
One core trait. That's the approach taken in the main RFC text: there's just a single core
Future
trait, which works onPinMut<Self>
. Separately there's apoll_unpin
helper for working withUnpin
futures in manual implementations. -
Two core traits. We can provide two traits, for example
MoveFuture
andFuture
, where one operates on&mut self
and the other onPinMut<Self>
. This makes it possible to continue writing code in the futures 0.2 style, i.e. without importingPinMut
/Unpin
or otherwise talking about pins. A critical requirement is the need for interoperation, so that aMoveFuture
can be used anywhere aFuture
is required. There are at least two ways to achieve such interop:-
Via a blanket impl of
Future
forT: MoveFuture
. This approach currently blocks some other desired impls (aroundBox
and&mut
specifically), but the problem doesn't appear to be fundamental. -
Via a subtrait relationship, so that
T: Future
is defined essentially as an alias forfor<'a> PinMut<'a, T>: MoveFuture
. Unfortunately, such "higher ranked" trait relationships don't currently work well in the trait system, and this approach also makes things more convoluted when implementingFuture
by hand, for relatively little gain.
-
The drawback of the "one core trait" approach taken by this RFC is its ergonomic
hit when writing moveable futures by hand: you now need to import PinMut
and
Unpin
, invoke poll_unpin
, and impl Unpin
for your types. This is all
pretty mechanical, but it's a pain. It's possible that improvements in PinMut
ergonomics will obviate some of these issues, but there are a lot of open
questions there still.
On the other hand, a two-trait approach has downsides as well. If we also
remove the error type, there's a combinatorial explosion, since we end up
needing Try
variants of each trait (and this extends to related traits, like
Stream
, as well). More broadly, with the one-trait approach, Unpin
acts as a
kind of "independent knob" that can be applied orthogonally from other concerns;
with the two-trait approach, it's "mixed in". And both of the two-trait
approaches run up against compiler limitations at the moment, though of course
that shouldn't be taken as a deciding factor.
The primary reason this RFC opts for the one-trait approach is that it's the
conservative, forward-compatible option. It's possible to add MoveFuture
,
together with a blanket impl, at any point in the future. Thus, starting with
just the single Future
trait as proposed in this RFC keeps our options
maximally open while we gain experience.
Rather than using the UnsafeWake
trait, we could factor "abstract Arc
-like
trait objects" out as a first-class concept, ArcObj
. We would also define an
ArcLike
trait to determine what concrete types can fit into it:
// An `Arc`-like trait object for a trait `T`
//
// Implements `Send`, `Sync` and `Clone`
struct ArcObj<T: ?Sized> {
inner: *mut T,
// a manually-constructed vtable for the Arc-like methods
drop_fn: unsafe fn(*mut T),
clone_fn: unsafe fn(*mut T) -> ArcObj<T>,
}
unsafe impl<T: ?Sized + Send + Sync> Send for ArcObj<T> {}
unsafe impl<T: ?Sized + Send + Sync> Sync for ArcObj<T> {}
impl<T: ?Sized> Deref for ArcObj<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.inner }
}
}
// An object that can be used like `Arc<T>`
unsafe trait ArcLike<T: ?Sized>: Send + Sync {
fn into_raw(self) -> *mut T;
unsafe fn drop_fn(*mut T);
unsafe fn clone_fn(*mut T) -> ArcObj<T>;
}
unsafe impl<T: ?Sized + Send + Sync> ArcLike<T> for Arc<T> {
fn into_raw(self) -> *mut T {
Arc::into_raw(self) as *mut T
}
unsafe fn drop_fn(t: *mut T) {
drop(Arc::from_raw(t));
}
unsafe fn clone_fn(t: *mut T) -> ArcObj<T> {
let val: Arc<T> = Arc::from_raw(t);
let cloned = val.clone();
mem::forget(val);
ArcObj::new(cloned)
}
}
impl<T: ?Sized> ArcObj<T> {
fn new<U: ArcLike<T>>(u: U) -> ArcObj<T> {
ArcObj {
inner: u.into_raw(),
drop_fn: U::drop_fn,
clone_fn: U::clone_fn,
}
}
}
impl<T: ?Sized> Clone for ArcObj<T> {
fn clone(&self) -> ArcObj<T> {
unsafe {
(self.clone_fn)(self.inner)
}
}
}
impl<T: ?Sized> Drop for ArcObj<T> {
fn drop(&mut self) {
unsafe {
(self.drop_fn)(self.inner)
}
}
}
With this setup, we can define Waker
as:
struct Waker {
obj: ArcObj<dyn Wake>,
}
and allow construction from any ArcObj<dyn Wake>
, rather than just
Arc<dyn Wake>
, without using UnsafeWake
.
However, this would involve ArcObj
appearing in multiple places throughout the
API, rather than sequestering the niche case into just the UnsafeWake
trait as
this RFC proposes.
There is substantial prior art both with async/await notation and with futures (aka promises) as a basis. The proposed futures API was influenced by Scala's futures in particular, and is broadly similar to APIs in a variety of other languages (in terms of the adapters provided).
What's more unique about the model in this RFC is the use of tasks, rather than callbacks. The RFC author is not aware of other futures libraries using this technique, but it is a fairly well-known technique more generally in functional programming. For a recent example, see this paper on parallelism in Haskell. What seems to be perhaps new with this RFC is the idea of melding the "trampoline" technique with an explicit, open-ended task/wakeup model.
None at the moment.