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.
This commit is contained in:
Alex Crichton 2018-08-04 10:00:30 -07:00
parent 11553a1af2
commit 37db88ebfa
10 changed files with 331 additions and 4 deletions

View File

@ -128,6 +128,7 @@ pub struct ImportType {
pub attrs: Vec<syn::Attribute>,
pub doc_comment: Option<String>,
pub instanceof_shim: String,
pub extends: Vec<Ident>,
}
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]

View File

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

View File

@ -182,6 +182,16 @@ impl BindgenAttrs {
})
.next()
}
/// Return the list of classes that a type extends
fn extends(&self) -> impl Iterator<Item = &Ident> {
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<String>)> for syn::ForeignItemFn
}
}
impl ConvertToAst<()> for syn::ForeignItemType {
impl ConvertToAst<BindgenAttrs> for syn::ForeignItemType {
type Target = ast::ImportKind;
fn convert(self, (): ()) -> Result<Self::Target, Diagnostic> {
fn convert(self, attrs: BindgenAttrs) -> Result<Self::Target, Diagnostic> {
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"),
};

View File

@ -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<OperationId<'src>, 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<Item = String> + 'me
{
let mut set = BTreeSet::new();
self.fill_superclasses(interface, &mut set);
set.into_iter()
}
fn fill_superclasses(&self, interface: &str, set: &mut BTreeSet<String>) {
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);
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Bar> for Foo { ... }
impl AsRef<Foo> for Bar { ... }
impl AsMut<Foo> 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();
```

View File

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

View File

@ -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::<JsCast1>().unwrap().myval(), 3);
assert_eq!(b.unchecked_ref::<JsCast1>().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::<JsCast4>());
assert!(a.is_instance_of::<JsCast3>());
assert!(a.is_instance_of::<JsCast1>());
let _: &JsCast3 = a.as_ref();
let b: &JsCast1 = a.as_ref();
assert_eq!(b.myval(), 4);
}