as-bind icon indicating copy to clipboard operation
as-bind copied to clipboard

Promise support

Open surma opened this issue 3 years ago • 3 comments

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.

surma avatar Apr 28 '21 21:04 surma

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

StEvUgnIn avatar Aug 23 '21 15:08 StEvUgnIn

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

surma avatar Aug 26 '21 13:08 surma

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

Upperfoot avatar Dec 21 '21 11:12 Upperfoot