We’re happy to announce the 1.0 release of Jco: a native Javascript WebAssembly toolchain and runtime built for WebAssembly Components and WASI 0.2 1. Jco can natively run Wasm Components inside of Node.js, making it easy to take libraries written in different programming languages and execute them using the Node.js runtime. And by implementing the entirety of the WASI 0.2 API surface, those components can access the network, filesystem, and other system APIs available in the Node.js runtime.

Our goal is for Jco to be a comprehensive tool for all Component-related operations for JavaScript. With Jco 1.0 we’re stabilizing a Node.js runtime for Wasm Components, as well as a toolchain to take Wasm Components written in other languages and import them into JavaScript. From here on out we hope to continue stabilizing more features for Jco. Some features are already available experimentally; this includes native support for the browser, as well as native support for compiling JavaScript code into WebAssembly. Other features such as support for the WebAssembly registry have not yet started, but we expect to add later.

JavaScript Toolchains in the Bytecode Alliance

Jco is the third JS toolchain project which is part of the Bytecode Alliance. To help people navigate this, here is a quick overview of the three projects and what their purpose is at the time of writing:

  • Javy: A JavaScript → WASI 0.1 compiler toolchain built using the QuickJS JavaScript engine. This work predates Wasm Components, but support may be added in the future.
  • ComponentizeJS: A JavaScript → Wasm Component toolchain with full support for WASI 0.2 built using the SpiderMonkey JavaScript engine. This project is newer and not yet considered stable.
  • Jco: A comprehensive WebAssembly Component toolchain and runtime for JavaScript. This includes a Wasm Component → JS toolchain with full support for WASI 0.2.

Example

To give you a taste of what it’s like to use Jco, let’s compile a little Rust program to a WASI 0.2.0 component, install Jco for Node.js, and then embed the newly built component inside of the runtime. If you want to skip ahead to the final result, you can view it here.

Dependencies and Repository

Because we’re going to be writing Rust code, and using it from Node.js we expect you to have both a working Node.js environment and Rust toolchain installed. Once you have that, we can install cargo-component to build our Rust code, and Jco to build it for Node.js.

$ npm install -g @bytecodealliance/jco        # Install the jco cli tool
$ npm install @bytecodealliance/preview2-shim # Install the jco WASI runtime as a local dep
$ cargo install cargo-component               # Install the `cargo component` subcommand

We can then proceed to create a new project repository, which has both Rust and JS code:

$ cargo component new playground-jco --lib # Create a new Rust component
$ cd playground-jco                        # Enter the newly created project
$ npm init -y                              # Setup a Node.js environment
$ touch index.js                           # Create a JS entrypoint
$ echo node_modules >> .gitignore          # Add node_modules to our .gitignore file

This should leave you with a structure that roughly looks like this:

src/lib.rs      # Our Rust library entry point
wit/world.wit   # Contains our WIT IDL definitions
Cargo.toml      # Configures our Rust project
index.js        # Our JS binary entry point
package.json    # Configures our JS Project

And with that out of the way, we’re ready to begin writing some code!

Defining the WIT interface

There are three steps to this project: defining the WIT interface, writing the Rust code to be exported, and writing the JS code to be imported. We’ll be starting with the WIT interface. For those of you new to Wasm Components: WIT is an Interface Description Language (IDL) which allows you to describe programming interfaces in a declarative, language agnostic way. Let’s define an interface which takes a regular string, and converts it to SHOUTING CASE!!1!.

package my-org:wit-playground

world playground {
  export scream: func(input: string) -> string;
}

We can save this to the generated wit/world.wit file 2, and now we have defined a function scream which takes a string, and returns a string. If this is your first time editing WIT files and you’re using VS Code, consider installing the WIT IDL package to get editor support for WIT files. And with that, it’s time to write our Rust code!

Writing a Rust library

As mentioned, we’re using the cargo-component tool here in this example to compile our Rust code to Wasm Components. It isn’t yet at 1.0, but it does already work well enough for our purposes. So we’ll show you how you can use it today. If you’re from the future (e.g. not February 2024), please check the cargo-component docs to see whether these instructions have changed, and if so - follow those instead. The steps we’re going to take here are: define a WIT API definition, generate the Rust bindings to it (e.g. Rust traits), and then implement those traits to implement the definitions. Even if the exact ways of doing this may change in the future, conceptually there will still be ways to do similar things.

First things first: we want cargo component to generate the bindings for our WIT file. This is done via Rust traits, and it will do this automatically when we compile. This will yield a compiler error, but that’s okay - we’ll update the code in a second:

$ cargo component check

This will have generated Rust bindings, which on the current version of cargo-component is places under lib/bindings.rs. Don’t worry about reading that code right now - it does a lot of heavy lifting so we don’t have to. The important part is that it provides us with traits which we can implement. So let’s implement those in our code, and write a little upper case function

mod bindings;                                     // Teach our program about `src/bindings.rs`

use bindings::Guest;                              // Import the trait repr of our WIT world
use wit_bindgen::rt::string::String as WitString; // Import the WIT string type

struct Component;                                 // Define the type to export
impl Guest for Component {                        // Import the WIT world + methods
    fn scream(input: WitString) -> WitString {    // Implement our `scream` method
        let mut s = input.to_uppercase();
        s.push_str("!!1!");
        s.into()
    }
}

Now if we re-run cargo component check, it should compile without any warnings. That means we’re ready to run:

$ cargo component build      # Compile our Rust library to a Wasm Component

This will have written the generated Wasm code to target/wasm32-wasi/debug/playground_jco.wasm. Once Rust lands support for a native WASI 0.2 target, expect this to change to target/wasm32-wasi-p2/debug/playground_jco.wasm or similar, as the current set of workarounds will no longer be needed (important if you’re reading this anytime after March 2024 or so).

Writing a JavaScript binary

Okay, it’s time to use Jco. First we want to take our Rust code, and generate JS bindings from it. We can do this using the jco transpile command. Let’s point it at our newly generated Wasm library, like so:

$ jco transpile \                                   # Call `jco transpile`
    target/wasm32-wasi/debug/playground_jco.wasm \  # Point it at our Rust library
    -o target/jco                                   # Write the result to `target/jco`

On success it will tell you it’s generated a number of JS files as part of its CLI output:

Transpiled JS Component Files:

 - target/jco/interfaces/wasi-cli-environment.d.ts      0.09 KiB
 - target/jco/interfaces/wasi-cli-exit.d.ts             0.16 KiB
 - target/jco/interfaces/wasi-cli-stderr.d.ts           0.17 KiB
 - target/jco/interfaces/wasi-cli-stdin.d.ts            0.17 KiB
 - target/jco/interfaces/wasi-cli-stdout.d.ts           0.17 KiB
 - target/jco/interfaces/wasi-clocks-wall-clock.d.ts    0.11 KiB
 - target/jco/interfaces/wasi-filesystem-preopens.d.ts   0.2 KiB
 - target/jco/interfaces/wasi-filesystem-types.d.ts     2.71 KiB
 - target/jco/interfaces/wasi-io-error.d.ts             0.08 KiB
 - target/jco/interfaces/wasi-io-streams.d.ts           0.58 KiB
 - target/jco/playground_jco.core.wasm                  1.83 MiB
 - target/jco/playground_jco.core2.wasm                 16.5 KiB
 - target/jco/playground_jco.d.ts                       0.72 KiB
 - target/jco/playground_jco.js                           40 KiB

The next step is to import this into our Node.js file. In order to do this, we first have to update our package.json to declare that we’re writing an ES module. In our package.json set the field "type" to "module":

{
  "name": "playground-jco",
  "version": "1.0.0",
  "type": "module",          //  Add this line
  // ...
}

And then finally, we’re ready to write some JS code. we’re going to import the scream function from our Rust library, pass it a string, and then print the output of the string.

import { scream } from "./target/jco/playground_jco.js";

let msg = scream("chashu would like some tuna");
console.log(msg);

Now if we run this using node index.js, we should get the following output:

CHASHU WOULD LIKE SOME TUNA!!1!

And with that, we’ve successfully written our first Rust component for Node.js. You can give yourself a little round of applause - because this is incredibly cool! By leveraging WIT and Wasm, we’ve been able to define a language-agnostic interface which resulted in strongly typed code for both programming languages. And by leveraging code generation, all of the hard FFI and data marshalling bits have been handled for us - enabling us to focus on what is truly important (screaming we want tuna).

In this example we happened to use JavaScript and Rust in order to demo the Jco runtime. But what’s important to understand is that this could have just as easily been using JavaScript and Go. Or Rust and Python. The point of the Wasm Components and WASI is that we can connect any number of languages to each other via a strongly typed ABI.

We’ve intentionally kept things fairly simple: one method which takes strings in, and returns strings out. However the latest version of WASI (0.2) provides among other things access to asynchronous sockets, timers, and http. Using the same broad techniques we’ve shown in this post, it should be possible to for example write your networked applications using different languages, all linked to one another using strongly typed WIT interfaces.

Conclusion

In this post we’ve introduced Jco 1.0.0: a native JavaScript runtime which can run WASI 0.2 components. We’ve discussed what has been stabilized, which features are currently in progress, as well as future directions. Finally we’ve shown an example of how to use Jco to embed a Rust library inside of a Node.js project.

We’re happy to report that several projects in the wild are already succesfully using Jco to build their projects with. One particularly impressive project is by the inimitable Catherine “whitequark”, who has ported the YoWASP FPGA toolchain to the browser using Jco. This project enables users to flash FPGA hardware directly from their browser over WebUSB, and even works on mobile devices.

Jco is a Bytecode Alliance project, designed and led by Guy Bedford (Fastly). The 1.0 release of Jco was made possible with the assistance of Pat Hickey (Fastly), Wassim Chegham (Microsoft), Dirk Bäumer (Microsoft), and Yosh Wuyts (Microsoft). On behalf of the Bytecode Alliance, we’d like to thank everyone who was involved with the conception, design, and development of Jco. And we’re very excited for people to be able to start using it in their projects!

  1. WebAssembly Components are an extension to WebAssembly to create strongly typed interfaces between different programs. WASI 0.2 is a re-work of the “WebAssembly System Interfaces” based on WebAssembly Components. The previous version of WASI was not based on components, and as a result was rather limited in what it could express. 

  2. If you’re from the future: cargo-component generated this file for us in this location. Since it is not yet advertised as “stable”, the output location for WIT files may change. If no wit/world.wit file was generated for you, please check a different location to see whether it might be somewhere else.