mirror of
https://github.com/rustwasm/wasm-bindgen.git
synced 2024-11-27 19:28:56 +03:00
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
This commit is contained in:
parent
6093fd29d1
commit
4c42aba007
@ -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 {
|
||||
@ -229,6 +237,7 @@ pub enum BindgenAttr {
|
||||
IndexingSetter,
|
||||
IndexingDeleter,
|
||||
Structural,
|
||||
HostBinding,
|
||||
Readonly,
|
||||
JsName(String, Span),
|
||||
JsClass(String),
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
|
@ -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)
|
||||
|
139
guide/src/reference/attributes/on-js-imports/host_binding.md
Normal file
139
guide/src/reference/attributes/on-js-imports/host_binding.md
Normal file
@ -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!
|
@ -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
|
||||
@ -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
tests/wasm/host_binding.js
Normal file
25
tests/wasm/host_binding.js
Normal file
@ -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
tests/wasm/host_binding.rs
Normal file
40
tests/wasm/host_binding.rs
Normal file
@ -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);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user