Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip generating JS import shims when unnecessary #1654

Merged
merged 7 commits into from
Jul 15, 2019

Conversation

fitzgen
Copy link
Member

@fitzgen fitzgen commented Jul 10, 2019

After this change, any import that only takes and returns ABI-safe numbers (signed integers less than 64 bits and unrestricted floating point numbers) will be a direct import, and will not have a little JS shim in the middle.

We don't have a great mechanism for testing the generated bindings' contents -- as opposed to its behavior -- but I manually verified that everything here does the Right Thing and doesn't have a JS shim:

#[wasm_bindgen]
extern "C" {
    fn trivial();

    fn incoming_i32() -> i32;
    fn incoming_f32() -> f32;
    fn incoming_f64() -> f64;

    fn outgoing_i32(x: i32);
    fn outgoing_f32(y: f32);
    fn outgoing_f64(z: f64);

    fn many(x: i32, y: f32, z: f64) -> i32;
}

Furthermore, I verified that when our support for emitting native anyref is enabled, then we do not have a JS shim for the following import, but if it is disabled, then we do have a JS shim:

#[wasm_bindgen]
extern "C" {
    fn works_when_anyref_support_is_enabled(v: JsValue) -> JsValue;
}

Fixes #1636.

@alexcrichton
Copy link
Contributor

Nice!

This looks great to me, I'm gonna poke around with it a bit more locally and see what's what tomorrow.

Copy link
Contributor

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok digging in some more today. So the way that this previously worked was a bit of a hack, but did have some good properties to it I think. Previously we'd actually do the whole rigamarole to generate the JS shim, and then if we ended up not actually emitting anything shim-like it was automatically considered candidate for this optimization. That meant that it wasn't possible to get out of sync with what the backend generated, but it did make the check pretty wonky.

The main thing that I did with this PR was execute WASM_BINDGEN_ANYREF=1 WASM_BINDGEN_NO_DEBUG=1 cargo test --target wasm32-unknown-unknown --test wasm and then look for functions in the wasm-bindgen-test.js file which shouldn't have a shim. I don't really have a preference as to how we structure this code so I'm fine fixing these however!

Some cases I found though which still had a shim but didn't need it were:

I think that was what I found? It looks like it's basically a few type conversions (like incoming boolean -> i32, or incoming unsigned long -> i32) missing and maybe something else to pattern match on for the namespace thing?

})?;
let js = format!("function{}", js);
let js = match import {
AuxImport::Value(AuxValue::Bare(js))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this may not be quite right in terms of matching where technically any value imported is a candidate for not requiring any glue. For example the other values, getters/setters, can all be hooked up directly in theory. They today, however, all require a method invocation style and will fail the Static check below very quickly, so it effectively ends up being the same.

Perhaps though it'd be good to refactor this in a forward-facing way where we match AuxImport::Value(v) here and then have shared code between way below in here about how to turn v into a JS snippet?

@alexcrichton
Copy link
Contributor

Oh one other thought I had while reviewing this, in terms of tests it might be nift to assert that a JS shim is "meaningful"? Could we perhaps have an assert of some form when we generate a JS shim that it's somewhat nontrivial? That way we don't necessarily need to test this directly but we could rely on if that assert trips we forgot to handle something, but if the assert always passes then we're good to go. That would also help us cover future cases such as when anyref is on/off or WebIDL bindings get more fleshed out.

@fitzgen
Copy link
Member Author

fitzgen commented Jul 11, 2019

functions returning boolean

For incoming bools we can rely on the auto conversion turning this into 0 or 1.

For outgoing bools that are actually going into a a Web IDL function, we can also rely on the auto conversion from a numbre to bool.

For outgoing bools that are going into random user JS functions, there won't be any auto conversions going on, and so we could pass the number 0 or 1 to functions that might be doing typeof reflection and could branch differently based on 1 vs true. Although, I suppose we already suffer from this today... I think ideally we would emit glue code to do arg != 0 to convert from the i32 into a boolean.

functions returning u32

I'm not sure if incoming unsigned numbers are safe. They get converted to signed integers by the JS Wasm interface, and although positive two's complement has the same representation as unsigned, I'm not sure about the edge cases here. Although, again, if our current behavior doesn't do anything, then I guess at most we are preserving bugs...

functions taking u8

I guess we are assuming that the wasm-level i32 won't be outside the range of the u8 so this should be safe. I think I need to update some wasm-webidl-bindings methods to assume that outgoing wasm values are in the range of the Web IDL type that they claim to match.

This error case is for an invalid free function, not an invalid constructor.
After this change, any import that only takes and returns ABI-safe numbers (signed
integers less than 64 bits and unrestricted floating point numbers) will be a
direct import, and will not have a little JS shim in the middle.

We don't have a great mechanism for testing the generated bindings' contents --
as opposed to its behavior -- but I manually verified that everything here does
the Right Thing and doesn't have a JS shim:

```rust
\#[wasm_bindgen]
extern "C" {
    fn trivial();

    fn incoming_i32() -> i32;
    fn incoming_f32() -> f32;
    fn incoming_f64() -> f64;

    fn outgoing_i32(x: i32);
    fn outgoing_f32(y: f32);
    fn outgoing_f64(z: f64);

    fn many(x: i32, y: f32, z: f64) -> i32;
}
```

Furthermore, I verified that when our support for emitting native `anyref` is
enabled, then we do not have a JS shim for the following import, but if it is
disabled, then we do have a JS shim:

```rust
\#[wasm_bindgen]
extern "C" {
    fn works_when_anyref_support_is_enabled(v: JsValue) -> JsValue;
}
```

Fixes rustwasm#1636.
@fitzgen
Copy link
Member Author

fitzgen commented Jul 11, 2019

For outgoing bools that are going into random user JS functions, there won't be any auto conversions going on, and so we could pass the number 0 or 1 to functions that might be doing typeof reflection and could branch differently based on 1 vs true. Although, I suppose we already suffer from this today... I think ideally we would emit glue code to do arg != 0 to convert from the i32 into a boolean.

Heh... yeah actually this breaks if we don't have a shim that does arg0 !== 0:

---- wasm::no_shims::no_shims output ----
    error output:
        wasm-bindgen: imported JS function that was not marked as `catch` threw an error: assert_eq failed: 1 != true
        
        Stack:
        Error: assert_eq failed: 1 != true
            at assert_eq (/home/fitzgen/wasm-bindgen/target/wasm32-unknown-unknown/wbg-tmp/snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js:4:19)
            at module.exports.outgoing_bool (/home/fitzgen/wasm-bindgen/target/wasm32-unknown-unknown/wbg-tmp/snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js:35:51)
            at wasm::no_shims::outgoing_bool::h2a763567395adc3a (wasm-function[5492]:74)
            at wasm::no_shims::no_shims::hdbc99c282a7dac93 (wasm-function[349]:3016)

@alexcrichton
Copy link
Contributor

Oh sorry so in general the functions I gisted above all had a shim in JS but the shim didn't actually do anything, so I figure that we just want to optimize them to ensure that the shim is gone. That's also where I think it'd help to have an assert that if we generate a shim it's nontrivial in one way or another.

For booleans for example yeah for outgoing booleans we need a shim, but for incoming booleans I think it's a missing statement here which catches booleans going to i32.

Although, again, if our current behavior doesn't do anything, then I guess at most we are preserving bugs...

I think this actually works out for incoming u32. It looks like the coercion to i32 in wasm recognizes that it's big and positive so it fits all the bits. I made a roundtrip function in wasm (takes i32 returns the same) and passing in 4294967295 spits out -1 on the other end. (according to JS).

In that sense I think we're actually covered for u32 here and I don't think there's existing bugs? (not that I'm confident)

@alexcrichton
Copy link
Contributor

If it's possible I think it's still worthwhile to have a default always-on assert for all generated bindings that something is nontrivial about the JS shim that we generate. That I think is a much stronger assert than an annotation b/c we're still playing whack-a-mole with the annotation. I think it's of course though fine to add this stronger assert later.

@fitzgen
Copy link
Member Author

fitzgen commented Jul 12, 2019

If it's possible I think it's still worthwhile to have a default always-on assert for all generated bindings that something is nontrivial about the JS shim that we generate. That I think is a much stronger assert than an annotation b/c we're still playing whack-a-mole with the annotation.

It is stronger, yes.

However, I am a bit less confident in our ability to live up to its guarantees. By adding an assertion, we are explicitly saying that we expect these incoming and outgoing types not to require glue and that we are confident that we can guarantee that forever. I'm less certain about catching every single case that might happen not to require glue for every combination of bindings configurations and types that we support.

Additionally, implementing the check seems like it would have to be fairly ad-hoc unless we change the way the builders work a bit, and I'm not certain that the assertion itself wouldn't be super buggy.

@alexcrichton
Copy link
Contributor

Heh good points as well :)

In any case let's stick with a tag for now, and maybe add the tag to a more exhaustive set of types going in/out as well?

@fitzgen
Copy link
Member Author

fitzgen commented Jul 12, 2019

In any case let's stick with a tag for now, and maybe add the tag to a more exhaustive set of types going in/out as well?

yeah working on it now

@fitzgen
Copy link
Member Author

fitzgen commented Jul 12, 2019

Ok, I think I got everything -- want to take another look @alexcrichton?

@alexcrichton
Copy link
Contributor

r=me on everything here, but I think this line may be wrong? While I think 0/1 have the same "truthy value" as true and false I think we still want a wrapper to generate honest-to-god boolean values?

@fitzgen
Copy link
Member Author

fitzgen commented Jul 15, 2019

r=me on everything here, but I think this line may be wrong?

Fixed! That was supposed to only be for incoming expressions >_<

@fitzgen fitzgen merged commit a48a0ae into rustwasm:master Jul 15, 2019
@fitzgen fitzgen deleted the no-import-shims branch July 15, 2019 17:13
alexcrichton added a commit to alexcrichton/wasm-bindgen that referenced this pull request Jul 31, 2019
Support was previously (re-)added in rustwasm#1654 for importing direct JS
values into a WebAssembly module by completely skipping JS shim
generation. This commit takes that PR one step further by *also*
embedding a direct import in the wasm file, where supported. The wasm
file currently largely just imports from the JS shim file that we
generate, but this allows it to directly improt from ES modules where
supported and where possible. Note that like rustwasm#1654 this only happens
when the function signature doesn't actually require any conversions to
happen in JS (such as handling closures).

For imports from ES modules, local snippets, or inline JS they'll all
have their import directives directly embedded into the final
WebAssembly binary without any shims necessary to hook it all up. For
imports from the global namespace or possibly vendor-prefixed items
these still unconditionally require an import shim to be generated
because there's no way to describe that import in an ES-friendly way
(yet).

There's a few consequences of this commit which are also worth noting:

* The logic in `wasm-bindgen` where it gracefully handles (to some
  degree) not-defined items now only is guaranteed to be applied to the
  global namespace. If you import from a module, it'll be an
  instantiation time error rather than today's runtime error when the
  import is called.

* Handling imports in the wasm module not registered with
  `#[wasm_bindgen]` has become more strict. Previously these imports
  were basically ignored, leaving them up for interpretation depending
  on the output format. The changes for each output target are:

  * `bundler` - not much has changed here. Previously these ignored
    imports would have been treated as ES module imports, and after this
    commit there might just be some more of these imports for bundlers
    to resolve.

  * `web` - previously the ignored imports would likely cause
    instantiation failures because the import object never actually
    included a binding for other imports. After this commit though the
    JS glue which instantiates the module now interprets all
    unrecognized wasm module imports as ES module imports, emitting an
    `import` directive. This matches what we want for the direct import
    functionality, and is also largely what we want for modules in
    general.

  * `nodejs` - previously ignored imports were handled in the
    translation shim for Node to generate `require` statements, so they
    were actually "correctly handled" sort of with module imports. The
    handling of this hasn't changed, and reflects what we want for
    direct imports of values where loading a wasm module in Node ends up
    translating the module field of each import to a `require`.

  * `no-modules` - this is very similar to the `web` target where
    previously this didn't really work one way or the other because we'd
    never fill in more fields of the import object when instantiating
    the module. After this PR though this is a hard-error to have
    unrecognized imports from `#[wasm_bindgen]` with the `no-modules`
    output type, because we don't know how to handle the imports.

  Note that this touches on rustwasm#1584 and will likely break the current use
  case being mentioned there. I think though that this tightening up of
  how we handle imports is what we'll want in the long run where
  everything is interpreted as modules, and we'll need to figure out
  best how wasi fits into this.

This commit is unlikely to have any real major immediate effects. The
goal here is to continue to inch us towards a world where there's less
and less JS glue necessary and `wasm-bindgen` is just a polyfill for web
standards that otherwise all already exist.

Also note that there's no explicitly added tests for this since this is
largely just a refactoring of an internal implementation detail of
`wasm-bindgen`, but the main `wasm` test suite has many instances of
this path being taken, for example having imports like:

    (import "tests/wasm/duplicates_a.js" "foo" (func $__wbg_foo_969c253238f136f0 (type 1)))
    (import "tests/wasm/duplicates_b.js" "foo" (func $__wbg_foo_027958cb2e320a94 (type 0)))
    (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "trivial" (func $__wbg_trivial_75e27c84882af23b (type 1)))
    (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "incoming_bool" (func $__wbg_incomingbool_0f2d9f55f73a256f (type 0)))
alexcrichton added a commit to alexcrichton/wasm-bindgen that referenced this pull request Jul 31, 2019
Support was previously (re-)added in rustwasm#1654 for importing direct JS
values into a WebAssembly module by completely skipping JS shim
generation. This commit takes that PR one step further by *also*
embedding a direct import in the wasm file, where supported. The wasm
file currently largely just imports from the JS shim file that we
generate, but this allows it to directly improt from ES modules where
supported and where possible. Note that like rustwasm#1654 this only happens
when the function signature doesn't actually require any conversions to
happen in JS (such as handling closures).

For imports from ES modules, local snippets, or inline JS they'll all
have their import directives directly embedded into the final
WebAssembly binary without any shims necessary to hook it all up. For
imports from the global namespace or possibly vendor-prefixed items
these still unconditionally require an import shim to be generated
because there's no way to describe that import in an ES-friendly way
(yet).

There's a few consequences of this commit which are also worth noting:

* The logic in `wasm-bindgen` where it gracefully handles (to some
  degree) not-defined items now only is guaranteed to be applied to the
  global namespace. If you import from a module, it'll be an
  instantiation time error rather than today's runtime error when the
  import is called.

* Handling imports in the wasm module not registered with
  `#[wasm_bindgen]` has become more strict. Previously these imports
  were basically ignored, leaving them up for interpretation depending
  on the output format. The changes for each output target are:

  * `bundler` - not much has changed here. Previously these ignored
    imports would have been treated as ES module imports, and after this
    commit there might just be some more of these imports for bundlers
    to resolve.

  * `web` - previously the ignored imports would likely cause
    instantiation failures because the import object never actually
    included a binding for other imports. After this commit though the
    JS glue which instantiates the module now interprets all
    unrecognized wasm module imports as ES module imports, emitting an
    `import` directive. This matches what we want for the direct import
    functionality, and is also largely what we want for modules in
    general.

  * `nodejs` - previously ignored imports were handled in the
    translation shim for Node to generate `require` statements, so they
    were actually "correctly handled" sort of with module imports. The
    handling of this hasn't changed, and reflects what we want for
    direct imports of values where loading a wasm module in Node ends up
    translating the module field of each import to a `require`.

  * `no-modules` - this is very similar to the `web` target where
    previously this didn't really work one way or the other because we'd
    never fill in more fields of the import object when instantiating
    the module. After this PR though this is a hard-error to have
    unrecognized imports from `#[wasm_bindgen]` with the `no-modules`
    output type, because we don't know how to handle the imports.

  Note that this touches on rustwasm#1584 and will likely break the current use
  case being mentioned there. I think though that this tightening up of
  how we handle imports is what we'll want in the long run where
  everything is interpreted as modules, and we'll need to figure out
  best how wasi fits into this.

This commit is unlikely to have any real major immediate effects. The
goal here is to continue to inch us towards a world where there's less
and less JS glue necessary and `wasm-bindgen` is just a polyfill for web
standards that otherwise all already exist.

Also note that there's no explicitly added tests for this since this is
largely just a refactoring of an internal implementation detail of
`wasm-bindgen`, but the main `wasm` test suite has many instances of
this path being taken, for example having imports like:

    (import "tests/wasm/duplicates_a.js" "foo" (func $__wbg_foo_969c253238f136f0 (type 1)))
    (import "tests/wasm/duplicates_b.js" "foo" (func $__wbg_foo_027958cb2e320a94 (type 0)))
    (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "trivial" (func $__wbg_trivial_75e27c84882af23b (type 1)))
    (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "incoming_bool" (func $__wbg_incomingbool_0f2d9f55f73a256f (type 0)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Restore optimization to import functions directly
2 participants