From 6d0ca32774dc12f8ea6b39dbd7821cac5943c407 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 5 Oct 2023 18:04:31 -0500 Subject: [PATCH] Add an error resource to WASI streams (#7152) * Change `bindgen!`'s trappable error to take an input type This commit removes the generated type for `trappable_error_type` from the `bindgen!` macro to allow users to provide a type instead. This works similarly as before with new conversion functions generated in traits which are used to convert the custom error into the ABI representation of what a WIT world expects. There are a few motivations for this commit: * This enables reducing the number of errors in play between async/sync bindings by using the same error type there. * This avoids an auto-generated type which is more difficult to inspect than one that's written down already and the source can more easily be inspected. * This enables richer conversions using `self` (e.g. `self.table_mut()`) between error types rather than purely relying on `Into`. This is important for #7017 where an error is going to be inserted into the table as it gets converted. * Fix tests * Update WASI to use new trappable errors This commit deals with the fallout of the previous commit for the WASI preview2 implementation. The main changes here are: * Bindgen-generated `Error` types no longer exist. These are replaced with `TrappableError` where type aliases are used such as ```rust type FsError = TrappableError; ``` * Type synonyms such as `FsResult` are now added for more conveniently writing down fallible signatures. * Some various error conversions are updated from converting to the old `Error` type to now instead directly into corresponding `ErrorCode` types. * A number of cases where unknown errors were turned into traps now return bland error codes and log the error instead since these aren't fatal events. * The `StreamError` type does not use `TrappableError` since it has other variants that it's concerned with such as a `LastOperationFailed` variant which has an `anyhow::Error` payload. * Some minor preview1 issues were fixed such as trapping errors being turned into normal I/O errors by accident. * Add an `error` resource to WASI streams 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 #7017 * Update crates/wasi-http/wit/deps/io/streams.wit Co-authored-by: Trevor Elliott * Update wasi-http wit too * Remove unnecessary clone --------- Co-authored-by: Trevor Elliott --- crates/component-macro/src/bindgen.rs | 5 +- crates/component-macro/tests/codegen.rs | 42 +++ .../wasi-http/wit/deps/filesystem/types.wit | 14 +- crates/wasi-http/wit/deps/io/streams.wit | 26 +- .../src/lib.rs | 28 +- crates/wasi/src/preview2/command.rs | 8 - crates/wasi/src/preview2/error.rs | 74 +++++ crates/wasi/src/preview2/filesystem.rs | 29 +- crates/wasi/src/preview2/host/filesystem.rs | 272 +++++++++--------- .../wasi/src/preview2/host/filesystem/sync.rs | 115 ++++---- crates/wasi/src/preview2/host/io.rs | 180 +++++------- crates/wasi/src/preview2/host/network.rs | 40 +-- crates/wasi/src/preview2/host/tcp.rs | 82 ++---- .../src/preview2/host/tcp_create_socket.rs | 9 +- crates/wasi/src/preview2/ip_name_lookup.rs | 10 +- crates/wasi/src/preview2/mod.rs | 25 +- crates/wasi/src/preview2/network.rs | 24 ++ crates/wasi/src/preview2/preview1.rs | 102 ++----- crates/wasi/src/preview2/stream.rs | 38 ++- crates/wasi/wit/deps/filesystem/types.wit | 14 +- crates/wasi/wit/deps/io/streams.wit | 26 +- crates/wit-bindgen/src/lib.rs | 168 +++++------ tests/all/component_model/bindgen/results.rs | 133 ++++++--- 23 files changed, 825 insertions(+), 639 deletions(-) diff --git a/crates/component-macro/src/bindgen.rs b/crates/component-macro/src/bindgen.rs index a87616e55d82..c5a9d2920ce8 100644 --- a/crates/component-macro/src/bindgen.rs +++ b/crates/component-macro/src/bindgen.rs @@ -1,10 +1,11 @@ use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; use std::collections::HashMap; use std::collections::HashSet; use std::path::{Path, PathBuf}; use syn::parse::{Error, Parse, ParseStream, Result}; use syn::punctuated::Punctuated; -use syn::{braced, token, Ident, Token}; +use syn::{braced, token, Token}; use wasmtime_wit_bindgen::{AsyncConfig, Opts, Ownership, TrappableError}; use wit_parser::{PackageId, Resolve, UnresolvedPackage, WorldId}; @@ -335,7 +336,7 @@ fn trappable_error_field_parse(input: ParseStream<'_>) -> Result input.parse::()?; let wit_type_name = ident_or_str(input)?; input.parse::()?; - let rust_type_name = input.parse::()?.to_string(); + let rust_type_name = input.parse::()?.to_token_stream().to_string(); Ok(TrappableError { wit_package_path, wit_type_name, diff --git a/crates/component-macro/tests/codegen.rs b/crates/component-macro/tests/codegen.rs index 90d58bd890f2..49bb8c6d5726 100644 --- a/crates/component-macro/tests/codegen.rs +++ b/crates/component-macro/tests/codegen.rs @@ -106,3 +106,45 @@ mod with_key_and_resources { } } } + +mod trappable_errors { + wasmtime::component::bindgen!({ + inline: " + package demo:pkg; + + interface a { + type b = u64; + + z1: func() -> result<_, b>; + z2: func() -> result<_, b>; + } + + interface b { + use a.{b}; + z: func() -> result<_, b>; + } + + interface c { + type b = u64; + } + + interface d { + use c.{b}; + z: func() -> result<_, b>; + } + + world foo { + import a; + import b; + import d; + } + ", + trappable_error_type: { + "demo:pkg/a"::"b": MyX, + "demo:pkg/c"::"b": MyX, + }, + }); + + #[allow(dead_code)] + type MyX = u32; +} diff --git a/crates/wasi-http/wit/deps/filesystem/types.wit b/crates/wasi-http/wit/deps/filesystem/types.wit index 3f69bf997a29..aecdd0ef354b 100644 --- a/crates/wasi-http/wit/deps/filesystem/types.wit +++ b/crates/wasi-http/wit/deps/filesystem/types.wit @@ -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. @@ -795,4 +795,16 @@ interface types { /// Read a single directory entry from a `directory-entry-stream`. read-directory-entry: func() -> result, 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) -> option } diff --git a/crates/wasi-http/wit/deps/io/streams.wit b/crates/wasi-http/wit/deps/io/streams.wit index 8240507976f7..55562d1cbfce 100644 --- a/crates/wasi-http/wit/deps/io/streams.wit +++ b/crates/wasi-http/wit/deps/io/streams.wit @@ -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 diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs index acbf82eea54d..56ae3294f4d0 100644 --- a/crates/wasi-preview1-component-adapter/src/lib.rs +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -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); @@ -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, @@ -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) @@ -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); @@ -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) diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs index 7702e2a706da..811e3cf18e2c 100644 --- a/crates/wasi/src/preview2/command.rs +++ b/crates/wasi/src/preview2/command.rs @@ -4,10 +4,6 @@ wasmtime::component::bindgen!({ world: "wasi:cli/command", tracing: true, async: true, - trappable_error_type: { - "wasi:filesystem/types"::"error-code": Error, - "wasi:sockets/tcp"::"error-code": Error, - }, with: { "wasi:filesystem/types": crate::preview2::bindings::filesystem::types, "wasi:filesystem/preopens": crate::preview2::bindings::filesystem::preopens, @@ -65,10 +61,6 @@ pub mod sync { world: "wasi:cli/command", tracing: true, async: false, - trappable_error_type: { - "wasi:filesystem/types"::"error-code": Error, - "wasi:sockets/tcp"::"error-code": Error, - }, with: { "wasi:filesystem/types": crate::preview2::bindings::sync_io::filesystem::types, "wasi:filesystem/preopens": crate::preview2::bindings::filesystem::preopens, diff --git a/crates/wasi/src/preview2/error.rs b/crates/wasi/src/preview2/error.rs index c3493b238bee..ccae912d4668 100644 --- a/crates/wasi/src/preview2/error.rs +++ b/crates/wasi/src/preview2/error.rs @@ -1,4 +1,6 @@ +use std::error::Error; use std::fmt; +use std::marker; /// An error returned from the `proc_exit` host syscall. /// @@ -14,3 +16,75 @@ impl fmt::Display for I32Exit { } impl std::error::Error for I32Exit {} + +/// A helper error type used by many other modules through type aliases. +/// +/// This type is an `Error` itself and is intended to be a representation of +/// either: +/// +/// * A custom error type `T` +/// * A trap, represented as `anyhow::Error` +/// +/// This error is created through either the `::trap` constructor representing a +/// full-fledged trap or the `From` constructor which is intended to be used +/// with `?`. The goal is to make normal errors `T` "automatic" but enable error +/// paths to return a `::trap` error optionally still as necessary without extra +/// boilerplate everywhere else. +/// +/// Note that this type isn't used directly but instead is intended to be used +/// as: +/// +/// ```rust,ignore +/// type MyError = TrappableError; +/// ``` +/// +/// where `MyError` is what you'll use throughout bindings code and +/// `bindgen::TheError` is the type that this represents as generated by the +/// `bindgen!` macro. +#[repr(transparent)] +pub struct TrappableError { + err: anyhow::Error, + _marker: marker::PhantomData, +} + +impl TrappableError { + pub fn trap(err: impl Into) -> TrappableError { + TrappableError { + err: err.into(), + _marker: marker::PhantomData, + } + } + + pub fn downcast(self) -> anyhow::Result + where + T: Error + Send + Sync + 'static, + { + self.err.downcast() + } +} + +impl From for TrappableError +where + T: Error + Send + Sync + 'static, +{ + fn from(error: T) -> Self { + Self { + err: error.into(), + _marker: marker::PhantomData, + } + } +} + +impl fmt::Debug for TrappableError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + +impl fmt::Display for TrappableError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + +impl Error for TrappableError {} diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 9b8467a86ed3..ac788414995b 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,6 +1,7 @@ use crate::preview2::bindings::filesystem::types; use crate::preview2::{ - spawn_blocking, AbortOnDropJoinHandle, HostOutputStream, StreamError, Subscribe, + spawn_blocking, AbortOnDropJoinHandle, HostOutputStream, StreamError, Subscribe, TableError, + TrappableError, }; use anyhow::anyhow; use bytes::{Bytes, BytesMut}; @@ -8,6 +9,22 @@ use std::io; use std::mem; use std::sync::Arc; +pub type FsResult = Result; + +pub type FsError = TrappableError; + +impl From for FsError { + fn from(error: TableError) -> Self { + Self::trap(error) + } +} + +impl From for FsError { + fn from(error: io::Error) -> Self { + types::ErrorCode::from(error).into() + } +} + pub enum Descriptor { File(File), Dir(Dir), @@ -276,24 +293,22 @@ impl Subscribe for FileOutputStream { } pub struct ReaddirIterator( - std::sync::Mutex< - Box> + Send + 'static>, - >, + std::sync::Mutex> + Send + 'static>>, ); impl ReaddirIterator { pub(crate) fn new( - i: impl Iterator> + Send + 'static, + i: impl Iterator> + Send + 'static, ) -> Self { ReaddirIterator(std::sync::Mutex::new(Box::new(i))) } - pub(crate) fn next(&self) -> Result, types::Error> { + pub(crate) fn next(&self) -> FsResult> { self.0.lock().unwrap().next().transpose() } } impl IntoIterator for ReaddirIterator { - type Item = Result; + type Item = FsResult; type IntoIter = Box + Send>; fn into_iter(self) -> Self::IntoIter { diff --git a/crates/wasi/src/preview2/host/filesystem.rs b/crates/wasi/src/preview2/host/filesystem.rs index a9a57e46c489..b0c77db3860b 100644 --- a/crates/wasi/src/preview2/host/filesystem.rs +++ b/crates/wasi/src/preview2/host/filesystem.rs @@ -1,23 +1,17 @@ use crate::preview2::bindings::clocks::wall_clock; -use crate::preview2::bindings::filesystem::types::{HostDescriptor, HostDirectoryEntryStream}; -use crate::preview2::bindings::filesystem::{preopens, types}; +use crate::preview2::bindings::filesystem::preopens; +use crate::preview2::bindings::filesystem::types::{ + self, ErrorCode, HostDescriptor, HostDirectoryEntryStream, +}; use crate::preview2::bindings::io::streams::{InputStream, OutputStream}; use crate::preview2::filesystem::{Descriptor, Dir, File, ReaddirIterator}; use crate::preview2::filesystem::{FileInputStream, FileOutputStream}; -use crate::preview2::{DirPerms, FilePerms, Table, TableError, WasiView}; +use crate::preview2::{DirPerms, FilePerms, FsError, FsResult, Table, WasiView}; use anyhow::Context; use wasmtime::component::Resource; -use types::ErrorCode; - mod sync; -impl From for types::Error { - fn from(error: TableError) -> Self { - Self::trap(error.into()) - } -} - impl preopens::Host for T { fn get_directories( &mut self, @@ -35,7 +29,26 @@ impl preopens::Host for T { } #[async_trait::async_trait] -impl types::Host for T {} +impl types::Host for T { + fn convert_error_code(&mut self, err: FsError) -> anyhow::Result { + err.downcast() + } + + fn filesystem_error_code( + &mut self, + err: Resource, + ) -> anyhow::Result> { + 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::() { + return Ok(Some(ErrorCode::from(err))); + } + + Ok(None) + } +} #[async_trait::async_trait] impl HostDescriptor for T { @@ -45,7 +58,7 @@ impl HostDescriptor for T { offset: types::Filesize, len: types::Filesize, advice: types::Advice, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { use system_interface::fs::{Advice as A, FileIoExt}; use types::Advice; @@ -64,7 +77,7 @@ impl HostDescriptor for T { Ok(()) } - async fn sync_data(&mut self, fd: Resource) -> Result<(), types::Error> { + async fn sync_data(&mut self, fd: Resource) -> FsResult<()> { let table = self.table(); match table.get_resource(&fd)? { @@ -94,7 +107,7 @@ impl HostDescriptor for T { async fn get_flags( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { use system_interface::fs::{FdFlags, GetSetFdFlags}; use types::DescriptorFlags; @@ -142,7 +155,7 @@ impl HostDescriptor for T { async fn get_type( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { let table = self.table(); match table.get_resource(&fd)? { @@ -158,7 +171,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, size: types::Filesize, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { let f = self.table().get_resource(&fd)?.file()?; if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; @@ -172,7 +185,7 @@ impl HostDescriptor for T { fd: Resource, atim: types::NewTimestamp, mtim: types::NewTimestamp, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { use fs_set_times::SetTimes; let table = self.table(); @@ -203,7 +216,7 @@ impl HostDescriptor for T { fd: Resource, len: types::Filesize, offset: types::Filesize, - ) -> Result<(Vec, bool), types::Error> { + ) -> FsResult<(Vec, bool)> { use std::io::IoSliceMut; use system_interface::fs::FileIoExt; @@ -241,7 +254,7 @@ impl HostDescriptor for T { fd: Resource, buf: Vec, offset: types::Filesize, - ) -> Result { + ) -> FsResult { use std::io::IoSlice; use system_interface::fs::FileIoExt; @@ -261,7 +274,7 @@ impl HostDescriptor for T { async fn read_directory( &mut self, fd: Resource, - ) -> Result, types::Error> { + ) -> FsResult> { let table = self.table_mut(); let d = table.get_resource(&fd)?.dir()?; if !d.perms.contains(DirPerms::READ) { @@ -317,13 +330,13 @@ impl HostDescriptor for T { }); let entries = entries.map(|r| match r { Ok(r) => Ok(r), - Err(ReaddirError::Io(e)) => Err(types::Error::from(e)), + Err(ReaddirError::Io(e)) => Err(e.into()), Err(ReaddirError::IllegalSequence) => Err(ErrorCode::IllegalByteSequence.into()), }); Ok(table.push_resource(ReaddirIterator::new(entries))?) } - async fn sync(&mut self, fd: Resource) -> Result<(), types::Error> { + async fn sync(&mut self, fd: Resource) -> FsResult<()> { let table = self.table(); match table.get_resource(&fd)? { @@ -354,7 +367,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { let table = self.table(); let d = table.get_resource(&fd)?.dir()?; if !d.perms.contains(DirPerms::MUTATE) { @@ -364,10 +377,7 @@ impl HostDescriptor for T { Ok(()) } - async fn stat( - &mut self, - fd: Resource, - ) -> Result { + async fn stat(&mut self, fd: Resource) -> FsResult { let table = self.table(); match table.get_resource(&fd)? { Descriptor::File(f) => { @@ -388,7 +398,7 @@ impl HostDescriptor for T { fd: Resource, path_flags: types::PathFlags, path: String, - ) -> Result { + ) -> FsResult { let table = self.table(); let d = table.get_resource(&fd)?.dir()?; if !d.perms.contains(DirPerms::READ) { @@ -410,7 +420,7 @@ impl HostDescriptor for T { path: String, atim: types::NewTimestamp, mtim: types::NewTimestamp, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { use cap_fs_ext::DirExt; let table = self.table(); @@ -450,7 +460,7 @@ impl HostDescriptor for T { old_path: String, new_descriptor: Resource, new_path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { let table = self.table(); let old_dir = table.get_resource(&fd)?.dir()?; if !old_dir.perms.contains(DirPerms::MUTATE) { @@ -480,7 +490,7 @@ impl HostDescriptor for T { // TODO: These are the permissions to use when creating a new file. // Not implemented yet. _mode: types::Modes, - ) -> Result, types::Error> { + ) -> FsResult> { use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt}; use system_interface::fs::{FdFlags, GetSetFdFlags}; use types::{DescriptorFlags, OpenFlags}; @@ -605,7 +615,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, path: String, - ) -> Result { + ) -> FsResult { let table = self.table(); let d = table.get_resource(&fd)?.dir()?; if !d.perms.contains(DirPerms::READ) { @@ -622,7 +632,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { let table = self.table(); let d = table.get_resource(&fd)?.dir()?; if !d.perms.contains(DirPerms::MUTATE) { @@ -637,7 +647,7 @@ impl HostDescriptor for T { old_path: String, new_fd: Resource, new_path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { let table = self.table(); let old_dir = table.get_resource(&fd)?.dir()?; if !old_dir.perms.contains(DirPerms::MUTATE) { @@ -658,7 +668,7 @@ impl HostDescriptor for T { fd: Resource, src_path: String, dest_path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { // On windows, Dir.symlink is provided by DirExt #[cfg(windows)] use cap_fs_ext::DirExt; @@ -676,7 +686,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, path: String, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { use cap_fs_ext::DirExt; let table = self.table(); @@ -694,7 +704,7 @@ impl HostDescriptor for T { _path_flags: types::PathFlags, _path: String, _access: types::AccessType, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { todo!("filesystem access_at is not implemented") } @@ -704,7 +714,7 @@ impl HostDescriptor for T { _path_flags: types::PathFlags, _path: String, _mode: types::Modes, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { todo!("filesystem change_file_permissions_at is not implemented") } @@ -714,36 +724,27 @@ impl HostDescriptor for T { _path_flags: types::PathFlags, _path: String, _mode: types::Modes, - ) -> Result<(), types::Error> { + ) -> FsResult<()> { todo!("filesystem change_directory_permissions_at is not implemented") } - async fn lock_shared(&mut self, _fd: Resource) -> Result<(), types::Error> { + async fn lock_shared(&mut self, _fd: Resource) -> FsResult<()> { todo!("filesystem lock_shared is not implemented") } - async fn lock_exclusive( - &mut self, - _fd: Resource, - ) -> Result<(), types::Error> { + async fn lock_exclusive(&mut self, _fd: Resource) -> FsResult<()> { todo!("filesystem lock_exclusive is not implemented") } - async fn try_lock_shared( - &mut self, - _fd: Resource, - ) -> Result<(), types::Error> { + async fn try_lock_shared(&mut self, _fd: Resource) -> FsResult<()> { todo!("filesystem try_lock_shared is not implemented") } - async fn try_lock_exclusive( - &mut self, - _fd: Resource, - ) -> Result<(), types::Error> { + async fn try_lock_exclusive(&mut self, _fd: Resource) -> FsResult<()> { todo!("filesystem try_lock_exclusive is not implemented") } - async fn unlock(&mut self, _fd: Resource) -> Result<(), types::Error> { + async fn unlock(&mut self, _fd: Resource) -> FsResult<()> { todo!("filesystem unlock is not implemented") } @@ -751,7 +752,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, offset: types::Filesize, - ) -> Result, types::Error> { + ) -> FsResult> { // Trap if fd lookup fails: let f = self.table().get_resource(&fd)?.file()?; @@ -774,7 +775,7 @@ impl HostDescriptor for T { &mut self, fd: Resource, offset: types::Filesize, - ) -> Result, types::Error> { + ) -> FsResult> { // Trap if fd lookup fails: let f = self.table().get_resource(&fd)?.file()?; @@ -798,7 +799,7 @@ impl HostDescriptor for T { fn append_via_stream( &mut self, fd: Resource, - ) -> Result, types::Error> { + ) -> FsResult> { // Trap if fd lookup fails: let f = self.table().get_resource(&fd)?.file()?; @@ -847,7 +848,7 @@ impl HostDescriptor for T { async fn metadata_hash( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { let table = self.table(); let meta = get_descriptor_metadata(table, fd).await?; Ok(calculate_metadata_hash(&meta)) @@ -857,7 +858,7 @@ impl HostDescriptor for T { fd: Resource, path_flags: types::PathFlags, path: String, - ) -> Result { + ) -> FsResult { let table = self.table(); let d = table.get_resource(&fd)?.dir()?; // No permissions check on metadata: if dir opened, allowed to stat it @@ -879,7 +880,7 @@ impl HostDirectoryEntryStream for T { async fn read_directory_entry( &mut self, stream: Resource, - ) -> Result, types::Error> { + ) -> FsResult> { let table = self.table(); let readdir = table.get_resource(&stream)?; readdir.next() @@ -894,7 +895,7 @@ impl HostDirectoryEntryStream for T { async fn get_descriptor_metadata( table: &Table, fd: Resource, -) -> Result { +) -> FsResult { match table.get_resource(&fd)? { Descriptor::File(f) => { // No permissions check on metadata: if opened, allowed to stat it @@ -932,100 +933,109 @@ fn calculate_metadata_hash(meta: &cap_std::fs::Metadata) -> types::MetadataHashV } #[cfg(unix)] -fn from_raw_os_error(err: Option) -> Option { +fn from_raw_os_error(err: Option) -> Option { use rustix::io::Errno as RustixErrno; if err.is_none() { return None; } Some(match RustixErrno::from_raw_os_error(err.unwrap()) { - RustixErrno::PIPE => ErrorCode::Pipe.into(), - RustixErrno::PERM => ErrorCode::NotPermitted.into(), - RustixErrno::NOENT => ErrorCode::NoEntry.into(), - RustixErrno::NOMEM => ErrorCode::InsufficientMemory.into(), - RustixErrno::IO => ErrorCode::Io.into(), - RustixErrno::BADF => ErrorCode::BadDescriptor.into(), - RustixErrno::BUSY => ErrorCode::Busy.into(), - RustixErrno::ACCESS => ErrorCode::Access.into(), - RustixErrno::NOTDIR => ErrorCode::NotDirectory.into(), - RustixErrno::ISDIR => ErrorCode::IsDirectory.into(), - RustixErrno::INVAL => ErrorCode::Invalid.into(), - RustixErrno::EXIST => ErrorCode::Exist.into(), - RustixErrno::FBIG => ErrorCode::FileTooLarge.into(), - RustixErrno::NOSPC => ErrorCode::InsufficientSpace.into(), - RustixErrno::SPIPE => ErrorCode::InvalidSeek.into(), - RustixErrno::MLINK => ErrorCode::TooManyLinks.into(), - RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong.into(), - RustixErrno::NOTEMPTY => ErrorCode::NotEmpty.into(), - RustixErrno::LOOP => ErrorCode::Loop.into(), - RustixErrno::OVERFLOW => ErrorCode::Overflow.into(), - RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence.into(), - RustixErrno::NOTSUP => ErrorCode::Unsupported.into(), - RustixErrno::ALREADY => ErrorCode::Already.into(), - RustixErrno::INPROGRESS => ErrorCode::InProgress.into(), - RustixErrno::INTR => ErrorCode::Interrupted.into(), - - // On some platforms.into(), these have the same value as other errno values. + RustixErrno::PIPE => ErrorCode::Pipe, + RustixErrno::PERM => ErrorCode::NotPermitted, + RustixErrno::NOENT => ErrorCode::NoEntry, + RustixErrno::NOMEM => ErrorCode::InsufficientMemory, + RustixErrno::IO => ErrorCode::Io, + RustixErrno::BADF => ErrorCode::BadDescriptor, + RustixErrno::BUSY => ErrorCode::Busy, + RustixErrno::ACCESS => ErrorCode::Access, + RustixErrno::NOTDIR => ErrorCode::NotDirectory, + RustixErrno::ISDIR => ErrorCode::IsDirectory, + RustixErrno::INVAL => ErrorCode::Invalid, + RustixErrno::EXIST => ErrorCode::Exist, + RustixErrno::FBIG => ErrorCode::FileTooLarge, + RustixErrno::NOSPC => ErrorCode::InsufficientSpace, + RustixErrno::SPIPE => ErrorCode::InvalidSeek, + RustixErrno::MLINK => ErrorCode::TooManyLinks, + RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong, + RustixErrno::NOTEMPTY => ErrorCode::NotEmpty, + RustixErrno::LOOP => ErrorCode::Loop, + RustixErrno::OVERFLOW => ErrorCode::Overflow, + RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence, + RustixErrno::NOTSUP => ErrorCode::Unsupported, + RustixErrno::ALREADY => ErrorCode::Already, + RustixErrno::INPROGRESS => ErrorCode::InProgress, + RustixErrno::INTR => ErrorCode::Interrupted, + + // On some platforms, these have the same value as other errno values. #[allow(unreachable_patterns)] - RustixErrno::OPNOTSUPP => ErrorCode::Unsupported.into(), + RustixErrno::OPNOTSUPP => ErrorCode::Unsupported, _ => return None, }) } #[cfg(windows)] -fn from_raw_os_error(raw_os_error: Option) -> Option { +fn from_raw_os_error(raw_os_error: Option) -> Option { use windows_sys::Win32::Foundation; Some(match raw_os_error.map(|code| code as u32) { - Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry.into(), - Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry.into(), - Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access.into(), - Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access.into(), - Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted.into(), - Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor.into(), - Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry.into(), - Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory.into(), - Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory.into(), - Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty.into(), - Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy.into(), - Some(Foundation::ERROR_BUSY) => ErrorCode::Busy.into(), - Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported.into(), - Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist.into(), - Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe.into(), - Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong.into(), - Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid.into(), - Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid.into(), - Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory.into(), - Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist.into(), - Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop.into(), - Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory.into(), + Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access, + Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access, + Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted, + Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor, + Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry, + Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty, + Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy, + Some(Foundation::ERROR_BUSY) => ErrorCode::Busy, + Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported, + Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe, + Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong, + Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid, + Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid, + Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory, + Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop, + Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory, _ => return None, }) } -impl From for types::Error { - fn from(err: std::io::Error) -> types::Error { +impl From 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 => match err.kind() { - std::io::ErrorKind::NotFound => ErrorCode::NoEntry.into(), - std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted.into(), - std::io::ErrorKind::AlreadyExists => ErrorCode::Exist.into(), - std::io::ErrorKind::InvalidInput => ErrorCode::Invalid.into(), - _ => types::Error::trap(anyhow::anyhow!(err).context("Unknown OS error")), - }, + None => { + log::debug!("unknown raw os error: {err}"); + match err.kind() { + std::io::ErrorKind::NotFound => ErrorCode::NoEntry, + std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted, + std::io::ErrorKind::AlreadyExists => ErrorCode::Exist, + std::io::ErrorKind::InvalidInput => ErrorCode::Invalid, + _ => ErrorCode::Io, + } + } } } } -impl From for types::Error { - fn from(err: cap_rand::Error) -> types::Error { +impl From for ErrorCode { + fn from(err: cap_rand::Error) -> ErrorCode { // I picked Error::Io as a 'reasonable default', FIXME dan is this ok? - from_raw_os_error(err.raw_os_error()).unwrap_or_else(|| types::Error::from(ErrorCode::Io)) + from_raw_os_error(err.raw_os_error()).unwrap_or(ErrorCode::Io) } } -impl From for types::Error { - fn from(_err: std::num::TryFromIntError) -> types::Error { - ErrorCode::Overflow.into() +impl From for ErrorCode { + fn from(_err: std::num::TryFromIntError) -> ErrorCode { + ErrorCode::Overflow } } @@ -1047,9 +1057,7 @@ fn descriptortype_from(ft: cap_std::fs::FileType) -> types::DescriptorType { } } -fn systemtimespec_from( - t: types::NewTimestamp, -) -> Result, types::Error> { +fn systemtimespec_from(t: types::NewTimestamp) -> FsResult> { use fs_set_times::SystemTimeSpec; use types::NewTimestamp; match t { @@ -1059,7 +1067,7 @@ fn systemtimespec_from( } } -fn systemtime_from(t: wall_clock::Datetime) -> Result { +fn systemtime_from(t: wall_clock::Datetime) -> FsResult { use std::time::{Duration, SystemTime}; SystemTime::UNIX_EPOCH .checked_add(Duration::new(t.seconds, t.nanoseconds)) diff --git a/crates/wasi/src/preview2/host/filesystem/sync.rs b/crates/wasi/src/preview2/host/filesystem/sync.rs index 36bb244b5719..6f6132a7b2af 100644 --- a/crates/wasi/src/preview2/host/filesystem/sync.rs +++ b/crates/wasi/src/preview2/host/filesystem/sync.rs @@ -1,10 +1,21 @@ use crate::preview2::bindings::filesystem::types as async_filesystem; use crate::preview2::bindings::sync_io::filesystem::types as sync_filesystem; use crate::preview2::bindings::sync_io::io::streams; -use crate::preview2::in_tokio; +use crate::preview2::{in_tokio, FsError, FsResult}; use wasmtime::component::Resource; -impl sync_filesystem::Host for T {} +impl sync_filesystem::Host for T { + fn convert_error_code(&mut self, err: FsError) -> anyhow::Result { + Ok(async_filesystem::Host::convert_error_code(self, err)?.into()) + } + + fn filesystem_error_code( + &mut self, + err: Resource, + ) -> anyhow::Result> { + Ok(async_filesystem::Host::filesystem_error_code(self, err)?.map(|e| e.into())) + } +} impl sync_filesystem::HostDescriptor for T { fn advise( @@ -13,16 +24,13 @@ impl sync_filesystem::HostDescriptor for T offset: sync_filesystem::Filesize, len: sync_filesystem::Filesize, advice: sync_filesystem::Advice, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::advise(self, fd, offset, len, advice.into()).await })?) } - fn sync_data( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn sync_data(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::sync_data(self, fd).await })?) @@ -31,14 +39,14 @@ impl sync_filesystem::HostDescriptor for T fn get_flags( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::get_flags(self, fd).await })?.into()) } fn get_type( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::get_type(self, fd).await })?.into()) } @@ -46,7 +54,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, size: sync_filesystem::Filesize, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::set_size(self, fd, size).await })?) @@ -57,7 +65,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, atim: sync_filesystem::NewTimestamp, mtim: sync_filesystem::NewTimestamp, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::set_times(self, fd, atim.into(), mtim.into()).await })?) @@ -68,7 +76,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, len: sync_filesystem::Filesize, offset: sync_filesystem::Filesize, - ) -> Result<(Vec, bool), sync_filesystem::Error> { + ) -> FsResult<(Vec, bool)> { Ok(in_tokio(async { async_filesystem::HostDescriptor::read(self, fd, len, offset).await })?) @@ -79,7 +87,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, buf: Vec, offset: sync_filesystem::Filesize, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::write(self, fd, buf, offset).await })?) @@ -88,16 +96,13 @@ impl sync_filesystem::HostDescriptor for T fn read_directory( &mut self, fd: Resource, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(in_tokio(async { async_filesystem::HostDescriptor::read_directory(self, fd).await })?) } - fn sync( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn sync(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::sync(self, fd).await })?) @@ -107,7 +112,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::create_directory_at(self, fd, path).await })?) @@ -116,7 +121,7 @@ impl sync_filesystem::HostDescriptor for T fn stat( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::stat(self, fd).await })?.into()) } @@ -125,7 +130,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, path_flags: sync_filesystem::PathFlags, path: String, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::stat_at(self, fd, path_flags.into(), path).await })? @@ -139,7 +144,7 @@ impl sync_filesystem::HostDescriptor for T path: String, atim: sync_filesystem::NewTimestamp, mtim: sync_filesystem::NewTimestamp, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::set_times_at( self, @@ -161,7 +166,7 @@ impl sync_filesystem::HostDescriptor for T old_path: String, new_descriptor: Resource, new_path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::link_at( self, @@ -183,7 +188,7 @@ impl sync_filesystem::HostDescriptor for T oflags: sync_filesystem::OpenFlags, flags: sync_filesystem::DescriptorFlags, mode: sync_filesystem::Modes, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(in_tokio(async { async_filesystem::HostDescriptor::open_at( self, @@ -206,7 +211,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, path: String, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::readlink_at(self, fd, path).await })?) @@ -216,7 +221,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::remove_directory_at(self, fd, path).await })?) @@ -228,7 +233,7 @@ impl sync_filesystem::HostDescriptor for T old_path: String, new_fd: Resource, new_path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::rename_at(self, fd, old_path, new_fd, new_path).await })?) @@ -239,7 +244,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, src_path: String, dest_path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::symlink_at(self, fd, src_path, dest_path).await })?) @@ -249,7 +254,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, path: String, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::unlink_file_at(self, fd, path).await })?) @@ -261,7 +266,7 @@ impl sync_filesystem::HostDescriptor for T path_flags: sync_filesystem::PathFlags, path: String, access: sync_filesystem::AccessType, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::access_at( self, @@ -280,7 +285,7 @@ impl sync_filesystem::HostDescriptor for T path_flags: sync_filesystem::PathFlags, path: String, mode: sync_filesystem::Modes, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::change_file_permissions_at( self, @@ -299,7 +304,7 @@ impl sync_filesystem::HostDescriptor for T path_flags: sync_filesystem::PathFlags, path: String, mode: sync_filesystem::Modes, - ) -> Result<(), sync_filesystem::Error> { + ) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::change_directory_permissions_at( self, @@ -312,46 +317,31 @@ impl sync_filesystem::HostDescriptor for T })?) } - fn lock_shared( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn lock_shared(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::lock_shared(self, fd).await })?) } - fn lock_exclusive( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn lock_exclusive(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::lock_exclusive(self, fd).await })?) } - fn try_lock_shared( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn try_lock_shared(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::try_lock_shared(self, fd).await })?) } - fn try_lock_exclusive( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn try_lock_exclusive(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::try_lock_exclusive(self, fd).await })?) } - fn unlock( - &mut self, - fd: Resource, - ) -> Result<(), sync_filesystem::Error> { + fn unlock(&mut self, fd: Resource) -> FsResult<()> { Ok(in_tokio(async { async_filesystem::HostDescriptor::unlock(self, fd).await })?) @@ -361,7 +351,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, offset: sync_filesystem::Filesize, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(async_filesystem::HostDescriptor::read_via_stream( self, fd, offset, )?) @@ -371,7 +361,7 @@ impl sync_filesystem::HostDescriptor for T &mut self, fd: Resource, offset: sync_filesystem::Filesize, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(async_filesystem::HostDescriptor::write_via_stream( self, fd, offset, )?) @@ -380,7 +370,7 @@ impl sync_filesystem::HostDescriptor for T fn append_via_stream( &mut self, fd: Resource, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(async_filesystem::HostDescriptor::append_via_stream( self, fd, )?) @@ -398,7 +388,7 @@ impl sync_filesystem::HostDescriptor for T fn metadata_hash( &mut self, fd: Resource, - ) -> Result { + ) -> FsResult { Ok( in_tokio(async { async_filesystem::HostDescriptor::metadata_hash(self, fd).await })? .into(), @@ -409,7 +399,7 @@ impl sync_filesystem::HostDescriptor for T fd: Resource, path_flags: sync_filesystem::PathFlags, path: String, - ) -> Result { + ) -> FsResult { Ok(in_tokio(async { async_filesystem::HostDescriptor::metadata_hash_at(self, fd, path_flags.into(), path) .await @@ -424,7 +414,7 @@ impl sync_filesystem::HostDirecto fn read_directory_entry( &mut self, stream: Resource, - ) -> Result, sync_filesystem::Error> { + ) -> FsResult> { Ok(in_tokio(async { async_filesystem::HostDirectoryEntryStream::read_directory_entry(self, stream).await })? @@ -484,15 +474,6 @@ impl From for sync_filesystem::ErrorCode { } } -impl From for sync_filesystem::Error { - fn from(other: async_filesystem::Error) -> Self { - match other.downcast() { - Ok(errorcode) => Self::from(sync_filesystem::ErrorCode::from(errorcode)), - Err(other) => Self::trap(other), - } - } -} - impl From for async_filesystem::Advice { fn from(other: sync_filesystem::Advice) -> Self { use sync_filesystem::Advice; diff --git a/crates/wasi/src/preview2/host/io.rs b/crates/wasi/src/preview2/host/io.rs index 92cfa0cdb0de..72796de5a8df 100644 --- a/crates/wasi/src/preview2/host/io.rs +++ b/crates/wasi/src/preview2/host/io.rs @@ -1,31 +1,32 @@ use crate::preview2::{ bindings::io::streams::{self, InputStream, OutputStream}, poll::subscribe, - stream::StreamError, - Pollable, TableError, WasiView, + Pollable, StreamError, StreamResult, WasiView, }; use wasmtime::component::Resource; -impl From for streams::Error { - fn from(e: TableError) -> streams::Error { - streams::Error::trap(e.into()) - } -} -impl From for streams::Error { - fn from(e: StreamError) -> streams::Error { - match e { - StreamError::Closed => streams::StreamError::Closed.into(), - StreamError::LastOperationFailed(e) => { - tracing::debug!("streams::StreamError::LastOperationFailed: {e:?}"); - streams::StreamError::LastOperationFailed.into() - } - StreamError::Trap(e) => streams::Error::trap(e), +impl streams::Host for T { + 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_mut().push_resource(e)?, + )), + StreamError::Trap(e) => Err(e), } } } -#[async_trait::async_trait] -impl streams::Host for T {} +impl streams::HostError for T { + fn drop(&mut self, err: Resource) -> anyhow::Result<()> { + self.table_mut().delete_resource(err)?; + Ok(()) + } + + fn to_debug_string(&mut self, err: Resource) -> anyhow::Result { + Ok(format!("{:?}", self.table_mut().get_resource(&err)?)) + } +} #[async_trait::async_trait] impl streams::HostOutputStream for T { @@ -34,16 +35,12 @@ impl streams::HostOutputStream for T { Ok(()) } - fn check_write(&mut self, stream: Resource) -> Result { + fn check_write(&mut self, stream: Resource) -> StreamResult { let bytes = self.table_mut().get_resource_mut(&stream)?.check_write()?; Ok(bytes as u64) } - fn write( - &mut self, - stream: Resource, - bytes: Vec, - ) -> Result<(), streams::Error> { + fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> { self.table_mut() .get_resource_mut(&stream)? .write(bytes.into())?; @@ -58,13 +55,13 @@ impl streams::HostOutputStream for T { &mut self, stream: Resource, bytes: Vec, - ) -> Result<(), streams::Error> { + ) -> StreamResult<()> { let s = self.table_mut().get_resource_mut(&stream)?; if bytes.len() > 4096 { - return Err(streams::Error::trap(anyhow::anyhow!( - "Buffer too large for blocking-write-and-flush (expected at most 4096)" - ))); + return Err(StreamError::trap( + "Buffer too large for blocking-write-and-flush (expected at most 4096)", + )); } let mut bytes = bytes::Bytes::from(bytes); @@ -85,13 +82,13 @@ impl streams::HostOutputStream for T { &mut self, stream: Resource, len: u64, - ) -> Result<(), streams::Error> { + ) -> StreamResult<()> { let s = self.table_mut().get_resource_mut(&stream)?; if len > 4096 { - return Err(streams::Error::trap(anyhow::anyhow!( - "Buffer too large for blocking-write-zeroes-and-flush (expected at most 4096)" - ))); + return Err(StreamError::trap( + "Buffer too large for blocking-write-zeroes-and-flush (expected at most 4096)", + )); } let mut len = len; @@ -108,26 +105,19 @@ impl streams::HostOutputStream for T { Ok(()) } - fn write_zeroes( - &mut self, - stream: Resource, - len: u64, - ) -> Result<(), streams::Error> { + fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> { self.table_mut() .get_resource_mut(&stream)? .write_zeroes(len as usize)?; Ok(()) } - fn flush(&mut self, stream: Resource) -> Result<(), streams::Error> { + fn flush(&mut self, stream: Resource) -> StreamResult<()> { self.table_mut().get_resource_mut(&stream)?.flush()?; Ok(()) } - async fn blocking_flush( - &mut self, - stream: Resource, - ) -> Result<(), streams::Error> { + async fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> { let s = self.table_mut().get_resource_mut(&stream)?; s.flush()?; s.write_ready().await?; @@ -139,7 +129,7 @@ impl streams::HostOutputStream for T { _dst: Resource, _src: Resource, _len: u64, - ) -> Result { + ) -> StreamResult { // TODO: We can't get two streams at the same time because they both // carry the exclusive lifetime of `ctx`. When [`get_many_mut`] is // stabilized, that could allow us to add a `get_many_stream_mut` or @@ -168,7 +158,7 @@ impl streams::HostOutputStream for T { _dst: Resource, _src: Resource, _len: u64, - ) -> Result { + ) -> StreamResult { // TODO: once splice is implemented, figure out what the blocking semantics are for waiting // on src and dest here. todo!("stream splice is not implemented") @@ -178,7 +168,7 @@ impl streams::HostOutputStream for T { &mut self, _dst: Resource, _src: Resource, - ) -> Result { + ) -> StreamResult { // TODO: We can't get two streams at the same time because they both // carry the exclusive lifetime of `ctx`. When [`get_many_mut`] is // stabilized, that could allow us to add a `get_many_stream_mut` or @@ -204,11 +194,6 @@ impl streams::HostOutputStream for T { } } -impl From for streams::Error { - fn from(e: std::num::TryFromIntError) -> Self { - streams::Error::trap(anyhow::anyhow!("length overflow: {e:?}")) - } -} #[async_trait::async_trait] impl streams::HostInputStream for T { fn drop(&mut self, stream: Resource) -> anyhow::Result<()> { @@ -216,12 +201,8 @@ impl streams::HostInputStream for T { Ok(()) } - async fn read( - &mut self, - stream: Resource, - len: u64, - ) -> Result, streams::Error> { - let len = len.try_into()?; + async fn read(&mut self, stream: Resource, len: u64) -> StreamResult> { + let len = len.try_into().unwrap_or(usize::MAX); let bytes = match self.table_mut().get_resource_mut(&stream)? { InputStream::Host(s) => s.read(len)?, InputStream::File(s) => s.read(len).await?, @@ -234,19 +215,15 @@ impl streams::HostInputStream for T { &mut self, stream: Resource, len: u64, - ) -> Result, streams::Error> { + ) -> StreamResult> { if let InputStream::Host(s) = self.table_mut().get_resource_mut(&stream)? { s.ready().await; } self.read(stream, len).await } - async fn skip( - &mut self, - stream: Resource, - len: u64, - ) -> Result { - let len = len.try_into()?; + async fn skip(&mut self, stream: Resource, len: u64) -> StreamResult { + let len = len.try_into().unwrap_or(usize::MAX); let written = match self.table_mut().get_resource_mut(&stream)? { InputStream::Host(s) => s.skip(len)?, InputStream::File(s) => s.skip(len).await?, @@ -258,7 +235,7 @@ impl streams::HostInputStream for T { &mut self, stream: Resource, len: u64, - ) -> Result { + ) -> StreamResult { if let InputStream::Host(s) = self.table_mut().get_resource_mut(&stream)? { s.ready().await; } @@ -273,48 +250,53 @@ impl streams::HostInputStream for T { pub mod sync { use crate::preview2::{ bindings::io::streams::{ - self as async_streams, 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}, - in_tokio, WasiView, + in_tokio, StreamError, StreamResult, WasiView, }; use wasmtime::component::Resource; impl From 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, } } } - impl From for streams::Error { - fn from(other: async_streams::Error) -> Self { - match other.downcast() { - Ok(write_error) => streams::Error::from(streams::StreamError::from(write_error)), - Err(e) => streams::Error::trap(e), - } + + impl streams::Host for T { + fn convert_stream_error( + &mut self, + err: StreamError, + ) -> anyhow::Result { + Ok(AsyncHost::convert_stream_error(self, err)?.into()) } } - impl streams::Host for T {} + impl streams::HostError for T { + fn drop(&mut self, err: Resource) -> anyhow::Result<()> { + AsyncHostError::drop(self, err) + } + + fn to_debug_string(&mut self, err: Resource) -> anyhow::Result { + AsyncHostError::to_debug_string(self, err) + } + } impl streams::HostOutputStream for T { fn drop(&mut self, stream: Resource) -> anyhow::Result<()> { AsyncHostOutputStream::drop(self, stream) } - fn check_write(&mut self, stream: Resource) -> Result { + fn check_write(&mut self, stream: Resource) -> StreamResult { Ok(AsyncHostOutputStream::check_write(self, stream)?) } - fn write( - &mut self, - stream: Resource, - bytes: Vec, - ) -> Result<(), streams::Error> { + fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> { Ok(AsyncHostOutputStream::write(self, stream, bytes)?) } @@ -322,7 +304,7 @@ pub mod sync { &mut self, stream: Resource, bytes: Vec, - ) -> Result<(), streams::Error> { + ) -> StreamResult<()> { Ok(in_tokio(async { AsyncHostOutputStream::blocking_write_and_flush(self, stream, bytes).await })?) @@ -332,7 +314,7 @@ pub mod sync { &mut self, stream: Resource, len: u64, - ) -> Result<(), streams::Error> { + ) -> StreamResult<()> { Ok(in_tokio(async { AsyncHostOutputStream::blocking_write_zeroes_and_flush(self, stream, len).await })?) @@ -345,22 +327,18 @@ pub mod sync { Ok(AsyncHostOutputStream::subscribe(self, stream)?) } - fn write_zeroes( - &mut self, - stream: Resource, - len: u64, - ) -> Result<(), streams::Error> { + fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> { Ok(AsyncHostOutputStream::write_zeroes(self, stream, len)?) } - fn flush(&mut self, stream: Resource) -> Result<(), streams::Error> { + fn flush(&mut self, stream: Resource) -> StreamResult<()> { Ok(AsyncHostOutputStream::flush( self, Resource::new_borrow(stream.rep()), )?) } - fn blocking_flush(&mut self, stream: Resource) -> Result<(), streams::Error> { + fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> { Ok(in_tokio(async { AsyncHostOutputStream::blocking_flush(self, Resource::new_borrow(stream.rep())) .await @@ -372,7 +350,7 @@ pub mod sync { dst: Resource, src: Resource, len: u64, - ) -> Result { + ) -> StreamResult { Ok(in_tokio(async { AsyncHostOutputStream::splice(self, dst, src, len).await })?) @@ -383,7 +361,7 @@ pub mod sync { dst: Resource, src: Resource, len: u64, - ) -> Result { + ) -> StreamResult { Ok(in_tokio(async { AsyncHostOutputStream::blocking_splice(self, dst, src, len).await })?) @@ -393,7 +371,7 @@ pub mod sync { &mut self, dst: Resource, src: Resource, - ) -> Result { + ) -> StreamResult { Ok(in_tokio(async { AsyncHostOutputStream::forward(self, dst, src).await })?) @@ -405,11 +383,7 @@ pub mod sync { AsyncHostInputStream::drop(self, stream) } - fn read( - &mut self, - stream: Resource, - len: u64, - ) -> Result, streams::Error> { + fn read(&mut self, stream: Resource, len: u64) -> StreamResult> { Ok(in_tokio(async { AsyncHostInputStream::read(self, stream, len).await })?) @@ -419,23 +393,19 @@ pub mod sync { &mut self, stream: Resource, len: u64, - ) -> Result, streams::Error> { + ) -> StreamResult> { Ok(in_tokio(async { AsyncHostInputStream::blocking_read(self, stream, len).await })?) } - fn skip(&mut self, stream: Resource, len: u64) -> Result { + fn skip(&mut self, stream: Resource, len: u64) -> StreamResult { Ok(in_tokio(async { AsyncHostInputStream::skip(self, stream, len).await })?) } - fn blocking_skip( - &mut self, - stream: Resource, - len: u64, - ) -> Result { + fn blocking_skip(&mut self, stream: Resource, len: u64) -> StreamResult { Ok(in_tokio(async { AsyncHostInputStream::blocking_skip(self, stream, len).await })?) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 811213050e8e..b2a8ff59a700 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -2,11 +2,15 @@ use crate::preview2::bindings::sockets::network::{ self, ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4Address, Ipv4SocketAddress, Ipv6Address, Ipv6SocketAddress, }; -use crate::preview2::{TableError, WasiView}; +use crate::preview2::{SocketError, WasiView}; use std::io; use wasmtime::component::Resource; -impl network::Host for T {} +impl network::Host for T { + fn convert_error_code(&mut self, error: SocketError) -> anyhow::Result { + error.downcast() + } +} impl crate::preview2::bindings::sockets::network::HostNetwork for T { fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { @@ -18,13 +22,7 @@ impl crate::preview2::bindings::sockets::network::HostNetwork for T } } -impl From for network::Error { - fn from(error: TableError) -> Self { - Self::trap(error.into()) - } -} - -impl From for network::Error { +impl From for ErrorCode { fn from(error: io::Error) -> Self { match error.kind() { // Errors that we can directly map. @@ -39,24 +37,8 @@ impl From for network::Error { io::ErrorKind::Unsupported => ErrorCode::NotSupported, io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory, - // Errors we don't expect to see here. - io::ErrorKind::Interrupted | io::ErrorKind::ConnectionAborted => { - // Transient errors should be skipped. - return Self::trap(error.into()); - } - - // Errors not expected from network APIs. - io::ErrorKind::WriteZero - | io::ErrorKind::InvalidInput - | io::ErrorKind::InvalidData - | io::ErrorKind::BrokenPipe - | io::ErrorKind::NotFound - | io::ErrorKind::UnexpectedEof - | io::ErrorKind::AlreadyExists => return Self::trap(error.into()), - // Errors that don't correspond to a Rust `io::ErrorKind`. io::ErrorKind::Other => match error.raw_os_error() { - None => return Self::trap(error.into()), Some(libc::ENOBUFS) | Some(libc::ENOMEM) => ErrorCode::OutOfMemory, Some(libc::EOPNOTSUPP) => ErrorCode::NotSupported, Some(libc::ENETUNREACH) | Some(libc::EHOSTUNREACH) | Some(libc::ENETDOWN) => { @@ -65,7 +47,10 @@ impl From for network::Error { Some(libc::ECONNRESET) => ErrorCode::ConnectionReset, Some(libc::ECONNREFUSED) => ErrorCode::ConnectionRefused, Some(libc::EADDRINUSE) => ErrorCode::AddressInUse, - Some(_) => return Self::trap(error.into()), + Some(_) | None => { + log::debug!("unknown I/O error: {error}"); + ErrorCode::Unknown + } }, _ => { @@ -73,11 +58,10 @@ impl From for network::Error { ErrorCode::Unknown } } - .into() } } -impl From for network::Error { +impl From for ErrorCode { fn from(error: rustix::io::Errno) -> Self { std::io::Error::from(error).into() } diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 6565a589413a..0cf88b9100a4 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -1,10 +1,10 @@ use crate::preview2::bindings::{ io::streams::{InputStream, OutputStream}, - sockets::network::{self, ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}, sockets::tcp::{self, ShutdownType}, }; use crate::preview2::tcp::{TcpSocket, TcpState}; -use crate::preview2::{Pollable, WasiView}; +use crate::preview2::{Pollable, SocketResult, WasiView}; use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; @@ -21,7 +21,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, network: Resource, local_address: IpSocketAddress, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource(&this)?; @@ -44,7 +44,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Ok(()) } - fn finish_bind(&mut self, this: Resource) -> Result<(), network::Error> { + fn finish_bind(&mut self, this: Resource) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; @@ -63,7 +63,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, network: Resource, remote_address: IpSocketAddress, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table_mut(); let r = { let socket = table.get_resource(&this)?; @@ -107,7 +107,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { fn finish_connect( &mut self, this: Resource, - ) -> Result<(Resource, Resource), network::Error> { + ) -> SocketResult<(Resource, Resource)> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; @@ -145,7 +145,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Ok((input_stream, output_stream)) } - fn start_listen(&mut self, this: Resource) -> Result<(), network::Error> { + fn start_listen(&mut self, this: Resource) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; @@ -166,7 +166,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Ok(()) } - fn finish_listen(&mut self, this: Resource) -> Result<(), network::Error> { + fn finish_listen(&mut self, this: Resource) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; @@ -183,14 +183,11 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { fn accept( &mut self, this: Resource, - ) -> Result< - ( - Resource, - Resource, - Resource, - ), - network::Error, - > { + ) -> SocketResult<( + Resource, + Resource, + Resource, + )> { let table = self.table(); let socket = table.get_resource(&this)?; @@ -222,10 +219,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Ok((tcp_socket, input_stream, output_stream)) } - fn local_address( - &mut self, - this: Resource, - ) -> Result { + fn local_address(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; let addr = socket @@ -235,10 +229,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Ok(addr.into()) } - fn remote_address( - &mut self, - this: Resource, - ) -> Result { + fn remote_address(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; let addr = socket @@ -296,17 +287,13 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } } - fn ipv6_only(&mut self, this: Resource) -> Result { + fn ipv6_only(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?) } - fn set_ipv6_only( - &mut self, - this: Resource, - value: bool, - ) -> Result<(), network::Error> { + fn set_ipv6_only(&mut self, this: Resource, value: bool) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?) @@ -316,7 +303,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { &mut self, this: Resource, value: u64, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { const MIN_BACKLOG: i32 = 1; const MAX_BACKLOG: i32 = i32::MAX; // OS'es will most likely limit it down even further. @@ -354,39 +341,31 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } } - fn keep_alive(&mut self, this: Resource) -> Result { + fn keep_alive(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::get_socket_keepalive(socket.tcp_socket())?) } - fn set_keep_alive( - &mut self, - this: Resource, - value: bool, - ) -> Result<(), network::Error> { + fn set_keep_alive(&mut self, this: Resource, value: bool) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::set_socket_keepalive(socket.tcp_socket(), value)?) } - fn no_delay(&mut self, this: Resource) -> Result { + fn no_delay(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::get_tcp_nodelay(socket.tcp_socket())?) } - fn set_no_delay( - &mut self, - this: Resource, - value: bool, - ) -> Result<(), network::Error> { + fn set_no_delay(&mut self, this: Resource, value: bool) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::set_tcp_nodelay(socket.tcp_socket(), value)?) } - fn unicast_hop_limit(&mut self, this: Resource) -> Result { + fn unicast_hop_limit(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; @@ -407,7 +386,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { &mut self, this: Resource, value: u8, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; @@ -420,10 +399,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } } - fn receive_buffer_size( - &mut self, - this: Resource, - ) -> Result { + fn receive_buffer_size(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::get_socket_recv_buffer_size(socket.tcp_socket())? as u64) @@ -433,7 +409,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { &mut self, this: Resource, value: u64, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; @@ -443,7 +419,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { )?) } - fn send_buffer_size(&mut self, this: Resource) -> Result { + fn send_buffer_size(&mut self, this: Resource) -> SocketResult { let table = self.table(); let socket = table.get_resource(&this)?; Ok(sockopt::get_socket_send_buffer_size(socket.tcp_socket())? as u64) @@ -453,7 +429,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { &mut self, this: Resource, value: u64, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; @@ -471,7 +447,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { &mut self, this: Resource, shutdown_type: ShutdownType, - ) -> Result<(), network::Error> { + ) -> SocketResult<()> { let table = self.table(); let socket = table.get_resource(&this)?; diff --git a/crates/wasi/src/preview2/host/tcp_create_socket.rs b/crates/wasi/src/preview2/host/tcp_create_socket.rs index 60fc34482399..417c03081b05 100644 --- a/crates/wasi/src/preview2/host/tcp_create_socket.rs +++ b/crates/wasi/src/preview2/host/tcp_create_socket.rs @@ -1,16 +1,13 @@ -use crate::preview2::bindings::{ - sockets::network::{self, IpAddressFamily}, - sockets::tcp_create_socket, -}; +use crate::preview2::bindings::{sockets::network::IpAddressFamily, sockets::tcp_create_socket}; use crate::preview2::tcp::TcpSocket; -use crate::preview2::WasiView; +use crate::preview2::{SocketResult, WasiView}; use wasmtime::component::Resource; impl tcp_create_socket::Host for T { fn create_tcp_socket( &mut self, address_family: IpAddressFamily, - ) -> Result, network::Error> { + ) -> SocketResult> { let socket = TcpSocket::new(address_family.into())?; let socket = self.table_mut().push_resource(socket)?; Ok(socket) diff --git a/crates/wasi/src/preview2/ip_name_lookup.rs b/crates/wasi/src/preview2/ip_name_lookup.rs index b4a02427b475..ce7cde93b9b9 100644 --- a/crates/wasi/src/preview2/ip_name_lookup.rs +++ b/crates/wasi/src/preview2/ip_name_lookup.rs @@ -1,9 +1,7 @@ use crate::preview2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream}; -use crate::preview2::bindings::sockets::network::{ - Error, ErrorCode, IpAddress, IpAddressFamily, Network, -}; +use crate::preview2::bindings::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, Network}; use crate::preview2::poll::{subscribe, Pollable, Subscribe}; -use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, WasiView}; +use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, SocketError, WasiView}; use anyhow::Result; use std::io; use std::mem; @@ -25,7 +23,7 @@ impl Host for T { name: String, family: Option, include_unavailable: bool, - ) -> Result, Error> { + ) -> Result, SocketError> { let network = self.table().get_resource(&network)?; // `Host::parse` serves us two functions: @@ -87,7 +85,7 @@ impl HostResolveAddressStream for T { fn resolve_next_address( &mut self, resource: Resource, - ) -> Result, Error> { + ) -> Result, SocketError> { let stream = self.table_mut().get_resource_mut(&resource)?; loop { match stream { diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 9b4a273b8af9..aecc414ea4b2 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -40,14 +40,15 @@ mod write_stream; pub use self::clocks::{HostMonotonicClock, HostWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; -pub use self::error::I32Exit; -pub use self::filesystem::{DirPerms, FilePerms}; +pub use self::error::{I32Exit, TrappableError}; +pub use self::filesystem::{DirPerms, FilePerms, FsError, FsResult}; +pub use self::network::{Network, SocketError, SocketResult}; pub use self::poll::{subscribe, ClosureFuture, MakeFuture, Pollable, PollableFuture, Subscribe}; pub use self::random::{thread_rng, Deterministic}; -pub use self::stdio::{ - stderr, stdin, stdout, IsATTY, Stderr, Stdin, StdinStream, Stdout, StdoutStream, +pub use self::stdio::{stderr, stdin, stdout, IsATTY, Stderr, Stdin, Stdout}; +pub use self::stream::{ + HostInputStream, HostOutputStream, InputStream, OutputStream, StreamError, StreamResult, }; -pub use self::stream::{HostInputStream, HostOutputStream, InputStream, OutputStream, StreamError}; pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; @@ -59,6 +60,8 @@ pub mod bindings { // have some functions in `only_imports` below for being async. pub mod sync_io { pub(crate) mod _internal { + use crate::preview2::{FsError, StreamError}; + wasmtime::component::bindgen!({ path: "wit", interfaces: " @@ -68,8 +71,8 @@ pub mod bindings { ", tracing: true, trappable_error_type: { - "wasi:io/streams"::"stream-error": Error, - "wasi:filesystem/types"::"error-code": Error, + "wasi:io/streams"::"stream-error": StreamError, + "wasi:filesystem/types"::"error-code": FsError, }, with: { "wasi:clocks/wall-clock": crate::preview2::bindings::clocks::wall_clock, @@ -78,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, } }); } @@ -149,9 +153,9 @@ pub mod bindings { "wasi:sockets/ip-name-lookup/resolve-address-stream": super::ip_name_lookup::ResolveAddressStream, }, trappable_error_type: { - "wasi:io/streams"::"stream-error": Error, - "wasi:filesystem/types"::"error-code": Error, - "wasi:sockets/network"::"error-code": Error, + "wasi:io/streams"::"stream-error": crate::preview2::StreamError, + "wasi:filesystem/types"::"error-code": crate::preview2::FsError, + "wasi:sockets/network"::"error-code": crate::preview2::SocketError, }, with: { "wasi:sockets/network/network": super::network::Network, @@ -160,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, diff --git a/crates/wasi/src/preview2/network.rs b/crates/wasi/src/preview2/network.rs index 614130392d29..1f49e06f349f 100644 --- a/crates/wasi/src/preview2/network.rs +++ b/crates/wasi/src/preview2/network.rs @@ -1,6 +1,30 @@ +use crate::preview2::bindings::wasi::sockets::network::ErrorCode; +use crate::preview2::{TableError, TrappableError}; use cap_std::net::Pool; pub struct Network { pub pool: Pool, pub allow_ip_name_lookup: bool, } + +pub type SocketResult = Result; + +pub type SocketError = TrappableError; + +impl From for SocketError { + fn from(error: TableError) -> Self { + Self::trap(error) + } +} + +impl From for SocketError { + fn from(error: std::io::Error) -> Self { + ErrorCode::from(error).into() + } +} + +impl From for SocketError { + fn from(error: rustix::io::Errno) -> Self { + ErrorCode::from(error).into() + } +} diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs index a4bdada9b7f6..1a94ad7d0213 100644 --- a/crates/wasi/src/preview2/preview1.rs +++ b/crates/wasi/src/preview2/preview1.rs @@ -8,7 +8,7 @@ use crate::preview2::bindings::{ filesystem::{preopens, types as filesystem}, io::{poll, streams}, }; -use crate::preview2::{IsATTY, StreamError, TableError, WasiView}; +use crate::preview2::{FsError, IsATTY, StreamError, StreamResult, TableError, WasiView}; use anyhow::{anyhow, bail, Context}; use std::borrow::Borrow; use std::collections::{BTreeMap, HashSet}; @@ -63,26 +63,16 @@ impl BlockingMode { match streams::HostInputStream::blocking_read(host, input_stream, max_size).await { Ok(r) if r.is_empty() => Err(types::Errno::Intr.into()), Ok(r) => Ok(r), - Err(e) if matches!(e.downcast_ref(), Some(streams::StreamError::Closed)) => { - Ok(Vec::new()) - } - Err(e) => { - tracing::trace!("throwing away read error to report as Errno::Io: {e:?}"); - Err(types::Errno::Io.into()) - } + Err(StreamError::Closed) => Ok(Vec::new()), + Err(e) => Err(e.into()), } } BlockingMode::NonBlocking => { match streams::HostInputStream::read(host, input_stream, max_size).await { Ok(r) => Ok(r), - Err(e) if matches!(e.downcast_ref(), Some(streams::StreamError::Closed)) => { - Ok(Vec::new()) - } - Err(e) => { - tracing::trace!("throwing away read error to report as Errno::Io: {e:?}"); - Err(types::Errno::Io.into()) - } + Err(StreamError::Closed) => Ok(Vec::new()), + Err(e) => Err(e.into()), } } } @@ -92,7 +82,7 @@ impl BlockingMode { host: &mut (impl streams::Host + poll::Host), output_stream: Resource, mut bytes: &[u8], - ) -> Result { + ) -> StreamResult { use streams::HostOutputStream as Streams; match self { @@ -117,7 +107,7 @@ impl BlockingMode { BlockingMode::NonBlocking => { let n = match Streams::check_write(host, output_stream.borrowed()) { Ok(n) => n, - Err(e) if matches!(e.downcast_ref(), Some(streams::StreamError::Closed)) => 0, + Err(StreamError::Closed) => 0, Err(e) => Err(e)?, }; @@ -128,17 +118,13 @@ impl BlockingMode { match Streams::write(host, output_stream.borrowed(), bytes[..len].to_vec()) { Ok(()) => {} - Err(e) if matches!(e.downcast_ref(), Some(streams::StreamError::Closed)) => { - return Ok(0) - } + Err(StreamError::Closed) => return Ok(0), Err(e) => Err(e)?, } match Streams::blocking_flush(host, output_stream.borrowed()).await { Ok(()) => {} - Err(e) if matches!(e.downcast_ref(), Some(streams::StreamError::Closed)) => { - return Ok(0) - } + Err(StreamError::Closed) => return Ok(0), Err(e) => Err(e)?, }; @@ -576,25 +562,25 @@ impl wiggle::GuestErrorType for types::Errno { impl From for types::Error { fn from(err: StreamError) -> Self { - types::Error::from(streams::Error::from(err)) - } -} - -impl From for types::Error { - fn from(err: streams::Error) -> Self { - match err.downcast() { - Ok(se) => se.into(), - Err(t) => types::Error::trap(t), + match err { + StreamError::Closed => types::Errno::Io.into(), + StreamError::LastOperationFailed(e) => match e.downcast::() { + Ok(err) => filesystem::ErrorCode::from(err).into(), + Err(e) => { + log::debug!("dropping error {e:?}"); + types::Errno::Io.into() + } + }, + StreamError::Trap(e) => types::Error::trap(e), } } } -impl From for types::Error { - fn from(err: streams::StreamError) -> Self { - match err { - streams::StreamError::Closed | streams::StreamError::LastOperationFailed => { - types::Errno::Io.into() - } +impl From for types::Error { + fn from(err: FsError) -> Self { + match err.downcast() { + Ok(code) => code.into(), + Err(e) => types::Error::trap(e), } } } @@ -788,28 +774,6 @@ impl From for types::Error { } } -impl TryFrom for types::Errno { - type Error = anyhow::Error; - - fn try_from(err: filesystem::Error) -> Result { - match err.downcast() { - Ok(code) => Ok(code.into()), - Err(e) => Err(e), - } - } -} - -impl TryFrom for types::Error { - type Error = anyhow::Error; - - fn try_from(err: filesystem::Error) -> Result { - match err.downcast() { - Ok(code) => Ok(code.into()), - Err(e) => Err(e), - } - } -} - impl From for types::Error { fn from(err: TableError) -> Self { types::Error::trap(err.into()) @@ -2231,12 +2195,8 @@ impl< let fd = fd.borrowed(); let position = position.clone(); drop(t); - match self - .stat(fd) - .await - .map_err(|e| e.try_into().context("failed to call `stat`")) - { - Ok(filesystem::DescriptorStat { size, .. }) => { + match self.stat(fd).await? { + filesystem::DescriptorStat { size, .. } => { let pos = position.load(Ordering::Relaxed); let nbytes = size.saturating_sub(pos); types::Event { @@ -2253,16 +2213,6 @@ impl< }, } } - Err(Ok(error)) => types::Event { - userdata: sub.userdata, - error, - type_: types::Eventtype::FdRead, - fd_readwrite: types::EventFdReadwrite { - flags: types::Eventrwflags::empty(), - nbytes: 1, - }, - }, - Err(Err(error)) => return Err(types::Error::trap(error)), } } // TODO: Support sockets diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 4143b9ad86c2..6200e1226d07 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,5 +1,6 @@ use crate::preview2::filesystem::FileInputStream; use crate::preview2::poll::Subscribe; +use crate::preview2::TableError; use anyhow::Result; use bytes::Bytes; @@ -19,24 +20,40 @@ pub trait HostInputStream: Subscribe { /// /// The [`StreamError`] return value communicates when this stream is /// closed, when a read fails, or when a trap should be generated. - fn read(&mut self, size: usize) -> Result; + fn read(&mut self, size: usize) -> StreamResult; /// Same as the `read` method except that bytes are skipped. /// /// Note that this method is non-blocking like `read` and returns the same /// errors. - fn skip(&mut self, nelem: usize) -> Result { + fn skip(&mut self, nelem: usize) -> StreamResult { let bs = self.read(nelem)?; Ok(bs.len()) } } +/// 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 = Result; + #[derive(Debug)] pub enum StreamError { Closed, LastOperationFailed(anyhow::Error), Trap(anyhow::Error), } + +impl StreamError { + pub fn trap(msg: &str) -> StreamError { + StreamError::Trap(anyhow::anyhow!("{msg}")) + } +} + impl std::fmt::Display for StreamError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -46,6 +63,7 @@ impl std::fmt::Display for StreamError { } } } + impl std::error::Error for StreamError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { @@ -55,6 +73,12 @@ impl std::error::Error for StreamError { } } +impl From for StreamError { + fn from(error: TableError) -> Self { + Self::Trap(error.into()) + } +} + /// Host trait for implementing the `wasi:io/streams.output-stream` resource: /// A bytestream which can be written to. #[async_trait::async_trait] @@ -75,7 +99,7 @@ pub trait HostOutputStream: Subscribe { /// - stream is closed /// - prior operation ([`write`](Self::write) or [`flush`](Self::flush)) failed /// - caller performed an illegal operation (e.g. wrote more bytes than were permitted) - fn write(&mut self, bytes: Bytes) -> Result<(), StreamError>; + fn write(&mut self, bytes: Bytes) -> StreamResult<()>; /// Trigger a flush of any bytes buffered in this stream implementation. /// @@ -96,7 +120,7 @@ pub trait HostOutputStream: Subscribe { /// - stream is closed /// - prior operation ([`write`](Self::write) or [`flush`](Self::flush)) failed /// - caller performed an illegal operation (e.g. wrote more bytes than were permitted) - fn flush(&mut self) -> Result<(), StreamError>; + fn flush(&mut self) -> StreamResult<()>; /// Returns the number of bytes that are ready to be written to this stream. /// @@ -110,13 +134,13 @@ pub trait HostOutputStream: Subscribe { /// Returns an [`StreamError`] if: /// - stream is closed /// - prior operation ([`write`](Self::write) or [`flush`](Self::flush)) failed - fn check_write(&mut self) -> Result; + fn check_write(&mut self) -> StreamResult; /// Repeatedly write a byte to a stream. /// Important: this write must be non-blocking! /// Returning an Err which downcasts to a [`StreamError`] will be /// reported to Wasm as the empty error result. Otherwise, errors will trap. - fn write_zeroes(&mut self, nelem: usize) -> Result<(), StreamError> { + fn write_zeroes(&mut self, nelem: usize) -> StreamResult<()> { // TODO: We could optimize this to not allocate one big zeroed buffer, and instead write // repeatedly from a 'static buffer of zeros. let bs = Bytes::from_iter(core::iter::repeat(0 as u8).take(nelem)); @@ -126,7 +150,7 @@ pub trait HostOutputStream: Subscribe { /// Simultaneously waits for this stream to be writable and then returns how /// much may be written or the last error that happened. - async fn write_ready(&mut self) -> Result { + async fn write_ready(&mut self) -> StreamResult { self.ready().await; self.check_write() } diff --git a/crates/wasi/wit/deps/filesystem/types.wit b/crates/wasi/wit/deps/filesystem/types.wit index 3f69bf997a29..aecdd0ef354b 100644 --- a/crates/wasi/wit/deps/filesystem/types.wit +++ b/crates/wasi/wit/deps/filesystem/types.wit @@ -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. @@ -795,4 +795,16 @@ interface types { /// Read a single directory entry from a `directory-entry-stream`. read-directory-entry: func() -> result, 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) -> option } diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi/wit/deps/io/streams.wit index 8240507976f7..55562d1cbfce 100644 --- a/crates/wasi/wit/deps/io/streams.wit +++ b/crates/wasi/wit/deps/io/streams.wit @@ -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 diff --git a/crates/wit-bindgen/src/lib.rs b/crates/wit-bindgen/src/lib.rs index 45fe6aedf3b1..7183e0bf232c 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -2,7 +2,7 @@ use crate::rust::{to_rust_ident, to_rust_upper_camel_case, RustGenerator, TypeMo use crate::types::{TypeInfo, Types}; use anyhow::{anyhow, bail, Context}; use heck::*; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Write as _; use std::io::{Read, Write}; @@ -48,6 +48,7 @@ struct Wasmtime { interface_names: HashMap, with_name_counter: usize, interface_last_seen_as_import: HashMap, + trappable_errors: IndexMap, } struct ImportInterface { @@ -218,6 +219,16 @@ impl Wasmtime { fn generate(&mut self, resolve: &Resolve, id: WorldId) -> String { self.types.analyze(resolve, id); + for (i, te) in self.opts.trappable_error_type.iter().enumerate() { + let id = resolve_type_in_package(resolve, &te.wit_package_path, &te.wit_type_name) + .context(format!("resolving {:?}", te)) + .unwrap(); + let name = format!("_TrappableError{i}"); + uwriteln!(self.src, "type {name} = {};", te.rust_type_name); + let prev = self.trappable_errors.insert(id, name); + assert!(prev.is_none()); + } + let world = &resolve.worlds[id]; for (name, import) in world.imports.iter() { if !self.opts.only_interfaces || matches!(import, WorldItem::Interface(_)) { @@ -837,32 +848,15 @@ struct InterfaceGenerator<'a> { gen: &'a mut Wasmtime, resolve: &'a Resolve, current_interface: Option<(InterfaceId, &'a WorldKey, bool)>, - - /// A mapping of wit types to their rust type name equivalent. This is the pre-processed - /// version of `gen.opts.trappable_error_types`, where the types have been eagerly resolved. - trappable_errors: IndexMap, } impl<'a> InterfaceGenerator<'a> { fn new(gen: &'a mut Wasmtime, resolve: &'a Resolve) -> InterfaceGenerator<'a> { - let trappable_errors = gen - .opts - .trappable_error_type - .iter() - .map(|te| { - let id = resolve_type_in_package(resolve, &te.wit_package_path, &te.wit_type_name) - .context(format!("resolving {:?}", te))?; - Ok((id, te.rust_type_name.clone())) - }) - .collect::>>() - .unwrap(); - InterfaceGenerator { src: Source::default(), gen, resolve, current_interface: None, - trappable_errors, } } @@ -876,10 +870,6 @@ impl<'a> InterfaceGenerator<'a> { fn types(&mut self, id: InterfaceId) { for (name, id) in self.resolve.interfaces[id].types.iter() { self.define_type(name, *id); - - if let Some(rust_name) = self.trappable_errors.get(id) { - self.define_trappable_error_type(*id, rust_name.clone()) - } } } @@ -1467,9 +1457,11 @@ impl<'a> InterfaceGenerator<'a> { _ => return None, }; - let rust_type = self.trappable_errors.get(&error_typeid)?; + let name = self.gen.trappable_errors.get(&error_typeid)?; - Some((result, error_typeid, rust_type.clone())) + let mut path = self.path_to_root(); + uwrite!(path, "{name}"); + Some((result, error_typeid, path)) } fn generate_add_to_linker(&mut self, id: InterfaceId, name: &str) { @@ -1499,14 +1491,59 @@ impl<'a> InterfaceGenerator<'a> { } self.generate_function_trait_sig(func); } + + // Generate `convert_*` functions to convert custom trappable errors + // into the representation required by Wasmtime's component API. + let mut required_conversion_traits = IndexSet::new(); + let mut errors_converted = IndexSet::new(); + let my_error_types = iface + .types + .iter() + .filter(|(_, id)| self.gen.trappable_errors.contains_key(*id)) + .map(|(_, id)| *id); + let used_error_types = iface + .functions + .iter() + .filter_map(|(_, func)| self.special_case_trappable_error(&func.results)) + .map(|(_, id, _)| id); + for err in my_error_types.chain(used_error_types).collect::>() { + let custom_name = &self.gen.trappable_errors[&err]; + let err = &self.resolve.types[resolve_type_definition_id(self.resolve, err)]; + let err_name = err.name.as_ref().unwrap(); + let err_snake = err_name.to_snake_case(); + let err_camel = err_name.to_upper_camel_case(); + let owner = match err.owner { + TypeOwner::Interface(i) => i, + _ => unimplemented!(), + }; + match self.path_to_interface(owner) { + Some(path) => { + required_conversion_traits.insert(format!("{path}::Host")); + } + None => { + if errors_converted.insert(err_name) { + let root = self.path_to_root(); + uwriteln!( + self.src, + "fn convert_{err_snake}(&mut self, err: {root}{custom_name}) -> wasmtime::Result<{err_camel}>;" + ); + } + } + } + } uwriteln!(self.src, "}}"); - let where_clause = if self.gen.opts.async_.maybe_async() { + let mut where_clause = if self.gen.opts.async_.maybe_async() { "T: Send, U: Host + Send".to_string() } else { "U: Host".to_string() }; + for t in required_conversion_traits { + where_clause.push_str(" + "); + where_clause.push_str(&t); + } + uwriteln!( self.src, " @@ -1655,16 +1692,24 @@ impl<'a> InterfaceGenerator<'a> { ); } - if self.special_case_trappable_error(&func.results).is_some() { + if let Some((_, err, _)) = self.special_case_trappable_error(&func.results) { + let err = &self.resolve.types[resolve_type_definition_id(self.resolve, err)]; + let err_name = err.name.as_ref().unwrap(); + let owner = match err.owner { + TypeOwner::Interface(i) => i, + _ => unimplemented!(), + }; + let convert_trait = match self.path_to_interface(owner) { + Some(path) => format!("{path}::Host"), + None => format!("Host"), + }; + let convert = format!("{}::convert_{}", convert_trait, err_name.to_snake_case()); uwrite!( self.src, - "match r {{ - Ok(a) => Ok((Ok(a),)), - Err(e) => match e.downcast() {{ - Ok(api_error) => Ok((Err(api_error),)), - Err(anyhow_error) => Err(anyhow_error), - }} - }}" + "Ok((match r {{ + Ok(a) => Ok(a), + Err(e) => Err({convert}(host, e)?), + }},))" ); } else if func.results.iter_types().len() == 1 { uwrite!(self.src, "Ok((r?,))\n"); @@ -1699,9 +1744,7 @@ impl<'a> InterfaceGenerator<'a> { self.push_str(")"); self.push_str(" -> "); - if let Some((r, error_id, error_typename)) = - self.special_case_trappable_error(&func.results) - { + if let Some((r, _id, error_typename)) = self.special_case_trappable_error(&func.results) { // Functions which have a single result `result` get special // cased to use the host_wasmtime_rust::Error, making it possible // for them to trap or use `?` to propogate their errors @@ -1712,12 +1755,6 @@ impl<'a> InterfaceGenerator<'a> { self.push_str("()"); } self.push_str(","); - if let TypeOwner::Interface(id) = self.resolve.types[error_id].owner { - if let Some(path) = self.path_to_interface(id) { - self.push_str(&path); - self.push_str("::"); - } - } self.push_str(&error_typename); self.push_str(">"); } else { @@ -1867,53 +1904,6 @@ impl<'a> InterfaceGenerator<'a> { self.src.push_str("}\n"); } - fn define_trappable_error_type(&mut self, id: TypeId, rust_name: String) { - let info = self.info(id); - if self.lifetime_for(&info, TypeMode::Owned).is_some() { - panic!("wit error for {rust_name} is not 'static") - } - let abi_type = self.param_name(id); - - uwriteln!( - self.src, - " - #[derive(Debug)] - pub struct {rust_name} {{ - inner: anyhow::Error, - }} - impl std::fmt::Display for {rust_name} {{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ - write!(f, \"{{}}\", self.inner) - }} - }} - impl std::error::Error for {rust_name} {{ - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {{ - self.inner.source() - }} - }} - impl {rust_name} {{ - pub fn trap(inner: anyhow::Error) -> Self {{ - Self {{ inner }} - }} - pub fn downcast(self) -> Result<{abi_type}, anyhow::Error> {{ - self.inner.downcast() - }} - pub fn downcast_ref(&self) -> Option<&{abi_type}> {{ - self.inner.downcast_ref() - }} - pub fn context(self, s: impl Into) -> Self {{ - Self {{ inner: self.inner.context(s.into()) }} - }} - }} - impl From<{abi_type}> for {rust_name} {{ - fn from(abi: {abi_type}) -> {rust_name} {{ - {rust_name} {{ inner: anyhow::Error::from(abi) }} - }} - }} - " - ); - } - fn rustdoc(&mut self, docs: &Docs) { let docs = match &docs.contents { Some(docs) => docs, diff --git a/tests/all/component_model/bindgen/results.rs b/tests/all/component_model/bindgen/results.rs index 6c0b3f8b6472..32a645682629 100644 --- a/tests/all/component_model/bindgen/results.rs +++ b/tests/all/component_model/bindgen/results.rs @@ -241,6 +241,39 @@ mod enum_error { trappable_error_type: { "inline:inline/imports"::e1: TrappableE1 } }); + // You can create concrete trap types which make it all the way out to the + // host caller, via downcast_ref below. + #[derive(Debug)] + pub struct MyTrap; + + impl std::fmt::Display for MyTrap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } + } + impl std::error::Error for MyTrap {} + + pub enum TrappableE1 { + Normal(imports::E1), + MyTrap(MyTrap), + } + + // It is possible to define From impls that target these generated trappable + // types. This allows you to integrate libraries with other error types, or + // use your own more descriptive error types, and use ? to convert them at + // their throw site. + impl From for TrappableE1 { + fn from(t: MyTrap) -> TrappableE1 { + TrappableE1::MyTrap(t) + } + } + + impl From for TrappableE1 { + fn from(t: imports::E1) -> TrappableE1 { + TrappableE1::Normal(t) + } + } + #[test] fn run() -> Result<(), Error> { let engine = engine(); @@ -305,33 +338,18 @@ mod enum_error { ), )?; - // You can create concrete trap types which make it all the way out to the - // host caller, via downcast_ref below. - #[derive(Debug)] - struct MyTrap; - - impl std::fmt::Display for MyTrap { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } - } - impl std::error::Error for MyTrap {} - - // It is possible to define From impls that target these generated trappable - // types. This allows you to integrate libraries with other error types, or - // use your own more descriptive error types, and use ? to convert them at - // their throw site. - impl From for imports::TrappableE1 { - fn from(t: MyTrap) -> imports::TrappableE1 { - imports::TrappableE1::trap(anyhow!(t)) - } - } - #[derive(Default)] struct MyImports {} impl imports::Host for MyImports { - fn enum_error(&mut self, a: f64) -> Result { + fn convert_e1(&mut self, err: TrappableE1) -> anyhow::Result { + match err { + TrappableE1::Normal(e) => Ok(e), + TrappableE1::MyTrap(e) => Err(e.into()), + } + } + + fn enum_error(&mut self, a: f64) -> Result { if a == 0.0 { Ok(a) } else if a == 1.0 { @@ -405,6 +423,17 @@ mod record_error { trappable_error_type: { "inline:inline/imports"::"e2": TrappableE2 } }); + pub enum TrappableE2 { + Normal(imports::E2), + Trap(anyhow::Error), + } + + impl From for TrappableE2 { + fn from(t: imports::E2) -> TrappableE2 { + TrappableE2::Normal(t) + } + } + #[test] fn run() -> Result<(), Error> { let engine = engine(); @@ -476,7 +505,13 @@ mod record_error { struct MyImports {} impl imports::Host for MyImports { - fn record_error(&mut self, a: f64) -> Result { + fn convert_e2(&mut self, err: TrappableE2) -> anyhow::Result { + match err { + TrappableE2::Normal(e) => Ok(e), + TrappableE2::Trap(e) => Err(e.into()), + } + } + fn record_error(&mut self, a: f64) -> Result { if a == 0.0 { Ok(a) } else if a == 1.0 { @@ -485,7 +520,7 @@ mod record_error { col: 1312, })? } else { - Err(imports::TrappableE2::trap(anyhow!("record_error: trap"))) + Err(TrappableE2::Trap(anyhow!("record_error: trap"))) } } } @@ -559,6 +594,17 @@ mod variant_error { trappable_error_type: { "inline:inline/imports"::e3: TrappableE3 } }); + pub enum TrappableE3 { + Normal(imports::E3), + Trap(anyhow::Error), + } + + impl From for TrappableE3 { + fn from(t: imports::E3) -> TrappableE3 { + TrappableE3::Normal(t) + } + } + #[test] fn run() -> Result<(), Error> { let engine = engine(); @@ -653,7 +699,13 @@ mod variant_error { struct MyImports {} impl imports::Host for MyImports { - fn variant_error(&mut self, a: f64) -> Result { + fn convert_e3(&mut self, err: TrappableE3) -> anyhow::Result { + match err { + TrappableE3::Normal(e) => Ok(e), + TrappableE3::Trap(e) => Err(e.into()), + } + } + fn variant_error(&mut self, a: f64) -> Result { if a == 0.0 { Ok(a) } else if a == 1.0 { @@ -662,7 +714,7 @@ mod variant_error { col: 1312, }))? } else { - Err(imports::TrappableE3::trap(anyhow!("variant_error: trap"))) + Err(TrappableE3::Trap(anyhow!("variant_error: trap"))) } } } @@ -737,6 +789,17 @@ mod multiple_interfaces_error { trappable_error_type: { "inline:inline/types"::e1: TrappableE1 } }); + pub enum TrappableE1 { + Normal(types::E1), + Trap(anyhow::Error), + } + + impl From for TrappableE1 { + fn from(t: imports::E1) -> TrappableE1 { + TrappableE1::Normal(t) + } + } + #[test] fn run() -> Result<(), Error> { let engine = engine(); @@ -819,9 +882,9 @@ mod multiple_interfaces_error { // types. This allows you to integrate libraries with other error types, or // use your own more descriptive error types, and use ? to convert them at // their throw site. - impl From for types::TrappableE1 { - fn from(t: MyTrap) -> types::TrappableE1 { - types::TrappableE1::trap(anyhow!(t)) + impl From for TrappableE1 { + fn from(t: MyTrap) -> TrappableE1 { + TrappableE1::Trap(anyhow!(t)) } } @@ -829,7 +892,13 @@ mod multiple_interfaces_error { struct MyImports {} impl types::Host for MyImports { - fn enum_error(&mut self, a: f64) -> Result { + fn convert_e1(&mut self, err: TrappableE1) -> anyhow::Result { + match err { + TrappableE1::Normal(e) => Ok(e), + TrappableE1::Trap(e) => Err(e.into()), + } + } + fn enum_error(&mut self, a: f64) -> Result { if a == 0.0 { Ok(a) } else if a == 1.0 { @@ -841,7 +910,7 @@ mod multiple_interfaces_error { } impl imports::Host for MyImports { - fn enum_error(&mut self, a: f64) -> Result { + fn enum_error(&mut self, a: f64) -> Result { if a == 0.0 { Ok(a) } else if a == 1.0 {