Stream: wasmtime

Topic: ✔ How do I get stdout and stderr while my program is runn...


view this post on Zulip Pat Hickey (May 15 2026 at 23:18):

Yes, there are ways to send stdout/stderr back async. You're currently making an instance of https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/p2/pipe/struct.MemoryOutputPipe.html to collect output from those - but the WasiCtxBuilder will accept anything that impls StdoutStream https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/struct.WasiCtxBuilder.html#method.stdout

view this post on Zulip Pat Hickey (May 15 2026 at 23:18):

https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/cli/trait.StdoutStream.html

view this post on Zulip Pat Hickey (May 15 2026 at 23:19):

so basically, you need to create your own type that can impl the StdoutStream trait method
fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync>; and its implementation ensures all of the writes to that AsyncWrite in there get forwarded to wherever you need, as soon as it possibly can

view this post on Zulip Srayan Jana (May 15 2026 at 23:37):

Pat Hickey said:

so basically, you need to create your own type that can impl the StdoutStream trait method
fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync>; and its implementation ensures all of the writes to that AsyncWrite in there get forwarded to wherever you need, as soon as it possibly can

Oo okay, ill try that out, thanks!
Also, is the setup I did with the wasmfile with the duration based busy loop work on wasm, or will wasi not work because I used the std api?

view this post on Zulip Srayan Jana (May 15 2026 at 23:42):

a wasm file that looks like this gives me an error

#![allow(unused)]

/// Component off of which implementation will hang (this can be named anything)
/// Has to be above bindings to get generated properly
struct Component;

mod bindings {
    use super::Component;

    wit_bindgen::generate!();

    export!(Component);
}

/// Implementation for the `greet` interface export
impl bindings::exports::example::runnable::greet::Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello {name}!")
    }
}

/// Implementation for `wasi:cli/run` interface export
impl bindings::exports::wasi::cli::run::Guest for Component {
    fn run() -> Result<(), ()> {
        for i in 0..100000 {
            eprintln!("Hello World! {i}");
        }
        Ok(())
    }
}
Error: error while executing at wasm backtrace:
    0:   0x8511 - runnable_example.wasm!abort
    1:   0x5e11 - runnable_example.wasm!std[d123b7a1a2ed1472]::sys::pal::wasi::helpers::abort_internal
    2:   0x4eb8 - runnable_example.wasm!std[d123b7a1a2ed1472]::process::abort
    3:   0x548a - runnable_example.wasm!__rustc[b7974e8690430dd9]::__rust_abort
    4:   0x3f3b - runnable_example.wasm!__rustc[b7974e8690430dd9]::__rust_start_panic
    5:   0x52b7 - runnable_example.wasm!__rustc[b7974e8690430dd9]::rust_panic
    6:   0x472c - runnable_example.wasm!std[d123b7a1a2ed1472]::panicking::panic_with_hook
    7:   0x434d - runnable_example.wasm!std[d123b7a1a2ed1472]::panicking::panic_handler::{closure#0}
    8:   0x429f - runnable_example.wasm!std[d123b7a1a2ed1472]::sys::backtrace::__rust_end_short_backtrace::<std[d123b7a1a2ed1472]::panicking::panic_handler::{closure#0}, !>
    9:   0x556d - runnable_example.wasm!__rustc[b7974e8690430dd9]::rust_begin_unwind
   10:   0xd036 - runnable_example.wasm!core[6d9e98711c484d2e]::panicking::panic_fmt
   11:   0x6564 - runnable_example.wasm!std[d123b7a1a2ed1472]::io::stdio::_eprint
   12:   0x2a03 - runnable_example.wasm!<runnable_example::Component as runnable_example::bindings::exports::wasi::cli::run::Guest>::run::h66398bf34cfb364d
   13:   0x22a8 - runnable_example.wasm!runnable_example::bindings::exports::wasi::cli::run::_export_run_cabi::h0f8046e4555532f1
   14:   0x3717 - runnable_example.wasm!wasi:cli/run@0.2.7#run
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging informatio

view this post on Zulip Srayan Jana (May 15 2026 at 23:44):

Ah nevermind, I think the problem was I made my MemoryOutputPipe too small lol

view this post on Zulip Srayan Jana (May 15 2026 at 23:52):

now i guess I just need to implement async read and async write over a channel

view this post on Zulip Victor Adossi (May 16 2026 at 00:47):

Hey @Srayan Jana one great place to search is actually the wasmtime codebase itself (and the tests therein, especially!)

https://github.com/search?q=repo%3Abytecodealliance%2Fwasmtime%20impl%20stdoutstream&type=code

But to jump to the chase, on P2 you actually have a builtin that does this which you might want to look at:

https://docs.rs/wasmtime-wasi/44.0.1/wasmtime_wasi/p2/pipe/struct.MemoryOutputPipe.html
(needless to say, MemoryOutputPipe implements StdoutStream)

view this post on Zulip Srayan Jana (May 16 2026 at 00:48):

Victor Adossi said:

But to jump to the chase, on P2 you actually have a builtin that does this which you might want to look at:

https://docs.rs/wasmtime-wasi/44.0.1/wasmtime_wasi/p2/pipe/struct.MemoryOutputPipe.html
(needless to say, MemoryOutputPipe implements StdoutStream)

no i was using MemoryOutputPIpe, but I couldnt get it working

I wasn't really sure how to read out of the MemoryOutputPipe as I'm getting stderr and stdout coming in

view this post on Zulip Victor Adossi (May 16 2026 at 00:49):

Also, I just realized Pat said all this!

view this post on Zulip Srayan Jana (May 16 2026 at 00:49):

Like, you can write to a MemoryOutputPipe, but you can't read from a MemoryOutputPIpe

view this post on Zulip Srayan Jana (May 16 2026 at 00:50):

maybe this is an api oversight?

view this post on Zulip Victor Adossi (May 16 2026 at 00:51):

Have you considered creating a cursor on the output Bytes ?

view this post on Zulip Victor Adossi (May 16 2026 at 00:52):

Just off the top of my head I'd assume that's the idiomatic way to do it -- You have access to the contents via MemoryOutputPipe::contents

view this post on Zulip Srayan Jana (May 16 2026 at 00:52):

i didn't even know you could do that, does that work concurrently?

view this post on Zulip Victor Adossi (May 16 2026 at 00:52):

Well considering the Bytes are just bytes, I don't see why not -- the write is concurrent and the bytes will change but it's more up to how you read, right?

view this post on Zulip Victor Adossi (May 16 2026 at 00:55):

I assume the standard stuff applies here -- if the output is newline delimited then you may need to buffer some, etc.

Just like when reading stdout in a traditional UNIX environment -- the pipe may be written to at any time/you have to decide how to buffer/interpret.

Maybe a BufReader is what you want here, depending on your use case. there's also bytes::buf::Reader

view this post on Zulip Victor Adossi (May 16 2026 at 00:57):

You can find exmaples where simple tests will just use the contents directly, but AFAIK contents() is your way in and you can use all the Rust idioms available to you to go from there :)

view this post on Zulip Srayan Jana (May 16 2026 at 01:43):

@Victor Adossi i figured out my problem.

wasmtime doesn't return the results of the code you call until it gets the Ok() back from the WASI component. Only then does it print out everything to the stdout/stderr you hooked up to it

view this post on Zulip Srayan Jana (May 16 2026 at 01:44):

this is a big problem for me, is there a way to work around this limitation?

view this post on Zulip Srayan Jana (May 16 2026 at 01:47):

actually wait hold on that might not be the case

view this post on Zulip Srayan Jana (May 16 2026 at 01:57):

okay no, its just that if you have a REALLY big loop, wasmtime takes a while to start up, (so like a while loop with 1000000 elements)

view this post on Zulip Srayan Jana (May 16 2026 at 03:45):

if anyone is curious, this is what my code looks like now

Client

use tokio::{
    fs::{self, File},
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpStream,
};

const BYTES_READ_AMOUNT: usize = 4096;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // send file to server
    let mut stream = TcpStream::connect("127.0.0.1:3451").await?;
    let wasm_file_path = std::env::args().nth(1).expect("No file path given");
    println!("Sending file {wasm_file_path}");
    let wasm_file_metadata = fs::metadata(wasm_file_path.clone()).await?;
    let wasm_file_len = wasm_file_metadata.len();
    if wasm_file_len == 0 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("input wasm file is empty: {wasm_file_path}"),
        )
        .into());
    }
    let message_len = u32::try_from(wasm_file_len)
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "file too long"))?;
    // we use big endian for both server and client
    stream.write_all(&message_len.to_be_bytes()).await?;
    let mut wasm_file = File::open(wasm_file_path).await?;
    tokio::io::copy(&mut wasm_file, &mut stream).await?;
    println!("File sent!");
    // Print streamed output as it arrives instead of waiting for EOF.
    let mut buf = vec![0_u8; BYTES_READ_AMOUNT];
    loop {
        let n = stream.read(&mut buf).await?;
        if n == 0 {
            break;
        }
        print_bytes::print_lossy(&buf[..n]);
    }
    Ok(())
}

wasm file

#![allow(unused)]

/// Component off of which implementation will hang (this can be named anything)
/// Has to be above bindings to get generated properly
struct Component;

mod bindings {
    use super::Component;

    wit_bindgen::generate!();

    export!(Component);
}

/// Implementation for the `greet` interface export
impl bindings::exports::example::runnable::greet::Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello {name}!")
    }
}

/// Implementation for `wasi:cli/run` interface export
impl bindings::exports::wasi::cli::run::Guest for Component {
    fn run() -> Result<(), ()> {
        let mut i = 0;
        let sleep_duration = std::time::Duration::from_secs(1);
        loop {
            eprintln!("Loop number {i}");
            i += 1;
            if i % 2 == 0 {
                let square = i * i;
                println!("{i} squared is {square}");
            }
            std::thread::sleep(sleep_duration);
        }
        Ok(())
    }
}

view this post on Zulip Srayan Jana (May 16 2026 at 03:46):

server

//! Example of instantiating a wasm module which uses WASI preview1 imports
//! implemented through the async preview2 WASI implementation.
/*
You can execute this example with:
    cmake examples/
    cargo run --example wasip2-async
*/
use bytes::Bytes;
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::watch;
use tokio::task::JoinSet;
use tokio::time::{Duration, sleep};
use tokio_util::sync::CancellationToken;
use wasmtime::component::{Component, Linker, ResourceTable};
use wasmtime::*;
use wasmtime_wasi::p2::bindings::Command;
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView};
pub struct ComponentRunStates {
    // These two are required basically as a standard way to enable the impl of IoView and
    // WasiView.
    // impl of WasiView is required by [`wasmtime_wasi::p2::add_to_linker_sync`]
    pub wasi_ctx: WasiCtx,
    pub resource_table: ResourceTable,
    // You can add other custom host states if needed
}
impl WasiView for ComponentRunStates {
    fn ctx(&mut self) -> WasiCtxView<'_> {
        WasiCtxView {
            ctx: &mut self.wasi_ctx,
            table: &mut self.resource_table,
        }
    }
}
async fn send_cursor_buffer_to_steam(
    cursor: &mut usize,
    buffer: Bytes,
    stream: &mut TcpStream,
) -> Result<()> {
    if *cursor < buffer.len() {
        let chunk = &buffer[*cursor..];
        stream.write_all(chunk).await?;
        // print to server stdout for now
        print_bytes::println_lossy(chunk);
        *cursor = buffer.len();
    }
    Ok(())
}
async fn handle_wasm_request(
    mut stream: TcpStream,
    engine: Engine,
    linker: Linker<ComponentRunStates>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // size of u32 is four u8
    let mut incoming_message_length = [0_u8; 4];
    stream.read_exact(&mut incoming_message_length).await?;
    let message_length = u32::from_be_bytes(incoming_message_length) as usize;
    if message_length == 0 {
        return Err("received empty wasm payload from client, ignoring".into());
    }
    let mut buffer = vec![0_u8; message_length];
    stream.read_exact(&mut buffer).await?;
    // Create a WASI context and put it in a Store; all instances in the store
    // share this context. `WasiCtx` provides a number of ways to
    // configure what the target program will have access to.
    let stdout = MemoryOutputPipe::new(usize::MAX);
    let stderr = MemoryOutputPipe::new(usize::MAX);
    let wasi = WasiCtx::builder()
        .stdout(stdout.clone())
        .stderr(stderr.clone())
        .build();
    let state = ComponentRunStates {
        wasi_ctx: wasi,
        resource_table: ResourceTable::new(),
    };
    let mut store = Store::new(&engine, state);
    let component = Component::from_binary(&engine, &buffer)?;
    let command = Command::instantiate_async(&mut store, &component, &linker).await?;
    let token = CancellationToken::new();
    let token_clone = token.clone();
    let print_to_stream = tokio::spawn(async move {
        // Cursor offsets into each growing MemoryOutputPipe buffer.
        let mut stdout_cursor = 0usize;
        let mut stderr_cursor = 0usize;
        let mut stream = stream;
        let token = token_clone;
        loop {
            if let Err(e) =
                send_cursor_buffer_to_steam(&mut stdout_cursor, stdout.contents(), &mut stream)
                    .await
            {
                eprintln!("Error from stdout: {e}");
                break;
            }
            if let Err(e) =
                send_cursor_buffer_to_steam(&mut stderr_cursor, stderr.contents(), &mut stream)
                    .await
            {
                eprintln!("Error from stderr: {e}");
                break;
            }
            tokio::select! {
                _ = token.cancelled() => {
                    break;
                }
                _ = sleep(Duration::from_millis(10)) => {}
            }
        }
        // Final drain to flush any bytes written between last poll and termination.
        println!("Leftovers, stdout");
        let _ =
            send_cursor_buffer_to_steam(&mut stdout_cursor, stdout.contents(), &mut stream).await;
        println!("Leftovers, stderr");
        let _ =
            send_cursor_buffer_to_steam(&mut stderr_cursor, stderr.contents(), &mut stream).await;
        println!("That's everything!");
    });
    let program_result = command.wasi_cli_run().call_run(&mut store).await?;
    token.cancel();
    let _ = print_to_stream.await;
    println!("Program finished!");
    match program_result {
        Ok(_) => Ok(()),
        Err(()) => Err("Program somehow failed".into()),
    }
}
#[tokio::main]
async fn main() -> Result<()> {
    let engine = Engine::default();
    let mut linker: Linker<ComponentRunStates> = Linker::new(&engine);
    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
    let addr = "0.0.0.0:3451";
    let listener = TcpListener::bind(addr).await?;
    println!("Server starting at {addr}");
    let mut join_set = JoinSet::new();
    loop {
        let (stream, client_addr) = listener.accept().await?;
        println!("incoming wasm coming from {client_addr}");
        let engine_clone = engine.clone();
        let linker_clone = linker.clone();
        join_set.spawn(async move {
            let result = handle_wasm_request(stream, engine_clone, linker_clone).await;
            if let Err(e) = result {
                println!("Error: {e}");
            }
        });
    }
}

view this post on Zulip Notification Bot (May 16 2026 at 11:03):

27 messages were moved here from #general > How do I get stdout and stderr while my program is running? by Till Schneidereit.

view this post on Zulip Victor Adossi (May 16 2026 at 11:07):

So I'm not sure what you mean by really big loop and taking a long time, but there is a lot of ground to cover as far as optimizing wasmtime -- I'd recommend looking around the docs and looking at other runtimes that use wasmtime to see the tricks (there are many of them!)

For example:
https://docs.wasmtime.dev/examples-fast-instantiation.html
https://docs.rs/wasmtime/latest/wasmtime/component/struct.Linker.html#method.instantiate_pre

view this post on Zulip Notification Bot (May 16 2026 at 11:07):

Victor Adossi has marked this topic as resolved.

view this post on Zulip Srayan Jana (May 16 2026 at 16:52):

Cool thanks!


Last updated: May 26 2026 at 09:09 UTC