JavaScript Tooling
WebAssembly was originally developed as a technology for running non-JavaScript workloads in the browser at near-native speed.
JavaScript WebAssembly component model support is provided by a combination of tools:
- StarlingMonkey, a WebAssembly component-aware JavaScript engine
componentize-js, a tool for building WebAssembly components from JavaScript filesjco, a multi-tool for componentizing, type generation, and running components in Node.js and browser contexts
Note that TypeScript can also be used, given that it is transpiled to JS first by relevant tooling (tsc).
jco generates [type declaration files (.d.ts)][ts-decl-file] by default,
and also has a jco types subcommand which generates typings that can be used with a TypeScript codebase.
warning
While popular projects like emscripten also build WebAssembly modules,
those modules are not Component Model aware.
Core WebAssembly modules do not contain the advanced features (rich types, structured language interoperation, composition) that the component model makes available.
Installing jco
jco (which uses componentize-js can be installed through
the Node Package Manager (npm):
npm install -g @bytecodealliance/jco
note
jco and componentize-js can be installed in a project-local manner with npm install -D.
Overview of Building a Component with JavaScript
Building a WebAssembly component with JavaScript often consists of:
- Determining which interface our component will target (i.e. given a WebAssembly Interface Types ("WIT") world)
- Creating the component by writing JavaScript that satisfies the interface
- Compiling the interface-compliant JavaScript to WebAssembly
Building Reactor Components with jco
Reactor components are WebAssembly components that are long-running and meant to be called repeatedly over time. Unlike "command" components, which are analogous to executables, reactor components are analogous to libraries of functionality.
Components expose their interfaces via WebAssembly Interface Types, hand-in-hand with the Component Model which enables components to use higher-level types interchangeably.
What is WIT?
WebAssembly Interface Types ("WIT") is a featureful Interface Definition Language ("IDL") for defining functionality, but most of the time, you shouldn't need to write WIT from scratch. Often, it's sufficient to download a pre-existing interface that defines what your component should do.
The adder world
contains an interface with a single add function that sums two numbers.
Create a new directory called adder and paste the following WIT code
into a file called world.wit.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
The export add; declaration inside the adder world means that
environments that interact with the resulting WebAssembly component
will be able to call the add function.
The fully qualified name of the add interface in this context is docs:adder/add.add@0.1.0.
The parts of this name are:
docs:adderis the namespace and package, withdocsbeing the namespace andadderbeing the package.addis the name of the interface containing theaddfunction.addalso happens to be the name of the function itself.@0.1.0is a version number that must match the declared version number of the package.
To learn more about the WIT syntax, check out the WIT explainer.
Implementing a JS WebAssembly Component
To implement the adder world, we can write a JavaScript ES module.
Paste the following code into a file called adder.js in your adder directory:
export const add = {
add(x, y) {
return x + y;
}
};
warning
If you create a JavaScript project using this file,
make sure you set the "type":"module" option in package.json,
as jco works exclusively with JavaScript modules.
In the code above:
- The JavaScript module (file) itself is analogous to the
adderworld - The exported
addobject corresponds to theexportedaddinterface in WIT - The
addfunction defined inside theaddobject corresponds to theaddfunction inside theaddinterface
With the WIT and JavaScript in place, we can use jco to create a WebAssembly component from the JS module, using jco componentize.
note
You can also call componentize-js directly—it can be used
both through an API and through the command line.
Our component is so simple (reminiscent of Core WebAssembly, which deals only in numeric values)
that we're actually not using any of the WebAssembly System Interface functionality
(access to files, networking, and other system capabilities).
This means that we can --disable all unneeded WASI functionality when we invoke jco componentize.
Inside your adder directory, execute:
jco componentize \
--wit world.wit \
--world-name adder \
--out adder.wasm \
--disable=all \
adder.js
note
If you're using jco as a project-local dependency, you can run npx jco.
You should see output like the following:
OK Successfully written adder.wasm.
You should now have an adder.wasm file in your adder directory.
You can verify that this file contains a component with:
$ wasm-tools print adder.wasm | head -1
(component
warning
By using --disable=all, your component won't get access to any WASI interfaces that
might be useful for debugging or logging.
For example, you can't console.log(...) or console.error(...) without stdio;
you can't use Math.random() without random;
and you can't use Date.now() or new Date() without clocks.
Please note that calls to Math.random() or Date.now() will return seemingly valid
outputs, but without actual randomness or timestamp correctness.
Running the Component in the example-host
note
The example-host Rust project uses the Rust toolchain,
in particular cargo,
so to run the code in this section you may need to install some more dependencies (like the Rust toolchain).
To run the component we've built, we can use the example-host project:
This repository contains an example WebAssembly host written in Rust
that can run components that implement the adder world.
git clone https://github.com/bytecodealliance/component-docs.gitcd component-docs/component-model/examples/example-hostcargo run --release -- 1 2 <PATH>/adder.wasm
- The double dashes separate the flags passed to
cargofrom the flags passed in to your code. - The arguments 1 and 2 are the arguments to the adder.
- In place of
<PATH>, substitute the directory that contains your generatedadder.wasmfile.
Note: When hosts run components that use WASI interfaces, they must explicitly add WASI to the linker to run the built component.
The output looks like:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
If not configured correctly, you may see errors like the following:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
This kind of error normally indicates that the host in question does not satisfy WASI imports.
While the output isn't exciting, the code contained in example-host does a lot to make it happen:
- Loads the WebAssembly binary at the provided path (in the command above,
/path/to/adder.wasm) - Calls the
exportedaddfunction inside theaddinterface with arguments - Prints the result
The important Rust code looks something like this:
#![allow(unused)] fn main() { let component = Component::from_file(&engine, path).context("Component file not found")?; let (instance, _) = Example::instantiate_async(&mut store, &component, &linker) .await .context("Failed to instantiate the example world")?; instance .call_add(&mut store, x, y) .await .context("Failed to call add function") }
A quick reminder on the power and new capabilities afforded by WebAssembly: we've written, loaded, instantiated and executed JavaScript from Rust with a strict interface, without the need for foreign function interfaces, subprocesses or a network call.
Running a Component from JavaScript Applications (including the Browser)
While JavaScript runtimes available in browsers can execute WebAssembly core modules, they cannot yet execute WebAssembly components, so WebAssembly components (JavaScript or otherwise) must be "transpiled" into a JavaScript wrapper and one or more WebAssembly core modules which can be run by browsers.
Given an existing WebAssembly component (e.g. adder.wasm which implements the adder world),
we can transpile the WebAssembly component into runnable JavaScript by using jco transpile.
In your adder directory, execute:
jco transpile adder.wasm -o dist/transpiled
You should see output similar to the following:
Transpiled JS Component Files:
- dist/transpiled/adder.core.wasm 10.6 MiB
- dist/transpiled/adder.d.ts 0.11 KiB
- dist/transpiled/adder.js 21.1 KiB
- dist/transpiled/interfaces/docs-adder-add.d.ts 0
note
For a complete project containing JS and WIT files similar to the ones you already created,
see the jco example adder component.
With this project pulled locally, you also run npm run transpile, which outputs to dist/transpiled.
Thanks to jco transpilation, you can import the resulting dist/transpiled/adder.js file
and run it from any JavaScript application
using a runtime that supports the core WebAssembly specification as implemented for JavaScript.
To use this component from Node.js, you can write code like the following:
import { add } from "./dist/transpiled/adder.js";
console.log("1 + 2 = " + add.add(1, 2));
Pasting this code into a file called run.js in your adder directory,
you can execute the JavaScript module with node directly.
First, you will need to create a package.json file
in the same directory:
{
"name": "adder-wasm",
"description": "Simple codebase for compiling an add interface to WebAssembly with jco",
"type": "module"
}
note
Without creating the package.json file, or if you omit the "type": "module" property,
you will see an error message like:
SyntaxError: Cannot use import statement outside a module.
Then you can run the module with:
node run.js
You should see output like the following:
1 + 2 = 3
This is directly comparable to the Rust host code mentioned in the previous section.
Here, we are able to use Node.js as a host for running WebAssembly,
thanks to jco's ability to transpile components.
With jco transpile, any WebAssembly binary (compiled from any language) can be run natively in JavaScript.