Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I execute an async/await function without using any external dependencies?

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.
}
  • I know async/await is still a nightly feature, and it is subject to change in the foreseeable future.
  • I know there is a whole lot of Future implementations, I am aware of the existence of tokio.
  • I am just trying to educate myself on the inner workings of standard library futures.

My helpless, clumsy endeavours

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 Pinned 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.

like image 903
Matteo Monti Avatar asked Jan 26 '23 06:01

Matteo Monti


1 Answers

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 Pinned 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.

like image 184
Shepmaster Avatar answered Jan 29 '23 08:01

Shepmaster