Skip to content

Commit

Permalink
Add an error resource to WASI streams
Browse files Browse the repository at this point in the history
This commit adds a new `error` resource to the `wasi:io/streams`
interface. This `error` resource is returned as part of
`last-operation-failed` and serves as a means to discover through other
interfaces more granular type information than a generic string. This
error type has a new function in the `filesystem` interface, for
example, which enables getting filesystem-related error codes from I/O
performed on filesystem-originating streams. This is plumbed through to
the adapter as well to return more than `ERRNO_IO` from failed
read/write operations now too.

This is not super fancy just yet but is intended to be a vector through
which future additions can be done. The main thing this enables is to
avoid dropping errors on the floor in the host and enabling the guest to
discover further information about I/O errors on streams.

Closes bytecodealliance#7017
  • Loading branch information
alexcrichton committed Oct 4, 2023
1 parent a7a0643 commit 83f84b5
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 20 deletions.
14 changes: 13 additions & 1 deletion crates/wasi-http/wit/deps/filesystem/types.wit
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
///
/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md
interface types {
use wasi:io/streams.{input-stream, output-stream}
use wasi:io/streams.{input-stream, output-stream, error}
use wasi:clocks/wall-clock.{datetime}

/// File size or length of a region within a file.
Expand Down Expand Up @@ -795,4 +795,16 @@ interface types {
/// Read a single directory entry from a `directory-entry-stream`.
read-directory-entry: func() -> result<option<directory-entry>, error-code>
}

/// Attempts to extract a filesystem-related `error-code` from the stream
/// `error` provided.
///
/// Stream operations which return `stream-error::last-operation-failed`
/// have a payload with more information about the operation that failed.
/// This payload can be passed through to this function to see if there's
/// filesystem-related information about the error to return.
///
/// Note that this function is fallible because not all stream-related
/// errors are filesystem-related errors.
filesystem-error-code: func(err: borrow<error>) -> option<error-code>
}
26 changes: 24 additions & 2 deletions crates/wasi-http/wit/deps/io/streams.wit
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,37 @@ interface streams {
use poll.{pollable}

/// An error for input-stream and output-stream operations.
enum stream-error {
variant stream-error {
/// The last operation (a write or flush) failed before completion.
last-operation-failed,
///
/// More information is available in the `error` payload.
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
}

/// Contextual error information about the last failure that happened on
/// a read, write, or flush from an `input-stream` or `output-stream`.
///
/// This type is returned through the `stream-error` type whenever an
/// operation on a stream directly fails or an error is discovered
/// after-the-fact, for example when a write's failure shows up through a
/// later `flush` or `check-write`.
///
/// Interfaces such as `wasi:filesystem/types` provide functionality to
/// further "downcast" this error into interface-specific error information.
resource error {
/// Returns a string that's suitable to assist humans in debugging this
/// error.
///
/// The returned string will change across platforms and hosts which
/// means that parsing it, for example, would be a
/// platform-compatibility hazard.
to-debug-string: func() -> string
}

/// An input bytestream.
///
/// `input-stream`s are *non-blocking* to the extent practical on underlying
Expand Down
28 changes: 23 additions & 5 deletions crates/wasi-preview1-component-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,9 @@ pub unsafe extern "C" fn fd_read(
*nread = 0;
return Ok(());
}
Err(_) => Err(ERRNO_IO)?,
Err(streams::StreamError::LastOperationFailed(e)) => {
Err(stream_error_to_errno(e))?
}
};

assert_eq!(data.as_ptr(), ptr);
Expand All @@ -925,6 +927,13 @@ pub unsafe extern "C" fn fd_read(
})
}

fn stream_error_to_errno(err: streams::Error) -> Errno {
match filesystem::filesystem_error_code(&err) {
Some(code) => code.into(),
None => ERRNO_IO,
}
}

/// Read directory entries from a directory.
/// When successful, the contents of the output buffer consist of a sequence of
/// directory entries. Each directory entry consists of a `dirent` object,
Expand Down Expand Up @@ -2160,7 +2169,10 @@ impl BlockingMode {
bytes = rest;
match output_stream.blocking_write_and_flush(chunk) {
Ok(()) => {}
Err(_) => return Err(ERRNO_IO),
Err(streams::StreamError::Closed) => return Err(ERRNO_IO),
Err(streams::StreamError::LastOperationFailed(e)) => {
return Err(stream_error_to_errno(e))
}
}
}
Ok(total)
Expand All @@ -2170,7 +2182,9 @@ impl BlockingMode {
let permit = match output_stream.check_write() {
Ok(n) => n,
Err(streams::StreamError::Closed) => 0,
Err(streams::StreamError::LastOperationFailed) => return Err(ERRNO_IO),
Err(streams::StreamError::LastOperationFailed(e)) => {
return Err(stream_error_to_errno(e))
}
};

let len = bytes.len().min(permit as usize);
Expand All @@ -2181,13 +2195,17 @@ impl BlockingMode {
match output_stream.write(&bytes[..len]) {
Ok(_) => {}
Err(streams::StreamError::Closed) => return Ok(0),
Err(streams::StreamError::LastOperationFailed) => return Err(ERRNO_IO),
Err(streams::StreamError::LastOperationFailed(e)) => {
return Err(stream_error_to_errno(e))
}
}

match output_stream.blocking_flush() {
Ok(_) => {}
Err(streams::StreamError::Closed) => return Ok(0),
Err(streams::StreamError::LastOperationFailed) => return Err(ERRNO_IO),
Err(streams::StreamError::LastOperationFailed(e)) => {
return Err(stream_error_to_errno(e))
}
}

Ok(len)
Expand Down
21 changes: 21 additions & 0 deletions crates/wasi/src/preview2/host/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ impl<T: WasiView> types::Host for T {
fn convert_error_code(&mut self, err: FsError) -> anyhow::Result<ErrorCode> {
err.downcast()
}

fn filesystem_error_code(
&mut self,
err: Resource<anyhow::Error>,
) -> anyhow::Result<Option<ErrorCode>> {
let err = self.table_mut().get_resource(&err)?;

// Currently `err` always comes from the stream implementation which
// uses standard reads/writes so only check for `std::io::Error` here.
if let Some(err) = err.downcast_ref::<std::io::Error>() {
return Ok(Some(ErrorCode::from(err.clone())));
}

Ok(None)
}
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -989,6 +1004,12 @@ fn from_raw_os_error(raw_os_error: Option<i32>) -> Option<ErrorCode> {

impl From<std::io::Error> for ErrorCode {
fn from(err: std::io::Error) -> ErrorCode {
ErrorCode::from(&err)
}
}

impl<'a> From<&'a std::io::Error> for ErrorCode {
fn from(err: &'a std::io::Error) -> ErrorCode {
match from_raw_os_error(err.raw_os_error()) {
Some(errno) => errno,
None => {
Expand Down
9 changes: 8 additions & 1 deletion crates/wasi/src/preview2/host/filesystem/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ use wasmtime::component::Resource;

impl<T: async_filesystem::Host> sync_filesystem::Host for T {
fn convert_error_code(&mut self, err: FsError) -> anyhow::Result<sync_filesystem::ErrorCode> {
Ok(err.downcast()?.into())
Ok(async_filesystem::Host::convert_error_code(self, err)?.into())
}

fn filesystem_error_code(
&mut self,
err: Resource<streams::Error>,
) -> anyhow::Result<Option<sync_filesystem::ErrorCode>> {
Ok(async_filesystem::Host::filesystem_error_code(self, err)?.map(|e| e.into()))
}
}

Expand Down
35 changes: 27 additions & 8 deletions crates/wasi/src/preview2/host/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ use crate::preview2::{
};
use wasmtime::component::Resource;

#[async_trait::async_trait]
impl<T: WasiView> streams::Host for T {
fn convert_stream_error(&mut self, err: StreamError) -> anyhow::Result<streams::StreamError> {
match err {
StreamError::Closed => Ok(streams::StreamError::Closed),
StreamError::LastOperationFailed(e) => {
log::debug!("dropping error {e:?}");
Ok(streams::StreamError::LastOperationFailed)
}
StreamError::LastOperationFailed(e) => Ok(streams::StreamError::LastOperationFailed(
self.table_mut().push_resource(e)?,
)),
StreamError::Trap(e) => Err(e),
}
}
}

impl<T: WasiView> streams::HostError for T {
fn drop(&mut self, err: Resource<streams::Error>) -> anyhow::Result<()> {
self.table_mut().delete_resource(err)?;
Ok(())
}

fn to_debug_string(&mut self, err: Resource<streams::Error>) -> anyhow::Result<String> {
Ok(format!("{:?}", self.table_mut().get_resource(&err)?))
}
}

#[async_trait::async_trait]
impl<T: WasiView> streams::HostOutputStream for T {
fn drop(&mut self, stream: Resource<OutputStream>) -> anyhow::Result<()> {
Expand Down Expand Up @@ -241,8 +250,8 @@ impl<T: WasiView> streams::HostInputStream for T {
pub mod sync {
use crate::preview2::{
bindings::io::streams::{
self as async_streams, Host as AsyncHost, HostInputStream as AsyncHostInputStream,
HostOutputStream as AsyncHostOutputStream,
self as async_streams, Host as AsyncHost, HostError as AsyncHostError,
HostInputStream as AsyncHostInputStream, HostOutputStream as AsyncHostOutputStream,
},
bindings::sync_io::io::poll::Pollable,
bindings::sync_io::io::streams::{self, InputStream, OutputStream},
Expand All @@ -253,7 +262,7 @@ pub mod sync {
impl From<async_streams::StreamError> for streams::StreamError {
fn from(other: async_streams::StreamError) -> Self {
match other {
async_streams::StreamError::LastOperationFailed => Self::LastOperationFailed,
async_streams::StreamError::LastOperationFailed(e) => Self::LastOperationFailed(e),
async_streams::StreamError::Closed => Self::Closed,
}
}
Expand All @@ -268,6 +277,16 @@ pub mod sync {
}
}

impl<T: WasiView> streams::HostError for T {
fn drop(&mut self, err: Resource<streams::Error>) -> anyhow::Result<()> {
AsyncHostError::drop(self, err)
}

fn to_debug_string(&mut self, err: Resource<streams::Error>) -> anyhow::Result<String> {
AsyncHostError::to_debug_string(self, err)
}
}

impl<T: WasiView> streams::HostOutputStream for T {
fn drop(&mut self, stream: Resource<OutputStream>) -> anyhow::Result<()> {
AsyncHostOutputStream::drop(self, stream)
Expand Down
2 changes: 2 additions & 0 deletions crates/wasi/src/preview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub mod bindings {
"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:io/streams/error": super::super::io::streams::Error,
}
});
}
Expand Down Expand Up @@ -163,6 +164,7 @@ pub mod bindings {
"wasi:filesystem/types/descriptor": super::filesystem::Descriptor,
"wasi:io/streams/input-stream": super::stream::InputStream,
"wasi:io/streams/output-stream": super::stream::OutputStream,
"wasi:io/streams/error": super::stream::Error,
"wasi:io/poll/pollable": super::poll::Pollable,
"wasi:cli/terminal-input/terminal-input": super::stdio::TerminalInput,
"wasi:cli/terminal-output/terminal-output": super::stdio::TerminalOutput,
Expand Down
7 changes: 7 additions & 0 deletions crates/wasi/src/preview2/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ pub trait HostInputStream: Subscribe {
}
}

/// Representation of the `error` resource type in the `wasi:io/streams`
/// interface.
///
/// This is currently `anyhow::Error` to retain full type information for
/// errors.
pub type Error = anyhow::Error;

pub type StreamResult<T> = Result<T, StreamError>;

#[derive(Debug)]
Expand Down
14 changes: 13 additions & 1 deletion crates/wasi/wit/deps/filesystem/types.wit
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
///
/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md
interface types {
use wasi:io/streams.{input-stream, output-stream}
use wasi:io/streams.{input-stream, output-stream, error}
use wasi:clocks/wall-clock.{datetime}

/// File size or length of a region within a file.
Expand Down Expand Up @@ -795,4 +795,16 @@ interface types {
/// Read a single directory entry from a `directory-entry-stream`.
read-directory-entry: func() -> result<option<directory-entry>, error-code>
}

/// Attempts to extract a filesystem-related `error-code` from the stream
/// `error` provided.
///
/// Stream operations which return `stream-error::last-operation-failed`
/// have a payload with more information about the operation that failed.
/// This payload can be passed through to this function to see if there's
/// filesystem-related information about the error to return.
///
/// Note that this function is fallible because not all stream-related
/// errors are filesystem-related errors.
filesystem-error-code: func(err: borrow<error>) -> option<error-code>
}
26 changes: 24 additions & 2 deletions crates/wasi/wit/deps/io/streams.wit
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,37 @@ interface streams {
use poll.{pollable}

/// An error for input-stream and output-stream operations.
enum stream-error {
variant stream-error {
/// The last operation (a write or flush) failed before completion.
last-operation-failed,
///
/// More information is available in the `error` payload.
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
}

/// Contextual error information about the last failure that happened on
/// a read, write, or flush from an `input-stream` or `output-stream`.
///
/// This type is returned through the `stream-error` type whenever an
/// operation on a stream directly fails or an error is discovered
/// after-the-fact, for example when a write's failure shows up through a
/// later `flush` or `check-write`.
///
/// Interfaces such as `wasi:filesystem/types` provide functionality to
/// further "downcast" this error into interface-specific error information.
resource error {
/// Returns a string that's suitable to assist humans in debugging this
/// error.
///
/// The returned string will change across platforms and hosts which
/// means that parsing it, for example, would be a
/// platform-compatibility hazard.
to-debug-string: func() -> string
}

/// An input bytestream.
///
/// `input-stream`s are *non-blocking* to the extent practical on underlying
Expand Down

0 comments on commit 83f84b5

Please sign in to comment.