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).
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
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);

