diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 3df58c40..b812ce24 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -535,7 +535,7 @@ impl ToTokens for ast::ImportType { use wasm_bindgen::convert::RefFromWasmAbi; use wasm_bindgen::describe::WasmDescribe; use wasm_bindgen::{JsValue, JsCast}; - use wasm_bindgen::__rt::core::mem::ManuallyDrop; + use wasm_bindgen::__rt::core; impl WasmDescribe for #rust_name { fn describe() { @@ -589,13 +589,13 @@ impl ToTokens for ast::ImportType { impl RefFromWasmAbi for #rust_name { type Abi = ::Abi; - type Anchor = ManuallyDrop<#rust_name>; + type Anchor = core::mem::ManuallyDrop<#rust_name>; #[inline] unsafe fn ref_from_abi(js: Self::Abi, extra: &mut Stack) -> Self::Anchor { let tmp = ::ref_from_abi(js, extra); - ManuallyDrop::new(#rust_name { - obj: ManuallyDrop::into_inner(tmp), + core::mem::ManuallyDrop::new(#rust_name { + obj: core::mem::ManuallyDrop::into_inner(tmp), }) } } @@ -657,6 +657,20 @@ impl ToTokens for ast::ImportType { }; }).to_tokens(tokens); + let deref_target = match self.extends.first() { + Some(target) => quote! { #target }, + None => quote! { JsValue }, + }; + (quote! { + impl core::ops::Deref for #rust_name { + type Target = #deref_target; + + #[inline] + fn deref(&self) -> &#deref_target { + self.as_ref() + } + } + }).to_tokens(tokens); for superclass in self.extends.iter() { (quote! { impl From<#rust_name> for #superclass { diff --git a/crates/cli-support/src/js/js2rust.rs b/crates/cli-support/src/js/js2rust.rs index 1ed37c91..07c4e327 100644 --- a/crates/cli-support/src/js/js2rust.rs +++ b/crates/cli-support/src/js/js2rust.rs @@ -413,7 +413,7 @@ impl<'a, 'b> Js2Rust<'a, 'b> { name = name )); } - self.rust_arguments.push(format!("{} ? 1 : 0", name)); + self.rust_arguments.push(format!("{}", name)); } Descriptor::Char => { self.js_arguments.push((name.clone(), "string".to_string())); diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index e7dd3ee3..e2392bdb 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::fmt::Write; use std::mem; use decode; @@ -65,6 +64,18 @@ pub struct SubContext<'a, 'b: 'a> { pub vendor_prefixes: HashMap<&'b str, Vec<&'b str>>, } +pub enum ImportTarget { + Function(String), + Method(String), + Constructor(String), + StructuralMethod(String), + StructuralGetter(Option, String), + StructuralSetter(Option, String), + StructuralIndexingGetter(Option), + StructuralIndexingSetter(Option), + StructuralIndexingDeleter(Option), +} + const INITIAL_SLAB_VALUES: &[&str] = &["undefined", "null", "true", "false"]; impl<'a> Context<'a> { @@ -1944,7 +1955,7 @@ impl<'a, 'b> SubContext<'a, 'b> { Some(d) => d, }; - let target = self.generated_import_target(info, import, &descriptor)?; + let target = self.generated_import_target(info, import)?; let js = Rust2Js::new(self.cx) .catch(import.catch) @@ -1959,137 +1970,103 @@ impl<'a, 'b> SubContext<'a, 'b> { &mut self, info: &decode::Import<'b>, import: &decode::ImportFunction, - descriptor: &Descriptor, - ) -> Result { + ) -> Result { let method_data = match &import.method { Some(data) => data, None => { let name = self.import_name(info, &import.function.name)?; - return Ok(if name.contains(".") { - self.cx.global(&format!( - " - const {}_target = {}; - ", - import.shim, name - )); - format!("{}_target", import.shim) - } else { - name - }); + if import.structural || !name.contains(".") { + return Ok(ImportTarget::Function(name)) + } + self.cx.global(&format!("const {}_target = {};", import.shim, name)); + let target = format!("{}_target", import.shim); + return Ok(ImportTarget::Function(target)) } }; let class = self.import_name(info, &method_data.class)?; let op = match &method_data.kind { - decode::MethodKind::Constructor => return Ok(format!("new {}", class)), + decode::MethodKind::Constructor => { + return Ok(ImportTarget::Constructor(class.to_string())) + } decode::MethodKind::Operation(op) => op, }; - let target = if import.structural { - let location = if op.is_static { &class } else { "this" }; + if import.structural { + let class = if op.is_static { Some(class.clone()) } else { None }; - match &op.kind { + return Ok(match &op.kind { decode::OperationKind::Regular => { - let nargs = descriptor.unwrap_function().arguments.len(); - let mut s = format!("function("); - for i in 0..nargs - 1 { - if i > 0 { - drop(write!(s, ", ")); - } - drop(write!(s, "x{}", i)); + let name = import.function.name.to_string(); + match class { + Some(c) => ImportTarget::Function(format!("{}.{}", c, name)), + None => ImportTarget::StructuralMethod(name), } - s.push_str(") { \nreturn this."); - s.push_str(&import.function.name); - s.push_str("("); - for i in 0..nargs - 1 { - if i > 0 { - drop(write!(s, ", ")); - } - drop(write!(s, "x{}", i)); - } - s.push_str(");\n}"); - s - } - decode::OperationKind::Getter(g) => format!( - "function() {{ - return {}.{}; - }}", - location, g - ), - decode::OperationKind::Setter(s) => format!( - "function(y) {{ - {}.{} = y; - }}", - location, s - ), - decode::OperationKind::IndexingGetter => format!( - "function(y) {{ - return {}[y]; - }}", - location - ), - decode::OperationKind::IndexingSetter => format!( - "function(y, z) {{ - {}[y] = z; - }}", - location - ), - decode::OperationKind::IndexingDeleter => format!( - "function(y) {{ - delete {}[y]; - }}", - location - ), - } - } else { - let target = format!("typeof {0} === 'undefined' ? null : {}{}", - class, - if op.is_static { "" } else { ".prototype" }); - let (mut target, name) = match &op.kind { - decode::OperationKind::Regular => { - (format!("{}.{}", target, import.function.name), &import.function.name) } decode::OperationKind::Getter(g) => { - self.cx.expose_get_inherited_descriptor(); - (format!( - "GetOwnOrInheritedPropertyDescriptor({}, '{}').get", - target, g, - ), g) + ImportTarget::StructuralGetter(class, g.to_string()) } decode::OperationKind::Setter(s) => { - self.cx.expose_get_inherited_descriptor(); - (format!( - "GetOwnOrInheritedPropertyDescriptor({}, '{}').set", - target, s, - ), s) + ImportTarget::StructuralSetter(class, s.to_string()) } decode::OperationKind::IndexingGetter => { - panic!("indexing getter should be structural") + ImportTarget::StructuralIndexingGetter(class) } decode::OperationKind::IndexingSetter => { - panic!("indexing setter should be structural") + ImportTarget::StructuralIndexingSetter(class) } decode::OperationKind::IndexingDeleter => { - panic!("indexing deleter should be structural") + ImportTarget::StructuralIndexingDeleter(class) } - }; - target.push_str(&format!(" || function() {{ - throw new Error(`wasm-bindgen: {}.{} does not exist`); - }}", class, name)); - if op.is_static { - target.insert(0, '('); - target.push_str(").bind("); - target.push_str(&class); - target.push_str(")"); + }) + } + + let target = format!("typeof {0} === 'undefined' ? null : {}{}", + class, + if op.is_static { "" } else { ".prototype" }); + let (mut target, name) = match &op.kind { + decode::OperationKind::Regular => { + (format!("{}.{}", target, import.function.name), &import.function.name) + } + decode::OperationKind::Getter(g) => { + self.cx.expose_get_inherited_descriptor(); + (format!( + "GetOwnOrInheritedPropertyDescriptor({}, '{}').get", + target, g, + ), g) + } + decode::OperationKind::Setter(s) => { + self.cx.expose_get_inherited_descriptor(); + (format!( + "GetOwnOrInheritedPropertyDescriptor({}, '{}').set", + target, s, + ), s) + } + decode::OperationKind::IndexingGetter => { + panic!("indexing getter should be structural") + } + decode::OperationKind::IndexingSetter => { + panic!("indexing setter should be structural") + } + decode::OperationKind::IndexingDeleter => { + panic!("indexing deleter should be structural") } - target }; + target.push_str(&format!(" || function() {{ + throw new Error(`wasm-bindgen: {}.{} does not exist`); + }}", class, name)); + if op.is_static { + target.insert(0, '('); + target.push_str(").bind("); + target.push_str(&class); + target.push_str(")"); + } self.cx.global(&format!("const {}_target = {};", import.shim, target)); - Ok(format!( - "{}_target{}", - import.shim, - if op.is_static { "" } else { ".call" } - )) + Ok(if op.is_static { + ImportTarget::Function(format!("{}_target", import.shim)) + } else { + ImportTarget::Method(format!("{}_target", import.shim)) + }) } fn generate_import_type( diff --git a/crates/cli-support/src/js/rust2js.rs b/crates/cli-support/src/js/rust2js.rs index 668b31e1..6e6cc246 100644 --- a/crates/cli-support/src/js/rust2js.rs +++ b/crates/cli-support/src/js/rust2js.rs @@ -1,6 +1,6 @@ -use failure::{self, Error}; +use failure::Error; -use super::{Context, Js2Rust}; +use super::{Context, Js2Rust, ImportTarget}; use descriptor::{Descriptor, Function}; /// Helper struct for manufacturing a shim in JS used to translate Rust types to @@ -489,7 +489,7 @@ impl<'a, 'b> Rust2Js<'a, 'b> { } self.ret_expr = match *ty { - Descriptor::Boolean => "return JS ? 1 : 0;".to_string(), + Descriptor::Boolean => "return JS;".to_string(), Descriptor::Char => "return JS.codePointAt(0);".to_string(), _ => bail!( "unsupported return type for calling JS function from Rust: {:?}", @@ -499,7 +499,7 @@ impl<'a, 'b> Rust2Js<'a, 'b> { Ok(()) } - pub fn finish(&self, invoc: &str) -> Result { + pub fn finish(&self, invoc: &ImportTarget) -> Result { let mut ret = String::new(); ret.push_str("function("); ret.push_str(&self.shim_arguments.join(", ")); @@ -512,35 +512,92 @@ impl<'a, 'b> Rust2Js<'a, 'b> { ret.push_str(") {\n"); ret.push_str(&self.prelude); - let mut invoc = if self.variadic { - if self.js_arguments.is_empty() { - return Err(failure::err_msg( - "a function with no arguments cannot be variadic", - )); - } - let last_arg = self.js_arguments.len() - 1; // check implies >= 0 - if self.js_arguments.len() != 1 { - self.ret_expr.replace( - "JS", - &format!( - "{}({}, ...{})", - invoc, - self.js_arguments[..last_arg].join(", "), - self.js_arguments[last_arg], - ), - ) + let handle_variadic = |invoc: &str, js_arguments: &[String]| { + let ret = if self.variadic { + let (last_arg, args) = match js_arguments.split_last() { + Some(pair) => pair, + None => bail!("a function with no arguments cannot be variadic"), + }; + if args.len() > 0 { + self.ret_expr.replace( + "JS", + &format!("{}({}, ...{})", invoc, args.join(", "), last_arg), + ) + } else { + self.ret_expr.replace( + "JS", + &format!("{}(...{})", invoc, last_arg), + ) + } } else { self.ret_expr.replace( "JS", - &format!("{}(...{})", invoc, self.js_arguments[last_arg],), + &format!("{}({})", invoc, js_arguments.join(", ")), ) - } - } else { - self.ret_expr.replace( - "JS", - &format!("{}({})", invoc, self.js_arguments.join(", ")), - ) + }; + Ok(ret) }; + + let fixed = |desc: &str, class: &Option, amt: usize| { + if self.variadic { + bail!("{} cannot be variadic", desc); + } + match (class, self.js_arguments.len()) { + (None, n) if n == amt + 1 => { + Ok((self.js_arguments[0].clone(), &self.js_arguments[1..])) + } + (None, _) => bail!("setters must have {} arguments", amt + 1), + (Some(class), n) if n == amt => { + Ok((class.clone(), &self.js_arguments[..])) + } + (Some(_), _) => bail!("static setters must have {} arguments", amt), + } + }; + + let mut invoc = match invoc { + ImportTarget::Function(f) => { + handle_variadic(&f, &self.js_arguments)? + } + ImportTarget::Constructor(c) => { + handle_variadic(&format!("new {}", c), &self.js_arguments)? + } + ImportTarget::Method(f) => { + handle_variadic(&format!("{}.call", f), &self.js_arguments)? + } + ImportTarget::StructuralMethod(f) => { + let (receiver, args) = match self.js_arguments.split_first() { + Some(pair) => pair, + None => bail!("methods must have at least one argument"), + }; + handle_variadic(&format!("{}.{}", receiver, f), args)? + } + ImportTarget::StructuralGetter(class, field) => { + let (receiver, _) = fixed("getter", class, 0)?; + let expr = format!("{}.{}", receiver, field); + self.ret_expr.replace("JS", &expr) + } + ImportTarget::StructuralSetter(class, field) => { + let (receiver, val) = fixed("setter", class, 1)?; + let expr = format!("{}.{} = {}", receiver, field, val[0]); + self.ret_expr.replace("JS", &expr) + } + ImportTarget::StructuralIndexingGetter(class) => { + let (receiver, field) = fixed("indexing getter", class, 1)?; + let expr = format!("{}[{}]", receiver, field[0]); + self.ret_expr.replace("JS", &expr) + } + ImportTarget::StructuralIndexingSetter(class) => { + let (receiver, field) = fixed("indexing setter", class, 2)?; + let expr = format!("{}[{}] = {}", receiver, field[0], field[1]); + self.ret_expr.replace("JS", &expr) + } + ImportTarget::StructuralIndexingDeleter(class) => { + let (receiver, field) = fixed("indexing deleter", class, 1)?; + let expr = format!("delete {}[{}]", receiver, field[0]); + self.ret_expr.replace("JS", &expr) + } + }; + if self.catch { let catch = "\ const view = getUint32Memory();\n\ diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html index c7eed7bb..d671a205 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html @@ -19,17 +19,17 @@ logs.innerHTML += `${msg}\n`; } }; - console.log = function() { + console.log = function(...args) { if (window.console_log_redirect) - window.console_log_redirect(orig_console_log, arguments); + window.console_log_redirect(orig_console_log, args); else - orig_console_log.apply(this, arguments); + orig_console_log.apply(this, args); }; - console.error = function() { + console.error = function(...args) { if (window.console_error_redirect) - window.console_error_redirect(orig_console_error, arguments); + window.console_error_redirect(orig_console_error, args); else - orig_console_error.apply(this, arguments); + orig_console_error.apply(this, args); }; window.__wbg_test_invoke = f => f(); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs index 537b0481..d94d8e92 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs @@ -23,19 +23,19 @@ pub fn execute( // ensure they're bound correctly in wasm. This'll allow us to intercept // all these calls and capture the output of tests const prev_log = console.log; - console.log = function() {{ + console.log = function(...args) {{ if (console_log_redirect === null) {{ - prev_log.apply(null, arguments); + prev_log.apply(null, args); }} else {{ - console_log_redirect(prev_log, arguments); + console_log_redirect(prev_log, args); }} }}; const prev_error = console.error; - console.error = function() {{ + console.error = function(...args) {{ if (console_error_redirect === null) {{ - prev_error.apply(null, arguments); + prev_error.apply(null, args); }} else {{ - console_error_redirect(prev_error, arguments); + console_error_redirect(prev_error, args); }} }}; diff --git a/crates/js-sys/tests/wasm/Object.rs b/crates/js-sys/tests/wasm/Object.rs index e89d16f5..cf49b7ac 100644 --- a/crates/js-sys/tests/wasm/Object.rs +++ b/crates/js-sys/tests/wasm/Object.rs @@ -129,7 +129,7 @@ fn get_own_property_descriptor() { let desc = Object::get_own_property_descriptor(&foo, &"foo".into()); assert_eq!(PropertyDescriptor::from(desc).value(), 42); let desc = Object::get_own_property_descriptor(&foo, &"bar".into()); - assert!(PropertyDescriptor::from(desc).value().is_undefined()); + assert!(desc.is_undefined()); } #[wasm_bindgen_test] diff --git a/crates/js-sys/tests/wasm/Reflect.rs b/crates/js-sys/tests/wasm/Reflect.rs index b2041136..9a7b8fb1 100644 --- a/crates/js-sys/tests/wasm/Reflect.rs +++ b/crates/js-sys/tests/wasm/Reflect.rs @@ -114,7 +114,7 @@ fn get_own_property_descriptor() { let desc = Reflect::get_own_property_descriptor(&obj, &"x".into()).unwrap(); assert_eq!(PropertyDescriptor::from(desc).value(), 10); let desc = Reflect::get_own_property_descriptor(&obj, &"foo".into()).unwrap(); - assert!(PropertyDescriptor::from(desc).value().is_undefined()); + assert!(desc.is_undefined()); } #[wasm_bindgen_test] diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 23dc483f..776a7d4a 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -149,6 +149,14 @@ impl BindgenAttrs { }) } + /// Whether the `final` attribute is present + fn final_(&self) -> Option<&Ident> { + self.attrs.iter().filter_map(|a| match a { + BindgenAttr::Final(i) => Some(i), + _ => None, + }).next() + } + /// Whether the readonly attributes is present fn readonly(&self) -> bool { self.attrs.iter().any(|a| match *a { @@ -229,6 +237,7 @@ pub enum BindgenAttr { IndexingSetter, IndexingDeleter, Structural, + Final(Ident), Readonly, JsName(String, Span), JsClass(String), @@ -240,7 +249,8 @@ pub enum BindgenAttr { impl Parse for BindgenAttr { fn parse(input: ParseStream) -> SynResult { let original = input.fork(); - let attr: Ident = input.parse()?; + let attr: AnyIdent = input.parse()?; + let attr = attr.0; if attr == "catch" { return Ok(BindgenAttr::Catch); } @@ -262,6 +272,9 @@ impl Parse for BindgenAttr { if attr == "structural" { return Ok(BindgenAttr::Structural); } + if attr == "final" { + return Ok(BindgenAttr::Final(attr)) + } if attr == "readonly" { return Ok(BindgenAttr::Readonly); } @@ -543,13 +556,18 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option)> for syn::ForeignItemFn ShortHash(data) ) }; + if let Some(ident) = opts.final_() { + if opts.structural() { + bail_span!(ident, "cannot specify both `structural` and `final`"); + } + } Ok(ast::ImportKind::Function(ast::ImportFunction { function: wasm, kind, js_ret, catch, variadic, - structural: opts.structural(), + structural: opts.structural() || opts.final_().is_none(), rust_name: self.ident.clone(), shim: Ident::new(&shim, Span::call_site()), doc_comment: None, diff --git a/crates/macro/ui-tests/structural-and-final.rs b/crates/macro/ui-tests/structural-and-final.rs new file mode 100644 index 00000000..5eb65220 --- /dev/null +++ b/crates/macro/ui-tests/structural-and-final.rs @@ -0,0 +1,11 @@ +extern crate wasm_bindgen; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + type Foo; + + #[wasm_bindgen(method, structural, final)] + fn bar(this: &Foo); +} diff --git a/crates/macro/ui-tests/structural-and-final.stderr b/crates/macro/ui-tests/structural-and-final.stderr new file mode 100644 index 00000000..405f65f1 --- /dev/null +++ b/crates/macro/ui-tests/structural-and-final.stderr @@ -0,0 +1,8 @@ +error: cannot specify both `structural` and `final` + --> $DIR/structural-and-final.rs:9:40 + | +9 | #[wasm_bindgen(method, structural, final)] + | ^^^^^ + +error: aborting due to previous error + diff --git a/crates/web-sys/tests/wasm/main.rs b/crates/web-sys/tests/wasm/main.rs index 71d88fff..0845e3fb 100644 --- a/crates/web-sys/tests/wasm/main.rs +++ b/crates/web-sys/tests/wasm/main.rs @@ -7,7 +7,7 @@ extern crate wasm_bindgen_futures; extern crate wasm_bindgen_test; extern crate web_sys; -use wasm_bindgen_test::wasm_bindgen_test_configure; +use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -56,3 +56,13 @@ pub mod table_element; pub mod title_element; pub mod xpath_result; pub mod indexeddb; + +#[wasm_bindgen_test] +fn deref_works() { + fn _check(a: &web_sys::XmlHttpRequestUpload) { + let _x: &web_sys::XmlHttpRequestEventTarget = a; + let _x: &web_sys::EventTarget = a; + let _x: &js_sys::Object = a; + let _x: &wasm_bindgen::JsValue = a; + } +} diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs index 0fc18c3a..663c726f 100644 --- a/crates/webidl/src/first_pass.rs +++ b/crates/webidl/src/first_pass.rs @@ -735,11 +735,17 @@ impl<'src> FirstPass<'src, ()> for weedle::CallbackInterfaceDefinition<'src> { impl<'a> FirstPassRecord<'a> { pub fn all_superclasses<'me>(&'me self, interface: &str) -> impl Iterator + 'me { let mut set = BTreeSet::new(); - self.fill_superclasses(interface, &mut set); - set.into_iter() + let mut list = Vec::new(); + self.fill_superclasses(interface, &mut set, &mut list); + list.into_iter() } - fn fill_superclasses(&self, interface: &str, set: &mut BTreeSet) { + fn fill_superclasses( + &self, + interface: &str, + set: &mut BTreeSet<&'a str>, + list: &mut Vec, + ) { let data = match self.interfaces.get(interface) { Some(data) => data, None => return, @@ -749,8 +755,9 @@ impl<'a> FirstPassRecord<'a> { None => return, }; if self.interfaces.contains_key(superclass) { - if set.insert(camel_case_ident(superclass)) { - self.fill_superclasses(superclass, set); + if set.insert(superclass) { + list.push(camel_case_ident(superclass)); + self.fill_superclasses(superclass, set, list); } } } diff --git a/examples/dom/src/lib.rs b/examples/dom/src/lib.rs index acf796f8..a30af3bc 100644 --- a/examples/dom/src/lib.rs +++ b/examples/dom/src/lib.rs @@ -16,10 +16,7 @@ pub fn run() -> Result<(), JsValue> { let val = document.create_element("p")?; val.set_inner_html("Hello from Rust!"); - // Right now the class inheritance hierarchy of the DOM isn't super - // ergonomic, so we manually cast `val: Element` to `&Node` to call the - // `append_child` method. - AsRef::::as_ref(&body).append_child(val.as_ref())?; + body.append_child(&val)?; Ok(()) } diff --git a/examples/paint/src/lib.rs b/examples/paint/src/lib.rs index b842bac8..67910de9 100644 --- a/examples/paint/src/lib.rs +++ b/examples/paint/src/lib.rs @@ -8,29 +8,19 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; #[wasm_bindgen] -pub fn main() { +pub fn main() -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); let canvas = document - .create_element("canvas") - .unwrap() - .dyn_into::() - .map_err(|_| ()) - .unwrap(); - (document.body().unwrap().as_ref() as &web_sys::Node) - .append_child(canvas.as_ref() as &web_sys::Node) - .unwrap(); + .create_element("canvas")? + .dyn_into::()?; + document.body().unwrap().append_child(&canvas)?; canvas.set_width(640); canvas.set_height(480); - (canvas.as_ref() as &web_sys::HtmlElement) - .style() - .set_property("border", "solid") - .unwrap(); + canvas.style().set_property("border", "solid")?; let context = canvas - .get_context("2d") + .get_context("2d")? .unwrap() - .unwrap() - .dyn_into::() - .unwrap(); + .dyn_into::()?; let context = Rc::new(context); let pressed = Rc::new(Cell::new(false)); { @@ -41,9 +31,10 @@ pub fn main() { context.move_to(event.offset_x() as f64, event.offset_y() as f64); pressed.set(true); }) as Box); - (canvas.as_ref() as &web_sys::EventTarget) - .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()) - .unwrap(); + canvas.add_event_listener_with_callback( + "mousedown", + closure.as_ref().unchecked_ref(), + )?; closure.forget(); } { @@ -57,9 +48,10 @@ pub fn main() { context.move_to(event.offset_x() as f64, event.offset_y() as f64); } }) as Box); - (canvas.as_ref() as &web_sys::EventTarget) - .add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()) - .unwrap(); + canvas.add_event_listener_with_callback( + "mousemove", + closure.as_ref().unchecked_ref(), + )?; closure.forget(); } { @@ -70,9 +62,12 @@ pub fn main() { context.line_to(event.offset_x() as f64, event.offset_y() as f64); context.stroke(); }) as Box); - (canvas.as_ref() as &web_sys::EventTarget) - .add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref()) - .unwrap(); + canvas.add_event_listener_with_callback( + "mouseup", + closure.as_ref().unchecked_ref(), + )?; closure.forget(); } + + Ok(()) } diff --git a/examples/raytrace-parallel/src/lib.rs b/examples/raytrace-parallel/src/lib.rs index c8d4a3ff..db4ca103 100644 --- a/examples/raytrace-parallel/src/lib.rs +++ b/examples/raytrace-parallel/src/lib.rs @@ -139,7 +139,7 @@ impl WorkerPool { // thread? // * Need to handle the `?` on `post_message` as well. array.push(&wasm_bindgen::memory()); - worker.post_message(array.as_ref())?; + worker.post_message(&array)?; worker.set_onmessage(Some(callback.as_ref().unchecked_ref())); worker.set_onerror(Some(callback.as_ref().unchecked_ref())); workers.push(worker); @@ -355,10 +355,10 @@ impl Shared { self.scene.height as f64, )?; let arr = Array::new(); - arr.push(data.as_ref()); + arr.push(&data); arr.push(&JsValue::from(done)); arr.push(&JsValue::from(self.id as f64)); - global.post_message(arr.as_ref())?; + global.post_message(&arr)?; Ok(()) } } diff --git a/examples/webaudio/src/lib.rs b/examples/webaudio/src/lib.rs index e518ce7e..fa283192 100644 --- a/examples/webaudio/src/lib.rs +++ b/examples/webaudio/src/lib.rs @@ -2,7 +2,7 @@ extern crate wasm_bindgen; extern crate web_sys; use wasm_bindgen::prelude::*; -use web_sys::{AudioContext, AudioNode, OscillatorType}; +use web_sys::{AudioContext, OscillatorType}; /// Converts a midi note to frequency /// @@ -60,34 +60,24 @@ impl FmOsc { fm_osc.set_type(OscillatorType::Sine); fm_osc.frequency().set_value(0.0); - // Create base class references: - { - let primary_node: &AudioNode = primary.as_ref(); - let gain_node: &AudioNode = gain.as_ref(); - let fm_osc_node: &AudioNode = fm_osc.as_ref(); - let fm_gain_node: &AudioNode = fm_gain.as_ref(); - let destination = ctx.destination(); - let destination_node: &AudioNode = destination.as_ref(); + // Connect the nodes up! - // Connect the nodes up! + // The primary oscillator is routed through the gain node, so that + // it can control the overall output volume. + primary.connect_with_audio_node(&gain)?; - // The primary oscillator is routed through the gain node, so that - // it can control the overall output volume. - primary_node.connect_with_audio_node(gain.as_ref())?; + // Then connect the gain node to the AudioContext destination (aka + // your speakers). + gain.connect_with_audio_node(&ctx.destination())?; - // Then connect the gain node to the AudioContext destination (aka - // your speakers). - gain_node.connect_with_audio_node(destination_node)?; - - // The FM oscillator is connected to its own gain node, so it can - // control the amount of modulation. - fm_osc_node.connect_with_audio_node(fm_gain.as_ref())?; + // The FM oscillator is connected to its own gain node, so it can + // control the amount of modulation. + fm_osc.connect_with_audio_node(&fm_gain)?; - // Connect the FM oscillator to the frequency parameter of the main - // oscillator, so that the FM node can modulate its frequency. - fm_gain_node.connect_with_audio_param(&primary.frequency())?; - } + // Connect the FM oscillator to the frequency parameter of the main + // oscillator, so that the FM node can modulate its frequency. + fm_gain.connect_with_audio_param(&primary.frequency())?; // Start the oscillators! primary.start()?; diff --git a/examples/webgl/src/lib.rs b/examples/webgl/src/lib.rs index 5e338fa2..f65b8d59 100644 --- a/examples/webgl/src/lib.rs +++ b/examples/webgl/src/lib.rs @@ -8,20 +8,16 @@ use web_sys::{WebGlProgram, WebGlRenderingContext, WebGlShader}; use js_sys::{WebAssembly}; #[wasm_bindgen] -pub fn draw() { +pub fn draw() -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas: web_sys::HtmlCanvasElement = canvas - .dyn_into::() - .map_err(|_| ()) - .unwrap(); + .dyn_into::()?; let context = canvas - .get_context("webgl") + .get_context("webgl")? .unwrap() - .unwrap() - .dyn_into::() - .unwrap(); + .dyn_into::()?; let vert_shader = compile_shader( &context, @@ -32,7 +28,7 @@ pub fn draw() { gl_Position = position; } "#, - ).unwrap(); + )?; let frag_shader = compile_shader( &context, WebGlRenderingContext::FRAGMENT_SHADER, @@ -41,23 +37,23 @@ pub fn draw() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } "#, - ).unwrap(); - let program = link_program(&context, [vert_shader, frag_shader].iter()).unwrap(); + )?; + let program = link_program(&context, [vert_shader, frag_shader].iter())?; context.use_program(Some(&program)); let vertices: [f32; 9] = [-0.7, -0.7, 0.0, 0.7, -0.7, 0.0, 0.0, 0.7, 0.0]; - let memory_buffer = wasm_bindgen::memory().dyn_into::().unwrap().buffer(); + let memory_buffer = wasm_bindgen::memory().dyn_into::()?.buffer(); let vertices_location = vertices.as_ptr() as u32 / 4; let vert_array = js_sys::Float32Array::new(&memory_buffer).subarray( vertices_location, vertices_location + vertices.len() as u32, ); - let buffer = context.create_buffer().unwrap(); + let buffer = context.create_buffer().ok_or("failed to create buffer")?; context.bind_buffer(WebGlRenderingContext::ARRAY_BUFFER, Some(&buffer)); context.buffer_data_with_array_buffer_view( WebGlRenderingContext::ARRAY_BUFFER, - vert_array.as_ref(), + &vert_array, WebGlRenderingContext::STATIC_DRAW, ); context.vertex_attrib_pointer_with_i32(0, 3, WebGlRenderingContext::FLOAT, false, 0, 0); @@ -71,6 +67,7 @@ pub fn draw() { 0, (vertices.len() / 3) as i32, ); + Ok(()) } pub fn compile_shader( diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 1faa7077..952f5b77 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -61,6 +61,7 @@ - [`constructor`](./reference/attributes/on-js-imports/constructor.md) - [`extends`](./reference/attributes/on-js-imports/extends.md) - [`getter` and `setter`](./reference/attributes/on-js-imports/getter-and-setter.md) + - [`final`](./reference/attributes/on-js-imports/final.md) - [`indexing_getter`, `indexing_setter`, and `indexing_deleter`](./reference/attributes/on-js-imports/indexing-getter-setter-deleter.md) - [`js_class = "Blah"`](./reference/attributes/on-js-imports/js_class.md) - [`js_name`](./reference/attributes/on-js-imports/js_name.md) @@ -83,6 +84,7 @@ - [Cargo Features](./web-sys/cargo-features.md) - [Function Overloads](./web-sys/function-overloads.md) - [Type Translations](./web-sys/type-translations.md) + - [Inheritance](./web-sys/inheritance.md) -------------------------------------------------------------------------------- diff --git a/guide/src/reference/attributes/on-js-imports/final.md b/guide/src/reference/attributes/on-js-imports/final.md new file mode 100644 index 00000000..8dc19975 --- /dev/null +++ b/guide/src/reference/attributes/on-js-imports/final.md @@ -0,0 +1,154 @@ +# `final` + +The `final` attribute is the converse of the [`structural` +attribute](structural.html). It configures how `wasm-bindgen` will generate JS +imports to call the imported function. Notably a function imported by `final` +never changes after it was imported, whereas a function imported by default (or +with `structural`) is subject to runtime lookup rules such as walking the +prototype chain of an object. + +[host-bindings]: https://github.com/WebAssembly/host-bindings +[reference-types]: https://github.com/WebAssembly/reference-types + +The `final` attribute is intended to be purely related to performance. It +ideally has no user-visible effect, and `structural` imports (the default) +should be able to transparently switch to `final` eventually. + +The eventual performance aspect is that with the [host bindings +proposal][host-bindings] then `wasm-bindgen` will need to generate far fewer JS +functino shims to import than it does today. For example, consider this import +today: + +```rust +#[wasm_bindgen] +extern { + type Foo; + #[wasm_bindgen(method)] + fn bar(this: &Foo, argument: &str) -> JsValue; +} +``` + +**Without the `final` attribute** the generated JS looks like this: + +```js +// without `final` +export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) { + let varg1 = getStringFromWasm(arg1, arg2); + return addHeapObject(getObject(arg0).bar(varg1)); +} +``` + +We can see here that this JS function shim is required, but it's all relatively +self-contained. It does, however, execute the `bar` method in a duck-type-y +fashion in the sense that it never validates `getObject(arg0)` is of type `Foo` +to actually call the `Foo.prototype.bar` method. + +If we instead, however, write this: + +```rust +#[wasm_bindgen] +extern { + type Foo; + #[wasm_bindgen(method, final)] // note the change here + fn bar(this: &Foo, argument: &str) -> JsValue; +} +``` + +it generates this JS glue (roughly): + +```js +const __wbg_bar_target = Foo.prototype.bar; + +export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) { + let varg1 = getStringFromWasm(arg1, arg2); + return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1)); +} +``` + +The difference here is pretty subtle, but we can see how the function being +called is hoisted out of the generated shim and is bound to always be +`Foo.prototype.bar`. This then uses the `Function.call` method to invoke that +function with `getObject(arg0)` as the receiver. + +But wait, there's still a JS function shim here even with `final`! That's true, +and this is simply a fact of future WebAssembly proposals not being implemented +yet. The semantics, though, match the future [host bindings +proposal][host-bindings] because the method being called is determined exactly +once, and it's located on the prototype chain rather than being resolved at +runtime when the function is called. + +## Interaction with future proposals + +If you're curious to see how our JS function shim will be eliminated entirely, +let's take a look at the generated bindings. We're starting off with this: + +```js +const __wbg_bar_target = Foo.prototype.bar; + +export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) { + let varg1 = getStringFromWasm(arg1, arg2); + return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1)); +} +``` + +... and once the [reference types proposal][reference-types] is implemented then +we won't need some of these pesky functions. That'll transform our generated JS +shim to look like: + +```js +const __wbg_bar_target = Foo.prototype.bar; + +export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) { + let varg1 = getStringFromWasm(arg1, arg2); + return __wbg_bar_target.call(arg0, varg1); +} +``` + +Getting better! Next up we need the host bindings proposal. Note that the +proposal is undergoing some changes right now so it's tough to link to reference +documentation, but it suffices to say that it'll empower us with at least two +different features. + +First, host bindings promises to provide the concept of "argument conversions". +The `arg1` and `arg2` values here are actually a pointer and a length to a utf-8 +encoded string, and with host bindings we'll be able to annotate that this +import should take those two arguments and convert them to a JS string (that is, +the *host* should do this, the WebAssembly engine). Using that feature we can +futher trim this down to: + +```js +const __wbg_bar_target = Foo.prototype.bar; + +export function __wbg_bar_a81456386e6b526f(arg0, varg1) { + return __wbg_bar_target.call(arg0, varg1); +} +``` + +And finally, the second promise of the host bindings proposal is that we can +flag a function call to indicate the first argument is the `this` binding of the +function call. Today the `this` value of all called imported functions is +`undefined`, and this flag (configured with host bindings) will indicate the +first argument here is actually the `this`. + +With that in mind we can further transform this to: + +```js +export const __wbg_bar_a81456386e6b526f = Foo.prototype.bar; +``` + +and voila! We, with [reference types][reference-types] and [host +bindings][host-bindings], now have no JS function shim at all necessary to call +the imported function. Additionally future wasm proposals to the ES module +system may also mean that don't even need the `export const ...` here too. + +It's also worth pointing out that with all these wasm proposals implemented the +default way to import the `bar` function (aka `structural`) would generate a JS +function shim that looks like: + +```js +export function __wbg_bar_a81456386e6b526f(varg1) { + return this.bar(varg1); +} +``` + +where this import is still subject to runtime prototype chain lookups and such. diff --git a/guide/src/reference/attributes/on-js-imports/structural.md b/guide/src/reference/attributes/on-js-imports/structural.md index bdcfef60..07fbfe01 100644 --- a/guide/src/reference/attributes/on-js-imports/structural.md +++ b/guide/src/reference/attributes/on-js-imports/structural.md @@ -1,5 +1,15 @@ # `structural` +> **Note**: As of [RFC 5] this attribute is the default for all imported +> functions. This attribute is largely ignored today and is only retained for +> backwards compatibility and learning purposes. +> +> The inverse of this attribute, [the `final` +> attribute](final.html) is more functionally interesting than +> `structural` (as `structural` is simply the default) + +[RFC 5]: https://rustwasm.github.io/rfcs/005-structural-and-deref.html + The `structural` flag can be added to `method` annotations, indicating that the method being accessed (or property with getters/setters) should be accessed in a structural, duck-type-y fashion. Rather than walking the constructor's prototype @@ -36,15 +46,3 @@ function quack(duck) { duck.quack(); } ``` - -## Why don't we always use the `structural` behavior? - -In theory, it is faster since the prototype chain doesn't need to be traversed -every time the method or property is accessed, but today's optimizing JIT -compilers are really good about eliminating that cost. The real reason is to be -future compatible with the ["host bindings" proposal][host-bindings], which -requires that there be no JavaScript shim between the caller and the native host -function. In this scenario, the properties and methods *must* be resolved before -the wasm is instantiated. - -[host-bindings]: https://github.com/WebAssembly/host-bindings/blob/master/proposals/host-bindings/Overview.md diff --git a/guide/src/web-sys/inheritance.md b/guide/src/web-sys/inheritance.md new file mode 100644 index 00000000..211117f3 --- /dev/null +++ b/guide/src/web-sys/inheritance.md @@ -0,0 +1,55 @@ +# Inheritance in `web-sys` + +Inheritance between JS classes is the bread and butter of how the DOM works on +the web, and as a result it's quite important for `web-sys` to provide access to +this inheritance hierarchy as well! There are few ways you can access the +inheritance hierarchy when using `web-sys`. + +### Accessing parent classes using `Deref` + +Like smart pointers in Rust, all types in `web_sys` implement `Deref` to their +parent JS class. This means, for example, if you have a `web_sys::Element` you +can create a `web_sys::Node` from that implicitly: + +```rust +let element: &Element = ...; + +element.append_child(..); // call a method on `Node` + +method_expecting_a_node(&element); // coerce to `&Node` implicitly + +let node: &Node = &element; // explicitly coerce to `&Node` +``` + +Using `Deref` allows ergonomic transitioning up the inheritance hierarchy to the +parent class and beyond, giving you access to all the methods using the `.` +operator. + +### Accessing parent classes using `AsRef` + +In addition to `Deref`, the `AsRef` trait is implemented for all types in +`web_sys` for all types in the inheritance hierarchy. For example for the +`HtmlAnchorElement` type you'll find: + +```rust +impl AsRef for HtmlAnchorElement +impl AsRef for HtmlAnchorElement +impl AsRef for HtmlAnchorElement +impl AsRef for HtmlAnchorElement +impl AsRef for HtmlAnchorElement +impl AsRef for HtmlAnchorElement +``` + +You can use `.as_ref()` to explicitly get a reference to any parent class from +from a type in `web_sys`. Note that because of the number of `AsRef` +implementations you'll likely need to have type inference guidance as well. + +### Accessing child clases using `JsCast` + +Finally the `wasm_bindgen::JsCast` trait can be used to implement all manner of +casts between types. It supports static unchecked casts between types as well as +dynamic runtime-checked casts (using `instanceof`) between types. + +More documentation about this can be found [on the trait itself][jscast] + +[jscast]: https://docs.rs/wasm-bindgen/0.2/wasm_bindgen/trait.JsCast.html diff --git a/tests/wasm/final.js b/tests/wasm/final.js new file mode 100644 index 00000000..15b576d0 --- /dev/null +++ b/tests/wasm/final.js @@ -0,0 +1,25 @@ +const assert = require('assert'); + +exports.MyType = class { + static foo(y) { + assert.equal(y, 'x'); + return y + 'y'; + } + + constructor(x) { + assert.equal(x, 2); + this._a = 1; + } + + bar(x) { + assert.equal(x, true); + return 3.2; + } + + get a() { + return this._a; + } + set a(v) { + this._a = v; + } +}; diff --git a/tests/wasm/final.rs b/tests/wasm/final.rs new file mode 100644 index 00000000..63aa661f --- /dev/null +++ b/tests/wasm/final.rs @@ -0,0 +1,40 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +#[wasm_bindgen] +extern "C" { + type Math; + #[wasm_bindgen(static_method_of = Math, final)] + fn log(f: f32) -> f32; +} + +#[wasm_bindgen(module = "tests/wasm/final.js")] +extern "C" { + type MyType; + #[wasm_bindgen(constructor, final)] + fn new(x: u32) -> MyType; + #[wasm_bindgen(static_method_of = MyType, final)] + fn foo(a: &str) -> String; + #[wasm_bindgen(method, final)] + fn bar(this: &MyType, arg: bool) -> f32; + + #[wasm_bindgen(method, getter, final)] + fn a(this: &MyType) -> u32; + #[wasm_bindgen(method, setter, final)] + fn set_a(this: &MyType, a: u32); +} + +#[wasm_bindgen_test] +fn simple() { + assert_eq!(Math::log(1.0), 0.0); +} + +#[wasm_bindgen_test] +fn classes() { + assert_eq!(MyType::foo("x"), "xy"); + let x = MyType::new(2); + assert_eq!(x.bar(true), 3.2); + assert_eq!(x.a(), 1); + x.set_a(3); + assert_eq!(x.a(), 3); +} diff --git a/tests/wasm/import_class.js b/tests/wasm/import_class.js index fa60847c..ebc29759 100644 --- a/tests/wasm/import_class.js +++ b/tests/wasm/import_class.js @@ -131,3 +131,9 @@ exports.CatchConstructors = class { } } }; + +exports.StaticStructural = class { + static static_structural(x) { + return x + 3; + } +}; diff --git a/tests/wasm/import_class.rs b/tests/wasm/import_class.rs index 60b5181c..10ce84ca 100644 --- a/tests/wasm/import_class.rs +++ b/tests/wasm/import_class.rs @@ -32,12 +32,12 @@ extern "C" { fn switch_methods_a(); fn switch_methods_b(); type SwitchMethods; - #[wasm_bindgen(constructor)] + #[wasm_bindgen(constructor, final)] fn new() -> SwitchMethods; - #[wasm_bindgen(js_namespace = SwitchMethods)] + #[wasm_bindgen(js_namespace = SwitchMethods, final)] fn a(); fn switch_methods_called() -> bool; - #[wasm_bindgen(method)] + #[wasm_bindgen(method, final)] fn b(this: &SwitchMethods); type Properties; @@ -84,6 +84,10 @@ extern "C" { type CatchConstructors; #[wasm_bindgen(constructor, catch)] fn new(x: u32) -> Result; + + type StaticStructural; + #[wasm_bindgen(static_method_of = StaticStructural, structural)] + fn static_structural(a: u32) -> u32; } #[wasm_bindgen] @@ -136,7 +140,7 @@ fn switch_methods() { SwitchMethods::new().b(); assert!(switch_methods_called()); - switch_methods_a(); + switch_methods_b(); assert!(!switch_methods_called()); SwitchMethods::new().b(); @@ -213,3 +217,8 @@ fn catch_constructors() { assert!(CatchConstructors::new(0).is_err()); assert!(CatchConstructors::new(1).is_ok()); } + +#[wasm_bindgen_test] +fn static_structural() { + assert_eq!(StaticStructural::static_structural(30), 33); +} diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs index 4ea2604d..e0793a78 100644 --- a/tests/wasm/main.rs +++ b/tests/wasm/main.rs @@ -18,6 +18,8 @@ pub mod comments; pub mod duplicate_deps; pub mod duplicates; pub mod enums; +#[path = "final.rs"] +pub mod final_; pub mod import_class; pub mod imports; pub mod js_objects;