Wasmtime component bindgen: opt-in trappable error types (#5397)

* wip

* start trying to write a runtime test

* cut out all the more complex test cases until i get this one working

* add macro parsing for the trappable error type config

* runtime result tests works for an empty and a string error type

* debugging: macro is broken because interfaces dont have names???

* thats how you name interfaces

* record error and variant error work

* show a concrete trap type, remove debug

* delete clap annotations from wit-bindgen crate

these are not used - clap isnt even an optional dep here - but were a holdover from the old home
This commit is contained in:
Pat Hickey
2022-12-14 10:44:05 -08:00
committed by GitHub
parent f0af622208
commit 2e0bc7dab6
8 changed files with 714 additions and 176 deletions

View File

@@ -5,6 +5,8 @@ use wasmtime::{
Store,
};
mod results;
mod no_imports {
use super::*;

View File

@@ -0,0 +1,604 @@
use super::{super::REALLOC_AND_FREE, engine};
use anyhow::{anyhow, Error};
use wasmtime::{
component::{Component, Linker},
Store,
};
mod empty_error {
use super::*;
wasmtime::component::bindgen!({
inline: "
world result-playground {
import imports: interface {
empty-error: func(a: float64) -> result<float64>
}
default export interface {
empty-error: func(a: float64) -> result<float64>
}
}",
});
#[test]
fn run() -> Result<(), Error> {
let engine = engine();
let component = Component::new(
&engine,
r#"
(component
(import "imports" (instance $i
(export "empty-error" (func (param "a" float64) (result (result float64))))
))
(core module $libc
(memory (export "memory") 1)
)
(core instance $libc (instantiate $libc))
(core module $m
(import "" "core_empty_error" (func $f (param f64 i32)))
(import "libc" "memory" (memory 0))
(func (export "core_empty_error_export") (param f64) (result i32)
(call $f (local.get 0) (i32.const 8))
(i32.const 8)
)
)
(core func $core_empty_error
(canon lower (func $i "empty-error") (memory $libc "memory"))
)
(core instance $i (instantiate $m
(with "" (instance (export "core_empty_error" (func $core_empty_error))))
(with "libc" (instance $libc))
))
(func $f_empty_error
(export "empty-error")
(param "a" float64)
(result (result float64))
(canon lift (core func $i "core_empty_error_export") (memory $libc "memory"))
)
)
"#,
)?;
#[derive(Default)]
struct MyImports {}
impl imports::Imports for MyImports {
fn empty_error(&mut self, a: f64) -> Result<Result<f64, ()>, Error> {
if a == 0.0 {
Ok(Ok(a))
} else if a == 1.0 {
Ok(Err(()))
} else {
Err(anyhow!("empty_error: trap"))
}
}
}
let mut linker = Linker::new(&engine);
imports::add_to_linker(&mut linker, |f: &mut MyImports| f)?;
let mut store = Store::new(&engine, MyImports::default());
let (results, _) = ResultPlayground::instantiate(&mut store, &component, &linker)?;
assert_eq!(
results
.empty_error(&mut store, 0.0)
.expect("no trap")
.expect("no error returned"),
0.0
);
results
.empty_error(&mut store, 1.0)
.expect("no trap")
.err()
.expect("() error returned");
let e = results.empty_error(&mut store, 2.0).err().expect("trap");
assert_eq!(
format!("{}", e.source().expect("trap message is stored in source")),
"empty_error: trap"
);
Ok(())
}
}
mod string_error {
use super::*;
wasmtime::component::bindgen!({
inline: "
world result-playground {
import imports: interface {
string-error: func(a: float64) -> result<float64, string>
}
default export interface {
string-error: func(a: float64) -> result<float64, string>
}
}",
});
#[test]
fn run() -> Result<(), Error> {
let engine = engine();
let component = Component::new(
&engine,
format!(
r#"
(component
(import "imports" (instance $i
(export "string-error" (func (param "a" float64) (result (result float64 (error string)))))
))
(core module $libc
(memory (export "memory") 1)
{REALLOC_AND_FREE}
)
(core instance $libc (instantiate $libc))
(core module $m
(import "" "core_string_error" (func $f (param f64 i32)))
(import "libc" "memory" (memory 0))
(import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
(func (export "core_string_error_export") (param f64) (result i32)
(local $retptr i32)
(local.set $retptr
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 16)))
(call $f (local.get 0) (local.get $retptr))
(local.get $retptr)
)
)
(core func $core_string_error
(canon lower (func $i "string-error") (memory $libc "memory") (realloc (func $libc "realloc")))
)
(core instance $i (instantiate $m
(with "" (instance (export "core_string_error" (func $core_string_error))))
(with "libc" (instance $libc))
))
(func $f_string_error
(export "string-error")
(param "a" float64)
(result (result float64 (error string)))
(canon lift (core func $i "core_string_error_export") (memory $libc "memory"))
)
)
"#
),
)?;
#[derive(Default)]
struct MyImports {}
impl imports::Imports for MyImports {
fn string_error(&mut self, a: f64) -> Result<Result<f64, String>, Error> {
if a == 0.0 {
Ok(Ok(a))
} else if a == 1.0 {
Ok(Err("string_error: error".to_owned()))
} else {
Err(anyhow!("string_error: trap"))
}
}
}
let mut linker = Linker::new(&engine);
imports::add_to_linker(&mut linker, |f: &mut MyImports| f)?;
let mut store = Store::new(&engine, MyImports::default());
let (results, _) = ResultPlayground::instantiate(&mut store, &component, &linker)?;
assert_eq!(
results
.string_error(&mut store, 0.0)
.expect("no trap")
.expect("no error returned"),
0.0
);
let e = results
.string_error(&mut store, 1.0)
.expect("no trap")
.err()
.expect("error returned");
assert_eq!(e, "string_error: error");
let e = results.string_error(&mut store, 2.0).err().expect("trap");
assert_eq!(
format!("{}", e.source().expect("trap message is stored in source")),
"string_error: trap"
);
Ok(())
}
}
mod enum_error {
use super::*;
wasmtime::component::bindgen!({
inline: "
interface imports {
enum e1 { a, b, c }
enum-error: func(a: float64) -> result<float64, e1>
}
world result-playground {
import imports: imports
default export interface {
enum e1 { a, b, c }
enum-error: func(a: float64) -> result<float64, e1>
}
}",
trappable_error_type: { imports::e1: TrappableE1 }
});
#[test]
fn run() -> Result<(), Error> {
let engine = engine();
let component = Component::new(
&engine,
format!(
r#"
(component
(import "imports" (instance $i
(export "enum-error" (func (param "a" float64) (result (result float64 (error (enum "a" "b" "c"))))))
))
(core module $libc
(memory (export "memory") 1)
{REALLOC_AND_FREE}
)
(core instance $libc (instantiate $libc))
(core module $m
(import "" "core_enum_error" (func $f (param f64 i32)))
(import "libc" "memory" (memory 0))
(import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
(func (export "core_enum_error_export") (param f64) (result i32)
(local $retptr i32)
(local.set $retptr
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 16)))
(call $f (local.get 0) (local.get $retptr))
(local.get $retptr)
)
)
(core func $core_enum_error
(canon lower (func $i "enum-error") (memory $libc "memory") (realloc (func $libc "realloc")))
)
(core instance $i (instantiate $m
(with "" (instance (export "core_enum_error" (func $core_enum_error))))
(with "libc" (instance $libc))
))
(func $f_enum_error
(export "enum-error")
(param "a" float64)
(result (result float64 (error (enum "a" "b" "c"))))
(canon lift (core func $i "core_enum_error_export") (memory $libc "memory"))
)
)
"#
),
)?;
// 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<MyTrap> for imports::TrappableE1 {
fn from(t: MyTrap) -> imports::TrappableE1 {
imports::TrappableE1::trap(anyhow!(t))
}
}
#[derive(Default)]
struct MyImports {}
impl imports::Imports for MyImports {
fn enum_error(&mut self, a: f64) -> Result<f64, imports::TrappableE1> {
if a == 0.0 {
Ok(a)
} else if a == 1.0 {
Err(imports::E1::A)?
} else {
Err(MyTrap)?
}
}
}
let mut linker = Linker::new(&engine);
imports::add_to_linker(&mut linker, |f: &mut MyImports| f)?;
let mut store = Store::new(&engine, MyImports::default());
let (results, _) = ResultPlayground::instantiate(&mut store, &component, &linker)?;
assert_eq!(
results
.enum_error(&mut store, 0.0)
.expect("no trap")
.expect("no error returned"),
0.0
);
let e = results
.enum_error(&mut store, 1.0)
.expect("no trap")
.err()
.expect("error returned");
assert_eq!(e, enum_error::E1::A);
let e = results.enum_error(&mut store, 2.0).err().expect("trap");
assert_eq!(
format!("{}", e.source().expect("trap message is stored in source")),
"MyTrap"
);
e.downcast_ref::<MyTrap>()
.expect("downcast trap to concrete MyTrap type");
Ok(())
}
}
mod record_error {
use super::*;
wasmtime::component::bindgen!({
inline: "
interface imports {
record e2 { line: u32, col: u32 }
record-error: func(a: float64) -> result<float64, e2>
}
world result-playground {
import imports: imports
default export interface {
record e2 { line: u32, col: u32 }
record-error: func(a: float64) -> result<float64, e2>
}
}",
trappable_error_type: { imports::e2: TrappableE2 }
});
#[test]
fn run() -> Result<(), Error> {
let engine = engine();
let component = Component::new(
&engine,
format!(
r#"
(component
(import "imports" (instance $i
(export "record-error" (func (param "a" float64) (result (result float64 (error (record (field "line" u32) (field "col" u32)))))))
))
(core module $libc
(memory (export "memory") 1)
{REALLOC_AND_FREE}
)
(core instance $libc (instantiate $libc))
(core module $m
(import "" "core_record_error" (func $f (param f64 i32)))
(import "libc" "memory" (memory 0))
(import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
(func (export "core_record_error_export") (param f64) (result i32)
(local $retptr i32)
(local.set $retptr
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 16)))
(call $f (local.get 0) (local.get $retptr))
(local.get $retptr)
)
)
(core func $core_record_error
(canon lower (func $i "record-error") (memory $libc "memory") (realloc (func $libc "realloc")))
)
(core instance $i (instantiate $m
(with "" (instance (export "core_record_error" (func $core_record_error))))
(with "libc" (instance $libc))
))
(func $f_record_error
(export "record-error")
(param "a" float64)
(result (result float64 (error (record (field "line" u32) (field "col" u32)))))
(canon lift (core func $i "core_record_error_export") (memory $libc "memory"))
)
)
"#
),
)?;
#[derive(Default)]
struct MyImports {}
impl imports::Imports for MyImports {
fn record_error(&mut self, a: f64) -> Result<f64, imports::TrappableE2> {
if a == 0.0 {
Ok(a)
} else if a == 1.0 {
Err(imports::E2 {
line: 420,
col: 1312,
})?
} else {
Err(imports::TrappableE2::trap(anyhow!("record_error: trap")))
}
}
}
let mut linker = Linker::new(&engine);
imports::add_to_linker(&mut linker, |f: &mut MyImports| f)?;
let mut store = Store::new(&engine, MyImports::default());
let (results, _) = ResultPlayground::instantiate(&mut store, &component, &linker)?;
assert_eq!(
results
.record_error(&mut store, 0.0)
.expect("no trap")
.expect("no error returned"),
0.0
);
let e = results
.record_error(&mut store, 1.0)
.expect("no trap")
.err()
.expect("error returned");
assert!(matches!(
e,
record_error::E2 {
line: 420,
col: 1312
}
));
let e = results.record_error(&mut store, 2.0).err().expect("trap");
assert_eq!(
format!("{}", e.source().expect("trap message is stored in source")),
"record_error: trap"
);
Ok(())
}
}
mod variant_error {
use super::*;
wasmtime::component::bindgen!({
inline: "
interface imports {
enum e1 { a, b, c }
record e2 { line: u32, col: u32 }
variant e3 { E1(e1), E2(e2) }
variant-error: func(a: float64) -> result<float64, e3>
}
world result-playground {
import imports: imports
default export interface {
enum e1 { a, b, c }
record e2 { line: u32, col: u32 }
variant e3 { E1(e1), E2(e2) }
variant-error: func(a: float64) -> result<float64, e3>
}
}",
trappable_error_type: { imports::e3: TrappableE3 }
});
#[test]
fn run() -> Result<(), Error> {
let engine = engine();
let component = Component::new(
&engine,
format!(
r#"
(component
(import "imports" (instance $i
(export "variant-error" (func (param "a" float64) (result (result float64 (error (variant (case "E1" (enum "a" "b" "c")) (case "E2" (record (field "line" u32) (field "col" u32)))))))))
))
(core module $libc
(memory (export "memory") 1)
{REALLOC_AND_FREE}
)
(core instance $libc (instantiate $libc))
(core module $m
(import "" "core_variant_error" (func $f (param f64 i32)))
(import "libc" "memory" (memory 0))
(import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
(func (export "core_variant_error_export") (param f64) (result i32)
(local $retptr i32)
(local.set $retptr
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 16)))
(call $f (local.get 0) (local.get $retptr))
(local.get $retptr)
)
)
(core func $core_variant_error
(canon lower (func $i "variant-error") (memory $libc "memory") (realloc (func $libc "realloc")))
)
(core instance $i (instantiate $m
(with "" (instance (export "core_variant_error" (func $core_variant_error))))
(with "libc" (instance $libc))
))
(func $f_variant_error
(export "variant-error")
(param "a" float64)
(result (result float64 (error (variant (case "E1" (enum "a" "b" "c")) (case "E2"(record (field "line" u32) (field "col" u32)))))))
(canon lift (core func $i "core_variant_error_export") (memory $libc "memory"))
)
)
"#
),
)?;
#[derive(Default)]
struct MyImports {}
impl imports::Imports for MyImports {
fn variant_error(&mut self, a: f64) -> Result<f64, imports::TrappableE3> {
if a == 0.0 {
Ok(a)
} else if a == 1.0 {
Err(imports::E3::E2(imports::E2 {
line: 420,
col: 1312,
}))?
} else {
Err(imports::TrappableE3::trap(anyhow!("variant_error: trap")))
}
}
}
let mut linker = Linker::new(&engine);
imports::add_to_linker(&mut linker, |f: &mut MyImports| f)?;
let mut store = Store::new(&engine, MyImports::default());
let (results, _) = ResultPlayground::instantiate(&mut store, &component, &linker)?;
assert_eq!(
results
.variant_error(&mut store, 0.0)
.expect("no trap")
.expect("no error returned"),
0.0
);
let e = results
.variant_error(&mut store, 1.0)
.expect("no trap")
.err()
.expect("error returned");
assert!(matches!(
e,
variant_error::E3::E2(variant_error::E2 {
line: 420,
col: 1312
})
));
let e = results.variant_error(&mut store, 2.0).err().expect("trap");
assert_eq!(
format!("{}", e.source().expect("trap message is stored in source")),
"variant_error: trap"
);
Ok(())
}
}