mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-04-25 06:02:13 +00:00
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:
parent
11553a1af2
commit
37db88ebfa
@ -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))]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
|
170
guide/src/design/import-customization.md
Normal file
170
guide/src/design/import-customization.md
Normal 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.
|
49
guide/src/reference/attributes/on-js-imports/extends.md
Normal file
49
guide/src/reference/attributes/on-js-imports/extends.md
Normal 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();
|
||||
```
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user