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

1

9

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.

Retouch answered 22/5, 2019 at 8:48 Comment(0)
B
11

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.

Begrime answered 22/5, 2019 at 14:8 Comment(4)
Amazing! Minor question: what's still in nightly is the async/await feature (hence the #![feature(async_await)]), but the std::Future interface is now stable, right?Retouch
@MatteoMonti kind of? Future has been stabilized in the nightly compiler, but that stabilization has not yet made it to the beta channel. It definitely hasn't made it to any released Rust version.Begrime
@MatteoMonti Future will be stabilized (in 1.36.0), but not in the current stable (1.34.0). You need the feature fo async fn hellow.Acetyl
this is exactly what we needed to use an async function in a sync FFI. thanks!Photoelectric

© 2022 - 2024 — McMap. All rights reserved.