Skip to content

Instantly share code, notes, and snippets.

@guest271314
Last active April 27, 2026 02:43
Show Gist options
  • Select an option

  • Save guest271314/399ddaef7fa589f14648d6f77fdb185d to your computer and use it in GitHub Desktop.

Select an option

Save guest271314/399ddaef7fa589f14648d6f77fdb185d to your computer and use it in GitHub Desktop.
WASM vs. WASM
import { webcrypto } from "node:crypto";
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
import { exec } from "node:child_process";
const { dirname } = import.meta;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const args = process.argv.at(-1);
let runtime = "wasmtime";
if (args.startsWith("--runtime=")) {
runtime = args.split("=").at(-1);
}
// const manifest = JSON.parse(decoder.decode(readFileSync("manifest.json")));
// Generate Chrome extension ID
// https://stackoverflow.com/questions/26053434
// https://gist.github.com/dfkaye/84feac3688b110e698ad3b81713414a9
async function generateIdForPath(path) {
return [
...[
...new Uint8Array(
await webcrypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(path),
),
),
].map((u8) => u8.toString(16).padStart(2, "0")).join("").slice(0, 32),
]
.map((hex) => String.fromCharCode(parseInt(hex, 16) + "a".charCodeAt(0)))
.join(
"",
);
}
const id = await generateIdForPath(dirname);
// Write Native Messaging host manifest to NativeMessagingHosts
// in Chromium or Chrome user data directory
const hosts = {
"nm_assemblyscript": {
"i": "nm_assemblyscript.ts",
"description": "AssemblyScript WASI Native Messaging host",
"compile":
"bun install assemblyscript@latest @assemblyscript/wasi-shim@latest && bun x --bun asc -Ospeed --converge --config ./node_modules/@assemblyscript/wasi-shim/asconfig.json nm_assemblyscript.ts -o nm_assemblyscript.wasm",
},
"nm_c_wasi": {
"i": "nm_c.c",
"description": "C WASI Native Messaging host",
"compile": "wasi-clang --wasm-opt -O3 -flto nm_c.c -o nm_c_wasi.wasm",
},
"nm_cpp_wasi": {
"i": "nm_cpp.cpp",
"description": "C++ WASI Native Messaging host",
"compile":
"wasi-clang++ -std=c++2a --wasm-opt -O3 -flto -fno-exceptions nm_cpp.cpp -o nm_cpp_wasi.wasm",
},
"nm_go_wasi": {
"i": "nm_go.go",
"description": "Go WASI Native Messaging host",
"compile":
'GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -a -o nm_go_wasi.wasm nm_go.go',
},
"nm_tinygo_wasi": {
"i": "nm_go.go",
"description": "TinyGo WASI Native Messaging host",
"compile":
"tinygo build -opt=2 -no-debug -panic=trap -scheduler=none -target=wasip1 -o nm_tinygo_wasi.wasm nm_go.go",
},
"nm_warpo": {
"i": "nm_assemblyscript.ts",
"description": "Warpo WASI Native Messaging host",
"compile":
"bun x warpo --optimizeLevel=3 --host=wasi_snapshot_preview1 nm_assemblyscript.ts -o nm_warpo.wasm",
},
"nm_javy": {
"i": "nm_javy.js",
"description": "Javy Native Messaging host",
"compile":
"javy build -C source=omitted -J simd-json-builtins -o nm_javy.wasm nm_javy.js",
},
"nm_rust_wasi": {
"i": "nm_rust.rs",
"description": "Rust WASI Native Messaging host",
"compile":
"rustc -C opt-level=s -C lto=fat -C codegen-units=1 -C panic=abort --target=wasm32-wasip1 -o nm_rust_wasi.wasm nm_rust.rs",
},
"nm_zig_wasi": {
"i": "nm_zig.zig",
"description": "Zig WASI Native Messaging host",
"compile":
"zig-stable build-exe nm_zig.zig -O ReleaseSmall -target wasm32-wasi --name nm_zig_wasi",
},
"nm_qjs_wasi": {
"i": "nm_qjs_wasi.js",
"description": "QuickJS WASI Native Messaging host",
"compile":
`bun -e 'await fetch("https://github.com/quickjs-ng/quickjs/releases/latest/download/qjs-wasi.wasm").then(async(r)=> [r.headers.get("content-disposition").match(/(?<=filename=).+$/)[0], await r.bytes()]).then(async([filename,file])=>await Bun.write(filename,file))' && bun build --minify --outfile=nm_qjs_wasi_min.js nm_qjs_wasi.js`,
},
};
writeFileSync("hosts.json", JSON.stringify(Object.keys(hosts)));
for (const [nativeHost, { compile, description, i }] of Object.entries(hosts)) {
const host = {};
console.log(nativeHost);
host.name = nativeHost;
host.description = description;
host.path = `${dirname}/${host.name}.sh`;
host.type = "stdio";
host.allowed_origins = [];
host.allowed_origins.push(`chrome-extension://${id}/`);
const { resolve, promise } = Promise.withResolvers();
exec(compile, (error, stdout, stderr) => {
if (error) {
console.error(`exec. error: ${error}`);
return;
}
console.log(`Successfully compiled ${i} to ${host.name}.wasm`);
// console.log(`stdout: ${stdout}`);
// console.error(`stderr: ${stderr}`);
resolve();
});
await promise;
const shellscript = host.name === "nm_qjs_wasi"
? `#!/bin/bash
exec /home/user/bin/wasmtime run --dir=. /home/user/bin/qjs-wasi.wasm --std -m -e '${
readFileSync("./nm_qjs_wasi_min.js", "utf8")
}'`
: `#!/usr/bin/env -S /home/user/bin/${runtime} ${dirname}/${host.name}.wasm`;
writeFileSync(`${host.name}.sh`, shellscript);
chmodSync(host.path, 0o764);
console.log(`${host.path} set to executable.`);
writeFileSync(`${host.name}.json`, JSON.stringify(host, null, 2));
// https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/user_data_dir.md
const chromeUserDataDir = `${
dirname.split("/").slice(0, 3).join("/")
}/.config/chromium/NativeMessagingHosts`;
writeFileSync(
`${chromeUserDataDir}/${host.name}.json`,
JSON.stringify(host, null, 2),
);
console.log(
`${host.name} Native Messaging host manifest written to ${chromeUserDataDir}/${host.name}.json`,
);
}
console.log(`
Launch chrome with \`chrome --load-extension=${dirname}\` and navigate to URL \`chrome-extension://${id}/index.html\`, open DevTools`);
import hosts from "./hosts.json" with { type: "json" };
async function nativeMessagingPerformanceTest(i = 10) {
const runtimes = new Map(hosts.map((host) => [host, 0]));
for (let j = 0; j < i; j++) {
for (const [runtime] of runtimes) {
console.log(`${runtime} run no. ${j} of ${i}}`);
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,
runtime,
});
const n = runtimes.get(runtime);
runtimes.set(runtime, n + ((performance.now() - now) / 1000));
port.disconnect();
resolve();
});
port.onDisconnect.addListener(() => reject(chrome.runtime.lastError));
port.postMessage(new Array(209715));
// Handle SpiderMonkey, send "\r\n\r\n" to process full message with js
if (runtime === "nm_spidermonkey") {
port.postMessage("\r\n\r\n");
}
await promise;
} catch (e) {
console.log(e, runtime);
continue;
}
}
await scheduler.postTask(() => {}, {
delay: 10,
});
}
const sorted = [...runtimes].map(([k, n]) => [k, n / i]).sort((
[, a],
[, b],
) => a < b ? -1 : a === b ? 0 : 1);
console.table(sorted);
}
await nativeMessagingPerformanceTest(100);

WASM vs. WASM

AsssemblyScript, C, C++ Go, Javy, Rust, Zig, Warpo, and QuickJS (compiled to WASM running JavaScript source code) Native Messaging hosts.

Fastest to slowest in Debian 13 live session run from Chromium Version 149.0.7800.0 (Developer Build) (64-bit).

Observations

WASI imports in WAT representation from jco print nm_*.wasm > nm_*.wat wind up different for each input language.

Building

Before building, if you are not using Chromium browser, rather Chrome, check ~/.config and see what the name of Chrome configuration folder is; it should be named something like google-chrome-stable or google-chrome, and adjust that line accordinging in install_host.js. bun is used to install AssemblyScript and AssemblyScript's wasi-shim.

git clone https://github.com/guest271314/native-messaging-webassembly
cd native-messaging-webassembly
node install_hosts.js

If everything goes as expected the respective nm_* files should be compiled to WASM with optimizations per the specific compiler, and WASI support. The Native Messaging host manifests will be written to ~/.config/<chromium|googlechrome*>/NativeMessagingHosts, and the URL to navigate to in the browser should be printed to terminal. Launch with chrome --load-extension=/path/to/native-messaging-webassembly (where you just cloned the repository), and navigate to the URL printed. Open DevTools and observe results of which WASM file was executed fastest by wasmtime, the base case.

There is a Bytecode Alliance Javy specific file, nm_javy_node_wasi.js that uses Node.js node:wasi module to run the same nm_javy.wasm using WebAssembly JavaScript API.

There is also a generic nm_node_wasi.js that can be run with any of the WASM files compiled by install_hosts.js, e.g.,

node nm_node_wasi.js ./nm_tinygo_wasi.js

There's a generic, minimal WAPI P1 implementation, too, nm_wasip1_min.js that can be run as such, after marking the file as executable, to test different JavaScript runtimes' execution (adjust shebang line accordingly)

./nm_wasip1_min.js ./nm_warpo.wasm

Additionally, there is a standalone test that uses deno to test Native Messaging hosts outside out the browser, that can be run as such

deno -A ./nm_standalone_test.js ./nm_node_wasi.js ./nm_c_wasi.wasm

or

deno -A ./nm_standalone_test.js ./nm_rust_wasi.sh

To test with a runtime other than wasmtime, the default WebAssembly runtime, rebuild with, for example, passing bun to the script

node install_hosts.js --runtime=bun

On Debian 13 lve my results for 100 runs of each WASM file is this running test_wasi.js in the unpacked extension

0	'nm_assemblyscript'	0.1976559999999405
1	'nm_zig_wasi'	0.19885799999967216
2	'nm_c_wasi'	0.2042730000005662
3	'nm_rust_wasi'	0.22548400000005958
4	'nm_warpo'	0.2342590000006556
5	'nm_tinygo_wasi'	0.2594749999994041
6	'nm_qjs_wasi'	0.27996299999997015
7	'nm_cpp_wasi'	0.3086060000002384
8	'nm_javy'	0.3109239999997616
9	'nm_go_wasi'	

I'm curious to see what results you get on your machine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment