wasi-rs icon indicating copy to clipboard operation
wasi-rs copied to clipboard

Improve Rust ergonomics for wasip3 async main functions

Open wingo opened this issue 4 months ago • 3 comments

It would be nice to write this kind of program:

extern crate wasi;

async fn main() {
    let one_millisecond = 1_000_000u64;
    wasi::clocks::monotonic_clock::wait_for(one_millisecond).await
}

However, there are a number of things that get in the way. Right now you need something like:

extern crate wit_bindgen;

wit_bindgen::generate!({
    inline: r"
  package test:test;

  world test {
      include wasi:clocks/[email protected];
      include wasi:cli/[email protected];
  }
",
    // Work around https://github.com/bytecodealliance/wasm-tools/issues/2285.
    features:["clocks-timezone"],
    async: [
        "wasi:cli/[email protected]#run",
    ],
    generate_all
});

struct Component;
export!(Component);

impl exports::wasi::cli::run::Guest for Component {
    async fn run() -> Result<(), ()> {
        wasi::clocks::monotonic_clock::wait_for(one_millisecond).await
        Ok(())
    }
}

fn main() { unreachable!("main is a stub"); }

Which:

  1. It's weird to have the stub main function
  2. It would be nice to have the same impl std::process::Termination setup for run so we don't have to OK(())
  3. Would be nice to have the wasi::cli::run::Guest implementation produced automatically and just call the async fn main()
  4. Would be nice to avoid having to explicitly declare wasi:cli/run as async when defining async fn main()

Alex mentioned that something like this might be possible, and it seems 95% of the way there:

#[wit_bindgen::async]
async fn main() {}

wingo avatar Aug 26 '25 07:08 wingo

I've transferred this issue to the wasi-rs repository as this is where the implementation would live, if any.

alexcrichton avatar Aug 26 '25 14:08 alexcrichton

As of now this is the current baseline:

wasip3::cli::command::export!(Example);

struct Example;

impl wasip3::exports::cli::run::Guest for Example {
    async fn run() -> Result<(), ()> {
        let (mut tx, rx) = wasip3::wit_stream::new();
        wasip3::cli::stdout::set_stdout(rx);

        let remaining = tx.write_all(b"Hello, WASI!".to_vec()).await;
        assert!(remaining.is_empty());
        Ok(())
    }
}

I still think it would be worthwhile to try to change that to:

#[wasip3::main]
async fn main() {
    let (mut tx, rx) = wasip3::wit_stream::new();
    wasip3::cli::stdout::set_stdout(rx);

    let remaining = tx.write_all(b"Hello, WASI!".to_vec()).await;
    assert!(remaining.is_empty());
}

alexcrichton avatar Sep 09 '25 20:09 alexcrichton

Attribute macro_rules appears to be moving towards stabilization https://github.com/rust-lang/rust/issues/143547, with which we could probably manage #[wasip3::cli::run::export] async fn main() {...} for exported interfaces that have just one function. I'm assuming we can't use a proc macro because it has to be possible to use the implementation created by bindgen in the same crate it is defined.

pchickey avatar Sep 09 '25 22:09 pchickey