From ca9557646aac2d625a6d1d040e6caf065d0340f6 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Wed, 22 Jan 2025 11:27:15 -0800
Subject: [PATCH] Split off wasmtime-wasi-io crate from wasmtime-wasi (#10036)
* stub: wasmtime-wasi-io crate
* wasmtime: component::ResourceTableError now impls core::error::Error
for compatibility without std
* relocate much of the wasi-io impl into wasmtime-wasi-io
* stump of poll that uses in_tokio
* finish moving instances over to wasmtime_wasi_io
* redirect wasmtime_wasi's bindgen properly over to wasmtime_wasi_io
* wasmtime-wasi-http: point directly at wasmtime_wasi_io in sources
it worked without these changes because all the re-exports are in the
right places, but this is nice to do
* comment work
* fix streams rename, migrate bindings to its own file
* move wasi-io impls into their own mod with appropriate name. check in CI.
* change ResourceTable::iter_entries from taking a HashMap to BTreeMap so it works without std
* crate-level docs for wasmtime-wasi-io
* more docs
* more docs, wasi-io gives an add_to_linker function for async only
* wasi-io: inline view into lib.rs. improve docs.
* more streams vs stream fixes...
* wasi-http stream->streams fixes
* fix adding wasmtime-wasi-io to public crates
* wasmtime-cli: drop overzealous `=` version constraint on wasmtime-wasi-http
wasmtime-wasi-http is part of the public API where we guarantee semver
is obeyed
* fix doctest
* mechanically rename the wasi-io pub traits, and resource types
resource type Pollable -> DynPollable
resource type InputStream -> DynInputStream
resource type OutputStream -> DynOutputStream
trait Subscribe -> Pollable
trait HostInputStream -> InputStream
trait HostOutputStream -> OutputStream
type alias PollableFuture -> DynFuture (little-used)
* delete unused ClosureFuture alias
* doc fixes
* wasmtime-wasi-http: use all of wasmtime-wasi-io through wasmtime-wasi re-exports
* fix nostd build
* missing separator. i love yml
* make wasmtime-wasi-io #![no_std]
---
.github/workflows/main.yml | 3 +-
Cargo.lock | 12 +
Cargo.toml | 5 +-
ci/vendor-wit.sh | 4 +
crates/wasi-http/src/bindings.rs | 8 +-
crates/wasi-http/src/body.rs | 16 +-
crates/wasi-http/src/error.rs | 2 +-
crates/wasi-http/src/lib.rs | 17 +-
crates/wasi-http/src/types.rs | 4 +-
crates/wasi-http/src/types_impl.rs | 17 +-
crates/wasi-io/Cargo.toml | 30 ++
crates/wasi-io/src/bindings.rs | 29 ++
crates/wasi-io/src/impls.rs | 297 +++++++++++++++
crates/wasi-io/src/lib.rs | 183 ++++++++++
crates/wasi-io/src/poll.rs | 120 ++++++
.../src/stream.rs => wasi-io/src/streams.rs} | 23 +-
crates/wasi-io/wit/deps/io/error.wit | 34 ++
crates/wasi-io/wit/deps/io/poll.wit | 47 +++
crates/wasi-io/wit/deps/io/streams.wit | 290 +++++++++++++++
crates/wasi-io/wit/deps/io/world.wit | 10 +
crates/wasi-io/wit/world.wit | 6 +
crates/wasi/Cargo.toml | 1 +
crates/wasi/src/bindings.rs | 38 +-
crates/wasi/src/ctx.rs | 25 +-
crates/wasi/src/filesystem.rs | 12 +-
crates/wasi/src/host/clocks.rs | 15 +-
crates/wasi/src/host/filesystem.rs | 14 +-
crates/wasi/src/host/io.rs | 344 +++---------------
crates/wasi/src/host/tcp.rs | 21 +-
crates/wasi/src/host/udp.rs | 24 +-
crates/wasi/src/ip_name_lookup.rs | 6 +-
crates/wasi/src/lib.rs | 126 +++++--
crates/wasi/src/pipe.rs | 34 +-
crates/wasi/src/poll.rs | 242 +-----------
crates/wasi/src/preview1.rs | 13 +-
crates/wasi/src/stdio.rs | 94 +++--
crates/wasi/src/stdio/worker_thread_stdin.rs | 14 +-
crates/wasi/src/tcp.rs | 26 +-
crates/wasi/src/udp.rs | 4 +-
crates/wasi/src/view.rs | 79 ++--
crates/wasi/src/write_stream.rs | 10 +-
crates/wasi/tests/all/api.rs | 2 +-
crates/wasi/tests/process_stdin.rs | 2 +-
.../src/runtime/component/resource_table.rs | 10 +-
scripts/publish.rs | 3 +
src/commands/serve.rs | 6 +-
46 files changed, 1505 insertions(+), 817 deletions(-)
create mode 100644 crates/wasi-io/Cargo.toml
create mode 100644 crates/wasi-io/src/bindings.rs
create mode 100644 crates/wasi-io/src/impls.rs
create mode 100644 crates/wasi-io/src/lib.rs
create mode 100644 crates/wasi-io/src/poll.rs
rename crates/{wasi/src/stream.rs => wasi-io/src/streams.rs} (94%)
create mode 100644 crates/wasi-io/wit/deps/io/error.wit
create mode 100644 crates/wasi-io/wit/deps/io/poll.wit
create mode 100644 crates/wasi-io/wit/deps/io/streams.wit
create mode 100644 crates/wasi-io/wit/deps/io/world.wit
create mode 100644 crates/wasi-io/wit/world.wit
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f1e67d490f70..1010c644c60a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -541,7 +541,8 @@ jobs:
cargo check -p wasmtime --no-default-features --features runtime,component-model &&
cargo check -p wasmtime --no-default-features --features runtime,gc,component-model &&
cargo check -p cranelift-control --no-default-features &&
- cargo check -p pulley-interpreter --features encode,decode,disas,interp
+ cargo check -p pulley-interpreter --features encode,decode,disas,interp &&
+ cargo check -p wasmtime-wasi-io --no-default-features
# Use `cross` for illumos to have a C compiler/linker available.
- target: x86_64-unknown-illumos
os: ubuntu-latest
diff --git a/Cargo.lock b/Cargo.lock
index 8dfad6436a54..4aec396b2f42 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4592,6 +4592,7 @@ dependencies = [
"tracing-subscriber",
"url",
"wasmtime",
+ "wasmtime-wasi-io",
"wiggle",
"windows-sys 0.59.0",
]
@@ -4633,6 +4634,17 @@ dependencies = [
"webpki-roots",
]
+[[package]]
+name = "wasmtime-wasi-io"
+version = "30.0.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bytes",
+ "futures",
+ "wasmtime",
+]
+
[[package]]
name = "wasmtime-wasi-keyvalue"
version = "30.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 5b59df6b36ed..1f778f93828a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -230,7 +230,8 @@ wasmtime-fiber = { path = "crates/fiber", version = "=30.0.0" }
wasmtime-jit-debug = { path = "crates/jit-debug", version = "=30.0.0" }
wasmtime-wast = { path = "crates/wast", version = "=30.0.0" }
wasmtime-wasi = { path = "crates/wasi", version = "30.0.0", default-features = false }
-wasmtime-wasi-http = { path = "crates/wasi-http", version = "=30.0.0", default-features = false }
+wasmtime-wasi-io = { path = "crates/wasi-io", version = "30.0.0", default-features = false }
+wasmtime-wasi-http = { path = "crates/wasi-http", version = "30.0.0", default-features = false }
wasmtime-wasi-nn = { path = "crates/wasi-nn", version = "30.0.0" }
wasmtime-wasi-config = { path = "crates/wasi-config", version = "30.0.0" }
wasmtime-wasi-keyvalue = { path = "crates/wasi-keyvalue", version = "30.0.0" }
@@ -355,7 +356,7 @@ hyper = "1.0.1"
http = "1.0.0"
http-body = "1.0.0"
http-body-util = "0.1.0"
-bytes = "1.4"
+bytes = { version = "1.4", default-features = false }
futures = { version = "0.3.27", default-features = false }
indexmap = { version = "2.0.0", default-features = false }
pretty_env_logger = "0.5.0"
diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh
index 252fb9a863e5..b71ca579a70a 100755
--- a/ci/vendor-wit.sh
+++ b/ci/vendor-wit.sh
@@ -36,6 +36,10 @@ make_vendor() {
cache_dir=$(mktemp -d)
+make_vendor "wasi-io" "
+ io@v0.2.3
+"
+
make_vendor "wasi" "
cli@v0.2.3
clocks@v0.2.3
diff --git a/crates/wasi-http/src/bindings.rs b/crates/wasi-http/src/bindings.rs
index 00054e5a644b..f88fd1216f7f 100644
--- a/crates/wasi-http/src/bindings.rs
+++ b/crates/wasi-http/src/bindings.rs
@@ -59,9 +59,11 @@ pub mod sync {
tracing: true,
async: false,
with: {
- "wasi:http": crate::bindings::http, // http is in this crate
- "wasi:io": wasmtime_wasi::bindings::sync::io, // io is sync
- "wasi": wasmtime_wasi::bindings, // everything else
+ // http is in this crate
+ "wasi:http": crate::bindings::http,
+ // sync requires the wrapper in the wasmtime_wasi crate, in
+ // order to have in_tokio
+ "wasi:io": wasmtime_wasi::bindings::sync::io,
},
require_store_data_send: true,
});
diff --git a/crates/wasi-http/src/body.rs b/crates/wasi-http/src/body.rs
index 96c783964746..8adab1352a22 100644
--- a/crates/wasi-http/src/body.rs
+++ b/crates/wasi-http/src/body.rs
@@ -13,7 +13,7 @@ use std::{pin::Pin, sync::Arc, time::Duration};
use tokio::sync::{mpsc, oneshot};
use wasmtime_wasi::{
runtime::{poll_noop, AbortOnDropJoinHandle},
- HostInputStream, HostOutputStream, StreamError, Subscribe,
+ InputStream, OutputStream, Pollable, StreamError,
};
/// Common type for incoming bodies.
@@ -234,7 +234,7 @@ enum IncomingBodyStreamState {
}
#[async_trait::async_trait]
-impl HostInputStream for HostIncomingBodyStream {
+impl InputStream for HostIncomingBodyStream {
fn read(&mut self, size: usize) -> Result {
loop {
// Handle buffered data/errors if any
@@ -271,7 +271,7 @@ impl HostInputStream for HostIncomingBodyStream {
}
#[async_trait::async_trait]
-impl Subscribe for HostIncomingBodyStream {
+impl Pollable for HostIncomingBodyStream {
async fn ready(&mut self) {
if !self.buffer.is_empty() || self.error.is_some() {
return;
@@ -327,7 +327,7 @@ pub enum HostFutureTrailers {
}
#[async_trait::async_trait]
-impl Subscribe for HostFutureTrailers {
+impl Pollable for HostFutureTrailers {
async fn ready(&mut self) {
let body = match self {
HostFutureTrailers::Waiting(body) => body,
@@ -415,7 +415,7 @@ impl WrittenState {
/// The concrete type behind a `wasi:http/types/outgoing-body` resource.
pub struct HostOutgoingBody {
/// The output stream that the body is written to.
- body_output_stream: Option>,
+ body_output_stream: Option>,
context: StreamContext,
written: Option,
finish_sender: Option>,
@@ -499,7 +499,7 @@ impl HostOutgoingBody {
}
/// Take the output stream, if it's available.
- pub fn take_output_stream(&mut self) -> Option> {
+ pub fn take_output_stream(&mut self) -> Option> {
self.body_output_stream.take()
}
@@ -605,7 +605,7 @@ impl BodyWriteStream {
}
#[async_trait::async_trait]
-impl HostOutputStream for BodyWriteStream {
+impl OutputStream for BodyWriteStream {
fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> {
let len = bytes.len();
match self.writer.try_send(bytes) {
@@ -665,7 +665,7 @@ impl HostOutputStream for BodyWriteStream {
}
#[async_trait::async_trait]
-impl Subscribe for BodyWriteStream {
+impl Pollable for BodyWriteStream {
async fn ready(&mut self) {
// Attempt to perform a reservation for a send. If there's capacity in
// the channel or it's already closed then this will return immediately.
diff --git a/crates/wasi-http/src/error.rs b/crates/wasi-http/src/error.rs
index 1c60a4b0771e..c2f440975d6d 100644
--- a/crates/wasi-http/src/error.rs
+++ b/crates/wasi-http/src/error.rs
@@ -1,7 +1,7 @@
use crate::bindings::http::types::ErrorCode;
use std::error::Error;
use std::fmt;
-use wasmtime_wasi::ResourceTableError;
+use wasmtime::component::ResourceTableError;
/// A [`Result`] type where the error type defaults to [`HttpError`].
pub type HttpResult = Result;
diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs
index 9c57a6b6a974..0a9544f7c4f8 100644
--- a/crates/wasi-http/src/lib.rs
+++ b/crates/wasi-http/src/lib.rs
@@ -285,12 +285,13 @@ where
T: WasiHttpView + wasmtime_wasi::WasiView,
{
let io_closure = type_annotate_io::(|t| wasmtime_wasi::IoImpl(t));
- let closure = type_annotate_wasi::(|t| wasmtime_wasi::WasiImpl(wasmtime_wasi::IoImpl(t)));
- wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?;
- wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::io::poll::add_to_linker_get_host(l, io_closure)?;
wasmtime_wasi::bindings::io::error::add_to_linker_get_host(l, io_closure)?;
wasmtime_wasi::bindings::io::streams::add_to_linker_get_host(l, io_closure)?;
+
+ let closure = type_annotate_wasi::(|t| wasmtime_wasi::WasiImpl(wasmtime_wasi::IoImpl(t)));
+ wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?;
+ wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::cli::stdin::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::cli::stdout::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::cli::stderr::add_to_linker_get_host(l, closure)?;
@@ -383,13 +384,17 @@ where
T: WasiHttpView + wasmtime_wasi::WasiView,
{
let io_closure = type_annotate_io::(|t| wasmtime_wasi::IoImpl(t));
+ // For the sync linker, use the definitions of poll and streams from the
+ // wasmtime_wasi::bindings::sync space because those are defined using in_tokio.
+ wasmtime_wasi::bindings::sync::io::poll::add_to_linker_get_host(l, io_closure)?;
+ wasmtime_wasi::bindings::sync::io::streams::add_to_linker_get_host(l, io_closure)?;
+ // The error interface in the wasmtime_wasi is synchronous
+ wasmtime_wasi::bindings::io::error::add_to_linker_get_host(l, io_closure)?;
+
let closure = type_annotate_wasi::(|t| wasmtime_wasi::WasiImpl(wasmtime_wasi::IoImpl(t)));
wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?;
- wasmtime_wasi::bindings::sync::io::poll::add_to_linker_get_host(l, io_closure)?;
- wasmtime_wasi::bindings::sync::io::streams::add_to_linker_get_host(l, io_closure)?;
- wasmtime_wasi::bindings::io::error::add_to_linker_get_host(l, io_closure)?;
wasmtime_wasi::bindings::cli::stdin::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::cli::stdout::add_to_linker_get_host(l, closure)?;
wasmtime_wasi::bindings::cli::stderr::add_to_linker_get_host(l, closure)?;
diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs
index 35bda46f3917..7a7b518db5ee 100644
--- a/crates/wasi-http/src/types.rs
+++ b/crates/wasi-http/src/types.rs
@@ -18,7 +18,7 @@ use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::timeout;
use wasmtime::component::{Resource, ResourceTable};
-use wasmtime_wasi::{runtime::AbortOnDropJoinHandle, IoImpl, IoView, Subscribe};
+use wasmtime_wasi::{runtime::AbortOnDropJoinHandle, IoImpl, IoView, Pollable};
/// Capture the state necessary for use in the wasi-http API implementation.
#[derive(Debug)]
@@ -715,7 +715,7 @@ impl HostFutureIncomingResponse {
}
#[async_trait::async_trait]
-impl Subscribe for HostFutureIncomingResponse {
+impl Pollable for HostFutureIncomingResponse {
async fn ready(&mut self) {
if let Self::Pending(handle) = self {
*self = Self::Ready(handle.await);
diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index f3a83f85d6d7..8f27143467a2 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -13,11 +13,8 @@ use crate::{
use anyhow::Context;
use std::any::Any;
use std::str::FromStr;
-use wasmtime::component::{Resource, ResourceTable};
-use wasmtime_wasi::{
- bindings::io::streams::{InputStream, OutputStream},
- IoView, Pollable, ResourceTableError,
-};
+use wasmtime::component::{Resource, ResourceTable, ResourceTableError};
+use wasmtime_wasi::{DynInputStream, DynOutputStream, DynPollable, IoView};
impl crate::bindings::http::types::Host for WasiHttpImpl
where
@@ -662,7 +659,7 @@ where
fn subscribe(
&mut self,
index: Resource,
- ) -> wasmtime::Result> {
+ ) -> wasmtime::Result> {
wasmtime_wasi::subscribe(self.table(), index)
}
@@ -704,11 +701,11 @@ where
fn stream(
&mut self,
id: Resource,
- ) -> wasmtime::Result, ()>> {
+ ) -> wasmtime::Result, ()>> {
let body = self.table().get_mut(&id)?;
if let Some(stream) = body.take_stream() {
- let stream: InputStream = Box::new(stream);
+ let stream: DynInputStream = Box::new(stream);
let stream = self.table().push_child(stream, &id)?;
return Ok(Ok(stream));
}
@@ -883,7 +880,7 @@ where
fn subscribe(
&mut self,
id: Resource,
- ) -> wasmtime::Result> {
+ ) -> wasmtime::Result> {
wasmtime_wasi::subscribe(self.table(), id)
}
}
@@ -895,7 +892,7 @@ where
fn write(
&mut self,
id: Resource,
- ) -> wasmtime::Result, ()>> {
+ ) -> wasmtime::Result, ()>> {
let body = self.table().get_mut(&id)?;
if let Some(stream) = body.take_output_stream() {
let id = self.table().push_child(stream, &id)?;
diff --git a/crates/wasi-io/Cargo.toml b/crates/wasi-io/Cargo.toml
new file mode 100644
index 000000000000..70e379ea6a8f
--- /dev/null
+++ b/crates/wasi-io/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "wasmtime-wasi-io"
+version.workspace = true
+authors.workspace = true
+description = "wasi-io common traits to be shared among other wasi implementations"
+license = "Apache-2.0 WITH LLVM-exception"
+categories = ["wasm"]
+keywords = ["webassembly", "wasm"]
+repository = "https://github.com/bytecodealliance/wasmtime"
+edition.workspace = true
+rust-version.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+wasmtime = { workspace = true, features = ["component-model", "async", "runtime"] }
+anyhow = { workspace = true }
+bytes = { workspace = true }
+async-trait = { workspace = true }
+futures = { workspace = true }
+
+[features]
+default = [ "std" ]
+std = [
+ "bytes/std",
+ "anyhow/std",
+ "wasmtime/std",
+]
+
diff --git a/crates/wasi-io/src/bindings.rs b/crates/wasi-io/src/bindings.rs
new file mode 100644
index 000000000000..b90c5a75f0db
--- /dev/null
+++ b/crates/wasi-io/src/bindings.rs
@@ -0,0 +1,29 @@
+wasmtime::component::bindgen!({
+ path: "wit",
+ trappable_imports: true,
+ with: {
+ "wasi:io/poll/pollable": crate::poll::DynPollable,
+ "wasi:io/streams/input-stream": crate::streams::DynInputStream,
+ "wasi:io/streams/output-stream": crate::streams::DynOutputStream,
+ "wasi:io/error/error": crate::streams::Error,
+ },
+ async: {
+ only_imports: [
+ "poll",
+ "[method]pollable.block",
+ "[method]pollable.ready",
+ "[method]input-stream.blocking-read",
+ "[method]input-stream.blocking-skip",
+ "[drop]input-stream",
+ "[method]output-stream.blocking-splice",
+ "[method]output-stream.blocking-flush",
+ "[method]output-stream.blocking-write",
+ "[method]output-stream.blocking-write-and-flush",
+ "[method]output-stream.blocking-write-zeroes-and-flush",
+ "[drop]output-stream",
+ ]
+ },
+ trappable_error_type: {
+ "wasi:io/streams/stream-error" => crate::streams::StreamError,
+ }
+});
diff --git a/crates/wasi-io/src/impls.rs b/crates/wasi-io/src/impls.rs
new file mode 100644
index 000000000000..374f0f11f816
--- /dev/null
+++ b/crates/wasi-io/src/impls.rs
@@ -0,0 +1,297 @@
+use crate::bindings::wasi::io::{error, poll, streams};
+use crate::poll::{subscribe, DynFuture, DynPollable, MakeFuture};
+use crate::streams::{DynInputStream, DynOutputStream, StreamError, StreamResult};
+use crate::{IoImpl, IoView};
+use alloc::collections::BTreeMap;
+use alloc::string::String;
+use alloc::vec::Vec;
+use anyhow::{anyhow, Result};
+use core::future::Future;
+use core::pin::Pin;
+use core::task::{Context, Poll};
+use wasmtime::component::Resource;
+
+impl poll::Host for IoImpl {
+ async fn poll(&mut self, pollables: Vec>) -> Result> {
+ type ReadylistIndex = u32;
+
+ if pollables.is_empty() {
+ return Err(anyhow!("empty poll list"));
+ }
+
+ let table = self.table();
+
+ let mut table_futures: BTreeMap)> = BTreeMap::new();
+
+ for (ix, p) in pollables.iter().enumerate() {
+ let ix: u32 = ix.try_into()?;
+
+ let pollable = table.get(p)?;
+ let (_, list) = table_futures
+ .entry(pollable.index)
+ .or_insert((pollable.make_future, Vec::new()));
+ list.push(ix);
+ }
+
+ let mut futures: Vec<(DynFuture<'_>, Vec)> = Vec::new();
+ for (entry, (make_future, readylist_indices)) in table.iter_entries(table_futures) {
+ let entry = entry?;
+ futures.push((make_future(entry), readylist_indices));
+ }
+
+ struct PollList<'a> {
+ futures: Vec<(DynFuture<'a>, Vec)>,
+ }
+ impl<'a> Future for PollList<'a> {
+ type Output = Vec;
+
+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ let mut any_ready = false;
+ let mut results = Vec::new();
+ for (fut, readylist_indicies) in self.futures.iter_mut() {
+ match fut.as_mut().poll(cx) {
+ Poll::Ready(()) => {
+ results.extend_from_slice(readylist_indicies);
+ any_ready = true;
+ }
+ Poll::Pending => {}
+ }
+ }
+ if any_ready {
+ Poll::Ready(results)
+ } else {
+ Poll::Pending
+ }
+ }
+ }
+
+ Ok(PollList { futures }.await)
+ }
+}
+
+impl crate::bindings::wasi::io::poll::HostPollable for IoImpl {
+ async fn block(&mut self, pollable: Resource) -> Result<()> {
+ let table = self.table();
+ let pollable = table.get(&pollable)?;
+ let ready = (pollable.make_future)(table.get_any_mut(pollable.index)?);
+ ready.await;
+ Ok(())
+ }
+ async fn ready(&mut self, pollable: Resource) -> Result {
+ let table = self.table();
+ let pollable = table.get(&pollable)?;
+ let ready = (pollable.make_future)(table.get_any_mut(pollable.index)?);
+ futures::pin_mut!(ready);
+ Ok(matches!(
+ futures::future::poll_immediate(ready).await,
+ Some(())
+ ))
+ }
+ fn drop(&mut self, pollable: Resource) -> Result<()> {
+ let pollable = self.table().delete(pollable)?;
+ if let Some(delete) = pollable.remove_index_on_delete {
+ delete(self.table(), pollable.index)?;
+ }
+ Ok(())
+ }
+}
+
+impl error::Host for IoImpl {}
+
+impl streams::Host for IoImpl {
+ fn convert_stream_error(&mut self, err: StreamError) -> Result {
+ match err {
+ StreamError::Closed => Ok(streams::StreamError::Closed),
+ StreamError::LastOperationFailed(e) => Ok(streams::StreamError::LastOperationFailed(
+ self.table().push(e)?,
+ )),
+ StreamError::Trap(e) => Err(e),
+ }
+ }
+}
+
+impl error::HostError for IoImpl {
+ fn drop(&mut self, err: Resource) -> Result<()> {
+ self.table().delete(err)?;
+ Ok(())
+ }
+
+ fn to_debug_string(&mut self, err: Resource) -> Result {
+ Ok(alloc::format!("{:?}", self.table().get(&err)?))
+ }
+}
+
+impl streams::HostOutputStream for IoImpl {
+ async fn drop(&mut self, stream: Resource) -> Result<()> {
+ self.table().delete(stream)?.cancel().await;
+ Ok(())
+ }
+
+ fn check_write(&mut self, stream: Resource) -> StreamResult {
+ let bytes = self.table().get_mut(&stream)?.check_write()?;
+ Ok(bytes as u64)
+ }
+
+ fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> {
+ self.table().get_mut(&stream)?.write(bytes.into())?;
+ Ok(())
+ }
+
+ fn subscribe(&mut self, stream: Resource) -> Result> {
+ subscribe(self.table(), stream)
+ }
+
+ async fn blocking_write_and_flush(
+ &mut self,
+ stream: Resource,
+ bytes: Vec,
+ ) -> StreamResult<()> {
+ if bytes.len() > 4096 {
+ return Err(StreamError::trap(
+ "Buffer too large for blocking-write-and-flush (expected at most 4096)",
+ ));
+ }
+
+ self.table()
+ .get_mut(&stream)?
+ .blocking_write_and_flush(bytes.into())
+ .await
+ }
+
+ async fn blocking_write_zeroes_and_flush(
+ &mut self,
+ stream: Resource,
+ len: u64,
+ ) -> StreamResult<()> {
+ if len > 4096 {
+ return Err(StreamError::trap(
+ "Buffer too large for blocking-write-zeroes-and-flush (expected at most 4096)",
+ ));
+ }
+
+ self.table()
+ .get_mut(&stream)?
+ .blocking_write_zeroes_and_flush(len as usize)
+ .await
+ }
+
+ fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> {
+ self.table().get_mut(&stream)?.write_zeroes(len as usize)?;
+ Ok(())
+ }
+
+ fn flush(&mut self, stream: Resource) -> StreamResult<()> {
+ self.table().get_mut(&stream)?.flush()?;
+ Ok(())
+ }
+
+ async fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> {
+ let s = self.table().get_mut(&stream)?;
+ s.flush()?;
+ s.write_ready().await?;
+ Ok(())
+ }
+
+ fn splice(
+ &mut self,
+ dest: Resource,
+ src: Resource,
+ len: u64,
+ ) -> StreamResult {
+ let len = len.try_into().unwrap_or(usize::MAX);
+
+ let permit = {
+ let output = self.table().get_mut(&dest)?;
+ output.check_write()?
+ };
+ let len = len.min(permit);
+ if len == 0 {
+ return Ok(0);
+ }
+
+ let contents = self.table().get_mut(&src)?.read(len)?;
+
+ let len = contents.len();
+ if len == 0 {
+ return Ok(0);
+ }
+
+ let output = self.table().get_mut(&dest)?;
+ output.write(contents)?;
+ Ok(len.try_into().expect("usize can fit in u64"))
+ }
+
+ async fn blocking_splice(
+ &mut self,
+ dest: Resource,
+ src: Resource,
+ len: u64,
+ ) -> StreamResult {
+ let len = len.try_into().unwrap_or(usize::MAX);
+
+ let permit = {
+ let output = self.table().get_mut(&dest)?;
+ output.write_ready().await?
+ };
+ let len = len.min(permit);
+ if len == 0 {
+ return Ok(0);
+ }
+
+ let contents = self.table().get_mut(&src)?.blocking_read(len).await?;
+
+ let len = contents.len();
+ if len == 0 {
+ return Ok(0);
+ }
+
+ let output = self.table().get_mut(&dest)?;
+ output.blocking_write_and_flush(contents).await?;
+ Ok(len.try_into().expect("usize can fit in u64"))
+ }
+}
+
+impl streams::HostInputStream for IoImpl {
+ async fn drop(&mut self, stream: Resource) -> Result<()> {
+ self.table().delete(stream)?.cancel().await;
+ Ok(())
+ }
+
+ fn read(&mut self, stream: Resource, len: u64) -> StreamResult> {
+ let len = len.try_into().unwrap_or(usize::MAX);
+ let bytes = self.table().get_mut(&stream)?.read(len)?;
+ debug_assert!(bytes.len() <= len);
+ Ok(bytes.into())
+ }
+
+ async fn blocking_read(
+ &mut self,
+ stream: Resource,
+ len: u64,
+ ) -> StreamResult> {
+ let len = len.try_into().unwrap_or(usize::MAX);
+ let bytes = self.table().get_mut(&stream)?.blocking_read(len).await?;
+ debug_assert!(bytes.len() <= len);
+ Ok(bytes.into())
+ }
+
+ fn skip(&mut self, stream: Resource, len: u64) -> StreamResult {
+ let len = len.try_into().unwrap_or(usize::MAX);
+ let written = self.table().get_mut(&stream)?.skip(len)?;
+ Ok(written.try_into().expect("usize always fits in u64"))
+ }
+
+ async fn blocking_skip(
+ &mut self,
+ stream: Resource,
+ len: u64,
+ ) -> StreamResult {
+ let len = len.try_into().unwrap_or(usize::MAX);
+ let written = self.table().get_mut(&stream)?.blocking_skip(len).await?;
+ Ok(written.try_into().expect("usize always fits in u64"))
+ }
+
+ fn subscribe(&mut self, stream: Resource) -> Result> {
+ crate::poll::subscribe(self.table(), stream)
+ }
+}
diff --git a/crates/wasi-io/src/lib.rs b/crates/wasi-io/src/lib.rs
new file mode 100644
index 000000000000..5ffae0025727
--- /dev/null
+++ b/crates/wasi-io/src/lib.rs
@@ -0,0 +1,183 @@
+//! # Wasmtime's wasi-io Implementation
+//!
+//! This crate provides a Wasmtime host implementation of the WASI 0.2 (aka
+//! WASIp2 aka Preview 2) wasi-io package. The host implementation is
+//! abstract: it is exposed as a set of traits which other crates provide
+//! impls of.
+//!
+//! The wasi-io package is the foundation which defines how WASI programs
+//! interact with the scheduler. It provides the `pollable`, `input-stream`,
+//! and `output-stream` Component Model resources, which other packages
+//! (including wasi-filesystem, wasi-sockets, wasi-cli, and wasi-http)
+//! expose as the standard way to wait for readiness, and asynchronously read
+//! and write to streams.
+//!
+//! This crate is designed to have no unnecessary dependencies and, in
+//! particular, to be #![no_std].
+
+#![no_std]
+
+extern crate alloc;
+#[cfg(feature = "std")]
+#[macro_use]
+extern crate std;
+
+pub mod bindings;
+mod impls;
+pub mod poll;
+pub mod streams;
+
+#[doc(no_inline)]
+pub use async_trait::async_trait;
+
+use alloc::boxed::Box;
+use wasmtime::component::ResourceTable;
+
+/// A trait which provides access to the [`ResourceTable`] inside the
+/// embedder's `T` of [`Store`][`Store`].
+///
+/// This crate's WASI Host implementations depend on the contents of
+/// [`ResourceTable`]. The `T` type [`Store`][`Store`] is defined in each
+/// embedding of Wasmtime. These implementations is connected to the
+/// [`Linker`][`Linker`] by the
+/// [`add_to_linker_async`] function.
+///
+/// # Example
+///
+/// ```
+/// use wasmtime::{Config, Engine};
+/// use wasmtime::component::{ResourceTable, Linker};
+/// use wasmtime_wasi_io::{IoView, add_to_linker_async};
+///
+/// struct MyState {
+/// table: ResourceTable,
+/// }
+///
+/// impl IoView for MyState {
+/// fn table(&mut self) -> &mut ResourceTable { &mut self.table }
+/// }
+/// let mut config = Config::new();
+/// config.async_support(true);
+/// let engine = Engine::new(&config).unwrap();
+/// let mut linker: Linker = Linker::new(&engine);
+/// add_to_linker_async(&mut linker).unwrap();
+/// ```
+/// [`Store`]: wasmtime::Store
+/// [`Linker`]: wasmtime::component::Linker
+/// [`ResourceTable`]: wasmtime::component::ResourceTable
+///
+pub trait IoView: Send {
+ /// Yields mutable access to the internal resource management that this
+ /// context contains.
+ ///
+ /// Embedders can add custom resources to this table as well to give
+ /// resources to wasm as well.
+ fn table(&mut self) -> &mut ResourceTable;
+}
+impl IoView for &mut T {
+ fn table(&mut self) -> &mut ResourceTable {
+ T::table(self)
+ }
+}
+impl IoView for Box {
+ fn table(&mut self) -> &mut ResourceTable {
+ T::table(self)
+ }
+}
+
+/// A small newtype wrapper which serves as the basis for implementations of
+/// `Host` WASI traits in this crate.
+///
+/// This type is used as the basis for the implementation of all `Host` traits
+/// generated by `bindgen!` for WASI interfaces.
+///
+/// You don't need to use this type if you are using the root
+/// [`add_to_linker_async`] in this crate.
+///
+/// If you're calling the `add_to_linker` functions generated by `bindgen!`
+/// from the [`bindings` module](crate::bindings), you'll want to create a
+/// value of this type in the closures added to a [`Linker`].
+///
+/// [`Linker`]: wasmtime::component::Linker
+#[repr(transparent)]
+pub struct IoImpl(pub T);
+impl IoView for IoImpl {
+ fn table(&mut self) -> &mut ResourceTable {
+ T::table(&mut self.0)
+ }
+}
+
+/// Add the wasi-io host implementation from this crate into the `linker`
+/// provided.
+///
+/// This function will add the `async` variant of all interfaces into the
+/// [`Linker`] provided. By `async` this means that this function is only
+/// compatible with [`Config::async_support(true)`][async]. For embeddings
+/// with async support disabled, you'll need to use other crates, such as the
+/// [`wasmtime-wasi`] crate, which provides an [`add_to_linker_sync`] that
+/// includes an appropriate wasi-io implementation based on this crate's.
+///
+/// This function will add all interfaces implemented by this crate to the
+/// [`Linker`], which corresponds to the `wasi:io/imports` world supported by
+/// this crate.
+///
+/// [async]: wasmtime::Config::async_support
+/// [`Linker`]: wasmtime::component::Linker
+/// [`wasmtime-wasi`]: https://crates.io/crates/wasmtime-wasi
+/// [`add_to_linker_sync`]: https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/fn.add_to_linker_sync.html
+///
+///
+/// # Example
+///
+/// ```
+/// use wasmtime::{Engine, Result, Store, Config};
+/// use wasmtime::component::{ResourceTable, Linker};
+/// use wasmtime_wasi_io::IoView;
+///
+/// fn main() -> Result<()> {
+/// let mut config = Config::new();
+/// config.async_support(true);
+/// let engine = Engine::new(&config)?;
+///
+/// let mut linker = Linker::::new(&engine);
+/// wasmtime_wasi_io::add_to_linker_async(&mut linker)?;
+/// // ... add any further functionality to `linker` if desired ...
+///
+/// let mut store = Store::new(
+/// &engine,
+/// MyState {
+/// table: ResourceTable::new(),
+/// },
+/// );
+///
+/// // ... use `linker` to instantiate within `store` ...
+///
+/// Ok(())
+/// }
+///
+/// struct MyState {
+/// table: ResourceTable,
+/// }
+///
+/// impl IoView for MyState {
+/// fn table(&mut self) -> &mut ResourceTable { &mut self.table }
+/// }
+/// ```
+pub fn add_to_linker_async(
+ l: &mut wasmtime::component::Linker,
+) -> wasmtime::Result<()> {
+ let closure = io_type_annotate::(|t| IoImpl(t));
+ crate::bindings::wasi::io::error::add_to_linker_get_host(l, closure)?;
+ crate::bindings::wasi::io::poll::add_to_linker_get_host(l, closure)?;
+ crate::bindings::wasi::io::streams::add_to_linker_get_host(l, closure)?;
+ Ok(())
+}
+
+// NB: workaround some rustc inference - a future refactoring may make this
+// obsolete.
+fn io_type_annotate(val: F) -> F
+where
+ F: Fn(&mut T) -> IoImpl<&mut T>,
+{
+ val
+}
diff --git a/crates/wasi-io/src/poll.rs b/crates/wasi-io/src/poll.rs
new file mode 100644
index 000000000000..6322038bbf02
--- /dev/null
+++ b/crates/wasi-io/src/poll.rs
@@ -0,0 +1,120 @@
+use alloc::boxed::Box;
+use anyhow::Result;
+use core::any::Any;
+use core::future::Future;
+use core::pin::Pin;
+use wasmtime::component::{Resource, ResourceTable};
+
+pub type DynFuture<'a> = Pin + Send + 'a>>;
+pub type MakeFuture = for<'a> fn(&'a mut dyn Any) -> DynFuture<'a>;
+
+/// The host representation of the `wasi:io/poll.pollable` resource.
+///
+/// A pollable is not the same thing as a Rust Future: the same pollable may be used to
+/// repeatedly check for readiness of a given condition, e.g. if a stream is readable
+/// or writable. So, rather than containing a Future, which can only become Ready once, a
+/// `DynPollable` contains a way to create a Future in each call to `poll`.
+pub struct DynPollable {
+ pub(crate) index: u32,
+ pub(crate) make_future: MakeFuture,
+ pub(crate) remove_index_on_delete: Option Result<()>>,
+}
+
+/// The trait used to implement [`DynPollable`] to create a `pollable`
+/// resource in `wasi:io/poll`.
+///
+/// This trait is the internal implementation detail of any pollable resource in
+/// this crate's implementation of WASI. The `ready` function is an `async fn`
+/// which resolves when the implementation is ready. Using native `async` Rust
+/// enables this type's readiness to compose with other types' readiness
+/// throughout the WASI implementation.
+///
+/// This trait is used in conjunction with [`subscribe`] to create a `pollable`
+/// resource.
+///
+/// # Example
+///
+/// This is a simple example of creating a `Pollable` resource from a few
+/// parameters.
+///
+/// ```
+/// # // stub out so we don't need a dep to build the doctests:
+/// # mod tokio { pub mod time { pub use std::time::{Duration, Instant}; pub async fn sleep_until(_:
+/// Instant) {} } }
+/// use tokio::time::{self, Duration, Instant};
+/// use wasmtime_wasi_io::{IoView, poll::{Pollable, subscribe, DynPollable}, async_trait};
+/// use wasmtime::component::Resource;
+/// use wasmtime::Result;
+///
+/// fn sleep(cx: &mut dyn IoView, dur: Duration) -> Result> {
+/// let end = Instant::now() + dur;
+/// let sleep = MySleep { end };
+/// let sleep_resource = cx.table().push(sleep)?;
+/// subscribe(cx.table(), sleep_resource)
+/// }
+///
+/// struct MySleep {
+/// end: Instant,
+/// }
+///
+/// #[async_trait]
+/// impl Pollable for MySleep {
+/// async fn ready(&mut self) {
+/// tokio::time::sleep_until(self.end).await;
+/// }
+/// }
+/// ```
+#[async_trait::async_trait]
+pub trait Pollable: Send + 'static {
+ /// An asynchronous function which resolves when this object's readiness
+ /// operation is ready.
+ ///
+ /// This function is invoked as part of `poll` in `wasi:io/poll`. The
+ /// meaning of when this function Returns depends on what object this
+ /// [`Pollable`] is attached to. When the returned future resolves then the
+ /// corresponding call to `wasi:io/poll` will return.
+ ///
+ /// Note that this method does not return an error. Returning an error
+ /// should be done through accessors on the object that this `pollable` is
+ /// connected to. The call to `wasi:io/poll` itself does not return errors,
+ /// only a list of ready objects.
+ async fn ready(&mut self);
+}
+
+/// Creates a `wasi:io/poll/pollable` resource which is subscribed to the provided
+/// `resource`.
+///
+/// If `resource` is an owned resource then it will be deleted when the returned
+/// resource is deleted. Otherwise the returned resource is considered a "child"
+/// of the given `resource` which means that the given resource cannot be
+/// deleted while the `pollable` is still alive.
+pub fn subscribe(
+ table: &mut ResourceTable,
+ resource: Resource,
+) -> Result>
+where
+ T: Pollable,
+{
+ fn make_future<'a, T>(stream: &'a mut dyn Any) -> DynFuture<'a>
+ where
+ T: Pollable,
+ {
+ stream.downcast_mut::().unwrap().ready()
+ }
+
+ let pollable = DynPollable {
+ index: resource.rep(),
+ remove_index_on_delete: if resource.owned() {
+ Some(|table, idx| {
+ let resource = Resource::::new_own(idx);
+ table.delete(resource)?;
+ Ok(())
+ })
+ } else {
+ None
+ },
+ make_future: make_future::,
+ };
+
+ Ok(table.push_child(pollable, &resource)?)
+}
diff --git a/crates/wasi/src/stream.rs b/crates/wasi-io/src/streams.rs
similarity index 94%
rename from crates/wasi/src/stream.rs
rename to crates/wasi-io/src/streams.rs
index 251133cdac66..8f2392697c7e 100644
--- a/crates/wasi/src/stream.rs
+++ b/crates/wasi-io/src/streams.rs
@@ -1,11 +1,12 @@
-use crate::poll::Subscribe;
+use crate::poll::Pollable;
+use alloc::boxed::Box;
use anyhow::Result;
use bytes::Bytes;
/// Host trait for implementing the `wasi:io/streams.input-stream` resource: A
/// bytestream which can be read from.
#[async_trait::async_trait]
-pub trait HostInputStream: Subscribe {
+pub trait InputStream: Pollable {
/// Reads up to `size` bytes, returning a buffer holding these bytes on
/// success.
///
@@ -69,8 +70,8 @@ impl StreamError {
}
}
-impl std::fmt::Display for StreamError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl alloc::fmt::Display for StreamError {
+ fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
match self {
StreamError::Closed => write!(f, "closed"),
StreamError::LastOperationFailed(e) => write!(f, "last operation failed: {e}"),
@@ -79,8 +80,8 @@ impl std::fmt::Display for StreamError {
}
}
-impl std::error::Error for StreamError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+impl core::error::Error for StreamError {
+ fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
StreamError::Closed => None,
StreamError::LastOperationFailed(e) | StreamError::Trap(e) => e.source(),
@@ -97,7 +98,7 @@ impl From for StreamError {
/// Host trait for implementing the `wasi:io/streams.output-stream` resource:
/// A bytestream which can be written to.
#[async_trait::async_trait]
-pub trait HostOutputStream: Subscribe {
+pub trait OutputStream: Pollable {
/// Write bytes after obtaining a permit to write those bytes
///
/// Prior to calling [`write`](Self::write) the caller must call
@@ -247,19 +248,19 @@ pub trait HostOutputStream: Subscribe {
}
#[async_trait::async_trait]
-impl Subscribe for Box {
+impl Pollable for Box {
async fn ready(&mut self) {
(**self).ready().await
}
}
#[async_trait::async_trait]
-impl Subscribe for Box {
+impl Pollable for Box {
async fn ready(&mut self) {
(**self).ready().await
}
}
-pub type InputStream = Box;
+pub type DynInputStream = Box;
-pub type OutputStream = Box;
+pub type DynOutputStream = Box;
diff --git a/crates/wasi-io/wit/deps/io/error.wit b/crates/wasi-io/wit/deps/io/error.wit
new file mode 100644
index 000000000000..97c6068779ac
--- /dev/null
+++ b/crates/wasi-io/wit/deps/io/error.wit
@@ -0,0 +1,34 @@
+package wasi:io@0.2.3;
+
+@since(version = 0.2.0)
+interface error {
+ /// A resource which represents some error information.
+ ///
+ /// The only method provided by this resource is `to-debug-string`,
+ /// which provides some human-readable information about the error.
+ ///
+ /// In the `wasi:io` package, this resource is returned through the
+ /// `wasi:io/streams/stream-error` type.
+ ///
+ /// To provide more specific error information, other interfaces may
+ /// offer functions to "downcast" this error into more specific types. For example,
+ /// errors returned from streams derived from filesystem types can be described using
+ /// the filesystem's own error-code type. This is done using the function
+ /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow`
+ /// parameter and returns an `option`.
+ ///
+ /// The set of functions which can "downcast" an `error` into a more
+ /// concrete type is open.
+ @since(version = 0.2.0)
+ resource error {
+ /// Returns a string that is suitable to assist humans in debugging
+ /// this error.
+ ///
+ /// WARNING: The returned string should not be consumed mechanically!
+ /// It may change across platforms, hosts, or other implementation
+ /// details. Parsing this string is a major platform-compatibility
+ /// hazard.
+ @since(version = 0.2.0)
+ to-debug-string: func() -> string;
+ }
+}
diff --git a/crates/wasi-io/wit/deps/io/poll.wit b/crates/wasi-io/wit/deps/io/poll.wit
new file mode 100644
index 000000000000..9bcbe8e03692
--- /dev/null
+++ b/crates/wasi-io/wit/deps/io/poll.wit
@@ -0,0 +1,47 @@
+package wasi:io@0.2.3;
+
+/// A poll API intended to let users wait for I/O events on multiple handles
+/// at once.
+@since(version = 0.2.0)
+interface poll {
+ /// `pollable` represents a single I/O event which may be ready, or not.
+ @since(version = 0.2.0)
+ resource pollable {
+
+ /// Return the readiness of a pollable. This function never blocks.
+ ///
+ /// Returns `true` when the pollable is ready, and `false` otherwise.
+ @since(version = 0.2.0)
+ ready: func() -> bool;
+
+ /// `block` returns immediately if the pollable is ready, and otherwise
+ /// blocks until ready.
+ ///
+ /// This function is equivalent to calling `poll.poll` on a list
+ /// containing only this pollable.
+ @since(version = 0.2.0)
+ block: func();
+ }
+
+ /// Poll for completion on a set of pollables.
+ ///
+ /// This function takes a list of pollables, which identify I/O sources of
+ /// interest, and waits until one or more of the events is ready for I/O.
+ ///
+ /// The result `list` contains one or more indices of handles in the
+ /// argument list that is ready for I/O.
+ ///
+ /// This function traps if either:
+ /// - the list is empty, or:
+ /// - the list contains more elements than can be indexed with a `u32` value.
+ ///
+ /// A timeout can be implemented by adding a pollable from the
+ /// wasi-clocks API to the list.
+ ///
+ /// This function does not return a `result`; polling in itself does not
+ /// do any I/O so it doesn't fail. If any of the I/O sources identified by
+ /// the pollables has an error, it is indicated by marking the source as
+ /// being ready for I/O.
+ @since(version = 0.2.0)
+ poll: func(in: list>) -> list;
+}
diff --git a/crates/wasi-io/wit/deps/io/streams.wit b/crates/wasi-io/wit/deps/io/streams.wit
new file mode 100644
index 000000000000..0de0846293ff
--- /dev/null
+++ b/crates/wasi-io/wit/deps/io/streams.wit
@@ -0,0 +1,290 @@
+package wasi:io@0.2.3;
+
+/// WASI I/O is an I/O abstraction API which is currently focused on providing
+/// stream types.
+///
+/// In the future, the component model is expected to add built-in stream types;
+/// when it does, they are expected to subsume this API.
+@since(version = 0.2.0)
+interface streams {
+ @since(version = 0.2.0)
+ use error.{error};
+ @since(version = 0.2.0)
+ use poll.{pollable};
+
+ /// An error for input-stream and output-stream operations.
+ @since(version = 0.2.0)
+ variant stream-error {
+ /// The last operation (a write or flush) failed before completion.
+ ///
+ /// More information is available in the `error` payload.
+ ///
+ /// After this, the stream will be closed. All future operations return
+ /// `stream-error::closed`.
+ last-operation-failed(error),
+ /// The stream is closed: no more input will be accepted by the
+ /// stream. A closed output-stream will return this error on all
+ /// future operations.
+ closed
+ }
+
+ /// An input bytestream.
+ ///
+ /// `input-stream`s are *non-blocking* to the extent practical on underlying
+ /// platforms. I/O operations always return promptly; if fewer bytes are
+ /// promptly available than requested, they return the number of bytes promptly
+ /// available, which could even be zero. To wait for data to be available,
+ /// use the `subscribe` function to obtain a `pollable` which can be polled
+ /// for using `wasi:io/poll`.
+ @since(version = 0.2.0)
+ resource input-stream {
+ /// Perform a non-blocking read from the stream.
+ ///
+ /// When the source of a `read` is binary data, the bytes from the source
+ /// are returned verbatim. When the source of a `read` is known to the
+ /// implementation to be text, bytes containing the UTF-8 encoding of the
+ /// text are returned.
+ ///
+ /// This function returns a list of bytes containing the read data,
+ /// when successful. The returned list will contain up to `len` bytes;
+ /// it may return fewer than requested, but not more. The list is
+ /// empty when no bytes are available for reading at this time. The
+ /// pollable given by `subscribe` will be ready when more bytes are
+ /// available.
+ ///
+ /// This function fails with a `stream-error` when the operation
+ /// encounters an error, giving `last-operation-failed`, or when the
+ /// stream is closed, giving `closed`.
+ ///
+ /// When the caller gives a `len` of 0, it represents a request to
+ /// read 0 bytes. If the stream is still open, this call should
+ /// succeed and return an empty list, or otherwise fail with `closed`.
+ ///
+ /// The `len` parameter is a `u64`, which could represent a list of u8 which
+ /// is not possible to allocate in wasm32, or not desirable to allocate as
+ /// as a return value by the callee. The callee may return a list of bytes
+ /// less than `len` in size while more bytes are available for reading.
+ @since(version = 0.2.0)
+ read: func(
+ /// The maximum number of bytes to read
+ len: u64
+ ) -> result, stream-error>;
+
+ /// Read bytes from a stream, after blocking until at least one byte can
+ /// be read. Except for blocking, behavior is identical to `read`.
+ @since(version = 0.2.0)
+ blocking-read: func(
+ /// The maximum number of bytes to read
+ len: u64
+ ) -> result, stream-error>;
+
+ /// Skip bytes from a stream. Returns number of bytes skipped.
+ ///
+ /// Behaves identical to `read`, except instead of returning a list
+ /// of bytes, returns the number of bytes consumed from the stream.
+ @since(version = 0.2.0)
+ skip: func(
+ /// The maximum number of bytes to skip.
+ len: u64,
+ ) -> result;
+
+ /// Skip bytes from a stream, after blocking until at least one byte
+ /// can be skipped. Except for blocking behavior, identical to `skip`.
+ @since(version = 0.2.0)
+ blocking-skip: func(
+ /// The maximum number of bytes to skip.
+ len: u64,
+ ) -> result;
+
+ /// Create a `pollable` which will resolve once either the specified stream
+ /// has bytes available to read or the other end of the stream has been
+ /// closed.
+ /// The created `pollable` is a child resource of the `input-stream`.
+ /// Implementations may trap if the `input-stream` is dropped before
+ /// all derived `pollable`s created with this function are dropped.
+ @since(version = 0.2.0)
+ subscribe: func() -> pollable;
+ }
+
+
+ /// An output bytestream.
+ ///
+ /// `output-stream`s are *non-blocking* to the extent practical on
+ /// underlying platforms. Except where specified otherwise, I/O operations also
+ /// always return promptly, after the number of bytes that can be written
+ /// promptly, which could even be zero. To wait for the stream to be ready to
+ /// accept data, the `subscribe` function to obtain a `pollable` which can be
+ /// polled for using `wasi:io/poll`.
+ ///
+ /// Dropping an `output-stream` while there's still an active write in
+ /// progress may result in the data being lost. Before dropping the stream,
+ /// be sure to fully flush your writes.
+ @since(version = 0.2.0)
+ resource output-stream {
+ /// Check readiness for writing. This function never blocks.
+ ///
+ /// Returns the number of bytes permitted for the next call to `write`,
+ /// or an error. Calling `write` with more bytes than this function has
+ /// permitted will trap.
+ ///
+ /// When this function returns 0 bytes, the `subscribe` pollable will
+ /// become ready when this function will report at least 1 byte, or an
+ /// error.
+ @since(version = 0.2.0)
+ check-write: func() -> result;
+
+ /// Perform a write. This function never blocks.
+ ///
+ /// When the destination of a `write` is binary data, the bytes from
+ /// `contents` are written verbatim. When the destination of a `write` is
+ /// known to the implementation to be text, the bytes of `contents` are
+ /// transcoded from UTF-8 into the encoding of the destination and then
+ /// written.
+ ///
+ /// Precondition: check-write gave permit of Ok(n) and contents has a
+ /// length of less than or equal to n. Otherwise, this function will trap.
+ ///
+ /// returns Err(closed) without writing if the stream has closed since
+ /// the last call to check-write provided a permit.
+ @since(version = 0.2.0)
+ write: func(
+ contents: list
+ ) -> result<_, stream-error>;
+
+ /// Perform a write of up to 4096 bytes, and then flush the stream. Block
+ /// until all of these operations are complete, or an error occurs.
+ ///
+ /// This is a convenience wrapper around the use of `check-write`,
+ /// `subscribe`, `write`, and `flush`, and is implemented with the
+ /// following pseudo-code:
+ ///
+ /// ```text
+ /// let pollable = this.subscribe();
+ /// while !contents.is_empty() {
+ /// // Wait for the stream to become writable
+ /// pollable.block();
+ /// let Ok(n) = this.check-write(); // eliding error handling
+ /// let len = min(n, contents.len());
+ /// let (chunk, rest) = contents.split_at(len);
+ /// this.write(chunk ); // eliding error handling
+ /// contents = rest;
+ /// }
+ /// this.flush();
+ /// // Wait for completion of `flush`
+ /// pollable.block();
+ /// // Check for any errors that arose during `flush`
+ /// let _ = this.check-write(); // eliding error handling
+ /// ```
+ @since(version = 0.2.0)
+ blocking-write-and-flush: func(
+ contents: list
+ ) -> result<_, stream-error>;
+
+ /// Request to flush buffered output. This function never blocks.
+ ///
+ /// This tells the output-stream that the caller intends any buffered
+ /// output to be flushed. the output which is expected to be flushed
+ /// is all that has been passed to `write` prior to this call.
+ ///
+ /// Upon calling this function, the `output-stream` will not accept any
+ /// writes (`check-write` will return `ok(0)`) until the flush has
+ /// completed. The `subscribe` pollable will become ready when the
+ /// flush has completed and the stream can accept more writes.
+ @since(version = 0.2.0)
+ flush: func() -> result<_, stream-error>;
+
+ /// Request to flush buffered output, and block until flush completes
+ /// and stream is ready for writing again.
+ @since(version = 0.2.0)
+ blocking-flush: func() -> result<_, stream-error>;
+
+ /// Create a `pollable` which will resolve once the output-stream
+ /// is ready for more writing, or an error has occurred. When this
+ /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an
+ /// error.
+ ///
+ /// If the stream is closed, this pollable is always ready immediately.
+ ///
+ /// The created `pollable` is a child resource of the `output-stream`.
+ /// Implementations may trap if the `output-stream` is dropped before
+ /// all derived `pollable`s created with this function are dropped.
+ @since(version = 0.2.0)
+ subscribe: func() -> pollable;
+
+ /// Write zeroes to a stream.
+ ///
+ /// This should be used precisely like `write` with the exact same
+ /// preconditions (must use check-write first), but instead of
+ /// passing a list of bytes, you simply pass the number of zero-bytes
+ /// that should be written.
+ @since(version = 0.2.0)
+ write-zeroes: func(
+ /// The number of zero-bytes to write
+ len: u64
+ ) -> result<_, stream-error>;
+
+ /// Perform a write of up to 4096 zeroes, and then flush the stream.
+ /// Block until all of these operations are complete, or an error
+ /// occurs.
+ ///
+ /// This is a convenience wrapper around the use of `check-write`,
+ /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with
+ /// the following pseudo-code:
+ ///
+ /// ```text
+ /// let pollable = this.subscribe();
+ /// while num_zeroes != 0 {
+ /// // Wait for the stream to become writable
+ /// pollable.block();
+ /// let Ok(n) = this.check-write(); // eliding error handling
+ /// let len = min(n, num_zeroes);
+ /// this.write-zeroes(len); // eliding error handling
+ /// num_zeroes -= len;
+ /// }
+ /// this.flush();
+ /// // Wait for completion of `flush`
+ /// pollable.block();
+ /// // Check for any errors that arose during `flush`
+ /// let _ = this.check-write(); // eliding error handling
+ /// ```
+ @since(version = 0.2.0)
+ blocking-write-zeroes-and-flush: func(
+ /// The number of zero-bytes to write
+ len: u64
+ ) -> result<_, stream-error>;
+
+ /// Read from one stream and write to another.
+ ///
+ /// The behavior of splice is equivalent to:
+ /// 1. calling `check-write` on the `output-stream`
+ /// 2. calling `read` on the `input-stream` with the smaller of the
+ /// `check-write` permitted length and the `len` provided to `splice`
+ /// 3. calling `write` on the `output-stream` with that read data.
+ ///
+ /// Any error reported by the call to `check-write`, `read`, or
+ /// `write` ends the splice and reports that error.
+ ///
+ /// This function returns the number of bytes transferred; it may be less
+ /// than `len`.
+ @since(version = 0.2.0)
+ splice: func(
+ /// The stream to read from
+ src: borrow,
+ /// The number of bytes to splice
+ len: u64,
+ ) -> result;
+
+ /// Read from one stream and write to another, with blocking.
+ ///
+ /// This is similar to `splice`, except that it blocks until the
+ /// `output-stream` is ready for writing, and the `input-stream`
+ /// is ready for reading, before performing the `splice`.
+ @since(version = 0.2.0)
+ blocking-splice: func(
+ /// The stream to read from
+ src: borrow,
+ /// The number of bytes to splice
+ len: u64,
+ ) -> result;
+ }
+}
diff --git a/crates/wasi-io/wit/deps/io/world.wit b/crates/wasi-io/wit/deps/io/world.wit
new file mode 100644
index 000000000000..f1d2102dca1d
--- /dev/null
+++ b/crates/wasi-io/wit/deps/io/world.wit
@@ -0,0 +1,10 @@
+package wasi:io@0.2.3;
+
+@since(version = 0.2.0)
+world imports {
+ @since(version = 0.2.0)
+ import streams;
+
+ @since(version = 0.2.0)
+ import poll;
+}
diff --git a/crates/wasi-io/wit/world.wit b/crates/wasi-io/wit/world.wit
new file mode 100644
index 000000000000..258bac64c4fa
--- /dev/null
+++ b/crates/wasi-io/wit/world.wit
@@ -0,0 +1,6 @@
+// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps.
+package wasmtime:wasi-io;
+
+world bindings {
+ include wasi:io/imports@0.2.3;
+}
diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml
index db02061ce315..e18afd81c2c0 100644
--- a/crates/wasi/Cargo.toml
+++ b/crates/wasi/Cargo.toml
@@ -17,6 +17,7 @@ workspace = true
[dependencies]
wasmtime = { workspace = true, features = ["component-model", "async", "runtime", "std"] }
+wasmtime-wasi-io = { workspace = true, features = ["std"] }
anyhow = { workspace = true }
wiggle = { workspace = true, optional = true, features = ["wasmtime"] }
tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] }
diff --git a/crates/wasi/src/bindings.rs b/crates/wasi/src/bindings.rs
index 7449b3a3f202..5f117cddf3e2 100644
--- a/crates/wasi/src/bindings.rs
+++ b/crates/wasi/src/bindings.rs
@@ -146,7 +146,8 @@
/// ```
pub mod sync {
mod generated {
- use crate::{FsError, SocketError, StreamError};
+ use crate::{FsError, SocketError};
+ use wasmtime_wasi_io::streams::StreamError;
wasmtime::component::bindgen!({
path: "wit",
@@ -159,12 +160,11 @@ pub mod sync {
},
trappable_imports: true,
with: {
- // These interfaces come from the outer module, as it's
- // sync/async agnostic.
+ // These interfaces contain only synchronous methods, so they
+ // can be aliased directly
"wasi:clocks": crate::bindings::clocks,
"wasi:random": crate::bindings::random,
"wasi:cli": crate::bindings::cli,
- "wasi:io/error": crate::bindings::io::error,
"wasi:filesystem/preopens": crate::bindings::filesystem::preopens,
"wasi:sockets/network": crate::bindings::sockets::network,
@@ -173,13 +173,19 @@ pub mod sync {
// way everything has the same type.
"wasi:filesystem/types/descriptor": super::super::filesystem::types::Descriptor,
"wasi:filesystem/types/directory-entry-stream": super::super::filesystem::types::DirectoryEntryStream,
- "wasi:io/poll/pollable": super::super::io::poll::Pollable,
- "wasi:io/streams/input-stream": super::super::io::streams::InputStream,
- "wasi:io/streams/output-stream": super::super::io::streams::OutputStream,
"wasi:sockets/tcp/tcp-socket": super::super::sockets::tcp::TcpSocket,
"wasi:sockets/udp/incoming-datagram-stream": super::super::sockets::udp::IncomingDatagramStream,
"wasi:sockets/udp/outgoing-datagram-stream": super::super::sockets::udp::OutgoingDatagramStream,
"wasi:sockets/udp/udp-socket": super::super::sockets::udp::UdpSocket,
+
+ // Error host trait from wasmtime-wasi-io is synchronous, so we can alias it
+ "wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error,
+ // Configure the resource types from wasmtime-wasi-io, though
+ // this bindgen will make a new synchronous Host traits
+ "wasi:io/poll/pollable": wasmtime_wasi_io::poll::DynPollable,
+ "wasi:io/streams/input-stream": wasmtime_wasi_io::streams::DynInputStream,
+ "wasi:io/streams/output-stream": wasmtime_wasi_io::streams::DynOutputStream,
+
},
require_store_data_send: true,
});
@@ -399,14 +405,20 @@ mod async_io {
],
},
trappable_error_type: {
- "wasi:io/streams/stream-error" => crate::StreamError,
+ "wasi:io/streams/stream-error" => wasmtime_wasi_io::streams::StreamError,
"wasi:filesystem/types/error-code" => crate::FsError,
"wasi:sockets/network/error-code" => crate::SocketError,
},
with: {
- // Configure all resources to be concrete types defined in this crate,
- // so that way we get to use nice typed helper methods with
- // `ResourceTable`.
+ // All interfaces in the wasi:io package should be aliased to
+ // the wasmtime-wasi-io generated code. Note that this will also
+ // map the resource types to those defined in that crate as well.
+ "wasi:io/poll": wasmtime_wasi_io::bindings::wasi::io::poll,
+ "wasi:io/streams": wasmtime_wasi_io::bindings::wasi::io::streams,
+ "wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error,
+
+ // Configure all other resources to be concrete types defined in
+ // this crate
"wasi:sockets/network/network": crate::network::Network,
"wasi:sockets/tcp/tcp-socket": crate::tcp::TcpSocket,
"wasi:sockets/udp/udp-socket": crate::udp::UdpSocket,
@@ -415,10 +427,6 @@ mod async_io {
"wasi:sockets/ip-name-lookup/resolve-address-stream": crate::ip_name_lookup::ResolveAddressStream,
"wasi:filesystem/types/directory-entry-stream": crate::filesystem::ReaddirIterator,
"wasi:filesystem/types/descriptor": crate::filesystem::Descriptor,
- "wasi:io/streams/input-stream": crate::stream::InputStream,
- "wasi:io/streams/output-stream": crate::stream::OutputStream,
- "wasi:io/error/error": crate::stream::Error,
- "wasi:io/poll/pollable": crate::poll::Pollable,
"wasi:cli/terminal-input/terminal-input": crate::stdio::TerminalInput,
"wasi:cli/terminal-output/terminal-output": crate::stdio::TerminalOutput,
},
diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs
index b86895712a9e..a64edd1dcf50 100644
--- a/crates/wasi/src/ctx.rs
+++ b/crates/wasi/src/ctx.rs
@@ -524,7 +524,18 @@ impl WasiCtxBuilder {
}
}
-/// A struct which provides access to internal WASI state.
+/// Per-[`Store`] state which holds state necessary to implement WASI from this
+/// crate.
+///
+/// This structure is created through [`WasiCtxBuilder`] and is stored within
+/// the `T` of [`Store`][`Store`]. Access to the structure is provided
+/// through the [`WasiView`](crate::WasiView) trait as an implementation on `T`.
+///
+/// Note that this structure itself does not have any accessors, it's here for
+/// internal use within the `wasmtime-wasi` crate's implementation of
+/// bindgen-generated traits.
+///
+/// [`Store`]: wasmtime::Store
///
/// # Example
///
@@ -557,18 +568,6 @@ impl WasiCtxBuilder {
/// }
/// }
/// ```
-/// Per-[`Store`] state which holds state necessary to implement WASI from this
-/// crate.
-///
-/// This structure is created through [`WasiCtxBuilder`] and is stored within
-/// the `T` of [`Store`][`Store`]. Access to the structure is provided
-/// through the [`WasiView`] trait as an implementation on `T`.
-///
-/// Note that this structure itself does not have any accessors, it's here for
-/// internal use within the `wasmtime-wasi` crate's implementation of
-/// bindgen-generated traits.
-///
-/// [`Store`]: wasmtime::Store
pub struct WasiCtx {
pub(crate) random: Box,
pub(crate) insecure_random: Box,
diff --git a/crates/wasi/src/filesystem.rs b/crates/wasi/src/filesystem.rs
index 2c6f039de4f0..14f70892068a 100644
--- a/crates/wasi/src/filesystem.rs
+++ b/crates/wasi/src/filesystem.rs
@@ -1,8 +1,6 @@
use crate::bindings::filesystem::types;
use crate::runtime::{spawn_blocking, AbortOnDropJoinHandle};
-use crate::{
- HostInputStream, HostOutputStream, StreamError, StreamResult, Subscribe, TrappableError,
-};
+use crate::{InputStream, OutputStream, Pollable, StreamError, StreamResult, TrappableError};
use anyhow::anyhow;
use bytes::{Bytes, BytesMut};
use std::io;
@@ -300,7 +298,7 @@ impl FileInputStream {
}
}
#[async_trait::async_trait]
-impl HostInputStream for FileInputStream {
+impl InputStream for FileInputStream {
fn read(&mut self, size: usize) -> StreamResult {
match &mut self.state {
ReadState::Idle => {
@@ -368,7 +366,7 @@ impl HostInputStream for FileInputStream {
}
}
#[async_trait::async_trait]
-impl Subscribe for FileInputStream {
+impl Pollable for FileInputStream {
async fn ready(&mut self) {
if let ReadState::Idle = self.state {
// The guest hasn't initiated any read, but is nonetheless waiting
@@ -467,7 +465,7 @@ impl FileOutputStream {
const FILE_WRITE_CAPACITY: usize = 1024 * 1024;
#[async_trait::async_trait]
-impl HostOutputStream for FileOutputStream {
+impl OutputStream for FileOutputStream {
fn write(&mut self, buf: Bytes) -> Result<(), StreamError> {
match self.state {
OutputState::Ready => {}
@@ -566,7 +564,7 @@ impl HostOutputStream for FileOutputStream {
}
#[async_trait::async_trait]
-impl Subscribe for FileOutputStream {
+impl Pollable for FileOutputStream {
async fn ready(&mut self) {
if let OutputState::Waiting(task) = &mut self.state {
self.state = match task.await {
diff --git a/crates/wasi/src/host/clocks.rs b/crates/wasi/src/host/clocks.rs
index feefb9473b27..fec0a6061c49 100644
--- a/crates/wasi/src/host/clocks.rs
+++ b/crates/wasi/src/host/clocks.rs
@@ -4,11 +4,11 @@ use crate::bindings::{
clocks::monotonic_clock::{self, Duration as WasiDuration, Instant},
clocks::wall_clock::{self, Datetime},
};
-use crate::poll::{subscribe, Subscribe};
-use crate::{IoView, Pollable, WasiImpl, WasiView};
+use crate::{DynPollable, IoView, WasiImpl, WasiView};
use cap_std::time::SystemTime;
use std::time::Duration;
use wasmtime::component::Resource;
+use wasmtime_wasi_io::poll::{subscribe, Pollable};
impl TryFrom for Datetime {
type Error = anyhow::Error;
@@ -48,7 +48,7 @@ where
fn subscribe_to_duration(
table: &mut wasmtime::component::ResourceTable,
duration: tokio::time::Duration,
-) -> anyhow::Result> {
+) -> anyhow::Result> {
let sleep = if duration.is_zero() {
table.push(Deadline::Past)?
} else if let Some(deadline) = tokio::time::Instant::now().checked_add(duration) {
@@ -76,7 +76,7 @@ where
Ok(self.ctx().monotonic_clock.resolution())
}
- fn subscribe_instant(&mut self, when: Instant) -> anyhow::Result> {
+ fn subscribe_instant(&mut self, when: Instant) -> anyhow::Result> {
let clock_now = self.ctx().monotonic_clock.now();
let duration = if when > clock_now {
Duration::from_nanos(when - clock_now)
@@ -86,7 +86,10 @@ where
subscribe_to_duration(&mut self.table(), duration)
}
- fn subscribe_duration(&mut self, duration: WasiDuration) -> anyhow::Result> {
+ fn subscribe_duration(
+ &mut self,
+ duration: WasiDuration,
+ ) -> anyhow::Result> {
subscribe_to_duration(&mut self.table(), Duration::from_nanos(duration))
}
}
@@ -98,7 +101,7 @@ enum Deadline {
}
#[async_trait::async_trait]
-impl Subscribe for Deadline {
+impl Pollable for Deadline {
async fn ready(&mut self) {
match self {
Deadline::Past => {}
diff --git a/crates/wasi/src/host/filesystem.rs b/crates/wasi/src/host/filesystem.rs
index a8cfb91754f3..0f7d096758fe 100644
--- a/crates/wasi/src/host/filesystem.rs
+++ b/crates/wasi/src/host/filesystem.rs
@@ -3,13 +3,13 @@ use crate::bindings::filesystem::preopens;
use crate::bindings::filesystem::types::{
self, ErrorCode, HostDescriptor, HostDirectoryEntryStream,
};
-use crate::bindings::io::streams::{InputStream, OutputStream};
use crate::filesystem::{
Descriptor, Dir, File, FileInputStream, FileOutputStream, OpenMode, ReaddirIterator,
};
use crate::{DirPerms, FilePerms, FsError, FsResult, IoView, WasiImpl, WasiView};
use anyhow::Context;
use wasmtime::component::Resource;
+use wasmtime_wasi_io::streams::{DynInputStream, DynOutputStream};
mod sync;
@@ -735,7 +735,7 @@ where
&mut self,
fd: Resource,
offset: types::Filesize,
- ) -> FsResult> {
+ ) -> FsResult> {
// Trap if fd lookup fails:
let f = self.table().get(&fd)?.file()?;
@@ -744,7 +744,7 @@ where
}
// Create a stream view for it.
- let reader: InputStream = Box::new(FileInputStream::new(f, offset));
+ let reader: DynInputStream = Box::new(FileInputStream::new(f, offset));
// Insert the stream view into the table. Trap if the table is full.
let index = self.table().push(reader)?;
@@ -756,7 +756,7 @@ where
&mut self,
fd: Resource,
offset: types::Filesize,
- ) -> FsResult> {
+ ) -> FsResult> {
// Trap if fd lookup fails:
let f = self.table().get(&fd)?.file()?;
@@ -766,7 +766,7 @@ where
// Create a stream view for it.
let writer = FileOutputStream::write_at(f, offset);
- let writer: OutputStream = Box::new(writer);
+ let writer: DynOutputStream = Box::new(writer);
// Insert the stream view into the table. Trap if the table is full.
let index = self.table().push(writer)?;
@@ -777,7 +777,7 @@ where
fn append_via_stream(
&mut self,
fd: Resource,
- ) -> FsResult> {
+ ) -> FsResult> {
// Trap if fd lookup fails:
let f = self.table().get(&fd)?.file()?;
@@ -787,7 +787,7 @@ where
// Create a stream view for it.
let appender = FileOutputStream::append(f);
- let appender: OutputStream = Box::new(appender);
+ let appender: DynOutputStream = Box::new(appender);
// Insert the stream view into the table. Trap if the table is full.
let index = self.table().push(appender)?;
diff --git a/crates/wasi/src/host/io.rs b/crates/wasi/src/host/io.rs
index 5ad1a65e84da..75d41865102e 100644
--- a/crates/wasi/src/host/io.rs
+++ b/crates/wasi/src/host/io.rs
@@ -1,39 +1,30 @@
use crate::{
- bindings::io::error,
- bindings::io::streams::{self, InputStream, OutputStream},
- poll::subscribe,
- IoImpl, IoView, Pollable, StreamError, StreamResult,
+ bindings::sync::io::poll::Pollable,
+ bindings::sync::io::streams::{self, InputStream, OutputStream},
+ runtime::in_tokio,
+ IoImpl, IoView, StreamError, StreamResult,
};
use wasmtime::component::Resource;
+use wasmtime_wasi_io::bindings::wasi::io::streams::{
+ self as async_streams, Host as AsyncHost, HostInputStream as AsyncHostInputStream,
+ HostOutputStream as AsyncHostOutputStream,
+};
-impl error::Host for IoImpl where T: IoView {}
-
-impl streams::Host for IoImpl
-where
- T: IoView,
-{
- fn convert_stream_error(&mut self, err: StreamError) -> anyhow::Result {
- match err {
- StreamError::Closed => Ok(streams::StreamError::Closed),
- StreamError::LastOperationFailed(e) => Ok(streams::StreamError::LastOperationFailed(
- self.table().push(e)?,
- )),
- StreamError::Trap(e) => Err(e),
+impl From for streams::StreamError {
+ fn from(other: async_streams::StreamError) -> Self {
+ match other {
+ async_streams::StreamError::LastOperationFailed(e) => Self::LastOperationFailed(e),
+ async_streams::StreamError::Closed => Self::Closed,
}
}
}
-impl error::HostError for IoImpl
+impl streams::Host for IoImpl
where
T: IoView,
{
- fn drop(&mut self, err: Resource) -> anyhow::Result<()> {
- self.table().delete(err)?;
- Ok(())
- }
-
- fn to_debug_string(&mut self, err: Resource) -> anyhow::Result {
- Ok(format!("{:?}", self.table().get(&err)?))
+ fn convert_stream_error(&mut self, err: StreamError) -> anyhow::Result {
+ Ok(AsyncHost::convert_stream_error(self, err)?.into())
}
}
@@ -41,132 +32,75 @@ impl streams::HostOutputStream for IoImpl
where
T: IoView,
{
- async fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
- self.table().delete(stream)?.cancel().await;
- Ok(())
+ fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
+ in_tokio(async { AsyncHostOutputStream::drop(self, stream).await })
}
fn check_write(&mut self, stream: Resource) -> StreamResult {
- let bytes = self.table().get_mut(&stream)?.check_write()?;
- Ok(bytes as u64)
+ Ok(AsyncHostOutputStream::check_write(self, stream)?)
}
fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> {
- self.table().get_mut(&stream)?.write(bytes.into())?;
- Ok(())
+ Ok(AsyncHostOutputStream::write(self, stream, bytes)?)
}
- fn subscribe(&mut self, stream: Resource) -> anyhow::Result> {
- subscribe(self.table(), stream)
- }
-
- async fn blocking_write_and_flush(
+ fn blocking_write_and_flush(
&mut self,
stream: Resource,
bytes: Vec,
) -> StreamResult<()> {
- if bytes.len() > 4096 {
- return Err(StreamError::trap(
- "Buffer too large for blocking-write-and-flush (expected at most 4096)",
- ));
- }
-
- self.table()
- .get_mut(&stream)?
- .blocking_write_and_flush(bytes.into())
- .await
+ in_tokio(async {
+ AsyncHostOutputStream::blocking_write_and_flush(self, stream, bytes).await
+ })
}
- async fn blocking_write_zeroes_and_flush(
+ fn blocking_write_zeroes_and_flush(
&mut self,
stream: Resource,
len: u64,
) -> StreamResult<()> {
- if len > 4096 {
- return Err(StreamError::trap(
- "Buffer too large for blocking-write-zeroes-and-flush (expected at most 4096)",
- ));
- }
+ in_tokio(async {
+ AsyncHostOutputStream::blocking_write_zeroes_and_flush(self, stream, len).await
+ })
+ }
- self.table()
- .get_mut(&stream)?
- .blocking_write_zeroes_and_flush(len as usize)
- .await
+ fn subscribe(&mut self, stream: Resource) -> anyhow::Result> {
+ Ok(AsyncHostOutputStream::subscribe(self, stream)?)
}
fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> {
- self.table().get_mut(&stream)?.write_zeroes(len as usize)?;
- Ok(())
+ Ok(AsyncHostOutputStream::write_zeroes(self, stream, len)?)
}
fn flush(&mut self, stream: Resource) -> StreamResult<()> {
- self.table().get_mut(&stream)?.flush()?;
- Ok(())
+ Ok(AsyncHostOutputStream::flush(
+ self,
+ Resource::new_borrow(stream.rep()),
+ )?)
}
- async fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> {
- let s = self.table().get_mut(&stream)?;
- s.flush()?;
- s.write_ready().await?;
- Ok(())
+ fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> {
+ in_tokio(async {
+ AsyncHostOutputStream::blocking_flush(self, Resource::new_borrow(stream.rep())).await
+ })
}
fn splice(
&mut self,
- dest: Resource,
+ dst: Resource,
src: Resource,
len: u64,
) -> StreamResult {
- let len = len.try_into().unwrap_or(usize::MAX);
-
- let permit = {
- let output = self.table().get_mut(&dest)?;
- output.check_write()?
- };
- let len = len.min(permit);
- if len == 0 {
- return Ok(0);
- }
-
- let contents = self.table().get_mut(&src)?.read(len)?;
-
- let len = contents.len();
- if len == 0 {
- return Ok(0);
- }
-
- let output = self.table().get_mut(&dest)?;
- output.write(contents)?;
- Ok(len.try_into().expect("usize can fit in u64"))
+ AsyncHostOutputStream::splice(self, dst, src, len)
}
- async fn blocking_splice(
+ fn blocking_splice(
&mut self,
- dest: Resource,
+ dst: Resource,
src: Resource,
len: u64,
) -> StreamResult {
- let len = len.try_into().unwrap_or(usize::MAX);
-
- let permit = {
- let output = self.table().get_mut(&dest)?;
- output.write_ready().await?
- };
- let len = len.min(permit);
- if len == 0 {
- return Ok(0);
- }
-
- let contents = self.table().get_mut(&src)?.blocking_read(len).await?;
-
- let len = contents.len();
- if len == 0 {
- return Ok(0);
- }
-
- let output = self.table().get_mut(&dest)?;
- output.blocking_write_and_flush(contents).await?;
- Ok(len.try_into().expect("usize can fit in u64"))
+ in_tokio(async { AsyncHostOutputStream::blocking_splice(self, dst, src, len).await })
}
}
@@ -174,197 +108,27 @@ impl streams::HostInputStream for IoImpl
where
T: IoView,
{
- async fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
- self.table().delete(stream)?.cancel().await;
- Ok(())
+ fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
+ in_tokio(async { AsyncHostInputStream::drop(self, stream).await })
}
fn read(&mut self, stream: Resource, len: u64) -> StreamResult> {
- let len = len.try_into().unwrap_or(usize::MAX);
- let bytes = self.table().get_mut(&stream)?.read(len)?;
- debug_assert!(bytes.len() <= len);
- Ok(bytes.into())
+ AsyncHostInputStream::read(self, stream, len)
}
- async fn blocking_read(
- &mut self,
- stream: Resource,
- len: u64,
- ) -> StreamResult> {
- let len = len.try_into().unwrap_or(usize::MAX);
- let bytes = self.table().get_mut(&stream)?.blocking_read(len).await?;
- debug_assert!(bytes.len() <= len);
- Ok(bytes.into())
+ fn blocking_read(&mut self, stream: Resource, len: u64) -> StreamResult> {
+ in_tokio(async { AsyncHostInputStream::blocking_read(self, stream, len).await })
}
fn skip(&mut self, stream: Resource, len: u64) -> StreamResult {
- let len = len.try_into().unwrap_or(usize::MAX);
- let written = self.table().get_mut(&stream)?.skip(len)?;
- Ok(written.try_into().expect("usize always fits in u64"))
+ AsyncHostInputStream::skip(self, stream, len)
}
- async fn blocking_skip(
- &mut self,
- stream: Resource,
- len: u64,
- ) -> StreamResult {
- let len = len.try_into().unwrap_or(usize::MAX);
- let written = self.table().get_mut(&stream)?.blocking_skip(len).await?;
- Ok(written.try_into().expect("usize always fits in u64"))
+ fn blocking_skip(&mut self, stream: Resource, len: u64) -> StreamResult {
+ in_tokio(async { AsyncHostInputStream::blocking_skip(self, stream, len).await })
}
fn subscribe(&mut self, stream: Resource) -> anyhow::Result> {
- crate::poll::subscribe(self.table(), stream)
- }
-}
-
-pub mod sync {
- use crate::{
- bindings::io::streams::{
- self as async_streams, Host as AsyncHost, HostInputStream as AsyncHostInputStream,
- HostOutputStream as AsyncHostOutputStream,
- },
- bindings::sync::io::poll::Pollable,
- bindings::sync::io::streams::{self, InputStream, OutputStream},
- runtime::in_tokio,
- IoImpl, IoView, StreamError, StreamResult,
- };
- use wasmtime::component::Resource;
-
- impl From for streams::StreamError {
- fn from(other: async_streams::StreamError) -> Self {
- match other {
- async_streams::StreamError::LastOperationFailed(e) => Self::LastOperationFailed(e),
- async_streams::StreamError::Closed => Self::Closed,
- }
- }
- }
-
- impl streams::Host for IoImpl
- where
- T: IoView,
- {
- fn convert_stream_error(
- &mut self,
- err: StreamError,
- ) -> anyhow::Result {
- Ok(AsyncHost::convert_stream_error(self, err)?.into())
- }
- }
-
- impl streams::HostOutputStream for IoImpl
- where
- T: IoView,
- {
- fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
- in_tokio(async { AsyncHostOutputStream::drop(self, stream).await })
- }
-
- fn check_write(&mut self, stream: Resource) -> StreamResult {
- Ok(AsyncHostOutputStream::check_write(self, stream)?)
- }
-
- fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> {
- Ok(AsyncHostOutputStream::write(self, stream, bytes)?)
- }
-
- fn blocking_write_and_flush(
- &mut self,
- stream: Resource,
- bytes: Vec,
- ) -> StreamResult<()> {
- in_tokio(async {
- AsyncHostOutputStream::blocking_write_and_flush(self, stream, bytes).await
- })
- }
-
- fn blocking_write_zeroes_and_flush(
- &mut self,
- stream: Resource,
- len: u64,
- ) -> StreamResult<()> {
- in_tokio(async {
- AsyncHostOutputStream::blocking_write_zeroes_and_flush(self, stream, len).await
- })
- }
-
- fn subscribe(
- &mut self,
- stream: Resource,
- ) -> anyhow::Result> {
- Ok(AsyncHostOutputStream::subscribe(self, stream)?)
- }
-
- fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> {
- Ok(AsyncHostOutputStream::write_zeroes(self, stream, len)?)
- }
-
- fn flush(&mut self, stream: Resource) -> StreamResult<()> {
- Ok(AsyncHostOutputStream::flush(
- self,
- Resource::new_borrow(stream.rep()),
- )?)
- }
-
- fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> {
- in_tokio(async {
- AsyncHostOutputStream::blocking_flush(self, Resource::new_borrow(stream.rep()))
- .await
- })
- }
-
- fn splice(
- &mut self,
- dst: Resource,
- src: Resource,
- len: u64,
- ) -> StreamResult {
- AsyncHostOutputStream::splice(self, dst, src, len)
- }
-
- fn blocking_splice(
- &mut self,
- dst: Resource,
- src: Resource,
- len: u64,
- ) -> StreamResult {
- in_tokio(async { AsyncHostOutputStream::blocking_splice(self, dst, src, len).await })
- }
- }
-
- impl streams::HostInputStream for IoImpl
- where
- T: IoView,
- {
- fn drop(&mut self, stream: Resource) -> anyhow::Result<()> {
- in_tokio(async { AsyncHostInputStream::drop(self, stream).await })
- }
-
- fn read(&mut self, stream: Resource, len: u64) -> StreamResult> {
- AsyncHostInputStream::read(self, stream, len)
- }
-
- fn blocking_read(
- &mut self,
- stream: Resource,
- len: u64,
- ) -> StreamResult> {
- in_tokio(async { AsyncHostInputStream::blocking_read(self, stream, len).await })
- }
-
- fn skip(&mut self, stream: Resource, len: u64) -> StreamResult {
- AsyncHostInputStream::skip(self, stream, len)
- }
-
- fn blocking_skip(&mut self, stream: Resource, len: u64) -> StreamResult {
- in_tokio(async { AsyncHostInputStream::blocking_skip(self, stream, len).await })
- }
-
- fn subscribe(
- &mut self,
- stream: Resource