I am attempting to create simplest possible example that can get async fn hello()
to eventually print out Hello World!
. This should happen without any external dependency like tokio
, just plain Rust and std
. Bonus points if we can get it done without ever using unsafe
.
#![feature(async_await)]
async fn hello() {
println!("Hello, World!");
}
fn main() {
let task = hello();
// Something beautiful happens here, and `Hello, World!` is printed on screen.
}
async/await
is still a nightly feature, and it is subject to change in the foreseeable future.Future
implementations, I am aware of the existence of tokio
.My vague understanding is that, first off, I need to Pin
task down. So I went ahead and
let pinned_task = Pin::new(&mut task);
but
the trait `std::marker::Unpin` is not implemented for `std::future::GenFuture<[static generator@src/main.rs:7:18: 9:2 {}]>`
so I thought, of course, I probably need to Box
it, so I'm sure it won't move around in memory. Somewhat surprisingly, I get the same error.
What I could get so far is
let pinned_task = unsafe {
Pin::new_unchecked(&mut task)
};
which is obviously not something I should do. Even so, let's say I got my hands on the Pin
ned Future
. Now I need to poll()
it somehow. For that, I need a Waker
.
So I tried to look around on how to get my hands on a Waker
. On the doc it kinda looks like the only way to get a Waker
is with another new_unchecked
that accepts a RawWaker
. From there I got here and from there here, where I just curled up on the floor and started crying.
This part of the futures stack is not intended to be implemented by many people. The rough estimate that I have seen in that maybe there will be 10 or so actual implementations.
That said, you can fill in the basic aspects of an executor that is extremely limited by following the function signatures needed:
async fn hello() {
println!("Hello, World!");
}
fn main() {
drive_to_completion(hello());
}
use std::{
future::Future,
ptr,
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
};
fn drive_to_completion<F>(f: F) -> F::Output
where
F: Future,
{
let waker = my_waker();
let mut context = Context::from_waker(&waker);
let mut t = Box::pin(f);
let t = t.as_mut();
loop {
match t.poll(&mut context) {
Poll::Ready(v) => return v,
Poll::Pending => panic!("This executor does not support futures that are not ready"),
}
}
}
type WakerData = *const ();
unsafe fn clone(_: WakerData) -> RawWaker {
my_raw_waker()
}
unsafe fn wake(_: WakerData) {}
unsafe fn wake_by_ref(_: WakerData) {}
unsafe fn drop(_: WakerData) {}
static MY_VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
fn my_raw_waker() -> RawWaker {
RawWaker::new(ptr::null(), &MY_VTABLE)
}
fn my_waker() -> Waker {
unsafe { Waker::from_raw(my_raw_waker()) }
}
Starting at Future::poll
, we see we need a Pin
ned future and a Context
. Context
is created from a Waker
which needs a RawWaker
. A RawWaker
needs a RawWakerVTable
. We create all of those pieces in the simplest possible ways:
Since we aren't trying to support NotReady
cases, we never need to actually do anything for that case and can instead panic. This also means that the implementations of wake
can be no-ops.
Since we aren't trying to be efficient, we don't need to store any data for our waker, so clone
and drop
can basically be no-ops as well.
The easiest way to pin the future is to Box
it, but this isn't the most efficient possibility.
If you wanted to support NotReady
, the simplest extension is to have a busy loop, polling forever. A slightly more efficient solution is to have a global variable that indicates that someone has called wake
and block on that becoming true.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With