Skip to content

zksecurity/wasmati

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
src
 
 
 
 
 
 
 
 
 
 

wasmati ๐Ÿš ย  npm version

Write low-level WebAssembly, from JavaScript

wasmati is a TS library that lets you create Wasm modules by writing out their instructions.

  • ๐Ÿฅท You want to create low-level, hand-optimized Wasm libraries? wasmati is the tool to do so effectively.
  • ๐Ÿš€ You want to sprinkle some Wasm in your JS app, to speed up critical parts? wasmati gives you a JS-native way to achieve that.
  • โš ๏ธ You want to compile Wasm modules from a high-level language, like Rust or C? wasmati is not for you.
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 });
$ ts-node-esm example.ts
{ result: 100n }

Features

  • 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.

  • 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);
});
  • Optional syntax sugar to reduce boilerplate assembly like 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)]);
});
  • Type-safe. Example: Local variables are typed; instructions know their input types:
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"'.
  }
);
  • Great debugging DX. Stack traces point to the exact line in your code where an invalid opcode is called:
Error: i32.add: Expected i32 on the stack, got i64.
    ...
    at file:///home/gregor/code/wasmati/examples/example.ts:16:9
  • Easy construction of modules. Just declare exports; dependencies and imports are collected for you. Nothing ends up in the module which isn't needed by any of its exports or its start function.
let mem = memory({ min: 10 });

let module = Module({ exports: { myFunction, mem } });
let instance = await module.instantiate();
  • Excellent type inference. Example: Exported function types are inferred from func definitions:
instance.exports.myFunction;
//                 ^ (arg_0: number, arg_1: number) => number
  • Atomic import declaration. Imports are declared as types along with their JS values. Abstracts away the global "import object" that is separate from "import declaration".
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);
});
  • Great composability and IO
    • Internal representation of modules / funcs / etc is a readable JSON object
    • Convert to/from Wasm bytecode with module.toBytes(), Module.fromBytes(bytes)

Features that aren't implemented yet

PRs welcome!

  • Wasmati build. We want to add an optional build step which takes as input a file that exports your 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

Some ideas that are a bit further out:

  • Decompiler: take any Wasm file and create wasmati TS code from it -- to modify it, debug it etc
  • Source maps, so you can look at the culprit JS code when Wasm throws an error
  • Optional JS interpreter which can take DSL code and execute it in JS
    • could enable even more flexible debugging -- inspect the stack, global/local scope etc

About

Write low-level WebAssembly, from JavaScript

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published