Skip to content

Commit 37db88e

Browse files
committed
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.
1 parent 11553a1 commit 37db88e

File tree

10 files changed

+331
-4
lines changed

10 files changed

+331
-4
lines changed

crates/backend/src/ast.rs

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ pub struct ImportType {
128128
pub attrs: Vec<syn::Attribute>,
129129
pub doc_comment: Option<String>,
130130
pub instanceof_shim: String,
131+
pub extends: Vec<Ident>,
131132
}
132133

133134
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]

crates/backend/src/codegen.rs

+25
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,31 @@ impl ToTokens for ast::ImportType {
656656
()
657657
};
658658
}).to_tokens(tokens);
659+
660+
for superclass in self.extends.iter() {
661+
(quote! {
662+
impl From<#name> for #superclass {
663+
fn from(obj: #name) -> #superclass {
664+
use wasm_bindgen::JsCast;
665+
#superclass::unchecked_from_js(obj.into())
666+
}
667+
}
668+
669+
impl AsRef<#superclass> for #name {
670+
fn as_ref(&self) -> &#superclass {
671+
use wasm_bindgen::JsCast;
672+
#superclass::unchecked_from_js_ref(self.as_ref())
673+
}
674+
}
675+
676+
impl AsMut<#superclass> for #name {
677+
fn as_mut(&mut self) -> &mut #superclass {
678+
use wasm_bindgen::JsCast;
679+
#superclass::unchecked_from_js_mut(self.as_mut())
680+
}
681+
}
682+
}).to_tokens(tokens);
683+
}
659684
}
660685
}
661686

crates/macro-support/src/parser.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ impl BindgenAttrs {
182182
})
183183
.next()
184184
}
185+
186+
/// Return the list of classes that a type extends
187+
fn extends(&self) -> impl Iterator<Item = &Ident> {
188+
self.attrs
189+
.iter()
190+
.filter_map(|a| match a {
191+
BindgenAttr::Extends(s) => Some(s),
192+
_ => None,
193+
})
194+
}
185195
}
186196

187197
impl syn::synom::Synom for BindgenAttrs {
@@ -217,6 +227,7 @@ pub enum BindgenAttr {
217227
Readonly,
218228
JsName(String),
219229
JsClass(String),
230+
Extends(Ident),
220231
}
221232

222233
impl syn::synom::Synom for BindgenAttr {
@@ -295,6 +306,13 @@ impl syn::synom::Synom for BindgenAttr {
295306
s: syn!(syn::LitStr) >>
296307
(s.value())
297308
)=> { BindgenAttr::JsClass }
309+
|
310+
do_parse!(
311+
call!(term, "extends") >>
312+
punct!(=) >>
313+
ns: call!(term2ident) >>
314+
(ns)
315+
)=> { BindgenAttr::Extends }
298316
));
299317
}
300318

@@ -520,17 +538,18 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option<String>)> for syn::ForeignItemFn
520538
}
521539
}
522540

523-
impl ConvertToAst<()> for syn::ForeignItemType {
541+
impl ConvertToAst<BindgenAttrs> for syn::ForeignItemType {
524542
type Target = ast::ImportKind;
525543

526-
fn convert(self, (): ()) -> Result<Self::Target, Diagnostic> {
544+
fn convert(self, attrs: BindgenAttrs) -> Result<Self::Target, Diagnostic> {
527545
let shim = format!("__wbg_instanceof_{}_{}", self.ident, ShortHash(&self.ident));
528546
Ok(ast::ImportKind::Type(ast::ImportType {
529547
vis: self.vis,
530548
attrs: self.attrs,
531549
doc_comment: None,
532550
instanceof_shim: shim,
533551
name: self.ident,
552+
extends: attrs.extends().cloned().collect(),
534553
}))
535554
}
536555
}
@@ -937,7 +956,7 @@ impl<'a> MacroParse<&'a BindgenAttrs> for syn::ForeignItem {
937956
let js_namespace = item_opts.js_namespace().or(opts.js_namespace()).cloned();
938957
let kind = match self {
939958
syn::ForeignItem::Fn(f) => f.convert((item_opts, &module))?,
940-
syn::ForeignItem::Type(t) => t.convert(())?,
959+
syn::ForeignItem::Type(t) => t.convert(item_opts)?,
941960
syn::ForeignItem::Static(s) => s.convert(item_opts)?,
942961
_ => panic!("only foreign functions/types allowed for now"),
943962
};

crates/webidl/src/first_pass.rs

+28
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use weedle;
1717

1818
use super::Result;
1919
use util;
20+
use util::camel_case_ident;
2021

2122
/// Collection of constructs that may use partial.
2223
#[derive(Default)]
@@ -36,6 +37,7 @@ pub(crate) struct InterfaceData<'src> {
3637
pub(crate) partial: bool,
3738
pub(crate) global: bool,
3839
pub(crate) operations: BTreeMap<OperationId<'src>, OperationData<'src>>,
40+
pub(crate) superclass: Option<&'src str>,
3941
}
4042

4143
#[derive(PartialEq, Eq, PartialOrd, Ord)]
@@ -146,6 +148,7 @@ impl<'src> FirstPass<'src, ()> for weedle::InterfaceDefinition<'src> {
146148
.entry(self.identifier.0)
147149
.or_insert_with(Default::default);
148150
interface.partial = false;
151+
interface.superclass = self.inheritance.map(|s| s.identifier.0);
149152
}
150153

151154
if util::is_chrome_only(&self.attributes) {
@@ -176,6 +179,7 @@ impl<'src> FirstPass<'src, ()> for weedle::PartialInterfaceDefinition<'src> {
176179
partial: true,
177180
operations: Default::default(),
178181
global: false,
182+
superclass: None,
179183
},
180184
);
181185

@@ -307,3 +311,27 @@ impl<'src> FirstPass<'src, ()> for weedle::TypedefDefinition<'src> {
307311
Ok(())
308312
}
309313
}
314+
315+
impl<'a> FirstPassRecord<'a> {
316+
pub fn all_superclasses<'me>(&'me self, interface: &str)
317+
-> impl Iterator<Item = String> + 'me
318+
{
319+
let mut set = BTreeSet::new();
320+
self.fill_superclasses(interface, &mut set);
321+
set.into_iter()
322+
}
323+
324+
fn fill_superclasses(&self, interface: &str, set: &mut BTreeSet<String>) {
325+
let data = match self.interfaces.get(interface) {
326+
Some(data) => data,
327+
None => return,
328+
};
329+
let superclass = match &data.superclass {
330+
Some(class) => class,
331+
None => return,
332+
};
333+
if set.insert(camel_case_ident(superclass)) {
334+
self.fill_superclasses(superclass, set);
335+
}
336+
}
337+
}

crates/webidl/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports};
3939
use backend::util::{ident_ty, rust_ident, wrap_import_function};
4040
use failure::ResultExt;
4141
use heck::{ShoutySnakeCase};
42+
use proc_macro2::{Ident, Span};
4243
use weedle::argument::Argument;
4344
use weedle::attribute::{ExtendedAttribute, ExtendedAttributeList};
4445

@@ -246,7 +247,10 @@ impl<'src> WebidlParse<'src, ()> for weedle::InterfaceDefinition<'src> {
246247
name: rust_ident(camel_case_ident(self.identifier.0).as_str()),
247248
attrs: Vec::new(),
248249
doc_comment,
249-
instanceof_shim: format!("__widl_instanceof_{}", self.name),
250+
instanceof_shim: format!("__widl_instanceof_{}", self.identifier.0),
251+
extends: first_pass.all_superclasses(self.identifier.0)
252+
.map(|name| Ident::new(&name, Span::call_site()))
253+
.collect(),
250254
}),
251255
});
252256

guide/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- [On JavaScript Imports](./reference/attributes/on-js-imports/index.md)
2121
- [`catch`](./reference/attributes/on-js-imports/catch.md)
2222
- [`constructor`](./reference/attributes/on-js-imports/constructor.md)
23+
- [`extends`](./reference/attributes/on-js-imports/extends.md)
2324
- [`getter` and `setter`](./reference/attributes/on-js-imports/getter-and-setter.md)
2425
- [`js_class = "Blah"`](./reference/attributes/on-js-imports/js_class.md)
2526
- [`js_name`](./reference/attributes/on-js-imports/js_name.md)
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Customizing import behavior
2+
3+
The `#[wasm_bindgen]` macro supports a good amount of configuration for
4+
controlling precisely how imports are imported and what they map to in JS. This
5+
section is intended to hopefully be an exhaustive reference of the
6+
possibilities!
7+
8+
* `catch` - this attribute allows catching a JS exception. This can be attached
9+
to any imported function and the function must return a `Result` where the
10+
`Err` payload is a `JsValue`, like so:
11+
12+
```rust
13+
#[wasm_bindgen]
14+
extern {
15+
#[wasm_bindgen(catch)]
16+
fn foo() -> Result<(), JsValue>;
17+
}
18+
```
19+
20+
If the imported function throws an exception then `Err` will be returned with
21+
the exception that was raised, and otherwise `Ok` is returned with the result
22+
of the function.
23+
24+
By default `wasm-bindgen` will take no action when wasm calls a JS function
25+
which ends up throwing an exception. The wasm spec right now doesn't support
26+
stack unwinding and as a result Rust code **will not execute destructors**.
27+
This can unfortunately cause memory leaks in Rust right now, but as soon as
28+
wasm implements catching exceptions we'll be sure to add support as well!
29+
30+
* `constructor` - this is used to indicate that the function being bound should
31+
actually translate to a `new` constructor in JS. The final argument must be a
32+
type that's imported from JS, and it's what'll get used in JS:
33+
34+
```rust
35+
#[wasm_bindgen]
36+
extern {
37+
type Foo;
38+
#[wasm_bindgen(constructor)]
39+
fn new() -> Foo;
40+
}
41+
```
42+
43+
This will attach the `new` function to the `Foo` type (implied by
44+
`constructor`) and in JS when this function is called it will be equivalent to
45+
`new Foo()`.
46+
47+
* `method` - this is the gateway to adding methods to imported objects or
48+
otherwise accessing properties on objects via methods and such. This should be
49+
done for doing the equivalent of expressions like `foo.bar()` in JS.
50+
51+
```rust
52+
#[wasm_bindgen]
53+
extern {
54+
type Foo;
55+
#[wasm_bindgen(method)]
56+
fn work(this: &Foo);
57+
}
58+
```
59+
60+
The first argument of a `method` annotation must be a borrowed reference (not
61+
mutable, shared) to the type that the method is attached to. In this case
62+
we'll be able to call this method like `foo.work()` in JS (where `foo` has
63+
type `Foo`).
64+
65+
In JS this invocation will correspond to accessing `Foo.prototype.work` and
66+
then calling that when the import is called. Note that `method` by default
67+
implies going through `prototype` to get a function pointer.
68+
69+
* `js_namespace` - this attribute indicates that the JS type is accessed through
70+
a particular namespace. For example the `WebAssembly.Module` APIs are all
71+
accessed through the `WebAssembly` namespace. The `js_namespace` can be
72+
applied to any import and whenever the generated JS attempts to reference a
73+
name (like a class or function name) it'll be accessed through this namespace.
74+
75+
```rust
76+
#[wasm_bindgen]
77+
extern {
78+
#[wasm_bindgen(js_namespace = console)]
79+
fn log(s: &str);
80+
}
81+
```
82+
83+
This is an example of how to bind `console.log(x)` in Rust. The `log` function
84+
will be available in the Rust module and will be invoked as `console.log` in
85+
JS.
86+
87+
* `getter` and `setter` - these two attributes can be combined with `method` to
88+
indicate that this is a getter or setter method. A `getter`-tagged function by
89+
default accesses the JS property with the same name as the getter function. A
90+
`setter`'s function name is currently required to start with "set\_" and the
91+
property it accesses is the suffix after "set\_".
92+
93+
```rust
94+
#[wasm_bindgen]
95+
extern {
96+
type Foo;
97+
#[wasm_bindgen(method, getter)]
98+
fn property(this: &Foo) -> u32;
99+
#[wasm_bindgen(method, setter)]
100+
fn set_property(this: &Foo, val: u32);
101+
}
102+
```
103+
104+
Here we're importing the `Foo` type and defining the ability to access each
105+
object's `property` property. The first function here is a getter and will be
106+
available in Rust as `foo.property()`, and the latter is the setter which is
107+
accessible as `foo.set_property(2)`. Note that both functions have a `this`
108+
argument as they're tagged with `method`.
109+
110+
Finally, you can also pass an argument to the `getter` and `setter`
111+
properties to configure what property is accessed. When the property is
112+
explicitly specified then there is no restriction on the method name. For
113+
example the below is equivalent to the above:
114+
115+
```rust
116+
#[wasm_bindgen]
117+
extern {
118+
type Foo;
119+
#[wasm_bindgen(method, getter = property)]
120+
fn assorted_method_name(this: &Foo) -> u32;
121+
#[wasm_bindgen(method, setter = "property")]
122+
fn some_other_method_name(this: &Foo, val: u32);
123+
}
124+
```
125+
126+
Properties in JS are accessed through `Object.getOwnPropertyDescriptor`. Note
127+
that this typically only works for class-like-defined properties which aren't
128+
just attached properties on any old object. For accessing any old property on
129+
an object we can use...
130+
131+
* `structural` - this is a flag to `method` annotations which indicates that the
132+
method being accessed (or property with getters/setters) should be accessed in
133+
a structural fashion. For example methods are *not* accessed through
134+
`prototype` and properties are accessed on the object directly rather than
135+
through `Object.getOwnPropertyDescriptor`.
136+
137+
```rust
138+
#[wasm_bindgen]
139+
extern {
140+
type Foo;
141+
#[wasm_bindgen(method, structural)]
142+
fn bar(this: &Foo);
143+
#[wasm_bindgen(method, getter, structural)]
144+
fn baz(this: &Foo) -> u32;
145+
}
146+
```
147+
148+
The type here, `Foo`, is not required to exist in JS (it's not referenced).
149+
Instead wasm-bindgen will generate shims that will access the passed in JS
150+
value's `bar` property to or the `baz` property (depending on the function).
151+
152+
* `js_name = foo` - this can be used to bind to a different function in JS than
153+
the identifier that's defined in Rust. For example you can also define
154+
multiple signatures for a polymorphic function in JS as well:
155+
156+
```rust
157+
#[wasm_bindgen]
158+
extern {
159+
type Foo;
160+
#[wasm_bindgen(js_namespace = console, js_name = log)]
161+
fn log_string(s: &str);
162+
#[wasm_bindgen(js_namespace = console, js_name = log)]
163+
fn log_u32(n: u32);
164+
#[wasm_bindgen(js_namespace = console, js_name = log)]
165+
fn log_many(a: u32, b: JsValue);
166+
}
167+
```
168+
169+
All of these functions will call `console.log` in JS, but each identifier
170+
will have only one signature in Rust.

0 commit comments

Comments
 (0)