WASI, Communication between Node.js and WASM modules, another way, with STDIN and STDOUT

My journey to free WASM from my browser

This blog post explains how to use stdin and stdout to communicate between the host application and the WebAssembly module.

The previous blog post is WASI, Communication between Node.js and WASM modules with the WASM buffer memory.

Requirements

You need to install the following:

Stdin, Stdout, Stderr?

In Node.js, stdin, stdout, and stderr are objects representing standard input, standard output, and standard error streams, respectively. These streams are used to read input from the user, write output to the console, and report errors to the console.

These streams are connected to the console by default, but you can redirect them to files or other streams. In addition, you can also listen for events on these streams to handle user input, output, and errors.

We can use this with the WebAssembly application.

If you read the beginning of the WASI section of the Node.js documentation, there is this (👋 this is an extract):

new WASI([options])

options <Object>

  • stdin <integer> The file descriptor used as standard input in the WebAssembly application. Default: 0.

  • stdout <integer> The file descriptor used as standard output in the WebAssembly application. Default: 1.

  • stderr <integer> The file descriptor used as standard error in the WebAssembly application. Default: 2.

That means we can use stdin to send data to the WASM program and stdout to receive data from the WASM program (or stderr to get the errors).

Let's create a new WASM program

We will first write a new WASM module without any function except the main function. Create a new GoLang module and a main.go file (in a directory):

go mod init function/hello
touch main.go

This is the content of the main.go file:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {

    // Read the data on stdin (from host)
    data, _ := io.ReadAll(os.Stdin) 

    // Send data on stdout (like a return value for the host)
    fmt.Println("👋 Data from Node.js:", "👋", string(data), "🌍")

    // Send data on stderr
    println("🤬 oups I did it again!")
}

Build the WASM module

tinygo build -o main.wasm -scheduler=none --no-debug -target wasi ./main.go

Run the WASM module from Node.js

Let's create a new file named index.mjs with the below content:

import * as fs from 'fs'
import * as path from 'path'
import * as os from 'node:os';
import * as crypto from 'node:crypto';

import { WASI } from 'wasi'

const uniqueId = crypto.randomUUID();

const stdinFile = path.join(os.tmpdir(), `stdin.wasm.${uniqueId}.txt`);
const stdoutFile = path.join(os.tmpdir(), `stdout.wasm.${uniqueId}.txt`);
const stderrFile = path.join(os.tmpdir(), `stderr.wadm.${uniqueId}.txt`);

fs.writeFileSync(stdinFile, "Start writing to stdin...");

const stdin = fs.openSync(stdinFile, 'r');
const stdout = fs.openSync(stdoutFile, 'a');
const stderr = fs.openSync(stderrFile, 'a');

const wasi = new WASI({
    args: [], 
    env: {},
    stdin, stdout, stderr,
    returnOnExit: true
});

const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

(async () => {

    const wasm = await WebAssembly.compile(
        fs.readFileSync("./function/main.wasm") // adapt the path
    );
    const instance = await WebAssembly.instantiate(wasm, importObject);

    // 👋 send data to the WASM program
    fs.writeFileSync(stdinFile, "Hello Bob Morane");

    wasi.start(instance);

    // 👋 get the result
    console.log(fs.readFileSync(stdoutFile, 'utf8').trim())
    // 👋 get the error
    console.log(fs.readFileSync(stderrFile, 'utf8').trim())

    fs.closeSync(stdin);
    fs.closeSync(stdout);
    fs.closeSync(stderr);

})();

And to execute the Node.js program, type the following command:

node --experimental-wasi-unstable-preview1 --no-warnings index.mjs

You should see this output:

👋 Data from Node.js: 👋 Hello Bob Morane 🌍
🤬 oups I did it again!

The complete source code is here: https://gitlab.com/wasmkitchen/wasi-nodejs-stdin-stdout

🎉 Thanks for reading! It was a little bit simpler than the previous method: WASI, Communication between Node.js and WASM modules with the WASM buffer memory, but keep in mind that the usage contexts and the performances could be different.

In the next blog post, we'll do our first steps with Wazero.

A very good information source that helps me a lot: