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
--mapoption tojco 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
loaderfunction is not necessary, andnull/undefinedcan be used.
This is identical to mapping all imports to those provided by @bytecodealliance/preview2-shim.
Node filesystem directory capabilities
The Node Preview 2 shim preserves legacy directory behavior by default, including
rejecting mutate-directory requests. To enable support for guests such as Perry,
create an isolated filesystem and provide it to WASIShim:
const shim = new WASIShim({
filesystem: filesystem.createFilesystem({
preopens: { "/data": "/tmp/guest-data" },
enableMutateDirectory: true,
}),
});
Here filesystem comes from @bytecodealliance/preview2-shim on Node.js.
The option is off by default because enforcing these permissions breaks some
adapted Preview 1 guests that omit the flag. With the option enabled, preopens
permit reads and mutations, subject to host permissions. A guest opening a child
directory must request the WASI
mutate-directory flag if it will modify entries, change metadata, or open files
for writing. Without that flag, mutation attempts through the directory handle
fail with read-only; the handle can still be used for reads.
Rust-based guests may also send mutate-directory when opening regular files.
With the option enabled, the shim accepts these requests and removes that directory-only flag from the
returned file’s flags. File writes still require write and a parent directory
that permits mutation. The option does not grant additional write access and
does not change other filesystem instances or previously opened handles.
For components using global filesystem imports, pass { enableMutateDirectory: true }
as the second argument to filesystem._setPreopens(...) before running the guest.
See the Preview 2 shim’s sandboxing documentation for the Node implementation’s capability and preopen behavior.
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 versions 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'>;
Sandboxing with WASIShim
By default, the preview2-shim provides full access to the host filesystem, environment variables,
and network - matching the default behavior of Node.js libraries. You can use the sandbox option
to restrict what guests can access.
Each WASIShim instance has its own isolated preopens, environment variables, and arguments.
Multiple instances with different configurations will not affect each other:
Fully sandboxed instance
To create a fully sandboxed instance with no filesystem, network, or environment access:
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
const sandboxedShim = new WASIShim({
sandbox: {
preopens: {}, // No filesystem access
env: {}, // No environment variables
args: ['my-program'], // Custom arguments
enableNetwork: false, // Disable network access
}
});
const component = await wasmESModule.instantiate(
loader,
sandboxedShim.getImportObject(),
);
Limited filesystem access
To provide limited filesystem access by mapping virtual paths to host paths:
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
const limitedShim = new WASIShim({
sandbox: {
preopens: {
'/data': '/tmp/guest-data', // Guest sees /data, maps to /tmp/guest-data
'/config': '/etc/app' // Guest sees /config, maps to /etc/app
},
env: { 'ENV1': '42' }, // Only expose specific env vars
}
});
const component = await wasmESModule.instantiate(
loader,
limitedShim.getImportObject(),
);
Sandbox options
The sandbox configuration object supports the following options:
| Option | Type | Default | Description |
|---|---|---|---|
preopens | Record<string, string> | Full filesystem | Map of virtual paths to host paths. Use {} for no filesystem access. |
env | Record<string, string> | process.env | Environment variables visible to the guest. Use {} for no env access. |
args | string[] | process.argv | Command-line arguments visible to the guest. |
enableNetwork | boolean | true | Whether to enable network access (sockets, HTTP). |