Skip to main content

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}