Skip to content

Commit

Permalink
Switch all imports to structural by default
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alexcrichton committed Nov 8, 2018
1 parent 6093fd2 commit 4c42aba
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 16 deletions.
14 changes: 13 additions & 1 deletion crates/macro-support/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ impl BindgenAttrs {
})
}

/// Whether the `host_binding` attribute is present
fn host_binding(&self) -> bool {
self.attrs.iter().any(|a| match *a {
BindgenAttr::HostBinding => true,
_ => false,
})
}

/// Whether the readonly attributes is present
fn readonly(&self) -> bool {
self.attrs.iter().any(|a| match *a {
Expand Down Expand Up @@ -229,6 +237,7 @@ pub enum BindgenAttr {
IndexingSetter,
IndexingDeleter,
Structural,
HostBinding,
Readonly,
JsName(String, Span),
JsClass(String),
Expand Down Expand Up @@ -262,6 +271,9 @@ impl Parse for BindgenAttr {
if attr == "structural" {
return Ok(BindgenAttr::Structural);
}
if attr == "host_binding" {
return Ok(BindgenAttr::HostBinding);
}
if attr == "readonly" {
return Ok(BindgenAttr::Readonly);
}
Expand Down Expand Up @@ -549,7 +561,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option<String>)> for syn::ForeignItemFn
js_ret,
catch,
variadic,
structural: opts.structural(),
structural: opts.structural() || !opts.host_binding(),
rust_name: self.ident.clone(),
shim: Ident::new(&shim, Span::call_site()),
doc_comment: None,
Expand Down
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
- [`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)
- [`host_binding`](./reference/attributes/on-js-imports/host_binding.md)
- [`indexing_getter`, `indexing_setter`, and `indexing_deleter`](./reference/attributes/on-js-imports/indexing-getter-setter-deleter.md)
- [`js_class = "Blah"`](./reference/attributes/on-js-imports/js_class.md)
- [`js_name`](./reference/attributes/on-js-imports/js_name.md)
Expand Down
139 changes: 139 additions & 0 deletions guide/src/reference/attributes/on-js-imports/host_binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# `host_binding`

The `host_binding` attribute is the converse of the [`structural`
attribute](structural.html). It configures how `wasm-bindgen` will generate JS
glue to call the imported function. The naming here is intended convey that this
attribute is intended to implement the semantics of the future [host bindings
proposal][host-bindings] for WebAssembly.

[host-bindings]: https://github.com/WebAssembly/host-bindings
[reference-types]: https://github.com/WebAssembly/reference-types

The `host_binding` attribute is intended to be purely related to performance. It
ideally has no user-visible effect, and well-typed `structural` imports (the
default) should be able to transparently switch to `host_binding` eventually.

The eventual performance aspect is that with the [host bindings
proposal][host-bindings] then `wasm-bindgen` will need to generate far fewer JS
shims to import than it does today. For example, consider this import today:

```rust
#[wasm_bindgen]
extern {
type Foo;
#[wasm_bindgen(method)]
fn bar(this: &Foo, argument: &str) -> JsValue;
}
```

**Without the `host_binding` attribute** the generated JS looks like this:

```js
// without `host_binding`
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
let varg1 = getStringFromWasm(arg1, arg2);
return addHeapObject(getObject(arg0).bar(varg1));
}
```

We can see here that this JS shim is required, but it's all relatively
self-contained. It does, however, execute the `bar` method in a duck-type-y
fashion in the sense that it never validates `getObject(arg0)` is of type
`Foo` to actually call the `Foo.prototype.bar` method.

If we instead, however, write this:

```rust
#[wasm_bindgen]
extern {
type Foo;
#[wasm_bindgen(method, host_binding)] // note the change here
fn bar(this: &Foo, argument: &str) -> JsValue;
}
```

it generates this JS glue (roughly):

```js
const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
let varg1 = getStringFromWasm(arg1, arg2);
return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
}
```

The difference here is pretty subtle, but we can see how the function being
called is hoisted out of the generated shim and is bound to always be
`Foo.prototype.bar`. This then uses the `Function.call` method to invoke that
function with `getObject(arg0)` as the receiver.

But wait, there's still a JS shim here even with `host_binding`! That's true,
and this is simply a fact of future WebAssembly proposals not being implemented
yet. The semantics, though, match the future [host bindings
proposal][host-bindings] because the method being called is determined exactly
once, and it's located on the prototype chain rather than being resolved at
runtime when the function is called.

## Interaction with future proposals

If you're curious to see how our JS shim will be eliminated entirely, let's take
a look at the generated bindings. We're starting off with this:

```js
const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
let varg1 = getStringFromWasm(arg1, arg2);
return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
}
```

... and once the [reference types proposal][reference-types] is implemented then
we won't need some of these pesky functions. That'll transform our generated JS
shim to look like:

```js
const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
let varg1 = getStringFromWasm(arg1, arg2);
return __wbg_bar_target.call(arg0, varg1);
}
```

Getting better! Next up we need the host bindings proposal. Note that the
proposal is undergoing some changes right now so it's tough to link to reference
documentation, but it suffices to say that it'll empower us with at least two
different features.

First, host bindings promises to provide the concept of "argument conversions".
The `arg1` and `arg2` values here are actually a pointer and a length to a utf-8
encoded string, and with host bindings we'll be able to annotate that this
import should take those two arguments and convert them to a JS string (that is,
the *host* should do this, the WebAssembly engine). Using that feature we can
futher trim this down to:

```js
const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, varg1) {
return __wbg_bar_target.call(arg0, varg1);
}
```

And finally, the second promise of the host bindings proposal is that we can
flag a function call to indicate the first argument is the `this` binding of the
function call. Today the `this` value of all called imported functions is
`undefined`, and this flag (configured with host bindings) will indicate the
first argument here is actually the `this`.

With that in mind we can further transform this to:

```js
export const __wbg_bar_a81456386e6b526f = Foo.prototype.bar;
```

and voila! We, with [reference types][reference-types] and [host
bindings][host-bindings], now have no JS shim at all necessary to call the
imported function!
22 changes: 10 additions & 12 deletions guide/src/reference/attributes/on-js-imports/structural.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# `structural`

> **Note**: As of [RFC 5] this attribute is the default for all imported
> functions. This attribute is largely ignored today and is only retained for
> backwards compatibility and learning purposes.
>
> The inverse of this attribute, [the `host_binding`
> attribute](host_binding.html) is more functionally interesting than
> `structural` (as `structural` is simply the default)
[RFC 5]: https://rustwasm.github.io/rfcs/005-structural-and-deref.html

The `structural` flag can be added to `method` annotations, indicating that the
method being accessed (or property with getters/setters) should be accessed in a
structural, duck-type-y fashion. Rather than walking the constructor's prototype
Expand Down Expand Up @@ -36,15 +46,3 @@ function quack(duck) {
duck.quack();
}
```

## Why don't we always use the `structural` behavior?

In theory, it is faster since the prototype chain doesn't need to be traversed
every time the method or property is accessed, but today's optimizing JIT
compilers are really good about eliminating that cost. The real reason is to be
future compatible with the ["host bindings" proposal][host-bindings], which
requires that there be no JavaScript shim between the caller and the native host
function. In this scenario, the properties and methods *must* be resolved before
the wasm is instantiated.

[host-bindings]: https://github.com/WebAssembly/host-bindings/blob/master/proposals/host-bindings/Overview.md
25 changes: 25 additions & 0 deletions tests/wasm/host_binding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const assert = require('assert');

exports.MyType = class {
static foo(y) {
assert.equal(y, 'x');
return y + 'y';
}

constructor(x) {
assert.equal(x, 2);
this._a = 1;
}

bar(x) {
assert.equal(x, true);
return 3.2;
}

get a() {
return this._a;
}
set a(v) {
this._a = v;
}
};
40 changes: 40 additions & 0 deletions tests/wasm/host_binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;

#[wasm_bindgen]
extern "C" {
type Math;
#[wasm_bindgen(static_method_of = Math, host_binding)]
fn log(f: f32) -> f32;
}

#[wasm_bindgen(module = "tests/wasm/host_binding.js")]
extern "C" {
type MyType;
#[wasm_bindgen(constructor, host_binding)]
fn new(x: u32) -> MyType;
#[wasm_bindgen(static_method_of = MyType, host_binding)]
fn foo(a: &str) -> String;
#[wasm_bindgen(method, host_binding)]
fn bar(this: &MyType, arg: bool) -> f32;

#[wasm_bindgen(method, getter, host_binding)]
fn a(this: &MyType) -> u32;
#[wasm_bindgen(method, setter, host_binding)]
fn set_a(this: &MyType, a: u32);
}

#[wasm_bindgen_test]
fn simple() {
assert_eq!(Math::log(1.0), 0.0);
}

#[wasm_bindgen_test]
fn classes() {
assert_eq!(MyType::foo("x"), "xy");
let x = MyType::new(2);
assert_eq!(x.bar(true), 3.2);
assert_eq!(x.a(), 1);
x.set_a(3);
assert_eq!(x.a(), 3);
}
6 changes: 3 additions & 3 deletions tests/wasm/import_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ extern "C" {
fn switch_methods_a();
fn switch_methods_b();
type SwitchMethods;
#[wasm_bindgen(constructor)]
#[wasm_bindgen(constructor, host_binding)]
fn new() -> SwitchMethods;
#[wasm_bindgen(js_namespace = SwitchMethods)]
#[wasm_bindgen(js_namespace = SwitchMethods, host_binding)]
fn a();
fn switch_methods_called() -> bool;
#[wasm_bindgen(method)]
#[wasm_bindgen(method, host_binding)]
fn b(this: &SwitchMethods);

type Properties;
Expand Down
1 change: 1 addition & 0 deletions tests/wasm/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod comments;
pub mod duplicate_deps;
pub mod duplicates;
pub mod enums;
pub mod host_binding;
pub mod import_class;
pub mod imports;
pub mod js_objects;
Expand Down

0 comments on commit 4c42aba

Please sign in to comment.