Skip to content

Commit fb5de96

Browse files
committed
Switch all imports to structural by default
This commit switches all imports of JS methods to `structural` by default. Proposed in [RFC 5] this should increase the performance of bindings today while also providing future-proofing for possible confusion with the recent addition of the `Deref` trait for all imported types by default as well. A new attribute, `host_binding`, is introduced in this PR as well to recover the old behavior of binding directly to an imported function which will one day be the precise function on the prototype. Eventually `web-sys` will switcsh over entirely to being driven via `host_binding` methods, but for now it's been measured to be not quite as fast so we're not making that switch yet. Note that `host_binding` differs from the proposed name of `final` due to the controversy, and its hoped that `host_binding` is a good middle-ground! [RFC 5]: https://rustwasm.github.io/rfcs/005-structural-and-deref.html
1 parent 6093fd2 commit fb5de96

File tree

9 files changed

+244
-16
lines changed

9 files changed

+244
-16
lines changed

crates/macro-support/src/parser.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ impl BindgenAttrs {
149149
})
150150
}
151151

152+
/// Whether the `host_binding` attribute is present
153+
fn host_binding(&self) -> bool {
154+
self.attrs.iter().any(|a| match *a {
155+
BindgenAttr::HostBinding => true,
156+
_ => false,
157+
})
158+
}
159+
152160
/// Whether the readonly attributes is present
153161
fn readonly(&self) -> bool {
154162
self.attrs.iter().any(|a| match *a {
@@ -229,6 +237,7 @@ pub enum BindgenAttr {
229237
IndexingSetter,
230238
IndexingDeleter,
231239
Structural,
240+
HostBinding,
232241
Readonly,
233242
JsName(String, Span),
234243
JsClass(String),
@@ -262,6 +271,9 @@ impl Parse for BindgenAttr {
262271
if attr == "structural" {
263272
return Ok(BindgenAttr::Structural);
264273
}
274+
if attr == "host_binding" {
275+
return Ok(BindgenAttr::HostBinding);
276+
}
265277
if attr == "readonly" {
266278
return Ok(BindgenAttr::Readonly);
267279
}
@@ -549,7 +561,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option<String>)> for syn::ForeignItemFn
549561
js_ret,
550562
catch,
551563
variadic,
552-
structural: opts.structural(),
564+
structural: opts.structural() || !opts.host_binding(),
553565
rust_name: self.ident.clone(),
554566
shim: Ident::new(&shim, Span::call_site()),
555567
doc_comment: None,

examples/add/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ extern crate wasm_bindgen;
22

33
use wasm_bindgen::prelude::*;
44

5+
#[wasm_bindgen]
6+
extern {
7+
pub type Foo;
8+
#[wasm_bindgen(method,host_binding)]
9+
fn bar(this: &Foo, argument: &str) -> JsValue;
10+
}
11+
12+
#[wasm_bindgen]
13+
pub fn wut(a: &Foo) {
14+
a.bar("x");
15+
}
16+
517
#[wasm_bindgen]
618
pub fn add(a: u32, b: u32) -> u32 {
719
a + b

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
- [`constructor`](./reference/attributes/on-js-imports/constructor.md)
6262
- [`extends`](./reference/attributes/on-js-imports/extends.md)
6363
- [`getter` and `setter`](./reference/attributes/on-js-imports/getter-and-setter.md)
64+
- [`host_binding`](./reference/attributes/on-js-imports/host_binding.md)
6465
- [`indexing_getter`, `indexing_setter`, and `indexing_deleter`](./reference/attributes/on-js-imports/indexing-getter-setter-deleter.md)
6566
- [`js_class = "Blah"`](./reference/attributes/on-js-imports/js_class.md)
6667
- [`js_name`](./reference/attributes/on-js-imports/js_name.md)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# `host_binding`
2+
3+
The `host_binding` attribute is the converse of the [`structural`
4+
attribute](structural.html). It configures how `wasm-bindgen` will generate JS
5+
glue to call the imported function. The naming here is intended convey that this
6+
attribute is intended to implement the semantics of the future [host bindings
7+
proposal][host-bindings] for WebAssembly.
8+
9+
[host-bindings]: https://github.com/WebAssembly/host-bindings
10+
[reference-types]: https://github.com/WebAssembly/reference-types
11+
12+
The `host_binding` attribute is intended to be purely related to performance. It
13+
ideally has no user-visible effect, and well-typed `structural` imports (the
14+
default) should be able to transparently switch to `host_binding` eventually.
15+
16+
The eventual performance aspect is that with the [host bindings
17+
proposal][host-bindings] then `wasm-bindgen` will need to generate far fewer JS
18+
shims to import than it does today. For example, consider this import today:
19+
20+
```rust
21+
#[wasm_bindgen]
22+
extern {
23+
type Foo;
24+
#[wasm_bindgen(method)]
25+
fn bar(this: &Foo, argument: &str) -> JsValue;
26+
}
27+
```
28+
29+
**Without the `host_binding` attribute** the generated JS looks like this:
30+
31+
```js
32+
// without `host_binding`
33+
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
34+
let varg1 = getStringFromWasm(arg1, arg2);
35+
return addHeapObject(getObject(arg0).bar(varg1));
36+
}
37+
```
38+
39+
We can see here that this JS shim is required, but it's all relatively
40+
self-contained. It does, however, execute the `bar` method in a duck-type-y
41+
fashion in the sense that it never validates `getObject(arg0)` is of type
42+
`Foo` to actually call the `Foo.prototype.bar` method.
43+
44+
If we instead, however, write this:
45+
46+
```rust
47+
#[wasm_bindgen]
48+
extern {
49+
type Foo;
50+
#[wasm_bindgen(method, host_binding)] // note the change here
51+
fn bar(this: &Foo, argument: &str) -> JsValue;
52+
}
53+
```
54+
55+
it generates this JS glue (roughly):
56+
57+
```js
58+
const __wbg_bar_target = Foo.prototype.bar;
59+
60+
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
61+
let varg1 = getStringFromWasm(arg1, arg2);
62+
return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
63+
}
64+
```
65+
66+
The difference here is pretty subtle, but we can see how the function being
67+
called is hoisted out of the generated shim and is bound to always be
68+
`Foo.prototype.bar`. This then uses the `Function.call` method to invoke that
69+
function with `getObject(arg0)` as the receiver.
70+
71+
But wait, there's still a JS shim here even with `host_binding`! That's true,
72+
and this is simply a fact of future WebAssembly proposals not being implemented
73+
yet. The semantics, though, match the future [host bindings
74+
proposal][host-bindings] because the method being called is determined exactly
75+
once, and it's located on the prototype chain rather than being resolved at
76+
runtime when the function is called.
77+
78+
## Interaction with future proposals
79+
80+
If you're curious to see how our JS shim will be eliminated entirely, let's take
81+
a look at the generated bindings. We're starting off with this:
82+
83+
```js
84+
const __wbg_bar_target = Foo.prototype.bar;
85+
86+
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
87+
let varg1 = getStringFromWasm(arg1, arg2);
88+
return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
89+
}
90+
```
91+
92+
... and once the [reference types proposal][reference-types] is implemented then
93+
we won't need some of these pesky functions. That'll transform our generated JS
94+
shim to look like:
95+
96+
```js
97+
const __wbg_bar_target = Foo.prototype.bar;
98+
99+
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
100+
let varg1 = getStringFromWasm(arg1, arg2);
101+
return __wbg_bar_target.call(arg0, varg1);
102+
}
103+
```
104+
105+
Getting better! Next up we need the host bindings proposal. Note that the
106+
proposal is undergoing some changes right now so it's tough to link to reference
107+
documentation, but it suffices to say that it'll empower us with at least two
108+
different features.
109+
110+
First, host bindings promises to provide the concept of "argument conversions".
111+
The `arg1` and `arg2` values here are actually a pointer and a length to a utf-8
112+
encoded string, and with host bindings we'll be able to annotate that this
113+
import should take those two arguments and convert them to a JS string (that is,
114+
the *host* should do this, the WebAssembly engine). Using that feature we can
115+
futher trim this down to:
116+
117+
```js
118+
const __wbg_bar_target = Foo.prototype.bar;
119+
120+
export function __wbg_bar_a81456386e6b526f(arg0, varg1) {
121+
return __wbg_bar_target.call(arg0, varg1);
122+
}
123+
```
124+
125+
And finally, the second promise of the host bindings proposal is that we can
126+
flag a function call to indicate the first argument is the `this` binding of the
127+
function call. Today the `this` value of all called imported functions is
128+
`undefined`, and this flag (configured with host bindings) will indicate the
129+
first argument here is actually the `this`.
130+
131+
With that in mind we can further transform this to:
132+
133+
```js
134+
export const __wbg_bar_a81456386e6b526f = Foo.prototype.bar;
135+
```
136+
137+
and voila! We, with [reference types][reference-types] and [host
138+
bindings][host-bindings], now have no JS shim at all necessary to call the
139+
imported function!

guide/src/reference/attributes/on-js-imports/structural.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# `structural`
22

3+
> **Note**: As of [RFC 5] this attribute is the default for all imported
4+
> functions. This attribute is largely ignored today and is only retained for
5+
> backwards compatibility and learning purposes.
6+
>
7+
> The inverse of this attribute, [the `host_binding`
8+
> attribute](host_binding.html) is more functionally interesting than
9+
> `structural` (as `structural` is simply the default)
10+
11+
[RFC 5]: https://rustwasm.github.io/rfcs/005-structural-and-deref.html
12+
313
The `structural` flag can be added to `method` annotations, indicating that the
414
method being accessed (or property with getters/setters) should be accessed in a
515
structural, duck-type-y fashion. Rather than walking the constructor's prototype
@@ -36,15 +46,3 @@ function quack(duck) {
3646
duck.quack();
3747
}
3848
```
39-
40-
## Why don't we always use the `structural` behavior?
41-
42-
In theory, it is faster since the prototype chain doesn't need to be traversed
43-
every time the method or property is accessed, but today's optimizing JIT
44-
compilers are really good about eliminating that cost. The real reason is to be
45-
future compatible with the ["host bindings" proposal][host-bindings], which
46-
requires that there be no JavaScript shim between the caller and the native host
47-
function. In this scenario, the properties and methods *must* be resolved before
48-
the wasm is instantiated.
49-
50-
[host-bindings]: https://github.com/WebAssembly/host-bindings/blob/master/proposals/host-bindings/Overview.md

tests/wasm/host_binding.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const assert = require('assert');
2+
3+
exports.MyType = class {
4+
static foo(y) {
5+
assert.equal(y, 'x');
6+
return y + 'y';
7+
}
8+
9+
constructor(x) {
10+
assert.equal(x, 2);
11+
this._a = 1;
12+
}
13+
14+
bar(x) {
15+
assert.equal(x, true);
16+
return 3.2;
17+
}
18+
19+
get a() {
20+
return this._a;
21+
}
22+
set a(v) {
23+
this._a = v;
24+
}
25+
};

tests/wasm/host_binding.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use wasm_bindgen::prelude::*;
2+
use wasm_bindgen_test::*;
3+
4+
#[wasm_bindgen]
5+
extern "C" {
6+
type Math;
7+
#[wasm_bindgen(static_method_of = Math, host_binding)]
8+
fn log(f: f32) -> f32;
9+
}
10+
11+
#[wasm_bindgen(module = "tests/wasm/host_binding.js")]
12+
extern "C" {
13+
type MyType;
14+
#[wasm_bindgen(constructor, host_binding)]
15+
fn new(x: u32) -> MyType;
16+
#[wasm_bindgen(static_method_of = MyType, host_binding)]
17+
fn foo(a: &str) -> String;
18+
#[wasm_bindgen(method, host_binding)]
19+
fn bar(this: &MyType, arg: bool) -> f32;
20+
21+
#[wasm_bindgen(method, getter, host_binding)]
22+
fn a(this: &MyType) -> u32;
23+
#[wasm_bindgen(method, setter, host_binding)]
24+
fn set_a(this: &MyType, a: u32);
25+
}
26+
27+
#[wasm_bindgen_test]
28+
fn simple() {
29+
assert_eq!(Math::log(1.0), 0.0);
30+
}
31+
32+
#[wasm_bindgen_test]
33+
fn classes() {
34+
assert_eq!(MyType::foo("x"), "xy");
35+
let x = MyType::new(2);
36+
assert_eq!(x.bar(true), 3.2);
37+
assert_eq!(x.a(), 1);
38+
x.set_a(3);
39+
assert_eq!(x.a(), 3);
40+
}

tests/wasm/import_class.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ extern "C" {
3232
fn switch_methods_a();
3333
fn switch_methods_b();
3434
type SwitchMethods;
35-
#[wasm_bindgen(constructor)]
35+
#[wasm_bindgen(constructor, host_binding)]
3636
fn new() -> SwitchMethods;
37-
#[wasm_bindgen(js_namespace = SwitchMethods)]
37+
#[wasm_bindgen(js_namespace = SwitchMethods, host_binding)]
3838
fn a();
3939
fn switch_methods_called() -> bool;
40-
#[wasm_bindgen(method)]
40+
#[wasm_bindgen(method, host_binding)]
4141
fn b(this: &SwitchMethods);
4242

4343
type Properties;

tests/wasm/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod comments;
1818
pub mod duplicate_deps;
1919
pub mod duplicates;
2020
pub mod enums;
21+
pub mod host_binding;
2122
pub mod import_class;
2223
pub mod imports;
2324
pub mod js_objects;

0 commit comments

Comments
 (0)