diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 23dc483fec83..f244a9cd8a2a 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -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)> 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, diff --git a/examples/add/src/lib.rs b/examples/add/src/lib.rs index d8c7633a27e8..3aaff07eb5fb 100644 --- a/examples/add/src/lib.rs +++ b/examples/add/src/lib.rs @@ -2,6 +2,18 @@ extern crate wasm_bindgen; use wasm_bindgen::prelude::*; +#[wasm_bindgen] +extern { + pub type Foo; + #[wasm_bindgen(method,host_binding)] + fn bar(this: &Foo, argument: &str) -> JsValue; +} + +#[wasm_bindgen] +pub fn wut(a: &Foo) { + a.bar("x"); +} + #[wasm_bindgen] pub fn add(a: u32, b: u32) -> u32 { a + b diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 47f9ec9b5238..2db0c17f4e63 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -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) diff --git a/guide/src/reference/attributes/on-js-imports/host_binding.md b/guide/src/reference/attributes/on-js-imports/host_binding.md new file mode 100644 index 000000000000..734bf48f7094 --- /dev/null +++ b/guide/src/reference/attributes/on-js-imports/host_binding.md @@ -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! diff --git a/guide/src/reference/attributes/on-js-imports/structural.md b/guide/src/reference/attributes/on-js-imports/structural.md index bdcfef601fde..4cc20807d1f8 100644 --- a/guide/src/reference/attributes/on-js-imports/structural.md +++ b/guide/src/reference/attributes/on-js-imports/structural.md @@ -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 diff --git a/tests/wasm/host_binding.js b/tests/wasm/host_binding.js new file mode 100644 index 000000000000..15b576d029b2 --- /dev/null +++ b/tests/wasm/host_binding.js @@ -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; + } +}; diff --git a/tests/wasm/host_binding.rs b/tests/wasm/host_binding.rs new file mode 100644 index 000000000000..951cfb867a09 --- /dev/null +++ b/tests/wasm/host_binding.rs @@ -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); +} diff --git a/tests/wasm/import_class.rs b/tests/wasm/import_class.rs index 8af90e0e9fe9..34a301e51a9b 100644 --- a/tests/wasm/import_class.rs +++ b/tests/wasm/import_class.rs @@ -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; diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs index 4ea2604d2e54..4c70f75e1988 100644 --- a/tests/wasm/main.rs +++ b/tests/wasm/main.rs @@ -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;