wasmtime_wasi/cli.rs
1use crate::p2;
2use crate::{NamedId, WasiCtxNamedView};
3use std::marker;
4use std::pin::Pin;
5use std::sync::Arc;
6use tokio::io::{AsyncRead, AsyncWrite, empty};
7use wasmtime::component::{HasData, ResourceTable};
8use wasmtime_wasi_io::streams::{InputStream, OutputStream, StreamError};
9
10mod empty;
11mod file;
12mod locked_async;
13mod mem;
14mod stdout;
15mod worker_thread_stdin;
16
17pub use self::file::{InputFile, OutputFile};
18pub use self::locked_async::{AsyncStdinStream, AsyncStdoutStream};
19
20/// Convert a host `io::Error` into a `StreamError`, matching the error-code
21/// recovery that wasip1 performs via `filesystem::ErrorCode::from`.
22///
23/// * `BrokenPipe` is mapped to `StreamError::Closed` so that downstream
24/// consumers (e.g. wasi-libc) can recover `EPIPE` rather than falling back
25/// to a generic `EIO`.
26///
27/// * All other errors (including `IsADirectory`, permission errors, etc.) are
28/// preserved as `LastOperationFailed` with the original `std::io::Error`
29/// intact. This allows guests to recover the specific error code via the
30/// `wasi:filesystem/types#filesystem-error-code` function, which downcasts
31/// the error back to `std::io::Error` and maps it through
32/// `ErrorCode::from`.
33fn stream_error_from(e: std::io::Error) -> StreamError {
34 if e.kind() == std::io::ErrorKind::BrokenPipe {
35 StreamError::Closed
36 } else {
37 StreamError::LastOperationFailed(e.into())
38 }
39}
40
41// Convenience reexport for stdio types so tokio doesn't have to be imported
42// itself.
43#[doc(no_inline)]
44pub use tokio::io::{Stderr, Stdin, Stdout, stderr, stdin, stdout};
45
46/// A helper struct which implements [`HasData`] for the `wasi:cli` APIs.
47///
48/// This can be useful when directly calling `add_to_linker` functions directly,
49/// such as [`wasmtime_wasi::p2::bindings::cli::environment::add_to_linker`] as
50/// the `D` type parameter. See [`HasData`] for more information about the type
51/// parameter's purpose.
52///
53/// When using this type you can skip the [`WasiCliView`] trait, for
54/// example.
55///
56/// [`wasmtime_wasi::p2::bindings::cli::environment::add_to_linker`]: crate::p2::bindings::cli::environment::add_to_linker
57///
58/// # Examples
59///
60/// ```
61/// use wasmtime::component::{Linker, ResourceTable};
62/// use wasmtime::{Engine, Result};
63/// use wasmtime_wasi::cli::*;
64///
65/// struct MyStoreState {
66/// table: ResourceTable,
67/// cli: WasiCliCtx,
68/// }
69///
70/// fn main() -> Result<()> {
71/// let engine = Engine::default();
72/// let mut linker = Linker::new(&engine);
73///
74/// wasmtime_wasi::p2::bindings::cli::environment::add_to_linker::<MyStoreState, WasiCli>(
75/// &mut linker,
76/// |state| WasiCliCtxView {
77/// table: &mut state.table,
78/// ctx: &mut state.cli,
79/// },
80/// )?;
81/// Ok(())
82/// }
83/// ```
84pub struct WasiCli;
85
86impl HasData for WasiCli {
87 type Data<'a> = WasiCliCtxView<'a>;
88}
89
90/// Provides a "view" of `wasi:cli`-related context used to implement host
91/// traits.
92pub trait WasiCliView: Send {
93 fn cli(&mut self) -> WasiCliCtxView<'_>;
94}
95
96pub struct WasiCliCtxView<'a> {
97 pub ctx: &'a mut WasiCliCtx,
98 pub table: &'a mut ResourceTable,
99}
100
101pub struct WasiCliCtx {
102 pub(crate) environment: Vec<(String, String)>,
103 pub(crate) arguments: Vec<String>,
104 pub(crate) initial_cwd: Option<String>,
105 pub(crate) stdin: Box<dyn StdinStream>,
106 pub(crate) stdout: Box<dyn StdoutStream>,
107 pub(crate) stderr: Box<dyn StdoutStream>,
108}
109
110impl Default for WasiCliCtx {
111 fn default() -> WasiCliCtx {
112 WasiCliCtx {
113 environment: Vec::new(),
114 arguments: Vec::new(),
115 initial_cwd: None,
116 stdin: Box::new(empty()),
117 stdout: Box::new(empty()),
118 stderr: Box::new(empty()),
119 }
120 }
121}
122
123pub trait IsTerminal {
124 /// Returns whether this stream is backed by a TTY.
125 fn is_terminal(&self) -> bool;
126}
127
128/// A trait used to represent the standard input to a guest program.
129///
130/// Note that there are many built-in implementations of this trait for various
131/// types such as [`tokio::io::Stdin`], [`tokio::io::Empty`], and
132/// [`p2::pipe::MemoryInputPipe`].
133pub trait StdinStream: IsTerminal + Send {
134 /// Creates a fresh stream which is reading stdin.
135 ///
136 /// Note that the returned stream must share state with all other streams
137 /// previously created. Guests may create multiple handles to the same stdin
138 /// and they should all be synchronized in their progress through the
139 /// program's input.
140 ///
141 /// Note that this means that if one handle becomes ready for reading they
142 /// all become ready for reading. Subsequently if one is read from it may
143 /// mean that all the others are no longer ready for reading. This is
144 /// basically a consequence of the way the WIT APIs are designed today.
145 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync>;
146
147 /// Same as [`Self::async_stream`] except that a WASIp2 [`InputStream`] is
148 /// returned.
149 ///
150 /// Note that this has a default implementation which uses
151 /// [`p2::pipe::AsyncReadStream`] as an adapter, but this can be overridden
152 /// if there's a more specialized implementation available.
153 fn p2_stream(&self) -> Box<dyn InputStream> {
154 Box::new(p2::pipe::AsyncReadStream::new(Pin::from(
155 self.async_stream(),
156 )))
157 }
158}
159
160/// Similar to [`StdinStream`], except for output.
161///
162/// This is used both for a guest stdin and a guest stdout.
163///
164/// Note that there are many built-in implementations of this trait for various
165/// types such as [`tokio::io::Stdout`], [`tokio::io::Empty`], and
166/// [`p2::pipe::MemoryOutputPipe`].
167pub trait StdoutStream: IsTerminal + Send {
168 /// Returns a fresh new stream which can write to this output stream.
169 ///
170 /// Note that all output streams should output to the same logical source.
171 /// This means that it's possible for each independent stream to acquire a
172 /// separate "permit" to write and then act on that permit. Note that
173 /// additionally at this time once a permit is "acquired" there's no way to
174 /// release it, for example you can wait for readiness and then never
175 /// actually write in WASI. This means that acquisition of a permit for one
176 /// stream cannot discount the size of a permit another stream could
177 /// obtain.
178 ///
179 /// Implementations must be able to handle this
180 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync>;
181
182 /// Same as [`Self::async_stream`] except that a WASIp2 [`OutputStream`] is
183 /// returned.
184 ///
185 /// Note that this has a default implementation which uses
186 /// [`p2::pipe::AsyncWriteStream`] as an adapter, but this can be overridden
187 /// if there's a more specialized implementation available.
188 fn p2_stream(&self) -> Box<dyn OutputStream> {
189 Box::new(p2::pipe::AsyncWriteStream::new(
190 8192, // FIXME: extract this to a constant.
191 Pin::from(self.async_stream()),
192 ))
193 }
194}
195
196// Forward `&T => T`
197impl<T: ?Sized + IsTerminal> IsTerminal for &T {
198 fn is_terminal(&self) -> bool {
199 T::is_terminal(self)
200 }
201}
202impl<T: ?Sized + StdinStream + Sync> StdinStream for &T {
203 fn p2_stream(&self) -> Box<dyn InputStream> {
204 T::p2_stream(self)
205 }
206 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
207 T::async_stream(self)
208 }
209}
210impl<T: ?Sized + StdoutStream + Sync> StdoutStream for &T {
211 fn p2_stream(&self) -> Box<dyn OutputStream> {
212 T::p2_stream(self)
213 }
214 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
215 T::async_stream(self)
216 }
217}
218
219// Forward `&mut T => T`
220impl<T: ?Sized + IsTerminal> IsTerminal for &mut T {
221 fn is_terminal(&self) -> bool {
222 T::is_terminal(self)
223 }
224}
225impl<T: ?Sized + StdinStream + Sync> StdinStream for &mut T {
226 fn p2_stream(&self) -> Box<dyn InputStream> {
227 T::p2_stream(self)
228 }
229 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
230 T::async_stream(self)
231 }
232}
233impl<T: ?Sized + StdoutStream + Sync> StdoutStream for &mut T {
234 fn p2_stream(&self) -> Box<dyn OutputStream> {
235 T::p2_stream(self)
236 }
237 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
238 T::async_stream(self)
239 }
240}
241
242// Forward `Box<T> => T`
243impl<T: ?Sized + IsTerminal> IsTerminal for Box<T> {
244 fn is_terminal(&self) -> bool {
245 T::is_terminal(self)
246 }
247}
248impl<T: ?Sized + StdinStream + Sync> StdinStream for Box<T> {
249 fn p2_stream(&self) -> Box<dyn InputStream> {
250 T::p2_stream(self)
251 }
252 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
253 T::async_stream(self)
254 }
255}
256impl<T: ?Sized + StdoutStream + Sync> StdoutStream for Box<T> {
257 fn p2_stream(&self) -> Box<dyn OutputStream> {
258 T::p2_stream(self)
259 }
260 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
261 T::async_stream(self)
262 }
263}
264
265// Forward `Arc<T> => T`
266impl<T: ?Sized + IsTerminal> IsTerminal for Arc<T> {
267 fn is_terminal(&self) -> bool {
268 T::is_terminal(self)
269 }
270}
271impl<T: ?Sized + StdinStream + Sync> StdinStream for Arc<T> {
272 fn p2_stream(&self) -> Box<dyn InputStream> {
273 T::p2_stream(self)
274 }
275 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
276 T::async_stream(self)
277 }
278}
279impl<T: ?Sized + StdoutStream + Sync> StdoutStream for Arc<T> {
280 fn p2_stream(&self) -> Box<dyn OutputStream> {
281 T::p2_stream(self)
282 }
283 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
284 T::async_stream(self)
285 }
286}
287
288/// A helper struct which implements [`HasData`] for the `wasi:cli` APIs when
289/// used in combination with named imports.
290///
291/// This structure is similar in purpose to [`WasiCli`] and is used
292/// when using the [`named_imports`] module for `wasi:cli`. This structure
293/// serves as the `D` type parameter for `add_to_linker` functions.
294///
295/// [`named_imports`]: crate::p3::bindings::named_imports::wasi::cli
296///
297/// # Meaning of the `T` parameter
298///
299/// Here the `T` must be something that implements [`WasiCliNamedView`]. The
300/// corresponding `Data` for this type is [`WasiCtxNamedView`] which internally
301/// will contain `&mut T`.
302///
303/// Effectively you're going to implement [`WasiCliNamedView`] for something in
304/// your embedding, and that's the `T` you'll fill in here.
305///
306/// # Examples
307///
308/// ```
309/// use wasmtime::component::{Linker, Component, ResourceTable};
310/// use wasmtime::{Engine, Result};
311/// use wasmtime_wasi::{NamedId, WasiCtxNamedView};
312/// use wasmtime_wasi::cli::*;
313/// use wasmtime_wasi::p2::bindings::named_imports;
314/// use std::collections::HashMap;
315///
316/// struct MyStoreState {
317/// table: ResourceTable,
318/// states: HashMap<NamedId, WasiCliCtx>,
319/// }
320///
321/// fn main() -> Result<()> {
322/// let engine = Engine::default();
323/// let mut linker = Linker::new(&engine);
324/// let component = Component::new(&engine, "(component)")?;
325/// let mut name_map = HashMap::new();
326///
327/// named_imports::wasi::cli::environment::add_to_linker::<MyStoreState, WasiCliNamed<MyStoreState>>(
328/// &mut linker,
329/// &component,
330/// |name| {
331/// let len = name_map.len();
332/// Ok(NamedId(*name_map.entry(name.to_string()).or_insert(len)))
333/// },
334/// |state| WasiCtxNamedView(state),
335/// )?;
336/// Ok(())
337/// }
338///
339/// impl WasiCliNamedView for MyStoreState {
340/// fn cli(&mut self, id: NamedId) -> WasiCliCtxView<'_> {
341/// let ctx = self.states.get_mut(&id).expect("state for id");
342/// WasiCliCtxView {
343/// table: &mut self.table,
344/// ctx,
345/// }
346/// }
347/// }
348/// ```
349pub struct WasiCliNamed<T>(marker::PhantomData<fn() -> T>);
350
351impl<T> HasData for WasiCliNamed<T>
352where
353 T: WasiCliNamedView,
354{
355 type Data<'a> = WasiCtxNamedView<'a, T>;
356}
357
358/// A trait used to look up a specific `wasi:cli` context for a named import.
359///
360/// This trait is used in conjunction with the [`named_imports`] bindings
361/// generated for all WASI interfaces. The purpose of this trait is for
362/// embedders to define how a [`NamedId`] maps to a particular `wasi:cli`
363/// context, here returned as [`WasiCliCtxView`]. Embedders are responsible
364/// for assigning meaning to [`NamedId`] values themselves. These IDs are
365/// assigned when [`add_named_to_linker`] is called, for example, as the
366/// `lookup` argument to that function.
367///
368/// When using [`add_named_to_linker`] it's sufficient to implement this trait
369/// for the `T` in `Store<T>`. You can also instead implement the
370/// [`WasiNamedView`] trait for `T` which implies an implementation of this
371/// trait.
372///
373/// When using `add_to_linker` in the generated `bindings::named_imports`
374/// module then values implementing this live within the `T` of `Store<T>`, and
375/// be temporarily referenced in [`WasiCtxNamedView`] where internally that'll
376/// hold `WasiCtxNamedView(&mut your_type)`.
377///
378/// [`named_imports`]: crate::p3::bindings::named_imports
379/// [`add_named_to_linker`]: crate::p3::cli::add_named_to_linker
380/// [`WasiNamedView`]: crate::WasiNamedView
381///
382/// # Examples
383///
384/// ```
385/// use wasmtime::component::{Linker, Component, ResourceTable};
386/// use wasmtime::{Engine, Result};
387/// use wasmtime_wasi::{NamedId, WasiCtxNamedView};
388/// use wasmtime_wasi::cli::*;
389/// use wasmtime_wasi::p2::bindings::named_imports;
390/// use std::collections::HashMap;
391///
392/// struct MyStoreState {
393/// table: ResourceTable,
394/// states: HashMap<NamedId, WasiCliCtx>,
395/// }
396///
397/// fn main() -> Result<()> {
398/// let engine = Engine::default();
399/// let mut linker = Linker::new(&engine);
400/// let component = Component::new(&engine, "(component)")?;
401/// let mut name_map = HashMap::new();
402///
403/// wasmtime_wasi::p3::cli::add_named_to_linker::<MyStoreState>(
404/// &mut linker,
405/// &component,
406/// |_, name| {
407/// let len = name_map.len();
408/// Ok(NamedId(*name_map.entry(name.to_string()).or_insert(len)))
409/// },
410/// )?;
411/// Ok(())
412/// }
413///
414/// impl WasiCliNamedView for MyStoreState {
415/// fn cli(&mut self, id: NamedId) -> WasiCliCtxView<'_> {
416/// let ctx = self.states.get_mut(&id).expect("state for id");
417/// WasiCliCtxView {
418/// table: &mut self.table,
419/// ctx,
420/// }
421/// }
422/// }
423/// ```
424pub trait WasiCliNamedView: Send + 'static {
425 /// Looks up the [`WasiCliCtxView`] for the given [`NamedId`].
426 ///
427 /// This method will resolve the `id` specified to a specific CLI context
428 /// that is available to be used. Note that this method is specifically
429 /// infallible meaning that a CLI context must be returned and this cannot
430 /// generate a trap or panic or similar.
431 ///
432 /// Embedders are responsible for allocating [`NamedId`] and assigning
433 /// meaning to ids. When a `Linker` is populated embedders will have the
434 /// ability to generate a `NamedId` for all imports found, and then that
435 /// embedder-allocated id is then passed back here when the corresponding
436 /// imported function is invoked.
437 ///
438 /// Note that the [`ResourceTable`] referenced in the returned
439 /// [`WasiCliCtxView`] need not be unique. It's ok to use the same
440 /// [`ResourceTable`] for all imports. This is not a guest-visible
441 /// abstraction and just helps the host allocate and manage state.
442 fn cli(&mut self, id: NamedId) -> WasiCliCtxView<'_>;
443}
444
445#[cfg(test)]
446mod test {
447 use crate::cli::{AsyncStdoutStream, StdinStream, StdoutStream};
448 use crate::p2::{self, OutputStream};
449 use bytes::Bytes;
450 use tokio::io::AsyncReadExt;
451 use wasmtime::Result;
452
453 #[test]
454 fn memory_stdin_stream() {
455 // A StdinStream has the property that there are multiple
456 // InputStreams created, using the stream() method which are each
457 // views on the same shared state underneath. Consuming input on one
458 // stream results in consuming that input on all streams.
459 //
460 // The simplest way to measure this is to check if the MemoryInputPipe
461 // impl of StdinStream follows this property.
462
463 let pipe =
464 p2::pipe::MemoryInputPipe::new("the quick brown fox jumped over the three lazy dogs");
465
466 let mut view1 = pipe.p2_stream();
467 let mut view2 = pipe.p2_stream();
468
469 let read1 = view1.read(10).expect("read first 10 bytes");
470 assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes");
471 let read2 = view2.read(10).expect("read second 10 bytes");
472 assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes");
473 let read3 = view1.read(10).expect("read third 10 bytes");
474 assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes");
475 let read4 = view2.read(10).expect("read fourth 10 bytes");
476 assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes");
477 }
478
479 #[tokio::test]
480 async fn async_stdin_stream() {
481 // A StdinStream has the property that there are multiple
482 // InputStreams created, using the stream() method which are each
483 // views on the same shared state underneath. Consuming input on one
484 // stream results in consuming that input on all streams.
485 //
486 // AsyncStdinStream is a slightly more complex impl of StdinStream
487 // than the MemoryInputPipe above. We can create an AsyncReadStream
488 // from a file on the disk, and an AsyncStdinStream from that common
489 // stream, then check that the same property holds as above.
490
491 let dir = tempfile::tempdir().unwrap();
492 let mut path = std::path::PathBuf::from(dir.path());
493 path.push("file");
494 std::fs::write(&path, "the quick brown fox jumped over the three lazy dogs").unwrap();
495
496 let file = tokio::fs::File::open(&path)
497 .await
498 .expect("open created file");
499 let stdin_stream = super::AsyncStdinStream::new(file);
500
501 use super::StdinStream;
502
503 let mut view1 = stdin_stream.p2_stream();
504 let mut view2 = stdin_stream.p2_stream();
505
506 view1.ready().await;
507
508 let read1 = view1.read(10).expect("read first 10 bytes");
509 assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes");
510 let read2 = view2.read(10).expect("read second 10 bytes");
511 assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes");
512 let read3 = view1.read(10).expect("read third 10 bytes");
513 assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes");
514 let read4 = view2.read(10).expect("read fourth 10 bytes");
515 assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes");
516 }
517
518 #[tokio::test]
519 async fn async_stdout_stream_unblocks() {
520 let (mut read, write) = tokio::io::duplex(32);
521 let stdout = AsyncStdoutStream::new(32, write);
522
523 let task = tokio::task::spawn(async move {
524 let mut stream = stdout.p2_stream();
525 blocking_write_and_flush(&mut *stream, "x".into())
526 .await
527 .unwrap();
528 });
529
530 let mut buf = [0; 100];
531 let n = read.read(&mut buf).await.unwrap();
532 assert_eq!(&buf[..n], b"x");
533
534 task.await.unwrap();
535 }
536
537 async fn blocking_write_and_flush(s: &mut dyn OutputStream, mut bytes: Bytes) -> Result<()> {
538 while !bytes.is_empty() {
539 let permit = s.write_ready().await?;
540 let len = bytes.len().min(permit);
541 let chunk = bytes.split_to(len);
542 s.write(chunk)?;
543 }
544
545 s.flush()?;
546 s.write_ready().await?;
547 Ok(())
548 }
549
550 // Verify that the stdio OutputStream implementation reports a usable
551 // write permit and can successfully write + flush (exercises the full
552 // trait impl including the error conversion path).
553 #[test]
554 fn stdio_output_stream_write_flush() {
555 let mut stream: Box<dyn wasmtime_wasi_io::streams::OutputStream> =
556 StdoutStream::p2_stream(&std::io::stderr());
557
558 let permit = stream.check_write().expect("check_write");
559 assert!(permit > 0, "permit should be nonzero");
560
561 // Writing empty bytes must succeed.
562 stream
563 .write(Bytes::new())
564 .expect("writing empty bytes should succeed");
565
566 // Flushing must succeed.
567 stream.flush().expect("flush should succeed");
568 }
569
570 #[test]
571 fn stream_error_from_broken_pipe_maps_to_closed() {
572 use std::io;
573 use wasmtime_wasi_io::streams::StreamError;
574
575 let err = super::stream_error_from(io::Error::from(io::ErrorKind::BrokenPipe));
576 assert!(matches!(err, StreamError::Closed));
577 }
578
579 #[test]
580 fn stream_error_from_preserves_io_error() {
581 use std::io;
582 use wasmtime_wasi_io::streams::StreamError;
583
584 let err = super::stream_error_from(io::Error::from(io::ErrorKind::IsADirectory));
585 match err {
586 StreamError::LastOperationFailed(e) => {
587 let io_err = e.downcast::<io::Error>().expect("should downcast");
588 assert_eq!(io_err.kind(), io::ErrorKind::IsADirectory);
589 }
590 other => panic!("expected LastOperationFailed, got: {other:?}"),
591 }
592 }
593
594 #[cfg(unix)]
595 #[test]
596 fn stream_error_from_raw_os_eisdir() {
597 use rustix::io::Errno;
598 use std::io;
599 use wasmtime_wasi_io::streams::StreamError;
600
601 let err =
602 super::stream_error_from(io::Error::from_raw_os_error(Errno::ISDIR.raw_os_error()));
603 match err {
604 StreamError::LastOperationFailed(e) => {
605 let io_err = e.downcast::<io::Error>().expect("should downcast");
606 assert_eq!(io_err.raw_os_error(), Some(Errno::ISDIR.raw_os_error()));
607 }
608 other => panic!("expected LastOperationFailed, got: {other:?}"),
609 }
610 }
611}