Curious if there's a nicer way to (ab)use async as co-routines without Box

⚓ Rust    📅 2025-09-17    👤 surdeus    👁️ 1      

surdeus

I'm trying to figure out if I can simplify my emulator logic by representing each bit of hardware with an async function that can just cycles(3).await rather than building a state machine.

Messing around a bit it's easy enough to do with Box::pin() and I expect I'll end up just using that, but I was wondering how far I could push it without any indirection at all, and I came up with this, err, gem:

#![feature(type_alias_impl_trait)]

use std::cell::Cell;
use std::future::Future;
use std::pin::{pin, Pin};
use std::time::Duration;

fn main() {
    let mut vm = pin!(Vm::new());

    loop {
        vm.as_mut().step();
        std::thread::sleep(Duration::from_millis(500));
    }
}

pin_project_lite::pin_project! {
    struct Vm {
        cycles: u32,
        #[pin]
        foo: Foo,
        #[pin]
        bar: Bar,
    }
}

impl Vm {
    fn new() -> Self {
        Self {
            cycles: 0,
            foo: foo(),
            bar: bar(),
        }
    }

    fn step(self: Pin<&mut Self>) {
        let mut this = self.project();
        *this.cycles = this.cycles.wrapping_add(1);
        CYCLES.set(*this.cycles);

        let waker = std::task::Waker::noop();
        let mut cx = std::task::Context::from_waker(waker);
        _ = this.foo.as_mut().poll(&mut cx);
        _ = this.bar.as_mut().poll(&mut cx);
    }
}

thread_local! {
    static CYCLES: Cell<u32> = const { Cell::new(0) };
}

async fn cycles(count: u32) {
    let deadline = CYCLES.get().wrapping_add(count);
    let fut = std::future::poll_fn(move |_cx| {
        if CYCLES.get() < deadline {
            std::task::Poll::Pending
        } else {
            std::task::Poll::Ready(())
        }
    });
    fut.await
}

type Foo = impl Future;

#[define_opaque(Foo)]
fn foo() -> Foo {
    async {
        loop {
            cycles(3).await;
            println!("foo");
        }
    }
}

type Bar = impl Future;

#[define_opaque(Bar)]
fn bar() -> Bar {
    async {
        loop {
            cycles(5).await;
            println!("bar");
        }
    }
}

(Playground)

Is this about where the state of the art is here, am I missing any nicer ways to do this, or should I be looking into other nightly features (generators?) if I'm trying to pull this?

1 post - 1 participant

Read full topic

🏷️ Rust_feed