Skip to content

Commit

Permalink
Support custom error types in Wasmtime (#23)
Browse files Browse the repository at this point in the history
This adds support for a configuration value for Wasmtime where all
Rust-defined methods will return a `Result` with a custom error, and
then the trait has conversion methods from this error payload back to
either a `Trap` to propagate or the original error value if applicable.
  • Loading branch information
alexcrichton authored Jun 25, 2021
1 parent a991b67 commit dd234ab
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 27 deletions.
7 changes: 0 additions & 7 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@

# wasmtime

* Functions need to be able to at least optionally return a trap, e.g.
`proc_raise` or they were passed an invalid buffer.

* buffer-in-buffer doesn't work. Doesn't work because we can't get a re-access
of the transaction to add more buffers into it after-the-fact.

Expand All @@ -18,10 +15,6 @@
* use `GuestError::InFunc` more liberally
- stores/loads
- `try_from` conversions
* user-defined conversion from user-defined type to the actual type
- used for converting `anyhow::Error` into either an errno or a trap
- check for specific type of error, otherwise assume trap
- conversion receives context to optionally store the error message
* generate just the trait (??? what to do about `wasmtime` dep ???)

# JS
Expand Down
1 change: 1 addition & 0 deletions crates/demo/demo.witx
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ render_wasmtime: function(
import: bool,
tracing: bool,
async: async,
custom_error: bool,
) -> expected<files, string>
5 changes: 5 additions & 0 deletions crates/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ <h1>Generated bindings</h1>

<input type="checkbox" id="wasmtime-async" name="async">
<label for="wasmtime-async"><code>async</code></label>

&middot;

<input type="checkbox" id="wasmtime-custom-error" name="custom-error">
<label for="wasmtime-custom-error">custom error</label>
</div>
</div>

Expand Down
10 changes: 9 additions & 1 deletion crates/demo/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const files = document.getElementById('file-select') as HTMLSelectElement;
const rustUnchecked = document.getElementById('rust-unchecked') as HTMLInputElement;
const wasmtimeTracing = document.getElementById('wasmtime-tracing') as HTMLInputElement;
const wasmtimeAsync = document.getElementById('wasmtime-async') as HTMLInputElement;
const wasmtimeCustomError = document.getElementById('wasmtime-custom-error') as HTMLInputElement;

const inputEditor = ace.edit("input");
const outputEditor = ace.edit("output");
Expand Down Expand Up @@ -58,7 +59,13 @@ async function render() {
async_ = { tag: 'all' };
else
async_ = { tag: 'none' };
result = wasm.render_wasmtime(witx, is_import, wasmtimeTracing.checked, async_);
result = wasm.render_wasmtime(
witx,
is_import,
wasmtimeTracing.checked,
async_,
wasmtimeCustomError.checked,
);
break;
default: return;
}
Expand Down Expand Up @@ -109,6 +116,7 @@ mode.addEventListener('change', render);
rustUnchecked.addEventListener('change', render);
wasmtimeTracing.addEventListener('change', render);
wasmtimeAsync.addEventListener('change', render);
wasmtimeCustomError.addEventListener('change', render);
files.addEventListener('change', updateSelectedFile);


Expand Down
2 changes: 2 additions & 0 deletions crates/demo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl demo::Demo for Demo {
import: bool,
tracing: bool,
async_: demo::Async,
custom_error: bool,
) -> Result<Vec<(String, String)>, String> {
use witx_bindgen_gen_wasmtime::Async;

Expand All @@ -77,6 +78,7 @@ impl demo::Demo for Demo {
demo::Async::None => Async::None,
demo::Async::Only(list) => Async::Only(list.into_iter().collect()),
};
opts.custom_error = custom_error;
self.generate(&witx, import, opts.build())
}
}
18 changes: 9 additions & 9 deletions crates/gen-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,20 +565,20 @@ pub trait RustGenerator {
self.push_str("{\n");

self.push_str("pub fn name(&self) -> &'static str {\n");
self.push_str("match self {");
self.push_str("match self {\n");
for case in variant.cases.iter() {
self.push_str(&name);
self.push_str("::");
self.push_str(&case_name(&case.name));
self.push_str(" => \"");
self.push_str(case.name.as_str());
self.push_str("\",");
self.push_str("\",\n");
}
self.push_str("}\n");
self.push_str("}\n");

self.push_str("pub fn message(&self) -> &'static str {\n");
self.push_str("match self {");
self.push_str("match self {\n");
for case in variant.cases.iter() {
self.push_str(&name);
self.push_str("::");
Expand All @@ -587,7 +587,7 @@ pub trait RustGenerator {
if let Some(contents) = &case.docs.contents {
self.push_str(contents.trim());
}
self.push_str("\",");
self.push_str("\",\n");
}
self.push_str("}\n");
self.push_str("}\n");
Expand All @@ -601,11 +601,11 @@ pub trait RustGenerator {
);
self.push_str("f.debug_struct(\"");
self.push_str(&name);
self.push_str("\")");
self.push_str(".field(\"code\", &(*self as i32))");
self.push_str(".field(\"name\", &self.name())");
self.push_str(".field(\"message\", &self.message())");
self.push_str(".finish()");
self.push_str("\")\n");
self.push_str(".field(\"code\", &(*self as i32))\n");
self.push_str(".field(\"name\", &self.name())\n");
self.push_str(".field(\"message\", &self.message())\n");
self.push_str(".finish()\n");
self.push_str("}\n");
self.push_str("}\n");

Expand Down
133 changes: 124 additions & 9 deletions crates/gen-wasmtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub struct Wasmtime {
needs_copy_slice: bool,
needs_buffer_glue: bool,
needs_le: bool,
needs_custom_error_to_trap: bool,
needs_custom_error_to_types: BTreeSet<String>,
all_needed_handles: BTreeSet<String>,
types: Types,
imports: HashMap<String, Vec<Import>>,
Expand Down Expand Up @@ -72,6 +74,11 @@ pub struct Opts {
/// comma-separated list.
#[cfg_attr(feature = "structopt", structopt(long = "async"))]
pub async_: Async,

/// A flag to indicate that all trait methods in imports should return a
/// custom trait-defined error. Applicable for import bindings.
#[cfg_attr(feature = "structopt", structopt(long))]
pub custom_error: bool,
}

#[derive(Debug)]
Expand Down Expand Up @@ -125,6 +132,19 @@ impl Opts {
}
}

enum FunctionRet {
/// The function return is normal and needs to extra handling.
Normal,
/// The function return was wrapped in a `Result` in Rust. The `Ok` variant
/// is the actual value that will be lowered, and the `Err`, if present,
/// means that a trap has occurred.
CustomToTrap,
/// The function returns a `Result` in both wasm and in Rust, but the
/// Rust error type is a custom error and must be converted to `err`. The
/// `ok` variant payload is provided here too.
CustomToError { ok: Option<Type>, err: String },
}

impl Wasmtime {
pub fn new() -> Wasmtime {
Wasmtime::default()
Expand Down Expand Up @@ -154,6 +174,37 @@ impl Wasmtime {
self.push_str("use witx_bindgen_wasmtime::rt::copy_slice;\n");
}
}

/// Classifies the return value of a function to see if it needs handling
/// with respect to the `custom_error` configuration option.
fn classify_fn_ret(&mut self, iface: &Interface, f: &Function) -> FunctionRet {
if !self.opts.custom_error {
return FunctionRet::Normal;
}

if f.results.len() != 1 {
self.needs_custom_error_to_trap = true;
return FunctionRet::CustomToTrap;
}
if let Type::Id(id) = &f.results[0].1 {
if let TypeDefKind::Variant(v) = &iface.types[*id].kind {
if let Some((ok, Some(err))) = v.as_expected() {
if let Type::Id(err) = err {
if let Some(name) = &iface.types[*err].name {
self.needs_custom_error_to_types.insert(name.clone());
return FunctionRet::CustomToError {
ok: ok.cloned(),
err: name.to_string(),
};
}
}
}
}
}

self.needs_custom_error_to_trap = true;
FunctionRet::CustomToTrap
}
}

impl RustGenerator for Wasmtime {
Expand Down Expand Up @@ -592,7 +643,8 @@ impl Generator for Wasmtime {
self_arg.push_str(", mem: witx_bindgen_wasmtime::RawMemory");
}
self.in_trait = true;
self.print_signature(

self.print_docs_and_params(
iface,
func,
Visibility::Private,
Expand All @@ -602,13 +654,37 @@ impl Generator for Wasmtime {
} else {
witx_bindgen_gen_rust::Async::No
},
None,
Some(&self_arg),
if is_dtor {
TypeMode::Owned
} else {
TypeMode::LeafBorrowed("'_")
},
);
// The Rust return type may differ from the wasm return type based on
// the `custom_error` configuration of this code generator.
match self.classify_fn_ret(iface, func) {
FunctionRet::Normal => {
if func.results.len() > 0 {
self.push_str(" -> ");
self.print_results(iface, func);
}
}
FunctionRet::CustomToTrap => {
self.push_str(" -> Result<");
self.print_results(iface, func);
self.push_str(", Self::Error>");
}
FunctionRet::CustomToError { ok, .. } => {
self.push_str(" -> Result<");
match ok {
Some(ty) => self.print_ty(iface, &ty, TypeMode::Owned),
None => self.push_str("()"),
}
self.push_str(", Self::Error>");
}
}
self.in_trait = false;
let trait_signature = mem::take(&mut self.src).into();

Expand Down Expand Up @@ -867,6 +943,21 @@ impl Generator for Wasmtime {
self.src.push_str(";\n");
}
}
if self.opts.custom_error {
self.src.push_str("type Error;\n");
if self.needs_custom_error_to_trap {
self.src.push_str(
"fn error_to_trap(&mut self, err: Self::Error) -> wasmtime::Trap;\n",
);
}
for ty in self.needs_custom_error_to_types.iter() {
self.src.push_str(&format!(
"fn error_to_{}(&mut self, err: Self::Error) -> Result<{}, wasmtime::Trap>;\n",
ty.to_snake_case(),
ty.to_camel_case(),
));
}
}
for f in funcs {
self.src.push_str(&f.trait_signature);
self.src.push_str(";\n\n");
Expand Down Expand Up @@ -1839,19 +1930,43 @@ impl Bindgen for FunctionBindgen<'_> {
self.push_str(&mem);
self.push_str(".raw() };\n");
}
self.let_results(func.results.len(), results);
self.push_str("host.");
self.push_str(&func.name);
self.push_str("(");

let mut call = format!("host.{}(", func.name);
if self.func_takes_all_memory {
self.push_str("raw_memory, ");
call.push_str("raw_memory, ");
}
for i in 0..operands.len() {
self.push_str(&format!("param{}, ", i));
call.push_str(&format!("param{}, ", i));
}
self.push_str(")");
call.push_str(")");
if self.gen.opts.async_.includes(&func.name) {
self.push_str(".await");
call.push_str(".await");
}

self.let_results(func.results.len(), results);
match self.gen.classify_fn_ret(iface, func) {
FunctionRet::Normal => self.push_str(&call),
// Unwrap the result, translating errors to unconditional
// traps
FunctionRet::CustomToTrap => {
self.push_str("match ");
self.push_str(&call);
self.push_str("{\n");
self.push_str("Ok(val) => val,\n");
self.push_str("Err(e) => return Err(host.error_to_trap(e)),\n");
self.push_str("}");
}
// Keep the `Result` as a `Result`, but convert the error
// to either the expected destination value or a trap,
// propagating a trap outwards.
FunctionRet::CustomToError { err, .. } => {
self.push_str("match ");
self.push_str(&call);
self.push_str("{\n");
self.push_str("Ok(val) => Ok(val),\n");
self.push_str(&format!("Err(e) => Err(host.error_to_{}(e)?),\n", err));
self.push_str("}");
}
}
self.push_str(";\n");
self.after_call = true;
Expand Down
15 changes: 15 additions & 0 deletions crates/gen-wasmtime/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,18 @@ mod async_tests {
});
}
}

mod custom_errors {
witx_bindgen_wasmtime::import!({
src["x"]: "
foo: function()
bar: function() -> expected<_, u32>
enum errno {
bad1,
bad2,
}
baz: function() -> expected<u32, errno>
",
custom_error: true,
});
}
3 changes: 2 additions & 1 deletion crates/test-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,11 @@ pub fn wasmtime_import(input: TokenStream) -> TokenStream {
|_| quote::quote!(),
),
(
"import-tracing",
"import-tracing-and-custom-error",
|| {
let mut opts = witx_bindgen_gen_wasmtime::Opts::default();
opts.tracing = true;
opts.custom_error = true;
opts.build()
},
|_| quote::quote!(),
Expand Down
Loading

0 comments on commit dd234ab

Please sign in to comment.