Introduction
jco is a fully native tool for working with WebAssembly Components in JavaScript.
Features
- Transpiling Wasm Component binaries into ECMAScript modules that can run in any JavaScript environment.
- WASI Preview2 support in Node.js & browsers (experimental).
- Component builds of Wasm Tools helpers, available for use as a library or CLI commands for use in native JS environments
- Optimization helper for Components via Binaryen.
componentizecommand to easily create components written in JavaScript (wrapper of ComponentizeJS).
Note: This is an experimental project. No guarantees are provided for stability, security or support and breaking changes may be made without notice.
Contributing
To contribute to the codebase of the project, refer to the Contributor guide.
To contribute to the documentation, refer to the Contributor guide.
If you find a mistake, omission, ambiguity, or other problem, please let us know via GitHub issues.
Transpiling
Components can be transpiled in two separate modes:
- ESM Integration (default)
- Instantiation - async or sync
When using the default direct ESM transpilation mode, the output file is a JavaScript module, which imports the component imports, and exports the component exports.
Instantiation mode allows dynamically providing the imports for the component instantiation, as well as for instantiating a component multiple times.
For the default output, you will likely want to ensure there is a package.json file with a { "type": "module" } set for Node.js ES module support (although this is not needed for browser module loading or JS build tooling).
Usage
To transpile a component into JS:
jco transpile component.wasm -o out-dir
The resultant file can be imported providing the bindings of the component as if it were imported directly:
app.js
import { fn } from './out-dir/component.js';
fn();
Imports can be remapped using the --map flag, or to provide imports as an argument use the --instantiation option.
Components relying on WASI bindings will contain external WASI imports, which are automatically updated
to the @bytecodealliance/preview2-shim package. This package can be installed from npm separately for
runtime usage. This shim layer supports both Node.js and browsers.
Options
Options include:
-
--name: Give a custom name for the component JS file inout-dir/[name].js -
--minify: Minify the component JS -
--optimize: Runs the internal core Wasm files through Binaryen for optimization. Optimization options can be passed with a-- <binaryen options>flag separator. -
--tla-compat: Instead of relying on top-level-await, requires an$initpromise to be imported and awaited first. -
--js: Converts core Wasm files to JavaScript for environments that don't even support core Wasm. -
--base64-cutoff=<number>: Sets the maximum number of bytes for inlining Wasm files into the JS using base64 encoding. Set to zero to disable base64 inlining entirely. -
--no-wasi-shim: Disable the WASI shim mapping to@bytecodealliance/preview2-shim. -
--map: Provide custom mappings for world imports. Supports both wildcard mappings (*similarly as in the package.json "exports" field) as well as#mappings for targetting exported interfaces. For example, the WASI mappings are internally defined with mappings like--map wasi:filesystem/*=@bytecodealliance/preview2-shim/filesystem#*to mapimport as * filesystem from 'wasi:filesystem/types'toimport { types } from '@bytecodealliance/preview2-shim/filesystem. -
--no-nodejs-compat: Disables Node.js compat in the output to load core Wasm with FS methods. -
--instantiation [mode]: Instead of a direct ES module, export aninstantiatefunction which can take the imports as an argument instead of implicit imports. Theinstantiatefunction can be async (with--instantiationor--instantiation async), or sync (with--instantiation sync). -
--valid-lifting-optimization: Internal validations are removed assuming that core Wasm binaries are valid components, providing a minor output size saving. -
--tracing: Emit tracing calls for all function entry and exits. -
--no-namespaced-exports: Removes exports of the typetest as "test:flavorful/test"which are not compatible with typescript -
--async-mode [mode]: EXPERIMENTAL: For the component imports and exports, functions and methods on resources can be specified asasync. The only option isjspi(JavaScript Promise Integration). -
--async-imports <imports...>: EXPERIMENTAL: Specify the component imports asasync. Used with--async-mode. -
--async-exports <exports...>: EXPERIMENTAL: Specify the component exports asasync. Used with--async-mode.
Browser Support
Jco itself can be used in the browser, which provides the simpler Jco API that is just exactly the same as the internal Jco component Jco uses to self-host.
To use this browser-supported internal component build, import the /component subpath directly:
import { transpile } from '@bytecodealliance/jco/component';
Most JS build tools should then correctly work with such code bundled for the browser.
Experimental WebIDL Imports
Jco has experimental support for zero-runtime and zero-configuration WEbIDL bindings, when using the
webidl: interface.
A canonical WebIDL resource is not yet available, but some examples of these IDLs and WITs can be found in the IDL fixtures directory.
Whenever the webidl: namespace is used, Jco will automatically bind such imports to the global object.
Two top-level conventions are then provided for global access:
- A top-level
getWindowfunction can be used (or for any singleton global name) to obtain the global object. - If the imported interface name starts with
global-such asglobal-console, then the interface is bound to that object name on the global object, with dashes replaced with.access, ieglobalThis.console.
Under these conventions, many WebIDL files can be directly supported for components without any additional runtime configuration needed. A WebIDL to WIT converter is in development at https://github.com/wasi-gfx/webidl2wit.
This work is highly experimental, and contributors and improvements would be welcome to help steer this feature to stability.
Transpilation Semantics
Export Conventions
Components can represent both bundles of modules and individual modules. Compponents export the direct export interface as well as the canonical named interface for the implementation to represent both of these cases.
For example a component that imports an interface will be output as:
export { interface, interface as 'my:pkg/interface@version' }
The exact version allows for disambiguation when a component exports multiple interfaces with the same name but different versions.
If not needing this disambiguation feature, and since support for string exports in JS can be limited, this feature can be disabled with the --no-namespaced-exports flag to instead output only:
export { interface }
Import Conventions
When using the ESM integration default transpilation output bindings are output directly in the registry:name/interface form, but with versions removed.
For example an import to my:pkg/interface@1.2.3 will become an import to import { fn } from 'my:pkg/interface';.
Map Configuration
To customize the import specifiers used in JS, a --map configuration can be provided to the transpilation operation to convert the imports.
For example, jco transpile component.wasm --map my:pkg/interface@1.2.3=./myinterface.js will instead output import { fn } from './myinterface.js'.
Where the file myinterface.js would contain the function that is being imported from the interface:
export function fn () {
// .. function implementation ..
}
Map configuration also supports # targets, which means that the interface can be read off of a nested JS object export.
For example with a JS file written:
export const interface = {
fn () {
// exported function to be imported from my:pkg/interface
}
}
We can map the interface directly to this object instead of the entire module using the map configuration:
jco transpile component.wasm --map my:pkg/interface@1.2.3=./mypkg.js#interface
This way a single JS file can define multiple interfaces together.
Furthermore, wildcard mappings are also supported so that using (and quoting for bash compatibility):
jco transpile component.wasm --map 'my:pkg/*@1.2.3=./mypkg.js#*'
we can map all interfaces into a single JS file reading them off of exported objects for those interfaces.
WASI Shims
WASI is given special treatment and is automatically mapped to the @bytecodealliance/preview2-shim npm package, with interfaces imported off of the relevant subsystem.
Using the above rules, this is effectively provided by the default map configuration which is always automatically provided:
jco transpile component.wasm --map wasi:cli/*@0.2.0=@bytecodealliance/preview2-shim/cli#*
For all subsystems - cli, clocks, filesystem, http, io, random and sockets.
To disable this automatic WASI handling the --no-wasi-shim flag can be provided and WASI will be treated like any other import without special handling.
Note that browser support for WASI is currently experimental.
Interface Implementation Example
Here's an example of implementing a custom WIT interface in JavaScript:
example.wit
package test:pkg;
interface interface-types {
type some-type = list<u32>;
record some-record {
some-field: some-type
}
}
interface iface {
use interface-types.{some-record};
interface-fn: func(%record: some-record) -> result<string, string>;
}
world myworld {
import iface;
export test: func() -> string;
}
When transpiling, we can use the map rules as described in the previous section to implement all interfaces from a single JS file.
Given a component compiled for this world, we could transpile it, but given this is only an example, we can use the --stub feature of transpile to inspect the bindings:
jco transpile example.wit --stub -o output --map 'test:pkg/*=./imports.js#*'
The output/example.js file contains the generated bindgen:
import { iface } from './imports.js';
const { interfaceFn } = iface;
// ... bindings ...
function test () {
// ...
}
export { test }
Therefore, we can implement this mapping of the world with the following JS file:
imports.js
export const iface = {
interfaceFn (record) {
return 'string';
}
};
Note: Top-level results are turned into JS exceptions, all other results are treated as tagged objects
{ tag: 'ok' | 'err', val }.
WASI Proposals
Jco will always take PRs to support all open WASI proposals.
These PRs can be implemented by extending the default map configuration provided by Jco to support the new --map wasi:subsytem/*=shimpkg/subsystem#* for the WASI subsystem being implemented.
shimpkgin the above refers to a published npm package implementation to install per JS ecosystem conventions. This way, polyfill packages can be published to npm.
Upstreaming into the @bytecodealliance/preview2-shim package is also possible for WASI proposals that have progressed to Phase 1 in the WASI proposal stage process.
Instantiation
Instantiation output is enabled via jco transpile component.wasm --instantiation sync|async.
When using instantiation mode, the output is a JS module with a single instantiate() function.
For async instantiation, the instantiate function takes the following signature:
export async function instantiate(
getCoreModule: (path: string) => Promise<WebAssembly.Module>,
imports: {
[importName: string]: any
},
instantiateCore?: (module: WebAssembly.Module, imports: Record<string, any>) => Promise<WebAssembly.Instance>
): Promise<{ [exportName: string]: any }>;
imports allows customizing the imports provided for instantiation.
instantiateCore defaults to WebAssembly.instantiate.
getCoreModule can typically be implemented as:
export async function getCoreModule(path: string) {
return await WebAssembly.compile(await readFile(new URL(`./${path}`, import.meta.url)));
}
For synchronous instantiation, the instantiate function has the following signature:
export function instantiate(
getCoreModule: (path: string) => WebAssembly.Module,
imports: {
[importName: string]: any
},
instantiateCore?: (module: WebAssembly.Module, imports: Record<string, any>) => WebAssembly.Instance
): Promise<{ [exportName: string]: any }>;
Where instead of promises, all functions are synchronous.
Jco Example Workflow
Given an existing Wasm Component, jco provides the tooling necessary to work with this Component fully natively in JS.
Jco also provides an experimental feature for generating components from JavaScript by wrapping ComponentizeJS in the jco componentize command.
To demonstrate a full end-to-end component, we can create a JavaScript component embedding Spidermnokey then run it in JavaScript.
Installing Jco
Either install Jco globally:
$ npm install -g @bytecodealliance/jco
$ jco --version
1.0.3
Or install it locally and use npx to run it:
$ npm install @bytecodealliance/jco
$ npx jco --version
1.0.3
Local usage can be preferable to ensure the project is reproducible and self-contained, but requires
replacing all jco shell calls in the following example with either ./node_modules/.bin/jco or npx jco.
Installing ComponentizeJS
To use ComponentizeJS, it must be separately installed, globally or locally depending on whether Jco was installed globally or locally. Globally:
$ npm install -g @bytecodealliance/componentize-js
Or locally:
$ npm install @bytecodealliance/componentize-js
Now the jco componentize command will be ready to use.
Creating a Component with ComponentizeJS
This Cowsay component uses the following WIT file (WIT is the typing language used for defining Components):
cowsay.wit
package local:cowsay;
world cowsay {
export cow: interface {
enum cows {
default,
owl
}
say: func(text: string, cow: option<cows>) -> string;
}
}
We can implement this with the following JS:
cowsay.js
export const cow = {
say (text, cow = 'default') {
switch (cow) {
case 'default':
return `${text}
\\ ^__^
\\ (oo)\\______
(__)\\ )\/\\
||----w |
|| ||
`;
case 'owl':
return `${text}
___
(o o)
( V )
/--m-m-
`;
}
}
};
To turn this into a component run:
$ jco componentize cowsay.js --wit cowsay.wit -o cowsay.wasm
OK Successfully written cowsay.wasm with imports ().
Inspecting Component WIT
As a first step, we might like to look instead this binary black box of a Component and see what it actually does.
$ jco wit cowsay.wasm
package root:component;
world root {
export cow: interface {
enum cows {
default,
owl,
}
say: func(text: string, cow: option<cows>) -> string;
}
}
Transpiling to JS
To execute the Component in a JS environment, use the jco transpile command to generate the JS for the Component:
$ jco transpile cowsay.wasm -o cowsay
Transpiled JS Component Files:
- cowsay/cowsay.core.wasm 7.61 MiB
- cowsay/cowsay.d.ts 0.07 KiB
- cowsay/cowsay.js 2.62 KiB
- cowsay/interfaces/cow.d.ts 0.21 KiB
Now the Component can be directly imported and used as an ES module:
test.js
import { cow } from './cowsay/cowsay.js';
console.log(cow.say('Hello Wasm Components!'));
For Node.js to allow us to run native ES modules, we must first create or edit the local package.json file to include a "type": "module" field:
package.json
{
"type": "module"
}
The above JavaScript can now be executed in Node.js:
$ node test.js
Hello Wasm Components!
\ ^__^
\ (oo)\______
(__)\ )/\
||----w |
|| ||
Passing in the optional second parameter, we can change the cow:
test.js
import { cow } from './cowsay/cowsay.js';
console.log(cow.say('Hello Wasm Components!', 'owl'));
$ node test.js
Hello Wasm Components!
___
(o o)
( V )
/--m-m-
It can also be executed in a browser via a module script:
<script type="module" src="test.js"></script>
There are a number of custom transpilation options available, detailed in the API section.
Host Bindings
The default mode for host bindings in JS hosts in Jco is through the high-level JS-based bindgen.
The benefit of this approach is that all host bindings are available as normal JS imports. For
example, JavaScript developers can directly import a function like
import { getRandomBytes } from 'wasi:random/random', and directly interact with the bindings
at a high level.
This also makes it easy to provide custom or virtual implementations for bindings using the same host semantic conventions.
But for performance-sensitive applications, host bindings still need to have a fast path for optimized bindgen.
Using Native Host Bindings
Given a JS host that implements such a binding, the --import-bindings flag may be used to customize
which host bindings mode to use:
- The default bindgen mode is
--import-bindings=jsusing high-level JS bindings for all imports. - When generating
--import-bindings=hybrid, Jco will still generate the high-level bindgen for all imports, but check for aSymbol.for('cabiLower')and use this optimized bindgen when available on a function-by-function basis. - For
--import-bindings=optimized, Jco will omit outputting the high-level JS bindgen for imports, and instead use the low-level bindgen function directly, assumingSymbol.for('cabiLower')is defined on all imports. - For
--import-bindings=direct-optimized, instead of reading aSymbol.for('cabiLower'), Jco will assume that imports are all these lower functions instead (useful in instantiatio mode).
This scheme implies instantiation mode to provide the host bindings, or for the host to support
providing the imports as a host ESM import scheme such as import { getRandomBytes } from 'wasi:random/random'.
Optimized Host Bindings Spec
fn[Symbol.for('cabiLower')](canonOpts) -> coreFn
A function that has a native optimized implementation, can expose its native optimized bindgen through
a Symbol.for('cabiLower') method, taking a canonOpts object.
The following canonOpts fields may be defined as needed:
memory: The WebAssembly memory object for the component to which we are binding, if needed.realloc: The realloc function inside of the component we are binding, per component model semantics, if needed.postReturn: The post-return function for the call, if needed.stringEncoding: If needed, with'utf8'as the default.resourceTables: If needed, an ordered list of resource tables in which they uniquely appear in the function parameters and results of typeResourceTable[].
The return value of this function is then a new function, coreFn, which represents an optimized
native function which can be provided as a direct core function import to the
WebAssembly.instantiate operation of the core binary for the component being linked, providing a
direct host-native binding to the inner core binary of the component without needing an intermediate
lowering operation in the component model semantics.
ResourceTable: number[]
Resource handles are tracked in handle tables, a set of shared slab data structures primarily relating handles to resource ids (reps) for the particular table. Each resource usually has a unique handle table assign for every component it is used in.
When handles are passed between component functions, resource state needs to be maintained between these tables, therefore in optimized bindgen, this shared state needs to be operated on. For example, resource creation creates an own handle in the table for that resource of the component caller, requiring the creator to populate a table of the caller.
In optimized bindgen, this is acheived by mutating the data structure accordingly. Great care needs to be taken to ensure the full component model semantics are followed in this process.
The implementation here is based on a JS array of integers. This is done instead of using typed arrays because we need resizability without reserving a large buffer like resizable typed arrays might for the same use case (and unless that changes in future).
The number bits are the lowest 29 bits, while the flag bit for all data values is 1 << 30. We avoid the use of the highest bit entirely to not trigger SMI deoptimization.
Each entry consists of a pair of u32s, with each pair either a free list entry, or a data entry.
Free List Entries:
| index (x, u30) | |
|---|---|
| 32 bits | 32 bits |
| 01xxxxxxxxxxxxxxxxx | ################### |
Free list entries use only the first value in the pair, with the high bit always set to indicate that the pair is part of the free list. The first entry pair at indices 0 and 1 is the free list head, with the initial values of 1 << 30 and 0 respectively. Removing the 1 << 30 flag gives 0, which indicates the end of the free list.
Data Entries:
| scope (x, u30) | own(o), rep(x, u30) |
|---|---|
| 32 bits | 32 bits |
| 00xxxxxxxxxxxxxxxxx | 0oxxxxxxxxxxxxxxxxx |
Data entry pairs consist of a first u30 scope value and a second rep value. The field is only called the scope for interface shape consistency, but is actually used for the ref count for own handles and the scope id for borrow handles. The high bit is never set for this first entry to distinguish the pair from the free list. The second value in the pair is the rep for the resource, with the high bit in this entry indicating if it is an own handle.
The free list numbering and the handle numbering are the same, indexing by pair, so to get from a handle or free list numbering to an index, we multiply by two.
For example, to access a handle n, we read the pair of values n * 2 and n * 2 + 1 in the array to get the context and rep respectively. If the high bit is set on the context, we throw for an invalid handle. The rep value is masked out from the ownership high bit, also throwing for an invalid zero rep.
resourceInstance[Symbol.for('cabiRep')]
Normally imported resource classes do not have to define any special symbols, as they are assigned rep numbers when passed in.
When using hybrid or optimized bindgen, high-level functions may still return and take high-level resource classes as parameters. For example, a resource type used optimized in import bindgen might still be constructible elsewhere to be passed in as a parameter to an exported function of a component attached to that optimized low-level import bindgen.
As a result, when using low-level bindgen, any high-level resource instances MUST define a
Symbol.for('cabiRep') symbol in order for these resources to correctly interact with low-level
bindgen functions referring to those same resources.
ResourceClass[Symbol.for('cabiDispose')](rep) -> void
Just like Symbol.dispose is used in high-level bindgen for imported resources to provide a
destructor for when an own handle to a resource is dropped, low-level bindgen provides this hook
for imported resources through the cabiDispose function.
The Symbol.for('cabiDispose') function is an optional destructor which is available as a direct
static method on the imported resource class.
Unlike the other low-level functions, this one does not need to be bound and is called directly, as it takes the rep directly to handle internal destructor mechanisms.
Imported resources created externally are always "captured" explicitly when passed in to high-level
functions, even when defining Symbol.for('cabiRep'), so any GC is implicitly averted. In these
capture cases their resourceInstance[Symbol.dispose]() disposal will always be called instead
of cabiDispose, even if they do not define a Symbol.dispose. This allows any custom GC hooks to
apply correctly.
WIT Type Representations
Similar to any other guest langauge, there are multiple type systems in play when dealing with JS WebAssembly components.
Types represented in WebAssembly Interface Types ("WIT") must be converted down to types that are familiar for Javascript,
and Typescript (if dealing with jco types or jco guest-types subcommands).
This document details the type representations and usage for types that are defined in WIT and built into components.
Basic types
Here is a basic table of conversions between WIT types and JS types:
More complicated types that are built into WIT but require more work to translate are explained below.
| WIT type | JS Type |
|---|---|
u8 | number |
u16 | number |
u32 | number |
u64 | BigInt |
s8 | number |
s16 | number |
s32 | number |
s64 | BigInt |
f32 | number |
f64 | number |
bool | boolean |
char | string |
string | string |
Variants (variant)
note
See the Variant section of the WIT IDL for more information on Variants
Variants are like basic enums in most languages with one exception; members of the variant can hold a single data type. Alternative variant members may hold different types to represent different cases. For example:
variant exit-code {
success,
failure-code(u32),
failure-msg(string),
}
WIT syntax
variant filter {
all,
none,
some(list<string>),
}
Jco Representation
Jco represents variants as objects with a tag that represents the variant, and val that represents the content:
For example, pseudo Typescript for the of the above filter variant would look like the following:
// Filter with all
{
tag: 'all';
}
// Filter with None
{
tag: 'none';
}
// Filter with some and a list of strings
{
tag: 'some';
val: string[];
}
note
WIT variant's options may only contain one piece of data.
You can work around this limitation of variants by having the contained type be a tuple,
(e.g. tuple<string, u32, string>), or using a named record as the related data.
Records (record)
WIT Syntax
record person {
name: string,
age: u32,
favorite-color: option<string>,
}
Jco Representation
Jco represents records as the Javascript Object basic data type:
Given the WIT record above, you can expect to deal with an object similar to the following Typescript:
interface Person {
person: string;
age: number;
favoriteColor?: number;
}
note
If using jco guest-types or jco types, you will be able to use Typescript types that
properly constrain the Typescript code you write.
Options (option)
WIT Syntax
option<u32, u32>
option<string, u32>
Jco Representation
Jco represents options as an optional value or undefined, so some examples:
| Type | Representation (TS) | Example |
|---|---|---|
option<u32> | number | undefined | option<u32> -> number | undefined |
option<option<u32>> | { tag: "some" | "none", val: number } | option<u32> -> number | undefined |
warning
"single level" options are easy to reason about, but the doubly nested case (option<option<_>>) is more complex.
Due to the important distinction between a missing optional versus an option that contains an empty value,
doubly-nested (or more) options are encoded with the object encoding described above, rather than as an optional value.
options in context: Records
When used in the context of a record (which becomes a JS Object), optional values are represented as optional properties (i.e in TS a propName?: value).
options in context: Function arguments/return values
When used in the context of arguments or return to a function, single level options are represented as optional values:
Consider the following interface:
interface optional {
f: func(n: option<u32>) -> string;
}
An implementation of the function optional.f would look like the following Typescript:
function f(n?: number): string {
if (n === undefined) { return "no n provided"; }
return "n was provided";
}
Result (result)
Result types, as a general concept represent a result that may or may not be present, due to a failure. A result value either contains
a value that represents a completed computation (SuccessType), or some "error" that indicates a failure (ErrorType).
You can think of the type of a Result as:
Result<SuccessType, ErrorType>
The value you ultimately deal with is one or the other -- either the successful result or the error that represents the failure.
WIT Syntax
result<_, string>
result<, string>
result<t,e>
Jco representation
In Javsacript, computation that fails or errors are often represented as exceptions -- and depending on how
the result is used, Jco adheres to that representations.
When used as an output to a function, throwing an error will suffice. Given the following WIT interface:
add-overflow: func(lhs: u32, rhs: u32) -> result<u32, string>;
The following JS function would satistfy the WIT interface:
function addOverflow(lhs, rhs) {
let sum = lhs + rhs;
if (Nan.isNan(sum)) {
throw "ERROR: addition produced non-number value";
} else if (sum > 4294967295) {
throw "ERROR: u32 overflow";
}
return sum;
}
While JS automatically converts numbers, we must be careful to not attempt passing a number
that would not fit in a u32 (unsigned 32 bit integer) via WebAssembly.
note
How JS treats large numbers is not in focus here, but it is worth noting that
Number.MAX_VALUE + Number.MAX_VALUE === Infinity.
Typescript Schema
type Result<T,E> = { tag: 'ok', val: T } | { tag: 'err', val: E };
results in context: Function return values
When a result is returned directly from a function, any thrown error of the function is treated as the result error type, while any direct return value is treated as the result success type.
Consider the following interface:
interface fallible {
f: func(n: u32) -> result<string, string>;
}
An implementation of the function fallible.f would look like the following Typescript:
function f(n: number): string {
if (n == 42) { return "correct"; }
throw "not correct";
}
results in context: Container types (record, optional, etc)
A result stored inside a container type or in non-function argument/return contexts will look like a variant
type of the form { tag: 'ok', val: SuccessType } | { tag: 'err', val: ErrorType }.
For example, consider the following WIT interface:
interface fallible-reaction {
r: func(r: result<string, string>) -> string;
}
An implementation of the function fallible-reaction.r would look like the following Typescript:
type Result<T,E> = { tag: 'ok', val: T } | { tag: 'err', val: E };
function f(input: Result<string, string>): string {
switch (input.tag) {
case 'ok': return `SUCCESS, returned: [${input.val}]";
case 'err': return `ERROR, returned: [${input.val}]";
// We we should never reach the case below
default: throw Error("something has gone seriously wrong");
}
}
result considerations: Idiomatic JS errors for Host implementations
When running a component in a JS host, it is likely for host functions to throw real JS errors (objects which are descendants of the Error global object),
rather than the exact type expected by Jco.
This means that the default conversion mechanism for Jco would be a JS anti-pattern (i.e. throw 12345 versus throw new Error("error code 12345")).
To ensure smooth use of Jco-generated code from hosts, Error objects with a payload property will have the payload extracted as the result error type.
Consider the following WIT:
type error-code = u32;
interface only-throws {
just-throw: func() -> result<string, error-code>;
}
Consider the following host function adhering to the interface, and making use of idiomatic JS errors:
// The below code assumes interaction with a WIT which looks like a
function justThrow() {
const plainError = new Error("Error for JS users");
const errorWithPayload = Object.assign(plainError, { payload: 1111 });
throw errorWithPayload;
}
Tuples (tuple)
Tuples are a container type that has a fixed size, types somewhat analogous to a fixed size list.
Tuples can be combined with type renaming to produce types that carry some semantic meaning. For example:
type point = tuple<u32,u32>
Note that tuples can be combined with custom user-defined types like records and variants, options and results. For example:
variant example-var {
nothing,
value(u64),
}
record example-rec {
fst: string,
snd: u32,
}
type maybe-num = option<u32>;
type num-or-err-str = result<u32, string>;
type examples = tuple<example-rec, example-var, maybe-num, num-or-err-str>;
WIT Syntax
tuple<u32, u32>
tuple<string, u32>
Jco Representation
Jco represents tuples as lists (arrays), so some examples:
| Type | Representation (TS) | Example |
|---|---|---|
tuple<u32, u32> | [number, number] | tuple<u32, u32> -> [number, number] |
tuple<string, u32> | [string, number] | tuple<string, u32> -> [string, number] |
List (list)
WIT Syntax
list<u8>
list<string>
Jco Representation
Jco represents lists with native Javscript Arrays, with the exception of a list<u8>:
| Type | Representation (TS) | Example |
|---|---|---|
list<u8> | Uint8Array | list<u8> -> Uint8Array |
list<t> | T[] | list<string> -> string[] |
Resources (resource)
note
See the WIT IDL description of Resources for more information
Resources represent objects that can not be trivially serialized and send copied to another component or the host. Components or host expose resources almost as a reference to internal state that methods can be called on -- without providing the actual internals of the resource in question.
WIT Syntax
resource blob {
constructor(init: list<u8>);
write: func(bytes: list<u8>);
read: func(n: u32) -> list<u8>;
merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
}
Jco representation
The example above could be represented with the following class in Typescript pseudo-code:
class Blob {
constructor(init: Uint8Array);
write(bytes: Uint8Array);
read(n: number): UInt8Array;
static merge(lhs: Uint8Array, rhs: Uint8Array): Blob;
}
Manual Wasm instantiation with WASI Overrides
When a Wasm component depends on functionality provided by WASI, the jco transpile produces a WebAssembly
module that can be loaded from NodeJS or the Browser that includes usages of unresolved imports like wasi:random/random.
note
Normally, WASI imports that need to be sourced from elsewhere would be mapped, using the
--map option to jco transpile.
These instructions are for when mapping is insufficient or implementations must be redirected or changed at instantiation time.
A common usage of transpilation is to map the imports to a known package, like @bytecodealliance/preview2-shim:
jco transpile \
component.wasm \
--output dist/transpiled \
--map wasi:cli/*@0.2.0=@bytecodealliance/preview2-shim/cli#*
note
For more information, see the Map Configuration section of the Transpiling documentation
Sometimes you may want to use your own implementation of WASI interfaces (whether partial or complete), known/resolved only at instantiation time.
Manual instantiation of a transpiled component with no overrides
To use custom instantiations, in NodeJS we must build with the async instantiation mode:
jco transpile \
component.wasm \
--instantiation async \
--output dist/transpiled
We can instantiate the WebAssembly component for use with no custom overrides
(i.e. the default WASI implementations provided by preview2-shim):
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
async function main() {
const wasmESModule = await import("path/to/transpiled/component.js");
const loader = async (path) => {
const buf = await readFile(`./dist/transpiled/${path}`);
return await WebAssembly.compile(buf.buffer);
};
const component = wasmESModule.instantiate(loader, new WASIShim().getImportObject());
// TODO: add code that utilizes the component's exports
}
await main();
note
When dealing with browser environments, the loader function is not necessary, and null/undefined can be used.
This is identical to mapping all imports to those provided by @bytecodealliance/preview2-shim.
Manual instantiation of a transpiled component with custom overrides
To use custom component overrides in addition to the WASI imports provided by preview2-shim,
as before build the component with the async instantiation mode:
jco transpile \
component.wasm \
--instsantiation async \
--output dist/transpiled \
--map wasi:cli/*@0.2.0=@bytecodealliance/preview2-shim/cli#*
Then write an ES Module like the following:
import { readFile } from "node:fs/promises";
import { random } from "@bytecodealliance/preview2-shim";
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
async function main() {
/// Load the ES module generated by `jco transpile`
const wasmESModule = await import("./dist/transpiled/component.js");
// Build a customized WASI shim by mizing custom implementations
// and the provided implementation
const customShim = new WASIShim({
random: {
// For these two interfaces we re-use the default provided shim
random: random.random,
"insecure-seed": random.insecureSeed,
// For insecure, we can supply our own custom implementation
// (in this case, one that is *VERY* insecure)
insecure: {
getInsecureRandomBytes: (len) => {
return new Uint8Array(Number(len)).fill(0);
},
getInsecureRandomU64: () => 42n,
},
},
});
const loader = async (path) => {
const buf = await readFile(`./dist/transpiled/${path}`);
return await WebAssembly.compile(buf.buffer);
};
// Instantiate the Wasm component's ES module
const component = await wasmESModule.instantiate(
loader,
customShim.getImportObject(),
);
// TODO: add code to utilize the component exports
}
await main();
Using WASIShim, you can generate your own custom implementations of WASI, making use of
the published shims where necessary.
Versioned imports with WASIShim
You can also use verisons with the import objects produced by WASIShim:
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';
import type {
VersionedWASIImportObject,
WASIImportObject,
} from '@bytecodealliance/preview2-shim/instantiation';
const shim = new WASIShim();
const unversioned: WASIImportObject = shim.getImportObject();
// console.log('unversioned', unversioned);
unversioned satisfies WASIImportObject;
unversioned satisfies VersionedWASIImportObject<''>;
const versioned: VersionedWASIImportObject<'0.2.3'> = shim.getImportObject({
asVersion: '0.2.3',
});
//console.log('versioned', versioned);
versioned satisfies VersionedWASIImportObject<'0.2.3'>;
Common issues
Contributing to the Codebase
Development is based on a standard NodeJS workflow, i.e.:
npm install
npm run build
npm run test
Prerequisites
Required prerequisites for building jco include:
- Latest stable Rust with the
wasm32-wasitarget - Node.js 18+ & npm (https://nodejs.org/en)
Rust Toolchain
The latest Rust stable toolchain can be installed using rustup.
Specifically:
rustup toolchain install stable
rustup target add wasm32-wasi
In case you do not have rustup installed on your system, please follow the installation instructions on the official Rust website based on your operating system
Project Structure
jco is effectively a monorepo consisting of the following projects:
crates/js-component-bindgen: Rust crate for creating JS component bindgen, published under https://crates.io/crates/js-component-bindgen.crates/js-component-bindgen-component: Component wrapper crate for the component bindgen. This allows bindgen to be self-hosted in JS.crates/wasm-tools-component: Component wrapper crate for wasm-tools, allowing jco to invoke various Wasm toolchain functionality and also make it available through the jco API.src/api.js: The jco API which can be used as a library dependency on npm. Published as https://npmjs.org/package/@bytecodealliance/jco.src/jco.js: The jco CLI. Published as https://npmjs.org/package/@bytecodealliance/jco.packages/preview2-shim: The WASI Preview2 host implementations for Node.js & browsers. Published as https://www.npmjs.com/package/@bytecodealliance/preview2-shim.packages/preview3-shim: The WASI Preview3 host implementations for Node.js
Files that should be checked in
The repository is for project related code only -- avoid checking in files related to specific platforms or IDEs. One off configuration and/or secrets should of course not be checked in either.
If there is information/configuration that is important for users or developers to see, include them in documentation and/or examples with appropriate context/explanation.
Building
To build jco, run:
npm install
npm run build
Testing
There are three test suites in jco:
npm run test: Project-level transpilation, CLI & API tests.npm run test --workspace packages/preview2-shim:preview2-shimunit tests.npm run test --workspace packages/preview3-shim:preview3-shimunit tests.test/browser.html: Bare-minimum browser validation test.cargo test: Wasmtime preview2 conformance tests (not currently passing).
warning
Generally, when running jco tests, you will want to build the project first during a test run,
to ensure the latest version of Rust code (i.e. js-component-bindgen is in use):
cd packages/jco
npm run build && npm run test api.js
The command above runs a single test file -- the name of the file is relative to packages/jco/test.
Running tests without bundling
Tests can be run without bundling via npm run build:dev && npm run test:dev.
Running specific tests
JS tests are powered by vitest, and a specific test suite can be run by passing
the filename to npm run test:
cd packages/jco
npm run test runtime.js
For example, to run multiple tests in a given folder:
cd packages/jco
npm run test test/p3/*.js
Commits
Jco and related subprojects use Conventional Commits. Using Conventional Commits helps the project maintain consistency in commit messages, and powers release automation.
CI enforces that commits are structured in a conventional commit style (see commitlint.config.mjs.
Special care must also be taken to ensure PR titles are formatted in a way that matches conventional commits as well,
when performing squash merges.
The following types are valid:
buildchorecidebugdocsfeatfixperfrefactorreleaserevertstyletest
The following project scopes are valid:
jcop2-shimp3-shimbindgentranspile
For changes made to projects in the repository to be included in releases, the appropriate project scope must be applied.
Since changes that should be made to the repo may not always have a project-specific scope, the following scopes can be used as well:
depsciops
Here are a few example commit messages:
chore(jco): update componentize-js dependency to X.X.X
feat(ci): add commitlint to actions workflows
jco is a Bytecode Alliance project and follows the Bytecode Alliance's Code of Conduct and Organizational Code of Conduct.
Using this repository
You can run the website locally using the mdBook command line tool.
Prerequisites
To use this repository, you need mdBook installed on your workstation.
Running the website locally
After installing mdBook, on GitHub, click the Fork button in the upper-right area of the screen to create a copy of the jco repository in your account. This copy is called a fork.
Next, clone it locally by executing the command below.
git clone https://github.com/bytecodealliance/jco/
cd docs
To build and test the site locally, run:
mdbook serve --open
Submitting Changes
- Follow the instructions above to make changes to your website locally.
- When you are ready to submit those changes, go to your fork and create a new pull request to let us know about it.
Everyone is welcome to submit a pull request! Once your pull request is created, we'll try to get to reviewing it or responding to it in at most a few days. As the owner of the pull request, it is your responsibility to modify your pull request to address the feedback that has been provided to you by the reviewer.