diff --git a/Cargo.lock b/Cargo.lock index 4a9f61608698..aeda7b2f8e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4600,6 +4600,7 @@ dependencies = [ name = "wasmtime-cli" version = "42.0.0" dependencies = [ + "anyhow", "async-trait", "bstr", "bytes", diff --git a/Cargo.toml b/Cargo.toml index edc4e99e1c9f..138c135482f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,11 @@ wasm-encoder = { workspace = true } cranelift-native = { workspace = true } futures = { workspace = true } +# Should only be used for tests that are explicitly testing `anyhow::Error` and +# `wasmtime::Error` integration and conversion. Otherwise, always prefer to use +# `wasmtime::Error`. +anyhow-for-testing = { workspace = true } + [target.'cfg(windows)'.dev-dependencies] windows-sys = { workspace = true, features = ["Win32_System_Memory"] } @@ -364,6 +369,7 @@ object = { version = "0.37.3", default-features = false, features = ['read_core' gimli = { version = "0.32.3", default-features = false, features = ['read'] } addr2line = { version = "0.25.1", default-features = false } anyhow = { version = "1.0.100", default-features = false } +anyhow-for-testing = { package = "anyhow", version = "1.0.100", default-features = false } windows-sys = "0.61.2" env_logger = "0.11.5" log = { version = "0.4.28", default-features = false } diff --git a/crates/component-macro/src/bindgen.rs b/crates/component-macro/src/bindgen.rs index 89f83211fab6..75106b762552 100644 --- a/crates/component-macro/src/bindgen.rs +++ b/crates/component-macro/src/bindgen.rs @@ -144,6 +144,9 @@ impl Parse for Config { Opt::WasmtimeCrate(f) => { opts.wasmtime_crate = Some(f.into_token_stream().to_string()) } + Opt::Anyhow(val) => { + opts.anyhow = val; + } Opt::IncludeGeneratedCodeFromFile(i) => include_generated_code_from_file = i, Opt::Imports(config, span) => { if imports_configured { @@ -253,6 +256,7 @@ mod kw { syn::custom_keyword!(skip_mut_forwarding_impls); syn::custom_keyword!(require_store_data_send); syn::custom_keyword!(wasmtime_crate); + syn::custom_keyword!(anyhow); syn::custom_keyword!(include_generated_code_from_file); syn::custom_keyword!(debug); syn::custom_keyword!(imports); @@ -277,6 +281,7 @@ enum Opt { SkipMutForwardingImpls(bool), RequireStoreDataSend(bool), WasmtimeCrate(syn::Path), + Anyhow(bool), IncludeGeneratedCodeFromFile(bool), Debug(bool), Imports(FunctionConfig, Span), @@ -403,6 +408,10 @@ impl Parse for Opt { input.parse::()?; input.parse::()?; Ok(Opt::WasmtimeCrate(input.parse()?)) + } else if l.peek(kw::anyhow) { + input.parse::()?; + input.parse::()?; + Ok(Opt::Anyhow(input.parse::()?.value)) } else if l.peek(kw::include_generated_code_from_file) { input.parse::()?; input.parse::()?; diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 12ab9c4313fd..38896e2e2a25 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -468,6 +468,16 @@ pub(crate) use self::store::ComponentStoreData; /// // By default this is `wasmtime`. /// wasmtime_crate: path::to::wasmtime, /// +/// // Whether to use `anyhow::Result` for trappable host-defined function +/// // imports, rather than `wasmtime::Result`. +/// // +/// // By default, this is false and `wasmtime::Result` is used instead of +/// // `anyhow::Result`. +/// // +/// // When enabled, the generated code requires the `"anyhow"` cargo feature +/// // to also be enabled in the `wasmtime` crate. +/// anyhow: false, +/// /// // This is an in-source alternative to using `WASMTIME_DEBUG_BINDGEN`. /// // /// // Note that if this option is specified then the compiler will always diff --git a/crates/wit-bindgen/src/lib.rs b/crates/wit-bindgen/src/lib.rs index 08e3730f5a12..2cd188cb8fbb 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -167,6 +167,15 @@ pub struct Opts { /// Path to the `wasmtime` crate if it's not the default path. pub wasmtime_crate: Option, + /// Whether to use `anyhow::Result` for trappable host-defined function + /// imports. + /// + /// By default, `wasmtime::Result` is used instead of `anyhow::Result`. + /// + /// When enabled, the generated code requires the `"anyhow"` cargo feature + /// to also be enabled in the `wasmtime` crate. + pub anyhow: bool, + /// If true, write the generated bindings to a file for better error /// messages from `rustc`. /// @@ -2595,7 +2604,14 @@ impl<'a> InterfaceGenerator<'a> { }},))" ); } else if func.result.is_some() { - uwrite!(self.src, "Ok((r?,))\n"); + if self.generator.opts.anyhow { + uwrite!( + self.src, + "Ok(({wt}::ToWasmtimeResult::to_wasmtime_result(r)?,))\n" + ); + } else { + uwrite!(self.src, "Ok((r?,))\n"); + } } else { uwrite!(self.src, "r\n"); } diff --git a/tests/all/component_model/bindgen.rs b/tests/all/component_model/bindgen.rs index bef8840eb4dc..83ddaa8ed08f 100644 --- a/tests/all/component_model/bindgen.rs +++ b/tests/all/component_model/bindgen.rs @@ -910,3 +910,96 @@ mod unstable_import { Ok(()) } } + +mod anyhow_errors { + use super::*; + use crate::ErrorExt; + use wasmtime::component::HasSelf; + use wasmtime::error::Context as _; + + wasmtime::component::bindgen!({ + anyhow: true, + imports: { default: trappable }, + inline: " + package foo:foo; + + interface my-interface { + ok: func() -> u32; + trap: func() -> u32; + } + + world my-world { + import my-interface; + export ok: func() -> u32; + export trap: func() -> u32; + } + ", + }); + + #[test] + fn run() -> Result<()> { + let engine = engine(); + + let component = Component::new( + &engine, + r#" + (component + (import "foo:foo/my-interface" (instance $i + (export "ok" (func (result u32))) + (export "trap" (func (result u32))) + )) + + (core module $m + (import "" "ok" (func (result i32))) + (import "" "trap" (func (result i32))) + (export "ok" (func 0)) + (export "trap" (func 1)) + ) + + (core func $ok (canon lower (func $i "ok"))) + (core func $trap (canon lower (func $i "trap"))) + + (core instance $r (instantiate $m + (with "" (instance (export "ok" (func $ok)) + (export "trap" (func $trap)))) + )) + + (func (export "ok") (result u32) (canon lift (core func $r "ok"))) + (func (export "trap") (result u32) (canon lift (core func $r "trap"))) + ) + "#, + )?; + + #[derive(Default)] + struct MyHost; + + impl foo::foo::my_interface::Host for MyHost { + // NB: these must return an `anyhow::Result` since we `bindgen!`ed + // with `anyhow: true`. + fn ok(&mut self) -> anyhow_for_testing::Result { + Ok(42) + } + fn trap(&mut self) -> anyhow_for_testing::Result { + anyhow_for_testing::bail!("anyhow error") + } + } + + let mut linker = Linker::new(&engine); + MyWorld::add_to_linker::<_, HasSelf<_>>(&mut linker, |h| h) + .context("failed to add to linker")?; + let mut store = Store::new(&engine, MyHost::default()); + let instance = MyWorld::instantiate(&mut store, &component, &linker) + .context("failed to instantiate")?; + + let x = instance + .call_ok(&mut store) + .context("failed to call `ok` function")?; + assert_eq!(x, 42); + + let result = instance.call_trap(&mut store); + let error = result.unwrap_err(); + error.assert_contains("anyhow error"); + + Ok(()) + } +}