mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-03-16 02:00:51 +00:00
Implement optionally catching exceptions
This commit is contained in:
parent
43ee52bcbf
commit
e9d612a343
30
DESIGN.md
30
DESIGN.md
@ -736,6 +736,36 @@ When calling `Bar::new` we'll get an index back which is wrapped up in `Bar`
|
||||
passes the index as the first argument and otherwise forwards everything along
|
||||
in Rust.
|
||||
|
||||
## Imports and JS exceptions
|
||||
|
||||
By default `wasm-bindgen` will take no action when wasm calls a JS function
|
||||
which ends up throwing an exception. The wasm spec right now doesn't support
|
||||
stack unwinding and as a result Rust code **will not execute destructors**. This
|
||||
can unfortunately cause memory leaks in Rust right now, but as soon as wasm
|
||||
implements catching exceptions we'll be sure to add support as well!
|
||||
|
||||
In the meantime though fear not! You can, if necessary, annotate some imports
|
||||
as whether they should catch an exception. For example:
|
||||
|
||||
```rust
|
||||
wasm_bindgen! {
|
||||
#[wasm_module = "./bar"]
|
||||
extern "JS" {
|
||||
#[wasm_bindgen(catch)]
|
||||
fn foo() -> Result<(), JsValue>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here the import of `foo` is annotated that it should catch the JS exception, if
|
||||
one occurs, and return it to wasm. This is expressed in Rust with a `Result`
|
||||
type where the `T` of the result is the otherwise successful result of the
|
||||
function, and the `E` *must* be `JsValue`.
|
||||
|
||||
Under the hood this generates shims that do a bunch of translation, but it
|
||||
suffices to say that a call in wasm to `foo` should always return.
|
||||
appropriately.
|
||||
|
||||
## Wrapping up
|
||||
|
||||
That's currently at least what `wasm-bindgen` has to offer! If you've got more
|
||||
|
@ -25,8 +25,10 @@ Notable features of this project includes:
|
||||
* Exposing Rust functions to JS
|
||||
* Managing arguments between JS/Rust (strings, numbers, classes, objects, etc)
|
||||
* Importing JS functions with richer types (strings, objects)
|
||||
* Importing JS classes and calling methods
|
||||
* Receiving arbitrary JS objects in Rust, passing them through to JS
|
||||
* Generates Typescript for now instead of JS (although that may come later)
|
||||
* Catching JS exceptions in imports
|
||||
|
||||
Planned features include:
|
||||
|
||||
@ -34,7 +36,10 @@ Planned features include:
|
||||
* ... and more coming soon!
|
||||
|
||||
This project is still very "early days" but feedback is of course always
|
||||
welcome!
|
||||
welcome! If you're curious about the design plus even more information about
|
||||
what this crate can do, check out the [design doc].
|
||||
|
||||
[design doc]: https://github.com/alexcrichton/wasm-bindgen/blob/master/DESIGN.md
|
||||
|
||||
## Basic usage
|
||||
|
||||
|
@ -588,7 +588,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
self.generate_free_function(f);
|
||||
}
|
||||
for f in self.program.imports.iter() {
|
||||
self.generate_import(&f.module, &f.function);
|
||||
self.generate_import(f);
|
||||
}
|
||||
for s in self.program.structs.iter() {
|
||||
self.generate_struct(s);
|
||||
@ -880,18 +880,19 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
(format!("{} {}", prefix, dst), format!("{} {}", prefix, dst_ts))
|
||||
}
|
||||
|
||||
pub fn generate_import(&mut self, module: &str, import: &shared::Function) {
|
||||
pub fn generate_import(&mut self, import: &shared::Import) {
|
||||
let imported_name = format!("import{}", self.cx.imports.len());
|
||||
|
||||
self.cx.imports.push_str(&format!("
|
||||
import {{ {} as {} }} from '{}';
|
||||
", import.name, imported_name, module));
|
||||
", import.function.name, imported_name, import.module));
|
||||
|
||||
let name = shared::mangled_import_name(None, &import.name);
|
||||
let name = shared::mangled_import_name(None, &import.function.name);
|
||||
self.gen_import_shim(&name,
|
||||
&imported_name,
|
||||
false,
|
||||
import);
|
||||
import.catch,
|
||||
&import.function);
|
||||
self.cx.imports_to_rewrite.insert(name);
|
||||
}
|
||||
|
||||
@ -924,6 +925,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
self.gen_import_shim(&name,
|
||||
&delegate,
|
||||
f.method,
|
||||
f.catch,
|
||||
&f.function);
|
||||
self.cx.imports_to_rewrite.insert(name);
|
||||
}
|
||||
@ -933,68 +935,67 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
shim_name: &str,
|
||||
shim_delegate: &str,
|
||||
is_method: bool,
|
||||
catch: bool,
|
||||
import: &shared::Function,
|
||||
) {
|
||||
let mut dst = String::new();
|
||||
|
||||
dst.push_str(&format!("function {}(", shim_name));
|
||||
let mut invocation = String::new();
|
||||
let mut invoc_args = Vec::new();
|
||||
let mut abi_args = Vec::new();
|
||||
|
||||
if is_method {
|
||||
dst.push_str("ptr");
|
||||
invocation.push_str("getObject(ptr)");
|
||||
abi_args.push("ptr".to_string());
|
||||
invoc_args.push("getObject(ptr)".to_string());
|
||||
self.cx.expose_get_object();
|
||||
}
|
||||
|
||||
let mut extra = String::new();
|
||||
|
||||
for (i, arg) in import.arguments.iter().enumerate() {
|
||||
if invocation.len() > 0 {
|
||||
invocation.push_str(", ");
|
||||
}
|
||||
if i > 0 || is_method {
|
||||
dst.push_str(", ");
|
||||
}
|
||||
match *arg {
|
||||
shared::TYPE_NUMBER => {
|
||||
invocation.push_str(&format!("arg{}", i));
|
||||
dst.push_str(&format!("arg{}", i));
|
||||
invoc_args.push(format!("arg{}", i));
|
||||
abi_args.push(format!("arg{}", i));
|
||||
}
|
||||
shared::TYPE_BOOLEAN => {
|
||||
invocation.push_str(&format!("arg{} != 0", i));
|
||||
dst.push_str(&format!("arg{}", i));
|
||||
invoc_args.push(format!("arg{} != 0", i));
|
||||
abi_args.push(format!("arg{}", i));
|
||||
}
|
||||
shared::TYPE_BORROWED_STR => {
|
||||
self.cx.expose_get_string_from_wasm();
|
||||
invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i));
|
||||
dst.push_str(&format!("ptr{0}, len{0}", i));
|
||||
invoc_args.push(format!("getStringFromWasm(ptr{0}, len{0})", i));
|
||||
abi_args.push(format!("ptr{}", i));
|
||||
abi_args.push(format!("len{}", i));
|
||||
}
|
||||
shared::TYPE_STRING => {
|
||||
self.cx.expose_get_string_from_wasm();
|
||||
dst.push_str(&format!("ptr{0}, len{0}", i));
|
||||
abi_args.push(format!("ptr{}", i));
|
||||
abi_args.push(format!("len{}", i));
|
||||
extra.push_str(&format!("
|
||||
let arg{0} = getStringFromWasm(ptr{0}, len{0});
|
||||
wasm.__wbindgen_free(ptr{0}, len{0});
|
||||
", i));
|
||||
invocation.push_str(&format!("arg{}", i));
|
||||
invoc_args.push(format!("arg{}", i));
|
||||
self.cx.required_internal_exports.insert("__wbindgen_free");
|
||||
}
|
||||
shared::TYPE_JS_OWNED => {
|
||||
self.cx.expose_take_object();
|
||||
invocation.push_str(&format!("takeObject(arg{})", i));
|
||||
dst.push_str(&format!("arg{}", i));
|
||||
invoc_args.push(format!("takeObject(arg{})", i));
|
||||
abi_args.push(format!("arg{}", i));
|
||||
}
|
||||
shared::TYPE_JS_REF => {
|
||||
self.cx.expose_get_object();
|
||||
invocation.push_str(&format!("getObject(arg{})", i));
|
||||
dst.push_str(&format!("arg{}", i));
|
||||
invoc_args.push(format!("getObject(arg{})", i));
|
||||
abi_args.push(format!("arg{}", i));
|
||||
}
|
||||
_ => {
|
||||
panic!("unsupported type in import");
|
||||
}
|
||||
}
|
||||
}
|
||||
let invoc = format!("{}({})", shim_delegate, invocation);
|
||||
|
||||
let invoc = format!("{}({})", shim_delegate, invoc_args.join(", "));
|
||||
let invoc = match import.ret {
|
||||
Some(shared::TYPE_NUMBER) => format!("return {};", invoc),
|
||||
Some(shared::TYPE_BOOLEAN) => format!("return {} ? 1 : 0;", invoc),
|
||||
@ -1005,10 +1006,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
Some(shared::TYPE_STRING) => {
|
||||
self.cx.expose_pass_string_to_wasm();
|
||||
self.cx.expose_uint32_memory();
|
||||
if import.arguments.len() > 0 || is_method {
|
||||
dst.push_str(", ");
|
||||
}
|
||||
dst.push_str("wasmretptr");
|
||||
abi_args.push("wasmretptr".to_string());
|
||||
format!("
|
||||
const [retptr, retlen] = passStringToWasm({});
|
||||
getUint32Memory()[wasmretptr / 4] = retlen;
|
||||
@ -1018,6 +1016,25 @@ impl<'a, 'b> SubContext<'a, 'b> {
|
||||
None => invoc,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let invoc = if catch {
|
||||
self.cx.expose_uint32_memory();
|
||||
self.cx.expose_add_heap_object();
|
||||
abi_args.push("exnptr".to_string());
|
||||
format!("
|
||||
try {{
|
||||
{}
|
||||
}} catch (e) {{
|
||||
const view = getUint32Memory();
|
||||
view[exnptr / 4] = 1;
|
||||
view[exnptr / 4 + 1] = addHeapObject(e);
|
||||
}}
|
||||
", invoc)
|
||||
} else {
|
||||
invoc
|
||||
};
|
||||
|
||||
dst.push_str(&abi_args.join(", "));
|
||||
dst.push_str(") {\n");
|
||||
dst.push_str(&extra);
|
||||
dst.push_str(&format!("{}\n}}", invoc));
|
||||
|
@ -22,6 +22,7 @@ pub struct Import {
|
||||
}
|
||||
|
||||
pub struct ImportFunction {
|
||||
pub catch: bool,
|
||||
pub ident: syn::Ident,
|
||||
pub wasm_function: Function,
|
||||
pub rust_decl: Box<syn::FnDecl>,
|
||||
@ -32,7 +33,12 @@ pub struct ImportFunction {
|
||||
pub struct ImportStruct {
|
||||
pub module: Option<String>,
|
||||
pub name: syn::Ident,
|
||||
pub functions: Vec<(ImportFunctionKind, ImportFunction)>,
|
||||
pub functions: Vec<ImportStructFunction>,
|
||||
}
|
||||
|
||||
pub struct ImportStructFunction {
|
||||
pub kind: ImportFunctionKind,
|
||||
pub function: ImportFunction,
|
||||
}
|
||||
|
||||
pub enum ImportFunctionKind {
|
||||
@ -138,7 +144,7 @@ impl Program {
|
||||
_ => panic!("only foreign functions allowed for now, not statics"),
|
||||
};
|
||||
|
||||
let (wasm, mutable) = Function::from_decl(f.ident, &f.decl, allow_self);
|
||||
let (mut wasm, mutable) = Function::from_decl(f.ident, &f.decl, allow_self);
|
||||
let is_method = match mutable {
|
||||
Some(false) => true,
|
||||
None => false,
|
||||
@ -146,6 +152,19 @@ impl Program {
|
||||
panic!("mutable self methods not allowed in extern structs");
|
||||
}
|
||||
};
|
||||
let opts = BindgenOpts::from(&f.attrs);
|
||||
|
||||
if opts.catch {
|
||||
// TODO: this assumes a whole bunch:
|
||||
//
|
||||
// * The outer type is actually a `Result`
|
||||
// * The error type is a `JsValue`
|
||||
// * The actual type is the first type parameter
|
||||
//
|
||||
// should probably fix this one day...
|
||||
wasm.ret = extract_first_ty_param(wasm.ret.as_ref())
|
||||
.expect("can't `catch` without returning a Result");
|
||||
}
|
||||
|
||||
(ImportFunction {
|
||||
rust_attrs: f.attrs.clone(),
|
||||
@ -153,6 +172,7 @@ impl Program {
|
||||
rust_decl: f.decl.clone(),
|
||||
ident: f.ident.clone(),
|
||||
wasm_function: wasm,
|
||||
catch: opts.catch,
|
||||
}, is_method)
|
||||
}
|
||||
|
||||
@ -163,40 +183,14 @@ impl Program {
|
||||
let kind = if method {
|
||||
ImportFunctionKind::Method
|
||||
} else {
|
||||
let new = f.rust_attrs.iter()
|
||||
.filter_map(|a| a.interpret_meta())
|
||||
.filter_map(|m| {
|
||||
match m {
|
||||
syn::Meta::List(i) => {
|
||||
if i.ident == "wasm_bindgen" {
|
||||
Some(i.nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.flat_map(|a| a)
|
||||
.filter_map(|a| {
|
||||
match a {
|
||||
syn::NestedMeta::Meta(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.any(|a| {
|
||||
match a {
|
||||
syn::Meta::Word(a) => a == "constructor",
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if new {
|
||||
let opts = BindgenOpts::from(&f.rust_attrs);
|
||||
if opts.constructor {
|
||||
ImportFunctionKind::JsConstructor
|
||||
} else {
|
||||
ImportFunctionKind::Static
|
||||
}
|
||||
};
|
||||
(kind, f)
|
||||
ImportStructFunction { kind, function: f }
|
||||
})
|
||||
.collect();
|
||||
self.imported_structs.push(ImportStruct {
|
||||
@ -473,8 +467,8 @@ impl ImportStruct {
|
||||
("name", &|a| a.str(self.name.as_ref())),
|
||||
("functions", &|a| {
|
||||
a.list(&self.functions,
|
||||
|&(ref kind, ref f), a| {
|
||||
let (method, new) = match *kind {
|
||||
|f, a| {
|
||||
let (method, new) = match f.kind {
|
||||
ImportFunctionKind::Method => (true, false),
|
||||
ImportFunctionKind::JsConstructor => (false, true),
|
||||
ImportFunctionKind::Static => (false, false),
|
||||
@ -482,7 +476,8 @@ impl ImportStruct {
|
||||
a.fields(&[
|
||||
("method", &|a| a.bool(method)),
|
||||
("js_new", &|a| a.bool(new)),
|
||||
("function", &|a| f.wasm_function.wbg_literal(a)),
|
||||
("catch", &|a| a.bool(f.function.catch)),
|
||||
("function", &|a| f.function.wasm_function.wbg_literal(a)),
|
||||
]);
|
||||
})
|
||||
}),
|
||||
@ -494,6 +489,7 @@ impl Import {
|
||||
fn wbg_literal(&self, a: &mut LiteralBuilder) {
|
||||
a.fields(&[
|
||||
("module", &|a| a.str(&self.module)),
|
||||
("catch", &|a| a.bool(self.function.catch)),
|
||||
("function", &|a| self.function.wasm_function.wbg_literal(a)),
|
||||
]);
|
||||
}
|
||||
@ -636,3 +632,78 @@ impl<'a> LiteralBuilder<'a> {
|
||||
self.append("]");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BindgenOpts {
|
||||
catch: bool,
|
||||
constructor: bool,
|
||||
}
|
||||
|
||||
impl BindgenOpts {
|
||||
fn from(attrs: &[syn::Attribute]) -> BindgenOpts {
|
||||
let mut opts = BindgenOpts::default();
|
||||
let attrs = attrs.iter()
|
||||
.filter_map(|a| a.interpret_meta())
|
||||
.filter_map(|m| {
|
||||
match m {
|
||||
syn::Meta::List(i) => {
|
||||
if i.ident == "wasm_bindgen" {
|
||||
Some(i.nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.flat_map(|a| a)
|
||||
.filter_map(|a| {
|
||||
match a {
|
||||
syn::NestedMeta::Meta(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
for attr in attrs {
|
||||
match attr {
|
||||
syn::Meta::Word(a) => {
|
||||
if a == "constructor" {
|
||||
opts.constructor = true;
|
||||
} else if a == "catch" {
|
||||
opts.catch = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_first_ty_param(ty: Option<&Type>) -> Option<Option<Type>> {
|
||||
let ty = match ty {
|
||||
Some(t) => t,
|
||||
None => return Some(None)
|
||||
};
|
||||
let ty = match *ty {
|
||||
Type::ByValue(ref t) => t,
|
||||
_ => return None,
|
||||
};
|
||||
let path = match *ty {
|
||||
syn::Type::Path(syn::TypePath { qself: None, ref path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let seg = path.segments.last()?.into_value();
|
||||
let generics = match seg.arguments {
|
||||
syn::PathArguments::AngleBracketed(ref t) => t,
|
||||
_ => return None,
|
||||
};
|
||||
let ty = match *generics.args.first()?.into_value() {
|
||||
syn::GenericArgument::Type(ref t) => t,
|
||||
_ => return None,
|
||||
};
|
||||
match *ty {
|
||||
syn::Type::Tuple(ref t) if t.elems.len() == 0 => return Some(None),
|
||||
_ => {}
|
||||
}
|
||||
Some(Some(Type::from(ty)))
|
||||
}
|
||||
|
@ -366,12 +366,12 @@ fn bindgen_imported_struct(import: &ast::ImportStruct, tokens: &mut Tokens) {
|
||||
|
||||
let mut methods = Tokens::new();
|
||||
|
||||
for &(_, ref f) in import.functions.iter() {
|
||||
for f in import.functions.iter() {
|
||||
let import_name = shared::mangled_import_name(
|
||||
Some(&import.name.to_string()),
|
||||
f.wasm_function.name.as_ref(),
|
||||
f.function.wasm_function.name.as_ref(),
|
||||
);
|
||||
bindgen_import_function(f, &import_name, &mut methods);
|
||||
bindgen_import_function(&f.function, &import_name, &mut methods);
|
||||
}
|
||||
|
||||
(my_quote! {
|
||||
@ -501,7 +501,7 @@ fn bindgen_import_function(import: &ast::ImportFunction,
|
||||
}
|
||||
}
|
||||
let abi_ret;
|
||||
let convert_ret;
|
||||
let mut convert_ret;
|
||||
match import.wasm_function.ret {
|
||||
Some(ast::Type::ByValue(ref t)) => {
|
||||
abi_ret = my_quote! {
|
||||
@ -534,10 +534,29 @@ fn bindgen_import_function(import: &ast::ImportFunction,
|
||||
Some(ast::Type::ByMutRef(_)) => panic!("can't return a borrowed ref"),
|
||||
None => {
|
||||
abi_ret = my_quote! { () };
|
||||
convert_ret = my_quote! {};
|
||||
convert_ret = my_quote! { () };
|
||||
}
|
||||
}
|
||||
|
||||
let mut exceptional_ret = my_quote! {};
|
||||
if import.catch {
|
||||
let exn_data = syn::Ident::from("exn_data");
|
||||
let exn_data_ptr = syn::Ident::from("exn_data_ptr");
|
||||
abi_argument_names.push(exn_data_ptr);
|
||||
abi_arguments.push(my_quote! { #exn_data_ptr: *mut u32 });
|
||||
arg_conversions.push(my_quote! {
|
||||
let mut #exn_data = [0; 2];
|
||||
let mut #exn_data_ptr = #exn_data.as_mut_ptr();
|
||||
});
|
||||
convert_ret = my_quote! { Ok(#convert_ret) };
|
||||
exceptional_ret = my_quote! {
|
||||
if #exn_data[0] == 1 {
|
||||
return Err(<::wasm_bindgen::JsValue as
|
||||
::wasm_bindgen::convert::WasmBoundary>::from_js(#exn_data[1]))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let name = import.ident;
|
||||
let import_name = syn::Ident::from(import_name);
|
||||
(quote! {
|
||||
@ -548,6 +567,7 @@ fn bindgen_import_function(import: &ast::ImportFunction,
|
||||
unsafe {
|
||||
#(#arg_conversions)*
|
||||
let #ret_ident = #import_name(#(#abi_argument_names),*);
|
||||
#exceptional_ret
|
||||
#convert_ret
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ pub struct Struct {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Import {
|
||||
pub module: String,
|
||||
pub catch: bool,
|
||||
pub function: Function,
|
||||
}
|
||||
|
||||
@ -38,6 +39,7 @@ pub struct ImportStruct {
|
||||
pub struct ImportStructFunction {
|
||||
pub method: bool,
|
||||
pub js_new: bool,
|
||||
pub catch: bool,
|
||||
pub function: Function,
|
||||
}
|
||||
|
||||
|
@ -148,3 +148,100 @@ fn strings() {
|
||||
"#)
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exceptions() {
|
||||
test_support::project()
|
||||
.file("src/lib.rs", r#"
|
||||
#![feature(proc_macro)]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
wasm_bindgen! {
|
||||
#[wasm_module = "./test"]
|
||||
extern "JS" {
|
||||
fn foo();
|
||||
fn bar();
|
||||
#[wasm_bindgen(catch)]
|
||||
fn baz() -> Result<(), JsValue>;
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
foo();
|
||||
bar();
|
||||
}
|
||||
|
||||
pub fn run2() {
|
||||
assert!(baz().is_err());
|
||||
bar();
|
||||
}
|
||||
}
|
||||
"#)
|
||||
.file("test.ts", r#"
|
||||
import { run, run2 } from "./out";
|
||||
import * as assert from "assert";
|
||||
|
||||
let called = false;
|
||||
|
||||
export function foo() {
|
||||
throw new Error('error!');
|
||||
}
|
||||
|
||||
export function baz() {
|
||||
throw new Error('error2');
|
||||
}
|
||||
|
||||
export function bar() {
|
||||
called = true;
|
||||
}
|
||||
|
||||
export function test() {
|
||||
assert.throws(run, /error!/);
|
||||
assert.strictEqual(called, false);
|
||||
run2();
|
||||
assert.strictEqual(called, true);
|
||||
}
|
||||
"#)
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exn_caught() {
|
||||
test_support::project()
|
||||
.file("src/lib.rs", r#"
|
||||
#![feature(proc_macro)]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
wasm_bindgen! {
|
||||
#[wasm_module = "./test"]
|
||||
extern "JS" {
|
||||
#[wasm_bindgen(catch)]
|
||||
fn foo() -> Result<(), JsValue>;
|
||||
}
|
||||
|
||||
pub fn run() -> JsValue {
|
||||
foo().unwrap_err()
|
||||
}
|
||||
}
|
||||
"#)
|
||||
.file("test.ts", r#"
|
||||
import { run } from "./out";
|
||||
import * as assert from "assert";
|
||||
|
||||
export function foo() {
|
||||
throw new Error('error!');
|
||||
}
|
||||
|
||||
export function test() {
|
||||
const obj = run();
|
||||
assert.strictEqual(obj instanceof Error, true);
|
||||
assert.strictEqual(obj.message, 'error!');
|
||||
}
|
||||
"#)
|
||||
.test();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user