Implement optionally catching exceptions

This commit is contained in:
Alex Crichton 2018-02-06 19:04:12 -08:00
parent 43ee52bcbf
commit e9d612a343
7 changed files with 313 additions and 71 deletions

View File

@ -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

View File

@ -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

View File

@ -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));

View File

@ -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)))
}

View File

@ -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
}
}

View File

@ -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,
}

View File

@ -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();
}