Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit de530d7

Browse files
committed
feat: implement wasi:cli
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent c72b8f8 commit de530d7

3 files changed

Lines changed: 238 additions & 19 deletions

File tree

crates/wasi/src/p3/bindings.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
//! async: {
5050
//! only_imports: [
5151
//! "example:wasi/custom-host#my-custom-function",
52+
//! "wasi:cli/stdin@0.3.0#get-stdin",
53+
//! "wasi:cli/stdout@0.3.0#set-stdout",
54+
//! "wasi:cli/stderr@0.3.0#set-stderr",
5255
//! "wasi:clocks/monotonic-clock@0.3.0#wait-for",
5356
//! "wasi:clocks/monotonic-clock@0.3.0#wait-until",
5457
//! "wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream",
@@ -153,6 +156,9 @@ mod generated {
153156
concurrent_imports: true,
154157
async: {
155158
only_imports: [
159+
"wasi:cli/stdin@0.3.0#get-stdin",
160+
"wasi:cli/stdout@0.3.0#set-stdout",
161+
"wasi:cli/stderr@0.3.0#set-stderr",
156162
"wasi:clocks/monotonic-clock@0.3.0#wait-for",
157163
"wasi:clocks/monotonic-clock@0.3.0#wait-until",
158164
"wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream",
@@ -193,6 +199,8 @@ mod generated {
193199
],
194200
},
195201
with: {
202+
"wasi:cli/terminal-input/terminal-input": crate::p3::cli::TerminalInput,
203+
"wasi:cli/terminal-output/terminal-output": crate::p3::cli::TerminalOutput,
196204
"wasi:filesystem/types/descriptor": crate::p3::filesystem::Descriptor,
197205
"wasi:sockets/types/tcp-socket": crate::p3::sockets::tcp::TcpSocket,
198206
"wasi:sockets/types/udp-socket": crate::p3::sockets::udp::UdpSocket,

crates/wasi/src/p3/cli/host.rs

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,96 @@
1-
#![allow(unused)] // TODO: remove
1+
use anyhow::{anyhow, Context as _};
2+
use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
3+
use wasmtime::component::{stream, Accessor, AccessorTask, Resource, StreamReader, StreamWriter};
24

3-
use anyhow::Context as _;
4-
use wasmtime::component::Resource;
5-
6-
use crate::p3::bindings::cli::terminal_input::TerminalInput;
7-
use crate::p3::bindings::cli::terminal_output::TerminalOutput;
85
use crate::p3::bindings::cli::{
96
environment, exit, stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr,
107
terminal_stdin, terminal_stdout,
118
};
12-
use crate::p3::cli::{WasiCliImpl, WasiCliView};
13-
use crate::p3::ResourceView as _;
9+
use crate::p3::cli::{I32Exit, TerminalInput, TerminalOutput, WasiCliImpl, WasiCliView};
10+
use crate::p3::{next_item, ResourceView as _};
11+
12+
struct InputTask<T> {
13+
input: T,
14+
tx: StreamWriter<u8>,
15+
}
16+
17+
impl<T, U, V> AccessorTask<T, U, wasmtime::Result<()>> for InputTask<V>
18+
where
19+
V: AsyncRead + Send + Sync + Unpin + 'static,
20+
{
21+
async fn run(mut self, store: &mut Accessor<T, U>) -> wasmtime::Result<()> {
22+
let mut tx = self.tx;
23+
loop {
24+
let mut buf = vec![0; 8096];
25+
match self.input.read(&mut buf).await {
26+
Ok(0) => {
27+
store.with(|mut view| tx.close(&mut view).context("failed to close stream"))?;
28+
return Ok(());
29+
}
30+
Ok(n) => {
31+
buf.truncate(n);
32+
let fut =
33+
store.with(|view| tx.write(view, buf).context("failed to send chunk"))?;
34+
let Some(tail) = fut.into_future().await else {
35+
break Ok(());
36+
};
37+
tx = tail;
38+
}
39+
Err(_err) => {
40+
// TODO: Close the stream with the real error context
41+
store.with(|mut view| {
42+
tx.close_with_error(&mut view, 0)
43+
.context("failed to close stream")
44+
})?;
45+
return Ok(());
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
struct OutputTask<T> {
53+
output: T,
54+
data: StreamReader<u8>,
55+
}
56+
57+
impl<T, U, V> AccessorTask<T, U, wasmtime::Result<()>> for OutputTask<V>
58+
where
59+
V: AsyncWrite + Send + Sync + Unpin + 'static,
60+
{
61+
async fn run(mut self, store: &mut Accessor<T, U>) -> wasmtime::Result<()> {
62+
let fut = store.with(|mut view| {
63+
self.data
64+
.read(&mut view)
65+
.context("failed to read from stream")
66+
})?;
67+
let mut fut = fut.into_future();
68+
'outer: loop {
69+
let Some((tail, buf)) = fut.await else {
70+
return Ok(());
71+
};
72+
let mut buf = buf.as_slice();
73+
loop {
74+
match self.output.write(&buf).await {
75+
Ok(n) => {
76+
if n == buf.len() {
77+
fut = next_item(store, tail)?;
78+
continue 'outer;
79+
} else {
80+
buf = &buf[n..];
81+
}
82+
}
83+
Err(_err) => {
84+
// TODO: Report the error to the guest
85+
return store.with(|mut view| {
86+
tail.close(&mut view).context("failed to close stream")
87+
});
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
1494

1595
impl<T> terminal_input::Host for WasiCliImpl<T> where T: WasiCliView {}
1696
impl<T> terminal_output::Host for WasiCliImpl<T> where T: WasiCliView {}
@@ -44,7 +124,15 @@ where
44124
T: WasiCliView,
45125
{
46126
fn get_terminal_stdin(&mut self) -> wasmtime::Result<Option<Resource<TerminalInput>>> {
47-
todo!()
127+
if self.cli().stdin.is_terminal() {
128+
let fd = self
129+
.table()
130+
.push(TerminalInput)
131+
.context("failed to push terminal resource to table")?;
132+
Ok(Some(fd))
133+
} else {
134+
Ok(None)
135+
}
48136
}
49137
}
50138

@@ -53,7 +141,15 @@ where
53141
T: WasiCliView,
54142
{
55143
fn get_terminal_stdout(&mut self) -> wasmtime::Result<Option<Resource<TerminalOutput>>> {
56-
todo!()
144+
if self.cli().stdout.is_terminal() {
145+
let fd = self
146+
.table()
147+
.push(TerminalOutput)
148+
.context("failed to push terminal resource to table")?;
149+
Ok(Some(fd))
150+
} else {
151+
Ok(None)
152+
}
57153
}
58154
}
59155

@@ -62,34 +158,69 @@ where
62158
T: WasiCliView,
63159
{
64160
fn get_terminal_stderr(&mut self) -> wasmtime::Result<Option<Resource<TerminalOutput>>> {
65-
todo!()
161+
if self.cli().stderr.is_terminal() {
162+
let fd = self
163+
.table()
164+
.push(TerminalOutput)
165+
.context("failed to push terminal resource to table")?;
166+
Ok(Some(fd))
167+
} else {
168+
Ok(None)
169+
}
66170
}
67171
}
68172

69173
impl<T> stdin::Host for WasiCliImpl<T>
70174
where
71175
T: WasiCliView,
72176
{
73-
fn get_stdin(&mut self) -> wasmtime::Result<wasmtime::component::StreamReader<u8>> {
74-
todo!()
177+
async fn get_stdin<U: 'static>(
178+
store: &mut Accessor<U, Self>,
179+
) -> wasmtime::Result<StreamReader<u8>> {
180+
store.with(|mut view| {
181+
let (tx, rx) = stream(&mut view).context("failed to create stream")?;
182+
let stdin = view.cli().stdin.reader();
183+
view.spawn(InputTask { input: stdin, tx });
184+
Ok(rx)
185+
})
75186
}
76187
}
77188

78189
impl<T> stdout::Host for WasiCliImpl<T>
79190
where
80191
T: WasiCliView,
81192
{
82-
fn set_stdout(&mut self, data: wasmtime::component::StreamReader<u8>) -> wasmtime::Result<()> {
83-
todo!()
193+
async fn set_stdout<U: 'static>(
194+
store: &mut Accessor<U, Self>,
195+
data: StreamReader<u8>,
196+
) -> wasmtime::Result<()> {
197+
store.with(|mut view| {
198+
let stdout = view.cli().stdout.writer();
199+
view.spawn(OutputTask {
200+
output: stdout,
201+
data,
202+
});
203+
Ok(())
204+
})
84205
}
85206
}
86207

87208
impl<T> stderr::Host for WasiCliImpl<T>
88209
where
89210
T: WasiCliView,
90211
{
91-
fn set_stderr(&mut self, data: wasmtime::component::StreamReader<u8>) -> wasmtime::Result<()> {
92-
todo!()
212+
async fn set_stderr<U: 'static>(
213+
store: &mut Accessor<U, Self>,
214+
data: StreamReader<u8>,
215+
) -> wasmtime::Result<()> {
216+
store.with(|mut view| {
217+
let stderr = view.cli().stderr.writer();
218+
view.spawn(OutputTask {
219+
output: stderr,
220+
data,
221+
});
222+
Ok(())
223+
})
93224
}
94225
}
95226

@@ -115,10 +246,14 @@ where
115246
T: WasiCliView,
116247
{
117248
fn exit(&mut self, status: Result<(), ()>) -> wasmtime::Result<()> {
118-
todo!()
249+
let status = match status {
250+
Ok(()) => 0,
251+
Err(()) => 1,
252+
};
253+
Err(anyhow!(I32Exit(status)))
119254
}
120255

121256
fn exit_with_code(&mut self, status_code: u8) -> wasmtime::Result<()> {
122-
todo!()
257+
Err(anyhow!(I32Exit(status_code.into())))
123258
}
124259
}

crates/wasi/src/p3/cli/mod.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
mod host;
22

3+
use core::fmt;
4+
5+
use tokio::io::{empty, AsyncRead, AsyncWrite, Empty};
36
use wasmtime::component::{Linker, ResourceTable};
47

58
use crate::p3::ResourceView;
@@ -33,6 +36,9 @@ pub struct WasiCliCtx {
3336
pub environment: Vec<(String, String)>,
3437
pub arguments: Vec<String>,
3538
pub initial_cwd: Option<String>,
39+
pub stdin: Box<dyn InputStream + Send>,
40+
pub stdout: Box<dyn OutputStream + Send>,
41+
pub stderr: Box<dyn OutputStream + Send>,
3642
}
3743

3844
impl Default for WasiCliCtx {
@@ -41,6 +47,9 @@ impl Default for WasiCliCtx {
4147
environment: Vec::default(),
4248
arguments: Vec::default(),
4349
initial_cwd: None,
50+
stdin: Box::new(empty()),
51+
stdout: Box::new(empty()),
52+
stderr: Box::new(empty()),
4453
}
4554
}
4655
}
@@ -137,3 +146,70 @@ where
137146
{
138147
val
139148
}
149+
150+
/// An error returned from the `proc_exit` host syscall.
151+
///
152+
/// Embedders can test if an error returned from wasm is this error, in which
153+
/// case it may signal a non-fatal trap.
154+
#[derive(Debug)]
155+
pub struct I32Exit(pub i32);
156+
157+
impl fmt::Display for I32Exit {
158+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159+
write!(f, "Exited with i32 exit status {}", self.0)
160+
}
161+
}
162+
163+
impl std::error::Error for I32Exit {}
164+
165+
pub struct TerminalInput;
166+
pub struct TerminalOutput;
167+
168+
pub trait IsTerminal {
169+
/// Returns whether this stream is backed by a TTY.
170+
fn is_terminal(&self) -> bool;
171+
}
172+
173+
impl IsTerminal for Empty {
174+
fn is_terminal(&self) -> bool {
175+
false
176+
}
177+
}
178+
179+
impl InputStream for Empty {
180+
fn reader(&self) -> Box<dyn AsyncRead + Send + Sync + Unpin> {
181+
Box::new(empty())
182+
}
183+
}
184+
185+
impl OutputStream for Empty {
186+
fn writer(&self) -> Box<dyn AsyncWrite + Send + Sync + Unpin> {
187+
Box::new(empty())
188+
}
189+
}
190+
191+
impl IsTerminal for std::io::Empty {
192+
fn is_terminal(&self) -> bool {
193+
false
194+
}
195+
}
196+
197+
impl InputStream for std::io::Empty {
198+
fn reader(&self) -> Box<dyn AsyncRead + Send + Sync + Unpin> {
199+
Box::new(empty())
200+
}
201+
}
202+
203+
impl OutputStream for std::io::Empty {
204+
fn writer(&self) -> Box<dyn AsyncWrite + Send + Sync + Unpin> {
205+
Box::new(empty())
206+
}
207+
}
208+
209+
pub trait InputStream: IsTerminal {
210+
fn reader(&self) -> Box<dyn AsyncRead + Send + Sync + Unpin>;
211+
}
212+
213+
pub trait OutputStream: IsTerminal {
214+
fn writer(&self) -> Box<dyn AsyncWrite + Send + Sync + Unpin>;
215+
}

0 commit comments

Comments
 (0)