Write low-level WebAssembly, from JavaScript
wasmati is a TS library that lets you create Wasm modules by writing out their instructions.
npm i wasmati
// example.ts
import { i64, func, Module } from "wasmati";
const myMultiply = func({ in: [i64, i64], out: [i64] }, ([x, y]) => {
i64.mul(x, y);
});
let module = Module({ exports: { myMultiply } });
let { instance } = await module.instantiate();
let result = instance.exports.myMultiply(5n, 20n);
console.log({ result });
$ node --experimental-strip-types example.ts
{ result: 100n }
Works in all modern browsers, node
and deno
Parity with WebAssembly. The API directly corresponds to Wasm opcodes, like i32.add
etc. All opcodes and language features of the latest WebAssembly spec (2.0) are supported.
In addition, wasmati supports the following extensions which are not part of the spec at the time of writing:
Readability. Wasm code looks imperative - like writing WAT by hand, just with better DX:
const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => {
local.get(x);
local.get(y);
i32.add();
i32.const(2);
i32.shl();
call(otherFunction);
});
local.get
and i32.const
const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => {
i32.add(x, y); // local.get(x), local.get(y) are filled in
i32.shl($, 2); // $ is the top of the stack; i32.const(2) is filled in
call(otherFunction);
});
// or also
const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => {
let z = i32.add(x, y);
call(otherFunction, [i32.shl(z, 2)]);
});
const myFunction = func(
{ in: [i32, i32], locals: [i64], out: [i32] },
([x, y], [u]) => {
i32.add(x, u); // type error: Type '"i64"' is not assignable to type '"i32"'.
}
);
Error: i32.add: Expected i32 on the stack, got i64.
...
at file:///home/gregor/code/wasmati/examples/example.ts:16:9
let mem = memory({ min: 10 });
let module = Module({ exports: { myFunction, mem } });
let instance = await module.instantiate();
func
definitions:instance.exports.myFunction;
// ^ (arg_0: number, arg_1: number) => number
const consoleLog = importFunc({ in: [i32], out: [] }, (x) =>
console.log("logging from wasm:", x)
);
const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => {
call(consoleLog, [x]);
i32.add(x, y);
});
module.toBytes()
, Module.fromBytes(bytes)
PRs welcome!
Module
, and compiles it to a file which doesn't depend on wasmati at runtime. Instead, it hard-codes the Wasm bytecode as base64 string, correctly imports all dependencies (imports) for the instantiation like the original file did, instantiates the module (top-level await) and exports the module's exports.// example.ts
let module = Module({ exports: { myFunction, mem } });
export { module as default };
import { myFunction } from "./example.wasm.js"; // example.wasm.js does not depend on wasmati at runtime
Experimental Wasm opcodes. We want to support opcodes from recently standardized or in-progress feature proposals (like this one) which haven't yet made it to the spec. The eventual goal is to support proposals as soon as they are implemented in at least one JS engine.
Custom module sections. We want to support creation and parsing of "custom sections" like the name section