如何使用Node.js运行Go的WASM程序?

huangapple go评论94阅读模式
英文:

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

问题

我使用Go创建了一个测试的WASM程序。在程序的主函数中,它向"global"添加了一个API,并在一个通道上等待,以避免退出。它类似于你可以在互联网上找到的典型的hello-world Go WASM程序。

我的测试WASM程序在浏览器中运行良好,但是我希望能够在Node.js中运行它并调用API。如果可能的话,我将基于它创建一些自动化测试。

我尝试了很多方法,但是我无法让它在Node.js中工作。问题是,在Node.js中无法在"global"中找到API。我该如何在Node.js中运行一个带有导出API的Go WASM程序?

(如果需要更多细节,请告诉我)

谢谢!


更多细节:

--- 在Go的一侧(伪代码)---

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())
        }
    }
}

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

--- 在浏览器的一侧---

<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>

--- 在Node.js的一侧---

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);
});

这段伪代码代表了我真实代码的99%内容(只删除了与业务相关的细节)。问题是,我不仅需要在Node.js中运行wasm程序(myecho.wasm),还需要调用"api"(myEcho),并且我需要传递参数并接收返回值,因为我想为这些"api"创建自动化测试。使用Node.js,我可以在命令行环境中启动测试js脚本并验证输出。对于这种情况,浏览器并不是一个方便的工具。

仅仅通过node wasm_exec.js myecho.wasm运行程序对于我的情况来说是不够的。

英文:

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(&quot;My Web Assembly&quot;)
    js.Global().Set(&quot;myEcho&quot;, myEcho())
    &lt;-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 ---

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

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

globalThis.require = require;
globalThis.fs = require(&quot;fs&quot;);
globalThis.TextEncoder = require(&quot;util&quot;).TextEncoder;
globalThis.TextDecoder = require(&quot;util&quot;).TextDecoder;

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

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

require(&quot;./wasm_exec&quot;);

const go = new Go();
go.argv = process.argv.slice(2);
go.env = Object.assign({ TMPDIR: require(&quot;os&quot;).tmpdir() }, process.env);
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) =&gt; {
	go.run(result.instance);
}).then(_ =&gt; {
	console.log(go.exports.myEcho(&quot;hello&quot;, &quot;ahoj&quot;, &quot;ciao&quot;));
}).catch((err) =&gt; {
	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.

答案1

得分: 1

很高兴了解更多关于你的环境和你实际想要做什么的细节。你可以发布代码本身、编译命令以及涉及的所有工具的版本。

在没有这些细节的情况下,尝试回答问题:

Go WASM非常面向浏览器,因为Go编译器需要wasm_exec.js中的glue js来运行。Node.js不应该有问题,以下命令应该可以工作:

node wasm_exec.js main.wasm

其中wasm_exec.js是与你的go发行版一起提供的glue code,通常可以在$(go env GOROOT)/misc/wasm/wasm_exec.js找到,而main.wasm是你编译的代码。如果失败了,你也可以发布输出。

还有另一种将Go代码编译为wasm的方法,绕过wasm_exec.js,即使用TinyGo编译器输出启用了wasi的代码。你可以尝试按照他们的说明编译你的代码。

例如:

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

你可以为例构建一个名为wasi.js的javascript文件:

"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);
})();

最近的node版本支持实验性的wasi功能:

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

这些通常是你在使用Go和WASM时尝试的方法,但是没有更多的细节,很难确定具体是什么问题。

英文:

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:

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

(async () =&gt; {
  const wasm = await WebAssembly.compile(
    fs.readFileSync(&quot;./main.wasm&quot;)
  );
  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.

答案2

得分: 0

经过一番努力,我发现原因比我预期的要简单。

我无法在Node.js中获取导出的API函数,只是因为在我尝试调用它们时,API尚未被导出!

当wasm程序加载并启动时,它与调用程序(在Node中运行的js)并行运行。

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

在"HERE"处的代码执行得太早了,wasm程序的main()还没有完成导出API。

当我将Node脚本更改为以下内容时,它起作用了:

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);
});

(只包括更改的部分)

我知道这似乎不是一个完美的解决方案,但至少证明了我对根本原因的猜测是正确的。

但是...为什么在浏览器中没有发生这种情况呢?叹气...

英文:

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) =&gt; {
	go.run(result.instance);
}).then(_ =&gt; {
	let retry = setInterval(function () {
		if (typeof(go.exports.myEcho) != &quot;function&quot;) {
			return;
		}

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

		clearInterval(retry);
	}, 500);
}).catch((err) =&gt; {
	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...

huangapple
  • 本文由 发表于 2023年2月7日 19:05:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/75372474.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定