Zhou
Zhou

Reputation: 773

How can I run a Go WASM program using Node.js?

I created a test WASM program using Go. In the program's main, it adds an API to the "global" and waits on a channel to avoid from exiting. It is similar to the typical hello-world Go WASM that you can find anywhere in the internet.

My test WASM program works well in Browsers, however, I hope to run it and call the API using Node.js. If it is possible, I will create some automation tests based on it.

I tried many ways but I just couldn't get it work with Node.js. The problem is that, in Node.js, the API cannot be found in the "global". How can I run a GO WASM program (with an exported API) in Node.js?

(Let me know if you need more details)

Thanks!


More details:

--- On Go's side (pseudo code) ---

func main() {
    fmt.Println("My Web Assembly")
    js.Global().Set("myEcho", myEcho())
    <-make(chan bool)
}

func myEcho() js.Func {
    return js.FuncOf(func(this js.Value, apiArgs []js.Value) any {
        for arg := range(apiArgs) {
            fmt.Println(arg.String())
        }
    }
}

// build: GOOS=js GOARCH=wasm go build -o myecho.wasm path/to/the/package

--- On browser's side ---

<html>  
    <head>
        <meta charset="utf-8"/>
    </head>
    <body>
        <p><pre style="font-family:courier;" id="my-canvas"/></p>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("myecho.wasm"), go.importObject).then((result) => {
                go.run(result.instance);
            }).then(_ => {
                // it also works without "window."
                document.getElementById("my-canvas").innerHTML = window.myEcho("hello", "ahoj", "ciao");
                })
            })
        </script>
    </body>
</html>

--- On Node.js' side ---

globalThis.require = require;
globalThis.fs = require("fs");
globalThis.TextEncoder = require("util").TextEncoder;
globalThis.TextDecoder = require("util").TextDecoder;

globalThis.performance = {
    now() {
        const [sec, nsec] = process.hrtime();
        return sec * 1000 + nsec / 1000000;
    },
};

const crypto = require("crypto");
globalThis.crypto = {
    getRandomValues(b) {
        crypto.randomFillSync(b);
    },
};

require("./wasm_exec");

const go = new Go();
go.argv = process.argv.slice(2);
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
    go.run(result.instance);
}).then(_ => {
    console.log(go.exports.myEcho("hello", "ahoj", "ciao"));
}).catch((err) => {
    console.error(err);
    process.exit(1);
});

This pseudo code represents 99% content of my real code (only removed business related details). The problem is that I not only need to run the wasm program (myecho.wasm) by Node.js, but I also need to call the "api" (myEcho), and I need to pass it parameters and receive the returned values, because I want to create automation tests for those "api"s. With Node.js, I can launch the test js scripts and validate the outputs all in the command line environment. The browser isn't a handy tool for this case.

Running the program by node wasm_exec.js myecho.wasm isn't enough for my case.

Upvotes: 2

Views: 3051

Answers (2)

Zhou
Zhou

Reputation: 773

After some struggling, I noticed that the reason is simpler than I expected.

I couldn't get the exported API function in Node.js simply because the API has not been exported yet when I tried to call them!

When the wasm program is loaded and started, it runs in parallel with the caller program (the js running in Node).

WebAssembly.instantiate(...).then(...go.run(result.instance)...).then(/*HERE!*/)

The code at "HERE" is executed too early and the main() of the wasm program hasn't finished exporting the APIs yet.

When I changed the Node script to following, it worked:

WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
    go.run(result.instance);
}).then(_ => {
    let retry = setInterval(function () {
        if (typeof(go.exports.myEcho) != "function") {
            return;
        }

        console.log(go.exports.myEcho("hello", "ahoj", "ciao"));

        clearInterval(retry);
    }, 500);
}).catch((err) => {
    console.error(err);
    process.exit(1);
});

(only includes the changed part)

I know it doesn't seem to be a perfect solution, but at least it proved my guess about the root cause to be true.

But... why it didn't happen in browser? sigh...

Upvotes: 0

TachyonicBytes
TachyonicBytes

Reputation: 1143

It would be nice to know more details about your environment and what are you actually trying to do. You can post the code itself, compilation commands, and versions for all the tools involved.

Trying to answer the question without these details:

Go WASM is very browser oriented, because the go compiler needs the glue js in wasm_exec.js to run. Nodejs shouldn't have a problem with that, and the following command should work:

node wasm_exec.js main.wasm

where wasm_exec.js is the glue code shipped with your go distribution, usually found at $(go env GOROOT)/misc/wasm/wasm_exec.js, and main.wasm is your compiled code. If this fails, you can post the output as well.

There is another way to compile go code to wasm that bypasses wasm_exec.js, and that way is by using the TinyGo compiler to output wasi-enabled code. You can try following their instructions to compile your code.

For example:

tinygo build -target=wasi -o main.wasm main.go

You can build for example a javascript file wasi.js:

"use strict";
const fs = require("fs");
const { WASI } = require("wasi");
const wasi = new WASI();
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

(async () => {
  const wasm = await WebAssembly.compile(
    fs.readFileSync("./main.wasm")
  );
  const instance = await WebAssembly.instantiate(wasm, importObject);

  wasi.start(instance);
})();

Recent versions of node have experimental wasi support:

node --experimental-wasi-unstable-preview1 wasi.js

These are usually the things you would try with Go and WASM, but without further details, it is hard to tell what exactly is not working.

Upvotes: 1

Related Questions