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

Promise support #78

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft

Promise support #78

wants to merge 3 commits into from

Conversation

surma
Copy link
Collaborator

@surma surma commented Apr 28, 2021

I’m publishing this Draft PR so folks can experiment with this. @torch2424 I’d love to hear what you think about this in general. I’m sure the code needs some cleaning up, but I’m pretty excited about having Web API Integration work almost out-of-the-box.


This PR gives as-bind the ability to handle async functions & promises in imported functions. This allows AssemblyScript modules to integrate with asynchronous web APIs like fetch(), idb, createImageBitmap, WebUSB, WebBluetooth, etc. You name it!

Example

// main. ts
declare function fetch(s: string): ArrayBuffer;
declare function log(s: string): void;

export function run(): void {
  const buffer = fetch("/package.json"); // ⬅️ 🎉🎉🎉🎉
 
  // Quick ad-hoc string decoding
  const chars = Uint8Array.wrap(buffer);
  let str = "";
  for (let i = 0; i < chars.length; i++) {
    str += String.fromCharCode(chars[i]);
  }
  log(str);
}
import { instantiate } from "as-bind";

import wasmUrl from "asc:./main.ts";

const instance = await instantiate(fetch(wasmUrl), {
  main: {
    log: (v) => (document.all.log.innerHTML += v + "\n"),
    // This returns a Promise<ArrayBuffer>. as-bind utilizes asyncify
    // to make the result appear synchronous to AS.
    fetch: (v) => fetch(v).then((r) => r.arrayBuffer()),
  },
});
instance.exports.run();
Screenshot Screenshot 2021-04-28 at 18 30 34

Here’s a gist with a working build system.

(In the future, you could even use externref to hand Response, allowing you to import fetch()` directly.)

How to use it

The heavy lifting is done under the hood by [Asyncify]. All you need to do is add --runPasses="asyncify" to your asc invocation, all the rest happens inside as-bind automatically.

If you want to play around with this, here are step-by-step instructions:

  1. Clone this repo and check out this branch promise-support
  2. run npm install && npm run build
  3. Clone the gist to somewhere else (or take any project that uses as-bind v0.7+)
  4. Run npm link /path/to/as-bind/clone/from/step/1
  5. Add --runPasses="asyncify" to your asc invocation
  6. ???
  7. Profit!

How does it work

WebAssembly is synchronous, and functions that are imported by your Wasm from the host environent (i.e. from JS) are expected to return their value synchronously. But the web is asynchronous. Making something synchronous look asynchronous is easy, but the other way is near impossible.

Asyncify is a tool (more specifically: a binaryen pass) that effectively allows you to “pause” WebAssembly. Through this, you can make make JS that’s asynchronous look synchronous to WebAssembly. The trick is to pause Wasm when an imported function returns a promise, wait until the promise has resolved and then unpause to continue with the resolved value. From the perspective of WebAssembly, it’s as if the pause never happened.

To facilitate the pausing and later resuming, Asyncify stores the current stack of the Wasm VM to memory. To avoid any corruption, as-bind allocates a block of memory via the runtime and prevents it from getting GC’d. How much memory is actually needed, depends on the number of function parameters and function calls you have on your stack. By default, as-bind reserves 8KiB per paused Wasm call. If you to change that number, add this just after instantiation: instance.asyncifyStorageSize = 8 * 1024;.

What does it not do

This does not add support for async/await to AssemblyScript itself, nor is it a full integration of AS into JavaScript’s event loop.

Noteworthy limitations

Asyncify has a small performance and binary size impact. I have not seen it become a problem, but it’s something to be aware of. Because Asyncify rewrites your Wasm binary, it will probably break source maps. Also, Asyncify currently can’t handle externref et al.

Copy link
Owner

@torch2424 torch2424 left a comment

Choose a reason for hiding this comment

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

Woooaaahhhh! Dude this is rad! 😄 Yeah this has a huge 👍 from me. I think it would be stoked to have asyncify support, and this API looks super clean (and in such a small amount of code!)

Let me know if you need help with anything! Either way, I think this would be dope 😄

@StEvUgnIn
Copy link

I'm not sure to understand. AsBind.instantiate(...) already returns a promise

@surma
Copy link
Collaborator Author

surma commented Aug 26, 2021

@StEvUgnIn Take a look at the code samples. The imported functions can return promises with this PR.

@Upperfoot
Copy link

@surma This is perfect, just wondering if we can get this pulled into master?

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.

4 participants