Handling Input and Output

Note: The final code for this example can be found on Github: handling-io.

In the Hello World example, we covered how to run the helloworld Wasm module, and then read its output. However, there may be times we want to interact with WASI modules that accept input as well!

In this example, we will be using the QuickJS WASI module, to execute Javascript in the QuickJS runtime. To handle input, we will create our own stdinRead function, that is bound to the zero-index file descriptor in WasmFS (/dev/stdin). This will allow us to intercept read requests, and send whatever input we would like to the WASI application. See the code below:

import { WASI } from '@wasmer/wasi';
import browserBindings from '@wasmer/wasi/lib/bindings/browser';
import { WasmFs } from '@wasmer/wasmfs';
import { lowerI64Imports } from "@wasmer/wasm-transformer";
​
// The file path to the wasi module we want to run
const wasmFilePath = './quickjs.wasm';
​
/**
This function removes the ansi escape characters
(normally used for printing colors and so)
Inspired by: https://github.com/chalk/ansi-regex/blob/master/index.js
MIT License Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
*/
const cleanStdout = (stdout) => {
const pattern = [
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
].join("|");
​
const regexPattern = new RegExp(pattern, "g");
return stdout.replace(regexPattern, "");
};
​
// Instantiate a new WASI and WasmFs Instance
// NOTE: For node WasmFs is not needed, and the native Fs module is assigned by default
// In this case, we want to show off WasmFs for the browser use case, and we want to
// "Sandbox" our file system operations
const wasmFs = new WasmFs();
let wasi = new WASI({
// Arguments to pass to the Wasm Module
// The first argument usually should be the filepath to the "executable wasi module"
// That we want to run.
args: [wasmFilePath],
// Environment variables that are accesible to the Wasi module
env: {},
// Bindings that are used by the Wasi Instance (fs, path, etc...)
bindings: {
...browserBindings,
fs: wasmFs.fs
}
});
​
// Assign all reads to fd 0 (in this case, /dev/stdin) to our custom function
// Handle read of stdin, similar to C read
// https://linux.die.net/man/2/read
// Implemented here within the WasmFs Dependancy, Memfs:
// https://github.com/streamich/memfs/blob/master/src/volume.ts#L1020
// NOTE: This function MUST BE SYNCHRONOUS,
// per the C api. Otherwise, the Wasi module will error.
let readStdinCounter = 0
const stdinRead = (
stdinBuffer, // Uint8Array of the buffer that is sent to the guest Wasm module's standard input
offset, // offset for the standard input
length, // length of the standard input
position // Position in the input
) => {
​
// Per the C API, first read should be the string
// Second read would be the end of the string
if (readStdinCounter % 2 !== 0) {
readStdinCounter++;
return 0;
}
​
// Use window.prompt to synchronously get input from the user
// This will block the entire main thread until this finishes.
// To do this more clean-ly, it would be best to use a Web Worker
// and Shared Array Buffer. And use prompt as a fallback
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
// https://github.com/wasmerio/wasmer-js/blob/master/packages/wasm-terminal/src/process/process.ts#L174
let responseStdin = prompt(
`Please enter standard input to the quickjs prompt\n`
);
​
// When the user cancels, throw an error to get out of the standard input read loop
// From the guest Wasm modules (quickjs)
if (responseStdin === null) {
const userError = new Error("Process killed by Prompt Cancellation");
userError.user = true;
throw userError;
return -1;
}
responseStdin += "\n";
​
// Encode the string into bytes to be placed into the buffer for standard input
const buffer = new TextEncoder().encode(responseStdin);
for (let x = 0; x < buffer.length; ++x) {
stdinBuffer[x] = buffer[x];
}
​
// Return the current stdin, per the C API
return buffer.length;
}
​
// Assign all reads to fd 0 (in this case, /dev/stdin) to our custom function
wasmFs.volume.fds[0].node.read = stdinRead;
​
// Async Function to run our wasi module/instance
const startWasiTask = async () => {
// Fetch our Wasm File
const response = await fetch(wasmFilePath);
const responseArrayBuffer = await response.arrayBuffer();
const wasmBytes = new Uint8Array(responseArrayBuffer);
​
// Lower the WebAssembly Module bytes
// This will create trampoline functions for i64 parameters
// in function calls like:
// https://github.com/WebAssembly/WASI/blob/master/phases/old/snapshot_0/docs/wasi_unstable.md#clock_time_get
// Allowing the Wasi module to work in the browser / node!
const loweredWasmBytes = await lowerI64Imports(wasmBytes);
​
// Instantiate the WebAssembly file
let wasmModule = await WebAssembly.compile(wasmBytes);
let instance = await WebAssembly.instantiate(wasmModule, {
...wasi.getImports(wasmModule)
});
// Start the WebAssembly WASI instance!
try {
wasi.start(instance);
} catch(e) {
// Catch errors, and if it is not a forced user error (User cancelled the prompt)
// Log the error and end the process
if (!e.user) {
console.error(e);
return;
}
}
​
// User cancelled the prompt!
​
// Output what's inside of /dev/stdout!
let stdout = await wasmFs.getStdOut();
​
// Clean up some of the ANSI Codes from QuickJS:
// 1. Split by the Clear ANSI Code ([J), and only get the input (-2), and the output (-1)
// 2. Cleanup the remaining ANSI Code Output
const splitClearStdout = stdout.split('[J');
stdout = splitClearStdout[splitClearStdout.length - 2] + splitClearStdout[splitClearStdout.length - 1];
stdout = `\n${cleanStdout(stdout)}\n`;
​
// Add the Standard output to the dom
console.log('Standard Output: ' + stdout);
};
startWasiTask();

If you want to run the examples from the docs codebase directly, you can also do:

git clone https://github.com/wasmerio/docs.wasmer.io.git
cd docs.wasmer.io/integrations/js/wasi/browser/examples/handling-io
npm run dev