This example builds a JavaScript program into a WebAssembly component and runs it as a native messaging host for both Firefox and Chromium.
Native messaging is a browser-extension feature for exchanging JSON with a program installed on the same machine.
The process goes like this:
- The browser starts the registered program and sends each UTF-8 JSON message over stdin, prefixed by a four-byte length.
- The program writes responses in the same framed format on stdout.
Note
As ordinary pages and content scripts cannot open native connections themselves, this example's
content script asks its privileged background context to call runtime.connectNative().
The browser extension needs a native-host manifest to work. Firefox identifies authorized
extensions with allowed_extensions; Chromium uses allowed_origins. stdout is reserved
entirely for framed protocol data, and other messages/diagnostics must use stderr or a
separate log.
For more about the browser feature and its security model, see Mozilla's native messaging and native manifest documentation, and Chrome's native messaging documentation.
The automated browser harness currently only supports Linux.
From this folder you can install both browser builds and run the example:
pnpm install
pnpm run test:setup:firefox
pnpm run test:setup:puppeteer
pnpm --filter native-messaging run allWarning
On Linux ARM64, where Chrome for Testing is not distributed, an installed
Chromium-based browser can be supplied through TEST_CHROMIUM_PATH.
src/component.jscontains the component's command loop.src/utils.jsowns message framing, write limits, exact reads, and large-array chunking.scripts/launch-host.mjsis the executable Node entrypoint registered with each browser.extension/background.jscontains the shared Firefox/Chromium extension behavior.extension/manifest.firefox.jsonandextension/manifest.chromium.jsoncontain the small browser-specific pieces.test/protocol.jstests the host independently of a browser.test/browser-harness.jsowns browser setup, registration, assertions, and teardown.all.jsruns the independent protocol and browser tests in parallel.
The build has two stages:
jco componentize --bundlebuilds the source as awasi:cli/runcomponent satisfying the WIT worldwit/component.wit.jco transpile --instantiation asyncconverts the built WebAssembly Component to Node-loadable bindings indist/transpiled.
Note
jco componentize normally treats a JavaScript input as one already-bundled file.
This example passes --bundle because src/component.js imports the protocol
helpers in src/utils.js. If your code fits in a single file, you don't need --bundle.
The executable scripts/launch-host.mjs instantiates those bindings with
WASI stdin/stdout streams connected directly to the browser.
To work in a cross-platform way, the launch-host.mjsscript accepts and ignores the browser-specific
command-line arguments: Firefox supplies the manifest path and extension ID, while Chromium supplies
the extension origin.
The browser does not require NodeJS or Jco-generated JavaScript.
Making this setup work only requires the native-host manifest to name an executable that implements
the length-prefixed JSON protocol over stdin and stdout. This example uses jco transpile
and a small Node launcher because that keeps the build, host, and browser tests
self-contained in the Jco repository.
An alternative setup would work is:
- A WebAssembly component built in any language that supports WebAssembly (you could also use the component here)
- A WebAssembly host (e.g.
wasmtime) that can run WebAssembly components - A binary that uses the WebAssembly Host mentioned in (2) to run the WebAssembly component in (1).
For example, the launcher (3) can be as simple as a bash script that runs the component with the wasmtime CLI, or a custom
Rust binary.
A production application would likely build a native executable in Rust, embedding wasmtime, and instantiating
a component with WASI CLI streams attached to the process's stdin and stdout.
Building and distributing a native host as described above is outside this example's scope and is left as an exercise for the reader.
The framing and chunking approach is based on the native-messaging example proposed by guest271314 in jco PR #1629. This version adapts that work to the repository's component example structure and adds automated Firefox and Chromium end-to-end coverage.