Skip to content

Compile Binaryen to WebAssembly - #2503

Merged
kripken merged 11 commits into
WebAssembly:masterfrom
dcodeIO:binaryenwasm
Dec 19, 2019
Merged

kripken merged 11 commits into
WebAssembly:masterfrom
dcodeIO:binaryenwasm

Conversation

@dcodeIO

@dcodeIO dcodeIO commented Dec 5, 2019 •

Copy link
Copy Markdown
Contributor

This PR enables compiling Binaryen to WebAssembly when building binaryen.js. Since WebAssembly is best compiled and instantiated asynchronously in browsers, it also adds a new mechanism to tell if respectively when the module is ready by means of one of the following:

// Using a promise
const binaryen = require("binaryen");
binaryen.ready.then(() => {
  ... use normally ...
});
// Using await
const binaryen = require("binaryen");
(async () => {
  await binaryen.ready;
  ... use normally ...
})();
// Where top-level await is available
const binaryen = await require("binaryen").ready;
... use normally ...

One can also tell if Binaryen is already ready (for example when assuming it in follow-up code) by:

if (/* we already know that */ binaryen.isReady) {
  ... use normally ...
} else {
  throw Error("Binaryen is supposed to be ready here but isn't");
}

The JS test cases have been updated accordingly by wrapping everything in a test function and invoking it once ready. Documentation will have to be updated as well to cover this of course. New file size is about 2.5mb, even though the Wasm becomes inlined into the JS file which makes distribution across different environments a lot easier.

Thoughts? :)

Supersedes and thus fixes #1381
With .ready it also fixes #2452

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable to me. I actually didn't know we had so much JS-specific stuff in binaryen. I wonder if keep this upstream is the best place for it? Maybe a separate binaryen-js repo would make sense at some point.

Comment thread src/js/binaryen.js-post.js Outdated
Module['WidenHighSVecI16x8ToVecI32x4'] = Module['_BinaryenWidenHighSVecI16x8ToVecI32x4']();
Module['WidenLowUVecI16x8ToVecI32x4'] = Module['_BinaryenWidenLowUVecI16x8ToVecI32x4']();
Module['WidenHighUVecI16x8ToVecI32x4'] = Module['_BinaryenWidenHighUVecI16x8ToVecI32x4']();
Module['SwizzleVec8x16'] = Module['_BinaryenSwizzleVec8x16']();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this all maintained by hand? It seems pretty fragile? Can this not be done by looking at all the keys in Module that start with _Binaryen?

@dcodeIO dcodeIO Dec 5, 2019 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's all maintained by hand. I believe one reason why it's hard to automate is that not every exported function of the form _Binaryen... can be called like that, for example some of these return the respective constants while others like _BinaryenModuleCreate create a module. But might be possible to simplify by keeping a list of the good ones, doing something like

["WidenHighSVecI16x8ToVecI32x4", ...].forEach(name => Module[name] = Module['_Binaryen' + name]());

or renaming the good ones to a common scheme, like _BinaryenOp... so JS can decide based on that?

Comment thread test/binaryen.js/fatal.js Outdated
wasm.addFunctionType("vI", Binaryen.i32, []);
// It will cause a fatal error to try to add the same name a second time
wasm.addFunctionType("vI", Binaryen.i32, []);
function test(Binaryen) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the Binaryen argument here different to the Binaryen in the global scope? If not then maybe we don't need it? Or at least maybe call it something else so its more obvious what it doesn't shadow the global one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's the same as the global one, yeah. Mostly a case of "why not" if we'd ever want to test multiple variants, like a fastcomp and an upstream one, but can as well remove the arguments since we don't do that.

@dcodeIO

dcodeIO commented Dec 6, 2019

Copy link
Copy Markdown
Contributor Author

Last commit makes it so that the constants are created from arrays, making this a bit less error prone and slightly less code. Tests don't shadow Binaryen anymore as well and I added a line to .gitattributes to make sure that binaryen.js fixtures always use LF line endings (on Windows) so the diff doesn't fail there.

@dcodeIO

dcodeIO commented Dec 6, 2019

Copy link
Copy Markdown
Contributor Author

Appears this already found something that was wrong before

Module['GeVecF32x4'] = Module['_BinaryenGeVecF32x4']();
Module['EqVecF64x2'] = Module['_BinaryenGeVecF32x4'](); // here

@dcodeIO

dcodeIO commented Dec 6, 2019

Copy link
Copy Markdown
Contributor Author

I wonder if keep this upstream is the best place for it?

One aspect is that running the JS tests also tests the wrapped C-API, so there are benefits in keeping the JS tests upstream. Afaik some tests even exist in JS only. Not ideal ofc, but still better than not having them.

@kripken kripken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice, thanks!

I don't remember what the general best practices are here, though - does wabt use the .ready.then approach, and other projects?

Does this make us lose support for building to JS and not to wasm? It would be good to still support both.

@dcodeIO

dcodeIO commented Dec 9, 2019

Copy link
Copy Markdown
Contributor Author

I don't remember what the general best practices are here, though - does wabt use the .ready.then approach, and other projects?

Hmm, I thought Wabt has a .ready promise, but from a quick check it seems it doesn't (anymore) and uses Emscripten's MODULARIZE=1 now without any extra code.

const wabt = require("wabt");
wabt().then(function(wabt) { ... });

This has a major problem though in that

(async () => {
  let w = await wabt();
})();

goes into an endless loop since the awaited object is promise-like again (here: the same object with a .then method), so the engine tries to resolve the result over and over, never returning.

I am not aware of general best practices here as something like this is uncommon in JS-space, and if there aren't any I suggest leading by example with a .ready promise. @MaxGraey, do you know if there are any JS libraries that depend on resolving a promise before becoming functional?

Does this make us lose support for building to JS and not to wasm? It would be good to still support both.

Yeah, this drops "compiling to just JS". Can try to make two build targets with binaryen_js building to JS and binaryen_wasm building to Wasm.

@kripken

kripken commented Dec 10, 2019

Copy link
Copy Markdown
Member

I'm not necessarily opposed to dropping the JS target. But one thing that's worried me is performance, specifically, binaryen uses exceptions, and with a JS build the VM can inline the invokes, but in wasm we end up with uninlineable wasm->js->wasm calls. Worth doing some benchmarking here to see if the wasm build is actually faster on a large input.

@dcodeIO

dcodeIO commented Dec 11, 2019

Copy link
Copy Markdown
Contributor Author

With the changes there are two targets binaryen_wasm and binaryen_js now that both output bin/binaryen.js, and the Travis emcc test now checks both. Feels a bit strange that both targets emit the same file but, considering that we'll pick and ship a default one anyway with the other being relevant for testing or specific use cases only, I think it's fine?

@kripken

kripken commented Dec 11, 2019

Copy link
Copy Markdown
Member

Makes sense to me. Overall this looks good! Last thing, please update the Changelog, as this is a large breaking change.

@MaxGraey

MaxGraey commented Dec 11, 2019 •

Copy link
Copy Markdown
Contributor

I guess Binaryen.ready approach will not very useful with esm wasm modules

Comment thread CHANGELOG.md
- Binaryen.js can now be compiled to Wasm using the `binaryen_wasm` target.
Unlike the JS variant, the Wasm variant requires asynchronously awaiting the
Wasm blob's instantiation and initialization before being usable, using the
`binaryen.ready` promise, e.g. `binaryen.ready.then(() => ...)`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we export as Binaryen (with a capital B)?

@dcodeIO dcodeIO Dec 19, 2019 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, yeah, the default is Binaryen when loading binaryen.js in the browser, that is just including the script. For the npm package I'm using const binaryen = require("binaryen"); simply because that's the package name and somewhat standard to do. Perhaps it makes sense to unify both to lower case to match the package name?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh, I see. Yeah, let's maybe standardize it on the lowercase name then. But maybe in a separate PR (with a changelog entry)?

So I think this is ready to land, except for the merge conflict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good! Conflict should be resolved if CI doesn't state otherwise.

@dcodeIO dcodeIO mentioned this pull request Dec 19, 2019
2 tasks done
@kripken
kripken merged commit 02e6ba2 into WebAssembly:master Dec 19, 2019
kripken pushed a commit that referenced this pull request Dec 19, 2019
This PR enables compiling Binaryen to WebAssembly when building binaryen.js. Since WebAssembly is best compiled and instantiated asynchronously in browsers, it also adds a new mechanism to tell if respectively when the module is ready by means of one of the following:

// Using a promise
const binaryen = require("binaryen");
binaryen.ready.then(() => {
  ... use normally ...
});

// Using await
const binaryen = require("binaryen");
(async () => {
  await binaryen.ready;
  ... use normally ...
})();

// Where top-level await is available
const binaryen = await require("binaryen").ready;
... use normally ...

One can also tell if Binaryen is already ready (for example when assuming it in follow-up code) by:

if (/* we already know that */ binaryen.isReady) {
  ... use normally ...
} else {
  throw Error("Binaryen is supposed to be ready here but isn't");
}

The JS test cases have been updated accordingly by wrapping everything in a test function and invoking it once ready. Documentation will have to be updated as well to cover this of course. New file size is about 2.5mb, even though the Wasm becomes inlined into the JS file which makes distribution across different environments a lot easier.

Also makes building binaryen (to either js or wasm) emit binaryen.js, and not binaryen_js.js etc.

Supersedes and thus fixes #1381
With .ready it also fixes #2452
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.

Add a .ready() API for binaryen.js?

4 participants