No description provided.
The goal of the waker-waiter crate is to implement [context reactor hook](https://jblog.andbit.net/2022/12/28/context-reactor-hook/) to enable async executor/reactor interoperability, for later inclusion in std. However, ideally we could reach community consensus on the API before beginning such an inclusion process. Please weigh in here! ## Goals * Provide a way for an executor to run special waiting/parking logic for when its managed futures have returned `Poll::Pending`, where such logic is dynamically supplied by the futures to the executor via `Context`. * Enable async runtimes to separate their executor and reactor parts, to allow developers to mix them. For example, using the executor from smol to run tasks containing network objects from tokio. * Enable the possibility of a universal `block_on` function able to run any future. Such a function could, for example, execute tasks containing network objects from tokio, without being aware of tokio and without any prior configuration. * Enable the possibility of a universal `async fn main`, which could simply wrap `block_on`. ## TopLevelPoller I propose introducing a `TopLevelPoller` trait, for which some kind of waitability mechanism (here a `WakerWaiter`, defined later) can be applied: ```rust pub trait TopLevelPoller { fn set_waiter(&mut self, waiter: &WakerWaiter) -> Result<(), SetWaiterError>; } ``` A value implementing this trait would then be exposed to futures via `Context`, with an API like: ```rust impl<'a> Context<'a> { // add a poller to the context pub fn with_top_level_poller(self, poller: &mut dyn TopLevelPoller) -> Self { // ... } // get the poller, if any pub fn top_level_poller(&mut self) -> Option<&mut dyn TopLevelPoller> { // ... } } ``` Outside of std, this could be simulated via an extension: ```rust pub trait ContextExt<'a> { fn with_top_level_poller(self, poller: &mut dyn TopLevelPoller) -> Self; fn top_level_poller(&mut self) -> Option<&mut dyn TopLevelPoller>; } impl<'a> ContextExt<'a> for Context<'a> { fn with_top_level_poller(self, poller: &mut dyn TopLevelPoller) -> Self { // ... } fn top_level_poller(&mut self) -> Option<&mut dyn TopLevelPoller> { // ... } } ``` This way, users could write `cx.top_level_poller()`, for example, as if the method existed on `Context` (under the hood, this is achievable using the nightly [waker_getters](https://github.com/rust-lang/rust/issues/96992) feature). Specifically, I propose the extension contain the following methods: ```rust pub trait ContextExt<'a> { fn with_top_level_poller<'b: 'a>( self, poller: &'b mut dyn TopLevelPoller, scratch: &'a mut MaybeUninit<Waker>, ) -> Self; fn top_level_poller(&mut self) -> Option<&mut dyn TopLevelPoller>; fn with_waker<'b>(&'b mut self, waker: &'b Waker) -> Context<'b>; } ``` The `with_top_level_poller` method definition is slightly different than how we'd want it in std, due to the need for the `scratch` argument. This is needed because the method internally creates a special `Waker` (capable of holding the `Context`'s original `Waker`, plus the `TopLevelPoller`) to be borrowed by the returned `Context`. Usage looks like this: ```rust let mut scratch = MaybeUninit::uninit(); let mut cx = Context::from_waker(&waker).with_top_level_poller(&mut poller, &mut scratch); ``` The `top_level_poller` method checks the waker vtable to see if it's our special waker, in order to find the poller and return it. The `with_waker` method provides a way to create a new context that inherits the poller of an existing context, in case a new context needs to be made with a different waker. For example, a "select" implementation may want to provide individual wakers to each future it manages, and it would need a way to do this without the poller getting lost. Note: The [LocalWaker proposal](https://github.com/rust-lang/libs-team/issues/191) suggests introducing `ContextBuilder`, for constructing a context out of parts. If that lands, we may want to change how the inheritance works, for example by doing it through the builder with some method like `ContextBuilder::from(cx: &mut Context)`. A caveat of wrapping the original waker with our special waker is `Waker::will_wake` won't return true when the inner waker would match. Technically this is legal, as the method is best-effort. However, ideally this kind of check would be possible, which could be done with separate method: ```rust pub trait WakerExt { fn will_wake2(&self, other: &Waker) -> bool; } impl WakerExt for Waker { fn will_wake2(&self, other: &Waker) -> bool { // ... if self is our special waker, then call inner.will_wake(other), else self.will_wake(other) ... } } ``` ## WakerWaiter To support no_std environments, the `WakerWaiter` API uses a raw vtable and safe wrapper struct, similar to `Waker`: ```rust pub struct RawWaiterVTable { clone: unsafe fn(*const ()) -> RawWaiter, wait: unsafe fn(*const ()), canceler: unsafe fn(*const ()) -> Option<RawCanceler>, drop: unsafe fn(*const ()), } pub struct RawWaiter { data: *const (), vtable: &'static RawWaiterVTable, } pub struct WakerWaiter { inner: RawWaiter, } impl WakerWaiter { pub fn wait(&self) { unsafe { (self.inner.vtable.wait)(self.inner.data) } } pub fn canceler(&self) -> WakerWaiterCanceler { let raw = unsafe { (self.inner.vtable.canceler)(self.inner.data).unwrap() }; WakerWaiterCanceler { inner: raw } } } impl Clone for WakerWaiter { fn clone(&self) -> Self { Self { inner: unsafe { (self.inner.vtable.clone)(self.inner.data) }, } } } impl Drop for WakerWaiter { fn drop(&mut self) { unsafe { (self.inner.vtable.drop)(self.inner.data) } } } unsafe impl Send for WakerWaiter {} unsafe impl Sync for WakerWaiter {} ``` As a convenience, we can provide an Arc-based trait, similar to [std::task::Wake](https://doc.rust-lang.org/std/task/trait.Wake.html): ```rust pub trait WakerWait { fn wait(self: &Arc<Self>); fn canceler(self: &Arc<Self>) -> WakerWaiterCanceler; } impl<W: WakerWait + Send + Sync + 'static> From<Arc<W>> for WakerWaiter { fn from(waiter: Arc<W>) -> Self { // ... } } ``` This `WakerWaiter` API enables a future to provide the necessary functions to block and wait for wakers to be triggered, and to unblock such waiting via a "canceler" object. ## Local waiters To support single-threaded reactors, there should be an alternative to `WakerWaiter` that is `!Send`. Call it `LocalWakerWaiter`. Single-threaded executors would work with either `WakerWaiter` or `LocalWakerWaiter`, but multi-threaded executors would only work with `WakerWaiter`. `TopLevelPoller` could then have two setters: ```rust pub trait TopLevelPoller { fn set_waiter(&mut self, waiter: &WakerWaiter) -> Result<(), SetWaiterError>; fn set_local_waiter(&mut self, waiter: &LocalWakerWaiter) -> Result<(), SetLocalWaiterError>; } ``` Only one waiter would be settable at a time though. It wouldn't make any sense to provide multiple waiters to the same poller. ## Cancellation To cancel a wait, the poller can obtain a `WakerWaiterCanceler` from the `WakerWaiter` in advance, and use it to cancel a wait. The canceler would contain a single method: ```rust impl WakerWaiterCanceler { pub fn cancel(&self) { ... } } ``` The reason for having cancellation go through a separate object, as opposed to offering a `cancel()` method directly on `WakerWaiter` and `LocalWakerWaiter`, is to allow the objects to have different `Send`-ness. Notably, `WakerWaiterCanceler` must be `Send` (cancellation must always happen from a separate thread), and separating it out allows `LocalWakerWaiter` to be `!Send`. That said, `LocalWakerWaiter` also shouldn't be required to offer a canceler, since implementing cancellation relies on thread synchronization. To allow for that, its API should deviate slightly from `WakerWaiter` by having its `canceler()` method return an `Option` instead. ## Design notes * The waiting mechanism (here `WakerWaiter`) is not applied directly to the `Context`, and instead it is applied to a new entity (`TopLevelPoller`), in order to add clarity that the mechanism transcends the context. This is especially important considering there could be levels of contexts. In other words, the mechanism is not *applied to* a context, but rather the nearest context can be used to *access an entity* for which the mechanism can be applied. A related benefit of this separation is`TopLevelPoller`'s API could be expanded later on without expanding `Context`. I might even go as far to suggest that other context extensions ought to work similarly (e.g. `Context` offering a `Spawner` object instead of directly offering a `spawn` method). * The names `TopLevelPoller` and `WakerWaiter` are given in the spirit of minimizing interface surface area. There may be alternative workable names, like `RootPoller`. However, the names `Executor` and `Reactor` are purposely avoided, as they imply a lot more than what is needed by this API. * Applying a waiter to the poller (or determining if one should be applied) should be very cheap, similar to what `Waker` enables with [Waker::will_wake](https://doc.rust-lang.org/std/task/struct.Waker.html#method.will_wake). This could be done by having `WakerWaiter::eq` compare the inner pointers/vtables.
This issue appears to be discussing a feature request or bug report related to the repository. Based on the content, it seems to be still under discussion. The issue was opened by jkarneges and has received 0 comments.