@deislabs/wasm-linker-js
TypeScript icon, indicating that this package has built-in type declarations

0.2.1 • Public • Published

wasm-linker-js

A simple WebAssembly Linker in JavaScript

actions badge NPM version

This is an experimental JavaScript library that helps instantiating WebAssembly modules with imports by providing functionality to link JavaScript objects (functions, memories, globals) as imports, as well as automatically perform name based resolution for linking entire modules.

The API loosely follows the Wasmtime linker, (see the linker documentation), and it exposes asynchronous import functionality enabled by Binaryen and Asyncify.

Using the Linker

For more examples of using the Linker in both TypeScript and JavaScript, check the linker tests and the Node.js examples.

First, add the package to your project:

$ npm install @deislabs/wasm-linker-js

Note that in order to run the examples shown here, binaryen is also required (npm install binaryen), in order to show the text format of the WebAssembly modules. In real world scenarios that is not necessary, and the modules can be compiled from their binary representation without additional dependencies.

Defining a single import

Assuming we are trying to instantiate the module represented in its text format below (transformed to its binary representation using Binaryen), we can satisfy its import using the define method available on the linker:

const { Linker } = require("@deislabs/wasm-linker-js");
const { parseText } = require("binaryen");

const usingAdd = `
(module
    (import "calculator" "add" (func $calc_add (param i32 i32) (result i32)))
  
    (memory 1 1)
    (export "memory" (memory 0))
    (export "add" (func $add))

    (func $add (param i32) (param i32) (result i32)
        (return
            (call $calc_add
                (local.get 0)
                (local.get 1)
            )
        )
    )
)
`;

(async () => {
  var linker = new Linker();

  // The "usingAdd" module imports calculator.add.
  // We define it,  provide a JS implementation, then
  // instantiate it.
  linker.define("calculator", "add", (a, b) => a + b);
  var calc = await linker.instantiate(parseText(usingAdd).emitBinary());

  var result = calc.instance.exports.add(1, 2);
  console.log(result);
})();

Linking an entire module

If we have a compiled module that exports items (defined below in its text format and contained in the add constant) that our initial module needs to import, we can add it to the linker, then continue instantiating our module (defined above in its text format and contained in the usingAdd constant):

const { Linker } = require("@deislabs/wasm-linker-js");
const { parseText } = require("binaryen");

const add = `
(module
  (memory 1 1)
  (export "memory" (memory 0))
  (export "add" (func $add))
  
  (func $add (param i32) (param i32) (result i32)
      (return
          (i32.add
              (local.get 0)
              (local.get 1)
          )
      )
  )
)
`;

(async () => {
  var linker = new Linker();

  // The "usingAdd" module above imports calculator.add.
  // We link a module that exports the functionality
  // required, then instantiate the module that uses it.
  await linker.module(
    "calculator",
    new WebAssembly.Module(parseText(add).emitBinary())
  );
  var calc = await linker.instantiate(parseText(usingAdd).emitBinary());
  var result = calc.instance.exports.add(1, 2);
  console.log(result);
})();

Defining asynchronous imports

The current WebAssembly MVP does not have a way of waiting for the execution of asynchronous imports (see this issue). To enable this functionality, Binaryen has a pass that transforms a Wasm module and allows it to pause and resume by unwiding and rewinding the call stack. When enabled, this library can use the JavaScript wrapper of Asyncify and define asynchronous import functions for WebAssembly modules (note that the Asyncify pass must have been applied to the module before instantiating using the linker):

const { Linker } = require("@deislabs/wasm-linker-js");
const { parseText } = require("binaryen");

(async () => {
  var useAsyncify = true;
  var linker = new Linker(useAsyncify);

  // Notice how we define an asynchronous import, which
  // will wait for 1.5s before returning the result.
  var sleep = function (ms) {
    return new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
  };
  linker.define("calculator", "add", async (a, b) => {
    await sleep(1500);
    return a + b;
  });

  let bytes = parseText(usingAdd);

  // we perform the asyncify compiler pass from Binaryen
  bytes.runPasses(["asyncify"]);
  var calc = await linker.instantiate(bytes.emitBinary());

  var result = await calc.instance.exports.add(1, 2);
  console.log(result);
})();

Using the streaming APIs in the browser

For browsers that support the WebAssembly streaming APIs, the linker exposes two methods that can be used to efficiently instantiate modules from a streamed source: moduleStreaming, which instantiates a module and adds its exports to the linker's cache, and instantiateStreaming, which instantiates a module and returns its the WebAssemblyInstantiatedSource:

(async () => {
  var linker = new Linker();
  await linker.moduleStreaming("calculator", fetch("calculator.wasm"));
  var mod = await linker.instantiateStreaming(fetch("using_calculator.wasm"));
  console.log(mod.instance.exports.multiply(3, 4));
}

See the documentation for the browser streaming APIs for more information about instantiating modules from streamed sources.

The linker also allows adding an already instantiated module, through the instance method, and aliasing a module under a new name, through the alias method. Most public methods defined on the Linker have a correspondent in the Wasmtime Linker, and we try to keep the APIs similar.

Implementation notes and known issues

  • When defining multiple import items with the same name, the last one takes precedence (the existing items are replaced). This behavior could change in the future to add a configurable property defining whether import shadowing should be allowed.
  • When instantiating a linker with Asyncify enabled, all modules linked and instantiated with the linker will be instantiated using Asyncify's JavaScript wrapper. This behavior could change in the future to allow a per-instance (and by extension per module linked) setting for Asyncify. (this can be avoided through instantiating a module separately and adding it to the linker using the instance method).
  • There is a browser example in the examples/ directory in this repository. While functional, the implementation is far from ideal - the WebPack configuration for generating a browser-compatible library is not optimal (this should be changed to use ECMAScript modules).
  • This library is experimental, and the API is not stable. We welcome feedback on both the public API and the implementation of this library.

Contributing

This project welcomes contributions through the GitHub pull request process. Prerequisites to building the project:

  • Node.js
  • npm

To iterate on the project locally:

$ npm run build
$ npm test
$ npm run examples

Code of Conduct

This project has adopted the Microsoft Open Source Code of Conduct.

For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Readme

Keywords

Package Sidebar

Install

npm i @deislabs/wasm-linker-js

Weekly Downloads

0

Version

0.2.1

License

MIT

Unpacked Size

77 kB

Total Files

13

Last publish

Collaborators

  • squillace
  • radu-matei
  • itowlson
  • technosophos