diff --git a/DESIGN.md b/DESIGN.md index 38b24deb..b2b6c72f 100644 --- a/DESIGN.md +++ b/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 diff --git a/README.md b/README.md index f4c8e038..3a489534 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs index 6ae3fd96..f4825f3c 100644 --- a/crates/wasm-bindgen-cli-support/src/js.rs +++ b/crates/wasm-bindgen-cli-support/src/js.rs @@ -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)); diff --git a/crates/wasm-bindgen-macro/src/ast.rs b/crates/wasm-bindgen-macro/src/ast.rs index 9fef0e1b..89a9a1be 100644 --- a/crates/wasm-bindgen-macro/src/ast.rs +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -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, @@ -32,7 +33,12 @@ pub struct ImportFunction { pub struct ImportStruct { pub module: Option, pub name: syn::Ident, - pub functions: Vec<(ImportFunctionKind, ImportFunction)>, + pub functions: Vec, +} + +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> { + 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))) +} diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs index 7f8cf34a..4505f1c9 100644 --- a/crates/wasm-bindgen-macro/src/lib.rs +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -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 } } diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs index cc193c42..eafc389c 100644 --- a/crates/wasm-bindgen-shared/src/lib.rs +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -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, } diff --git a/tests/imports.rs b/tests/imports.rs index 6f600a79..8164385b 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -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(); +}