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
https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/cli/trait.StdoutStream.html
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
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?
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
Ah nevermind, I think the problem was I made my MemoryOutputPipe too small lol
now i guess I just need to implement async read and async write over a channel
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)
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,MemoryOutputPipeimplementsStdoutStream)
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
Also, I just realized Pat said all this!
Like, you can write to a MemoryOutputPipe, but you can't read from a MemoryOutputPIpe
maybe this is an api oversight?
Have you considered creating a cursor on the output Bytes ?
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
i didn't even know you could do that, does that work concurrently?
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?
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
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 :)
@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
this is a big problem for me, is there a way to work around this limitation?
actually wait hold on that might not be the case
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)
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(())
}
}
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}");
}
});
}
}
27 messages were moved here from #general > How do I get stdout and stderr while my program is running? by Till Schneidereit.
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
Victor Adossi has marked this topic as resolved.
Cool thanks!
Last updated: May 26 2026 at 09:09 UTC