Repository: bytecodealliance/wasmtime
Pull request #8064 (2024-03-07 23:16:43)

wasmtime c-api: warn that wasi_config_preopen_socket is deprecated in next release

this PR will be backported to the release-19.0.0 branch prior to the 19 release.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/crates/c-api/src/wasi.rs b/crates/c-api/src/wasi.rs
index 7061eacc0..091bf5816 100644
--- a/crates/c-api/src/wasi.rs
+++ b/crates/c-api/src/wasi.rs
@@ -292,6 +292,14 @@ pub unsafe extern "C" fn wasi_config_preopen_socket(
     fd_num: u32,
     host_port: *const c_char,
 ) -> bool {
+    const VAR: &str = "WASMTIME_WASI_CONFIG_PREOPEN_SOCKET_ALLOW";
+    if std::env::var(VAR).is_err() {
+        panic!(
+            "wasmtime c-api: wasi_config_preopen_socket will be deprecated in the \
+            wasmtime 20.0.0 release. set {VAR} to enable temporarily"
+        );
+    }
+
     let address = match cstr_to_str(host_port) {
         Some(s) => s,
         None => return false,
diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml
index 5ee1cc235..f35908fb5 100644
--- a/crates/wasi/Cargo.toml
+++ b/crates/wasi/Cargo.toml
@@ -37,7 +37,7 @@ url = { workspace = true }
 once_cell = { workspace = true }
 
 [dev-dependencies]
-tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros"] }
+tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros", "fs"] }
 test-log = { workspace = true }
 tracing-subscriber = { workspace = true }
 test-programs-artifacts = { workspace = true }
diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs
index 090135a0f..4e9f3578e 100644
--- a/crates/wasi/src/ctx.rs
+++ b/crates/wasi/src/ctx.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "preview1")]
+use crate::WasiP1Ctx;
 use crate::{
     clocks::{
         host::{monotonic_clock, wall_clock},
@@ -127,6 +129,10 @@ impl WasiCtxBuilder {
         self
     }
 
+    pub fn inherit_env(&mut self) -> &mut Self {
+        self.envs(&std::env::vars().collect::<Vec<(String, String)>>())
+    }
+
     pub fn args(&mut self, args: &[impl AsRef<str>]) -> &mut Self {
         self.args.extend(args.iter().map(|a| a.as_ref().to_owned()));
         self
@@ -137,6 +143,10 @@ impl WasiCtxBuilder {
         self
     }
 
+    pub fn inherit_args(&mut self) -> &mut Self {
+        self.args(&std::env::args().collect::<Vec<String>>())
+    }
+
     pub fn preopened_dir(
         &mut self,
         dir: cap_std::fs::Dir,
@@ -273,6 +283,12 @@ impl WasiCtxBuilder {
             allowed_network_uses,
         }
     }
+
+    #[cfg(feature = "preview1")]
+    pub fn build_p1(&mut self) -> WasiP1Ctx {
+        let wasi = self.build();
+        WasiP1Ctx::new(wasi)
+    }
 }
 
 pub trait WasiView: Send {
diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs
index 225245b47..6ef80d83e 100644
--- a/crates/wasi/src/lib.rs
+++ b/crates/wasi/src/lib.rs
@@ -15,6 +15,8 @@ mod filesystem;
 mod host;
 mod ip_name_lookup;
 mod network;
+#[cfg(feature = "preview1")]
+mod p1ctx;
 pub mod pipe;
 mod poll;
 #[cfg(feature = "preview1")]
@@ -34,10 +36,13 @@ pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView};
 pub use self::error::{I32Exit, TrappableError};
 pub use self::filesystem::{DirPerms, FilePerms, FsError, FsResult};
 pub use self::network::{Network, SocketError, SocketResult};
+#[cfg(feature = "preview1")]
+pub use self::p1ctx::WasiP1Ctx;
 pub use self::poll::{subscribe, ClosureFuture, MakeFuture, Pollable, PollableFuture, Subscribe};
 pub use self::random::{thread_rng, Deterministic};
 pub use self::stdio::{
-    stderr, stdin, stdout, IsATTY, Stderr, Stdin, StdinStream, Stdout, StdoutStream,
+    stderr, stdin, stdout, AsyncStdinStream, AsyncStdoutStream, IsATTY, Stderr, Stdin, StdinStream,
+    Stdout, StdoutStream,
 };
 pub use self::stream::{
     HostInputStream, HostOutputStream, InputStream, OutputStream, StreamError, StreamResult,
diff --git a/crates/wasi/src/p1ctx.rs b/crates/wasi/src/p1ctx.rs
new file mode 100644
index 000000000..990609de3
--- /dev/null
+++ b/crates/wasi/src/p1ctx.rs
@@ -0,0 +1,37 @@
+use crate::preview1::{WasiPreview1Adapter, WasiPreview1View};
+use crate::{WasiCtx, WasiView};
+use wasmtime::component::ResourceTable;
+
+pub struct WasiP1Ctx {
+    pub table: ResourceTable,
+    pub wasi: WasiCtx,
+    pub adapter: WasiPreview1Adapter,
+}
+
+impl WasiP1Ctx {
+    pub fn new(wasi: WasiCtx) -> Self {
+        Self {
+            table: ResourceTable::new(),
+            wasi,
+            adapter: WasiPreview1Adapter::new(),
+        }
+    }
+}
+
+impl WasiView for WasiP1Ctx {
+    fn table(&mut self) -> &mut ResourceTable {
+        &mut self.table
+    }
+    fn ctx(&mut self) -> &mut WasiCtx {
+        &mut self.wasi
+    }
+}
+
+impl WasiPreview1View for WasiP1Ctx {
+    fn adapter(&self) -> &WasiPreview1Adapter {
+        &self.adapter
+    }
+    fn adapter_mut(&mut self) -> &mut WasiPreview1Adapter {
+        &mut self.adapter
+    }
+}
diff --git a/crates/wasi/src/pipe.rs b/crates/wasi/src/pipe.rs
index 819455c59..3ecc9cd3b 100644
--- a/crates/wasi/src/pipe.rs
+++ b/crates/wasi/src/pipe.rs
@@ -22,9 +22,9 @@ pub struct MemoryInputPipe {
 }
 
 impl MemoryInputPipe {
-    pub fn new(bytes: Bytes) -> Self {
+    pub fn new(bytes: impl Into<Bytes>) -> Self {
         Self {
-            buffer: Arc::new(Mutex::new(bytes)),
+            buffer: Arc::new(Mutex::new(bytes.into())),
         }
     }
 
diff --git a/crates/wasi/src/preview0.rs b/crates/wasi/src/preview0.rs
index b7c8c40a7..38c0de8dd 100644
--- a/crates/wasi/src/preview0.rs
+++ b/crates/wasi/src/preview0.rs
@@ -4,16 +4,18 @@ use crate::preview1::wasi_snapshot_preview1::WasiSnapshotPreview1 as Snapshot1;
 use crate::preview1::WasiPreview1View;
 use wiggle::{GuestError, GuestPtr};
 
-pub fn add_to_linker_async<T: WasiPreview1View>(
+pub fn add_to_linker_async<T: Send, W: WasiPreview1View>(
     linker: &mut wasmtime::Linker<T>,
+    f: impl Fn(&mut T) -> &mut W + Copy + Send + Sync + 'static,
 ) -> anyhow::Result<()> {
-    wasi_unstable::add_to_linker(linker, |t| t)
+    wasi_unstable::add_to_linker(linker, f)
 }
 
-pub fn add_to_linker_sync<T: WasiPreview1View>(
+pub fn add_to_linker_sync<T: Send, W: WasiPreview1View>(
     linker: &mut wasmtime::Linker<T>,
+    f: impl Fn(&mut T) -> &mut W + Copy + Send + Sync + 'static,
 ) -> anyhow::Result<()> {
-    sync::add_wasi_unstable_to_linker(linker, |t| t)
+    sync::add_wasi_unstable_to_linker(linker, f)
 }
 
 wiggle::from_witx!({
diff --git a/crates/wasi/src/preview1.rs b/crates/wasi/src/preview1.rs
index 16a5e58ee..0147a8103 100644
--- a/crates/wasi/src/preview1.rs
+++ b/crates/wasi/src/preview1.rs
@@ -490,18 +490,19 @@ trait WasiPreview1ViewExt:
 
 impl<T: WasiPreview1View + preopens::Host> WasiPreview1ViewExt for T {}
 
-pub fn add_to_linker_async<T: WasiPreview1View>(
+pub fn add_to_linker_async<T: Send, W: WasiPreview1View>(
     linker: &mut wasmtime::Linker<T>,
+    f: impl Fn(&mut T) -> &mut W + Copy + Send + Sync + 'static,
 ) -> anyhow::Result<()> {
-    wasi_snapshot_preview1::add_to_linker(linker, |t| t)
+    crate::preview1::wasi_snapshot_preview1::add_to_linker(linker, f)
 }
 
-pub fn add_to_linker_sync<T: WasiPreview1View>(
+pub fn add_to_linker_sync<T: Send, W: WasiPreview1View>(
     linker: &mut wasmtime::Linker<T>,
+    f: impl Fn(&mut T) -> &mut W + Copy + Send + Sync + 'static,
 ) -> anyhow::Result<()> {
-    sync::add_wasi_snapshot_preview1_to_linker(linker, |t| t)
+    crate::preview1::sync::add_wasi_snapshot_preview1_to_linker(linker, f)
 }
-
 // Generate the wasi_snapshot_preview1::WasiSnapshotPreview1 trait,
 // and the module types.
 // None of the generated modules, traits, or types should be used externally
@@ -520,7 +521,7 @@ wiggle::from_witx!({
     errors: { errno => trappable Error },
 });
 
-mod sync {
+pub(crate) mod sync {
     use anyhow::Result;
     use std::future::Future;
 
diff --git a/crates/wasi/src/stdio.rs b/crates/wasi/src/stdio.rs
index 5c6b191eb..04bab3bdc 100644
--- a/crates/wasi/src/stdio.rs
+++ b/crates/wasi/src/stdio.rs
@@ -6,7 +6,11 @@ use crate::bindings::io::streams;
 use crate::pipe;
 use crate::{HostInputStream, HostOutputStream, StreamError, StreamResult, Subscribe, WasiView};
 use bytes::Bytes;
+use std::future::Future;
 use std::io::IsTerminal;
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
 use wasmtime::component::Resource;
 
 /// A trait used to represent the standard input to a guest program.
@@ -54,6 +58,52 @@ impl StdinStream for pipe::ClosedInputStream {
     }
 }
 
+/// An impl of [`StdinStream`] built on top of [`crate::pipe::AsyncReadStream`].
+pub struct AsyncStdinStream(Arc<Mutex<crate::pipe::AsyncReadStream>>);
+
+impl AsyncStdinStream {
+    pub fn new(s: crate::pipe::AsyncReadStream) -> Self {
+        Self(Arc::new(Mutex::new(s)))
+    }
+}
+
+impl StdinStream for AsyncStdinStream {
+    fn stream(&self) -> Box<dyn HostInputStream> {
+        Box::new(Self(self.0.clone()))
+    }
+    fn isatty(&self) -> bool {
+        false
+    }
+}
+
+impl HostInputStream for AsyncStdinStream {
+    fn read(&mut self, size: usize) -> Result<bytes::Bytes, crate::StreamError> {
+        self.0.lock().unwrap().read(size)
+    }
+    fn skip(&mut self, size: usize) -> Result<usize, crate::StreamError> {
+        self.0.lock().unwrap().skip(size)
+    }
+}
+
+impl Subscribe for AsyncStdinStream {
+    fn ready<'a, 'b>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + Send + 'b>>
+    where
+        Self: 'b,
+        'a: 'b,
+    {
+        struct F(AsyncStdinStream);
+        impl Future for F {
+            type Output = ();
+            fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+                let mut inner = self.0 .0.lock().unwrap();
+                let mut fut = inner.ready();
+                fut.as_mut().poll(cx)
+            }
+        }
+        Box::pin(F(Self(self.0.clone())))
+    }
+}
+
 mod worker_thread_stdin;
 pub use self::worker_thread_stdin::{stdin, Stdin};
 
@@ -181,6 +231,72 @@ impl Subscribe for OutputStream {
     async fn ready(&mut self) {}
 }
 
+/// A wrapper of [`crate::pipe::AsyncWriteStream`] that implements
+/// [`StdoutStream`]. Note that the [`HostOutputStream`] impl for this is not
+/// correct when used for interleaved async IO.
+pub struct AsyncStdoutStream(Arc<Mutex<crate::pipe::AsyncWriteStream>>);
+
+impl AsyncStdoutStream {
+    pub fn new(s: crate::pipe::AsyncWriteStream) -> Self {
+        Self(Arc::new(Mutex::new(s)))
+    }
+}
+
+impl StdoutStream for AsyncStdoutStream {
+    fn stream(&self) -> Box<dyn HostOutputStream> {
+        Box::new(Self(self.0.clone()))
+    }
+    fn isatty(&self) -> bool {
+        false
+    }
+}
+
+// This implementation is known to be bogus. All check-writes and writes are
+// directed at the same underlying stream. The check-write/write protocol does
+// require the size returned by a check-write to be accepted by write, even if
+// other side-effects happen between those calls, and this implementation
+// permits another view (created by StdoutStream::stream()) of the same
+// underlying stream to accept a write which will invalidate a prior
+// check-write of another view.
+// Ultimately, the Std{in,out}Stream::stream() methods exist because many
+// different places in a linked component (which may itself contain many
+// modules) may need to access stdio without any coordination to keep those
+// accesses all using pointing to the same resource. So, we allow many
+// resources to be created. We have the reasonable expectation that programs
+// won't attempt to interleave async IO from these disparate uses of stdio.
+// If that expectation doesn't turn out to be true, and you find yourself at
+// this comment to correct it: sorry about that.
+impl HostOutputStream for AsyncStdoutStream {
+    fn check_write(&mut self) -> Result<usize, StreamError> {
+        self.0.lock().unwrap().check_write()
+    }
+    fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> {
+        self.0.lock().unwrap().write(bytes)
+    }
+    fn flush(&mut self) -> Result<(), StreamError> {
+        self.0.lock().unwrap().flush()
+    }
+}
+
+impl Subscribe for AsyncStdoutStream {
+    fn ready<'a, 'b>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + Send + 'b>>
+    where
+        Self: 'b,
+        'a: 'b,
+    {
+        struct F(AsyncStdoutStream);
+        impl Future for F {
+            type Output = ();
+            fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+                let mut inner = self.0 .0.lock().unwrap();
+                let mut fut = inner.ready();
+                fut.as_mut().poll(cx)
+            }
+        }
+        Box::pin(F(Self(self.0.clone())))
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum IsATTY {
     Yes,
@@ -255,3 +371,73 @@ impl<T: WasiView> terminal_stderr::Host for T {
         }
     }
 }
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn memory_stdin_stream() {
+        // A StdinStream has the property that there are multiple
+        // HostInputStreams created, using the stream() method which are each
+        // views on the same shared state underneath. Consuming input on one
+        // stream results in consuming that input on all streams.
+        //
+        // The simplest way to measure this is to check if the MemoryInputPipe
+        // impl of StdinStream follows this property.
+
+        let pipe = super::pipe::MemoryInputPipe::new(
+            "the quick brown fox jumped over the three lazy dogs",
+        );
+
+        use super::StdinStream;
+
+        let mut view1 = pipe.stream();
+        let mut view2 = pipe.stream();
+
+        let read1 = view1.read(10).expect("read first 10 bytes");
+        assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes");
+        let read2 = view2.read(10).expect("read second 10 bytes");
+        assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes");
+        let read3 = view1.read(10).expect("read third 10 bytes");
+        assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes");
+        let read4 = view2.read(10).expect("read fourth 10 bytes");
+        assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes");
+    }
+    #[tokio::test]
+    async fn async_stdin_stream() {
+        // A StdinStream has the property that there are multiple
+        // HostInputStreams created, using the stream() method which are each
+        // views on the same shared state underneath. Consuming input on one
+        // stream results in consuming that input on all streams.
+        //
+        // AsyncStdinStream is a slightly more complex impl of StdinStream
+        // than the MemoryInputPipe above. We can create an AsyncReadStream
+        // from a file on the disk, and an AsyncStdinStream from that common
+        // stream, then check that the same property holds as above.
+
+        let dir = tempfile::tempdir().unwrap();
+        let mut path = std::path::PathBuf::from(dir.path());
+        path.push("file");
+        std::fs::write(&path, "the quick brown fox jumped over the three lazy dogs").unwrap();
+
+        let file = tokio::fs::File::open(&path)
+            .await
+            .expect("open created file");
+        let stdin_stream = super::AsyncStdinStream::new(crate::pipe::AsyncReadStream::new(file));
+
+        use super::StdinStream;
+
+        let mut view1 = stdin_stream.stream();
+        let mut view2 = stdin_stream.stream();
+
+        view1.ready().await;
+
+        let read1 = view1.read(10).expect("read first 10 bytes");
+        assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes");
+        let read2 = view2.read(10).expect("read second 10 bytes");
+        assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes");
+        let read3 = view1.read(10).expect("read third 10 bytes");
+        assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes");
+        let read4 = view2.read(10).expect("read fourth 10 bytes");
+        assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes");
+    }
+}
diff --git a/examples/wasi-async/main.rs b/examples/wasi-async/main.rs
index 87661cd24..9fea4ce8a 100644
--- a/examples/wasi-async/main.rs
+++ b/examples/wasi-async/main.rs
@@ -46,7 +46,7 @@ async fn main() -> Result<()> {
     // Add the WASI preview1 API to the linker (will be implemented in terms of
     // the preview2 API)
     let mut linker: Linker<WasiHostCtx> = Linker::new(&engine);
-    wasmtime_wasi::preview1::add_to_linker_async(&mut linker)?;
+    wasmtime_wasi::preview1::add_to_linker_async(&mut linker, |t| t)?;
 
     // Add capabilities (e.g. filesystem access) to the WASI preview2 context here.
     let wasi_ctx = wasmtime_wasi::WasiCtxBuilder::new().inherit_stdio().build();
diff --git a/src/commands/run.rs b/src/commands/run.rs
index c0c04c787..8f25ba91e 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -628,9 +628,9 @@ impl RunCommand {
                         // default-disabled in the future.
                         (Some(true), _) | (None, Some(false) | None) => {
                             if self.run.common.wasi.preview0 != Some(false) {
-                                wasmtime_wasi::preview0::add_to_linker_sync(linker)?;
+                                wasmtime_wasi::preview0::add_to_linker_sync(linker, |t| t)?;
                             }
-                            wasmtime_wasi::preview1::add_to_linker_sync(linker)?;
+                            wasmtime_wasi::preview1::add_to_linker_sync(linker, |t| t)?;
                             self.set_preview2_ctx(store)?;
                         }
                     }


Test patch:
diff --git a/crates/wasi/tests/all/preview1.rs b/crates/wasi/tests/all/preview1.rs
index 7dd8f2db5..f0d958fdf 100644
--- a/crates/wasi/tests/all/preview1.rs
+++ b/crates/wasi/tests/all/preview1.rs
@@ -8,10 +8,10 @@ async fn run(path: &str, inherit_stdio: bool) -> Result<()> {
     let path = Path::new(path);
     let name = path.file_stem().unwrap().to_str().unwrap();
     let mut config = Config::new();
-    config.async_support(true).wasm_component_model(true);
+    config.async_support(true);
     let engine = Engine::new(&config)?;
     let mut linker = Linker::new(&engine);
-    add_to_linker_async(&mut linker)?;
+    add_to_linker_async(&mut linker, |t| t)?;
 
     let module = Module::from_file(&engine, path)?;
     let (mut store, _td) = store(&engine, name, inherit_stdio)?;
diff --git a/tests/all/host_funcs.rs b/tests/all/host_funcs.rs
index bf8f4b6de..9814b6140 100644
--- a/tests/all/host_funcs.rs
+++ b/tests/all/host_funcs.rs
@@ -1,7 +1,5 @@
 use anyhow::bail;
 use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
-use wasi_common::sync::WasiCtxBuilder;
-use wasi_common::I32Exit;
 use wasmtime::*;
 
 #[test]
@@ -717,7 +715,7 @@ fn store_with_context() -> Result<()> {
 fn wasi_imports() -> Result<()> {
     let engine = Engine::default();
     let mut linker = Linker::new(&engine);
-    wasi_common::sync::add_to_linker(&mut linker, |s| s)?;
+    wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |t| t)?;
 
     let wasm = wat::parse_str(
         r#"
@@ -730,14 +728,14 @@ fn wasi_imports() -> Result<()> {
     )?;
 
     let module = Module::new(&engine, wasm)?;
-    let mut store = Store::new(&engine, WasiCtxBuilder::new().build());
+    let mut store = Store::new(&engine, wasmtime_wasi::WasiCtxBuilder::new().build_p1());
     let instance = linker.instantiate(&mut store, &module)?;
 
     let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
     let exit = start
         .call(&mut store, ())
         .unwrap_err()
-        .downcast::<I32Exit>()?;
+        .downcast::<wasmtime_wasi::I32Exit>()?;
     assert_eq!(exit.0, 123);
 
     Ok(())
diff --git a/tests/all/traps.rs b/tests/all/traps.rs
index affb2ac7c..6679704e0 100644
--- a/tests/all/traps.rs
+++ b/tests/all/traps.rs
@@ -751,13 +751,8 @@ fn parse_dwarf_info() -> Result<()> {
     let engine = Engine::new(&config)?;
     let module = Module::new(&engine, &wasm)?;
     let mut linker = Linker::new(&engine);
-    wasi_common::sync::add_to_linker(&mut linker, |s| s)?;
-    let mut store = Store::new(
-        &engine,
-        wasi_common::sync::WasiCtxBuilder::new()
-            .inherit_stdio()
-            .build(),
-    );
+    wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |t| t)?;
+    let mut store = Store::new(&engine, wasmtime_wasi::WasiCtxBuilder::new().build_p1());
     linker.module(&mut store, "", &module)?;
     let run = linker.get_default(&mut store, "")?;
     let trap = run.call(&mut store, &[], &mut []).unwrap_err();