@deislabs/wasm-linker-js
v0.2.1
Published
A simple WebAssembly linker
Downloads
12
Readme
wasm-linker-js
A simple WebAssembly Linker in JavaScript
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 [email protected] with any additional questions or comments.