From 37db88ebfa66adf09bfe4dc3f65d8bd1687b9fd1 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sat, 4 Aug 2018 10:00:30 -0700 Subject: [PATCH] Implement `#[wasm_bindgen(extends = ...)]` This commit implements the `extends` attribute for `#[wasm_bindgen]` to statically draw the inheritance hierarchy in the generated bindings, generating appropriate `AsRef`, `AsMut`, and `From` implementations. --- crates/backend/src/ast.rs | 1 + crates/backend/src/codegen.rs | 25 +++ crates/macro-support/src/parser.rs | 25 ++- crates/webidl/src/first_pass.rs | 28 +++ crates/webidl/src/lib.rs | 6 +- guide/src/SUMMARY.md | 1 + guide/src/design/import-customization.md | 170 ++++++++++++++++++ .../attributes/on-js-imports/extends.md | 49 +++++ tests/wasm/jscast.js | 10 ++ tests/wasm/jscast.rs | 20 +++ 10 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 guide/src/design/import-customization.md create mode 100644 guide/src/reference/attributes/on-js-imports/extends.md diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 9a6d8a54..f13ec469 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -128,6 +128,7 @@ pub struct ImportType { pub attrs: Vec, pub doc_comment: Option, pub instanceof_shim: String, + pub extends: Vec, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index af989672..d66f9867 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -656,6 +656,31 @@ impl ToTokens for ast::ImportType { () }; }).to_tokens(tokens); + + for superclass in self.extends.iter() { + (quote! { + impl From<#name> for #superclass { + fn from(obj: #name) -> #superclass { + use wasm_bindgen::JsCast; + #superclass::unchecked_from_js(obj.into()) + } + } + + impl AsRef<#superclass> for #name { + fn as_ref(&self) -> &#superclass { + use wasm_bindgen::JsCast; + #superclass::unchecked_from_js_ref(self.as_ref()) + } + } + + impl AsMut<#superclass> for #name { + fn as_mut(&mut self) -> &mut #superclass { + use wasm_bindgen::JsCast; + #superclass::unchecked_from_js_mut(self.as_mut()) + } + } + }).to_tokens(tokens); + } } } diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 7e3c9b38..84fa0638 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -182,6 +182,16 @@ impl BindgenAttrs { }) .next() } + + /// Return the list of classes that a type extends + fn extends(&self) -> impl Iterator { + self.attrs + .iter() + .filter_map(|a| match a { + BindgenAttr::Extends(s) => Some(s), + _ => None, + }) + } } impl syn::synom::Synom for BindgenAttrs { @@ -217,6 +227,7 @@ pub enum BindgenAttr { Readonly, JsName(String), JsClass(String), + Extends(Ident), } impl syn::synom::Synom for BindgenAttr { @@ -295,6 +306,13 @@ impl syn::synom::Synom for BindgenAttr { s: syn!(syn::LitStr) >> (s.value()) )=> { BindgenAttr::JsClass } + | + do_parse!( + call!(term, "extends") >> + punct!(=) >> + ns: call!(term2ident) >> + (ns) + )=> { BindgenAttr::Extends } )); } @@ -520,10 +538,10 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option)> for syn::ForeignItemFn } } -impl ConvertToAst<()> for syn::ForeignItemType { +impl ConvertToAst for syn::ForeignItemType { type Target = ast::ImportKind; - fn convert(self, (): ()) -> Result { + fn convert(self, attrs: BindgenAttrs) -> Result { let shim = format!("__wbg_instanceof_{}_{}", self.ident, ShortHash(&self.ident)); Ok(ast::ImportKind::Type(ast::ImportType { vis: self.vis, @@ -531,6 +549,7 @@ impl ConvertToAst<()> for syn::ForeignItemType { doc_comment: None, instanceof_shim: shim, name: self.ident, + extends: attrs.extends().cloned().collect(), })) } } @@ -937,7 +956,7 @@ impl<'a> MacroParse<&'a BindgenAttrs> for syn::ForeignItem { let js_namespace = item_opts.js_namespace().or(opts.js_namespace()).cloned(); let kind = match self { syn::ForeignItem::Fn(f) => f.convert((item_opts, &module))?, - syn::ForeignItem::Type(t) => t.convert(())?, + syn::ForeignItem::Type(t) => t.convert(item_opts)?, syn::ForeignItem::Static(s) => s.convert(item_opts)?, _ => panic!("only foreign functions/types allowed for now"), }; diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs index 7d65d2b8..4e58443a 100644 --- a/crates/webidl/src/first_pass.rs +++ b/crates/webidl/src/first_pass.rs @@ -17,6 +17,7 @@ use weedle; use super::Result; use util; +use util::camel_case_ident; /// Collection of constructs that may use partial. #[derive(Default)] @@ -36,6 +37,7 @@ pub(crate) struct InterfaceData<'src> { pub(crate) partial: bool, pub(crate) global: bool, pub(crate) operations: BTreeMap, OperationData<'src>>, + pub(crate) superclass: Option<&'src str>, } #[derive(PartialEq, Eq, PartialOrd, Ord)] @@ -146,6 +148,7 @@ impl<'src> FirstPass<'src, ()> for weedle::InterfaceDefinition<'src> { .entry(self.identifier.0) .or_insert_with(Default::default); interface.partial = false; + interface.superclass = self.inheritance.map(|s| s.identifier.0); } if util::is_chrome_only(&self.attributes) { @@ -176,6 +179,7 @@ impl<'src> FirstPass<'src, ()> for weedle::PartialInterfaceDefinition<'src> { partial: true, operations: Default::default(), global: false, + superclass: None, }, ); @@ -307,3 +311,27 @@ impl<'src> FirstPass<'src, ()> for weedle::TypedefDefinition<'src> { Ok(()) } } + +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() + } + + fn fill_superclasses(&self, interface: &str, set: &mut BTreeSet) { + let data = match self.interfaces.get(interface) { + Some(data) => data, + None => return, + }; + let superclass = match &data.superclass { + Some(class) => class, + None => return, + }; + if set.insert(camel_case_ident(superclass)) { + self.fill_superclasses(superclass, set); + } + } +} diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index 94812fd4..5410012c 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -39,6 +39,7 @@ use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports}; use backend::util::{ident_ty, rust_ident, wrap_import_function}; use failure::ResultExt; use heck::{ShoutySnakeCase}; +use proc_macro2::{Ident, Span}; use weedle::argument::Argument; use weedle::attribute::{ExtendedAttribute, ExtendedAttributeList}; @@ -246,7 +247,10 @@ impl<'src> WebidlParse<'src, ()> for weedle::InterfaceDefinition<'src> { name: rust_ident(camel_case_ident(self.identifier.0).as_str()), attrs: Vec::new(), doc_comment, - instanceof_shim: format!("__widl_instanceof_{}", self.name), + instanceof_shim: format!("__widl_instanceof_{}", self.identifier.0), + extends: first_pass.all_superclasses(self.identifier.0) + .map(|name| Ident::new(&name, Span::call_site())) + .collect(), }), }); diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 57246bd4..4ab2b0cb 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -20,6 +20,7 @@ - [On JavaScript Imports](./reference/attributes/on-js-imports/index.md) - [`catch`](./reference/attributes/on-js-imports/catch.md) - [`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) - [`js_class = "Blah"`](./reference/attributes/on-js-imports/js_class.md) - [`js_name`](./reference/attributes/on-js-imports/js_name.md) diff --git a/guide/src/design/import-customization.md b/guide/src/design/import-customization.md new file mode 100644 index 00000000..4f1b6d9a --- /dev/null +++ b/guide/src/design/import-customization.md @@ -0,0 +1,170 @@ +# Customizing import behavior + +The `#[wasm_bindgen]` macro supports a good amount of configuration for +controlling precisely how imports are imported and what they map to in JS. This +section is intended to hopefully be an exhaustive reference of the +possibilities! + +* `catch` - this attribute allows catching a JS exception. This can be attached + to any imported function and the function must return a `Result` where the + `Err` payload is a `JsValue`, like so: + + ```rust + #[wasm_bindgen] + extern { + #[wasm_bindgen(catch)] + fn foo() -> Result<(), JsValue>; + } + ``` + + If the imported function throws an exception then `Err` will be returned with + the exception that was raised, and otherwise `Ok` is returned with the result + of the function. + + 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! + +* `constructor` - this is used to indicate that the function being bound should + actually translate to a `new` constructor in JS. The final argument must be a + type that's imported from JS, and it's what'll get used in JS: + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(constructor)] + fn new() -> Foo; + } + ``` + + This will attach the `new` function to the `Foo` type (implied by + `constructor`) and in JS when this function is called it will be equivalent to + `new Foo()`. + +* `method` - this is the gateway to adding methods to imported objects or + otherwise accessing properties on objects via methods and such. This should be + done for doing the equivalent of expressions like `foo.bar()` in JS. + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(method)] + fn work(this: &Foo); + } + ``` + + The first argument of a `method` annotation must be a borrowed reference (not + mutable, shared) to the type that the method is attached to. In this case + we'll be able to call this method like `foo.work()` in JS (where `foo` has + type `Foo`). + + In JS this invocation will correspond to accessing `Foo.prototype.work` and + then calling that when the import is called. Note that `method` by default + implies going through `prototype` to get a function pointer. + +* `js_namespace` - this attribute indicates that the JS type is accessed through + a particular namespace. For example the `WebAssembly.Module` APIs are all + accessed through the `WebAssembly` namespace. The `js_namespace` can be + applied to any import and whenever the generated JS attempts to reference a + name (like a class or function name) it'll be accessed through this namespace. + + ```rust + #[wasm_bindgen] + extern { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + } + ``` + + This is an example of how to bind `console.log(x)` in Rust. The `log` function + will be available in the Rust module and will be invoked as `console.log` in + JS. + +* `getter` and `setter` - these two attributes can be combined with `method` to + indicate that this is a getter or setter method. A `getter`-tagged function by + default accesses the JS property with the same name as the getter function. A + `setter`'s function name is currently required to start with "set\_" and the + property it accesses is the suffix after "set\_". + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(method, getter)] + fn property(this: &Foo) -> u32; + #[wasm_bindgen(method, setter)] + fn set_property(this: &Foo, val: u32); + } + ``` + + Here we're importing the `Foo` type and defining the ability to access each + object's `property` property. The first function here is a getter and will be + available in Rust as `foo.property()`, and the latter is the setter which is + accessible as `foo.set_property(2)`. Note that both functions have a `this` + argument as they're tagged with `method`. + + Finally, you can also pass an argument to the `getter` and `setter` + properties to configure what property is accessed. When the property is + explicitly specified then there is no restriction on the method name. For + example the below is equivalent to the above: + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(method, getter = property)] + fn assorted_method_name(this: &Foo) -> u32; + #[wasm_bindgen(method, setter = "property")] + fn some_other_method_name(this: &Foo, val: u32); + } + ``` + + Properties in JS are accessed through `Object.getOwnPropertyDescriptor`. Note + that this typically only works for class-like-defined properties which aren't + just attached properties on any old object. For accessing any old property on + an object we can use... + +* `structural` - this is a flag to `method` annotations which indicates that the + method being accessed (or property with getters/setters) should be accessed in + a structural fashion. For example methods are *not* accessed through + `prototype` and properties are accessed on the object directly rather than + through `Object.getOwnPropertyDescriptor`. + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(method, structural)] + fn bar(this: &Foo); + #[wasm_bindgen(method, getter, structural)] + fn baz(this: &Foo) -> u32; + } + ``` + + The type here, `Foo`, is not required to exist in JS (it's not referenced). + Instead wasm-bindgen will generate shims that will access the passed in JS + value's `bar` property to or the `baz` property (depending on the function). + +* `js_name = foo` - this can be used to bind to a different function in JS than + the identifier that's defined in Rust. For example you can also define + multiple signatures for a polymorphic function in JS as well: + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_string(s: &str); + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_u32(n: u32); + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_many(a: u32, b: JsValue); + } + ``` + + All of these functions will call `console.log` in JS, but each identifier + will have only one signature in Rust. diff --git a/guide/src/reference/attributes/on-js-imports/extends.md b/guide/src/reference/attributes/on-js-imports/extends.md new file mode 100644 index 00000000..7eda0f9f --- /dev/null +++ b/guide/src/reference/attributes/on-js-imports/extends.md @@ -0,0 +1,49 @@ +# `extends = Class` + +The `extends` attribute can be used to say that an imported type extends (in the +JS class hierarchy sense) another type. This will generate `AsRef`, `AsMut`, and +`From` impls for converting a type into another given that we statically know +the inheritance hierarchy: + +```rust +#[wasm_bindgen] +extern { + type Foo; + + #[wasm_bindgen(extends = Foo)] + type Bar; +} + +let x: &Bar = ...; +let y: &Foo = x.as_ref(); // zero cost cast +``` + +The trait implementations generated for the above block are: + +```rust +impl From for Foo { ... } +impl AsRef for Bar { ... } +impl AsMut for Bar { ... } +``` + + +The `extends = ...` attribute can be specified multiple times for longer +inheritance chains, and `AsRef` and such impls will be generated for each of +the types. + +```rust +#[wasm_bindgen] +extern { + type Foo; + + #[wasm_bindgen(extends = Foo)] + type Bar; + + #[wasm_bindgen(extends = Foo, extends = Bar)] + type Baz; +} + +let x: &Baz = ...; +let y1: &Bar = x.as_ref(); +let y2: &Foo = x.as_ref(); +``` diff --git a/tests/wasm/jscast.js b/tests/wasm/jscast.js index d93489ff..3cecd991 100644 --- a/tests/wasm/jscast.js +++ b/tests/wasm/jscast.js @@ -4,8 +4,10 @@ class JsCast1 { } myval() { return this.val; } } + class JsCast2 { } + class JsCast3 extends JsCast1 { constructor() { super(); @@ -13,6 +15,14 @@ class JsCast3 extends JsCast1 { } } +class JsCast4 extends JsCast3 { + constructor() { + super(); + this.val = 4; + } +} + exports.JsCast1 = JsCast1; exports.JsCast2 = JsCast2; exports.JsCast3 = JsCast3; +exports.JsCast4 = JsCast4; diff --git a/tests/wasm/jscast.rs b/tests/wasm/jscast.rs index d98187be..acdbf561 100644 --- a/tests/wasm/jscast.rs +++ b/tests/wasm/jscast.rs @@ -14,9 +14,15 @@ extern { #[wasm_bindgen(constructor)] fn new() -> JsCast2; + #[wasm_bindgen(extends = JsCast1)] type JsCast3; #[wasm_bindgen(constructor)] fn new() -> JsCast3; + + #[wasm_bindgen(extends = JsCast1, extends = JsCast3)] + type JsCast4; + #[wasm_bindgen(constructor)] + fn new() -> JsCast4; } #[wasm_bindgen_test] @@ -65,4 +71,18 @@ fn method_calling() { assert_eq!(a.myval(), 1); assert_eq!(b.dyn_ref::().unwrap().myval(), 3); assert_eq!(b.unchecked_ref::().myval(), 3); + let c: &JsCast1 = b.as_ref(); + assert_eq!(c.myval(), 3); +} + +#[wasm_bindgen_test] +fn multiple_layers_of_inheritance() { + let a = JsCast4::new(); + assert!(a.is_instance_of::()); + assert!(a.is_instance_of::()); + assert!(a.is_instance_of::()); + + let _: &JsCast3 = a.as_ref(); + let b: &JsCast1 = a.as_ref(); + assert_eq!(b.myval(), 4); }