Skip to content

Instantly share code, notes, and snippets.

@guest271314
Created August 10, 2024 21:20
Show Gist options
  • Select an option

  • Save guest271314/43aa0386ac9b85c1718349c20896e316 to your computer and use it in GitHub Desktop.

Select an option

Save guest271314/43aa0386ac9b85c1718349c20896e316 to your computer and use it in GitHub Desktop.
Compiling JavaScript to WASM using Bytecode Alliance's javy

Today we are going to compile JavaScript source code to WASM using javy.

The compiled WASM module will read and echo standard input as a Native Messaging host.

The protocol, in brief is

Chrome starts each native messaging host in a separate process and communicates with it using standard input (stdin) and standard output (stdout). The same format is used to send messages in both directions; each message is serialized using JSON, UTF-8 encoded and is preceded with 32-bit message length in native byte order. The maximum size of a single message from the native messaging host is 1 MB, mainly to protect Chrome from misbehaving native applications. The maximum size of the message sent to the native messaging host is 4 GB.

JavaScript implementation of a host using Javy.IO.readSync() and Javy.IO.writeSync()

nm_javy.js

const stdin = 0;
const stdout = 1;

function encodeMessage(str) {
  return new TextEncoder().encode(JSON.stringify(str));
}

function getMessage() {
  let offset = 0;
  const header = new Uint8Array(4);
  Javy.IO.readSync(stdin, header);
  const [length] = new Uint32Array(header.buffer);
  const message = new Uint8Array(length);
  while (1) {
    const buffer = new Uint8Array(1024);
    const bytesRead = Javy.IO.readSync(stdin, buffer);
    message.set(buffer.subarray(0, bytesRead), offset);
    offset += bytesRead;
    if (offset === length) {
      break;
    }
  }
  return message;
}

function sendMessage(json) {
  let header = Uint32Array.from({
    length: 4,
  }, (_, index) => (json.length >> (index * 8)) & 0xff);
  let output = new Uint8Array(header.length + json.length);
  output.set(header, 0);
  output.set(json, 4);
  Javy.IO.writeSync(stdout, output);
}

function main() {
  while (1) {
    try {
      const message = getMessage();
      sendMessage(message);
    } catch (e) {
      sendMessage(encodeMessage(e.message));
    }
  }
}

main();

Compile to WASM

javy compile nm_javy_.js -o nm_javy.wasm

Test using wasmtime as the WASM runtime

#!/usr/bin/env -S /home/user/bin/wasmtime -C cache=n --allow-precompiled /home/user/native-messaging-webassembly/nm_javy.wasm

Compare the speed of reading standard input and writing standard output (1 MB of JSON) to QuickJS, txiki.js, Node.js, Deno, Bun (node, deno, bun are running the same .js file), running TypeScript .ts file directly with bun, Google V8 d8 shell, Mozilla SpiderMonkey js shell, Amazon Web Services - Labs Low Latency Runtime (llrt).

Screenshot_2024-08-10_14-12-01

Optimize the WASM file with wasmtime to observe if the speed of using a compiled WASM module changes anything

wasmtime compile --optimize opt-level=s nm_javy.wasm 

which outputs a file names nm_javy.cwasm.

Adjust the path to the c.wasm file on the shebang line

#!/usr/bin/env -S /home/user/bin/wasmtime -C cache=n --allow-precompiled /home/user/native-messaging-webassembly/nm_javy.cwasm

re-test

Screenshot_2024-08-10_14-15-37

We observe a significant improvement in speed of reading and writing standard input streams.

Code used to test from the browser

var runtimes = new Map([
  ["nm_nodejs", 0],
  ["nm_deno", 0],
  ["nm_bun", 0],
  ["nm_tjs", 0],
  ["nm_qjs", 0],
  ["nm_spidermonkey", 0],
  ["nm_d8", 0],
  ["nm_typescript", 0],
  ["nm_llrt", 0],
  ["nm_wasm", 0],
  // ["nm_rust", 0]
]);
for (const [runtime] of runtimes) {
  try {
    const {
      resolve,
      reject,
      promise,
    } = Promise.withResolvers();
    const now = performance.now();
    const port = chrome.runtime.connectNative(runtime);
    port.onMessage.addListener((message) => {
      console.assert(message.length === 209715, { message });
      runtimes.set(runtime, (performance.now() - now) / 1000);
      port.disconnect();
      resolve();
    });
    port.onDisconnect.addListener(() => reject(chrome.runtime.lastError));
    port.postMessage(new Array(209715));
    if (runtime === "nm_spidermonkey") {
      port.postMessage("\r\n\r\n");
    }
    await promise;
  } catch (e) {
    console.log(e, runtime);
    continue;
  }
}
var sorted = [...runtimes].sort(([, a], [, b]) => a < b ? -1 : a === b ? 0 : 1);
console.table(sorted);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment