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:
@@ -5,6 +5,8 @@ use wasmtime::{
|
||||
Store,
|
||||
};
|
||||
|
||||
mod results;
|
||||
|
||||
mod no_imports {
|
||||
use super::*;
|
||||
|
||||
|
||||
604
tests/all/component_model/bindgen/results.rs
Normal file
604
tests/all/component_model/bindgen/results.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user