prep / wasmexec

Implementation of wasm_exec.js for Go Wasm runtimes

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Panic when flag package is used in wasm

mengqiy opened this issue · comments

No matter if the flag package is a direct dependency or transitive dependency, it will panic.

Minimum steps to reproduce it:

  1. Change main.go in https://github.com/prep/wasmexec/blob/main/examples/hello/main.go to:
package main

import (
	"os"
	_ "flag"
)

func main() {
	_, _ = os.Stdout.Write([]byte("Hello from Go!\n"))
}
  1. Compile it to wasm:
GOOS=js GOARCH=wasm go build -o hello.wasm ./main.go
  1. Build wasmtime exec runtime
$ cd wasmtimexec/example
$ go build -o wasmtimeexec ./main.go
  1. Run it with hello.wasm:
./wasmtimeexec ../../examples/hello/hello.wasm

It will produce the following error:

panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
flag.init()
        /usr/local/go/src/flag/flag.go:1047 +0x2f

It seems the problem is that: flag pacakge try to run var CommandLine = NewFlagSet(os.Args[0], ExitOnError) and os.Args is expected to hold the command-line arguments, starting with the program name.
It seems os.Args is not correctly populated somehow.

I would guess you need to pass args in your script to wasmtime run. args are totally optional as they aren't defined by WebAssembly, rather WASI, which is optional

wasmtime run [OPTIONS] <MODULE> [--] [ARGS]...

@codefromthecrypt This isn't being run from the wasmtime command line tool. Instead, it's using my example wasmtimeexec binary to run the hello example. The example itself is compiled with the standard Go runtime (as opposed to tinygo), so WASI doesn't come into play 😉

The solution here would be to take a hint from wasm_exec.js and implement this before calling the exported run() function:

// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;

const strPtr = (str) => {
	const ptr = offset;
	const bytes = encoder.encode(str + "\0");
	new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
	offset += bytes.length;
	if (offset % 8 !== 0) {
		offset += 8 - (offset % 8);
	}
	return ptr;
};

const argc = this.argv.length;

const argvPtrs = [];
this.argv.forEach((arg) => {
	argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);

const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
	argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);

const argv = offset;
argvPtrs.forEach((ptr) => {
	this.mem.setUint32(offset, ptr, true);
	this.mem.setUint32(offset + 4, 0, true);
	offset += 8;
});

// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
	throw new Error("total length of command line and environment variables exceeds limit");
}

this._inst.exports.run(argc, argv);

This shouldn't be too hard and should probably be implemented on the Memory interface. I'll have a look at this when I can. Work is kind of hectic right now so it might not be immediate 🙏

oops sorry indeed this isn't wasi :D motor memory fail!

@prep @codefromthecrypt Thanks for investigating it!

I've just pushed a change to main that adds the wasmexec.SetArgs() function and updated the example implementations for reference. For wasmtime (which you seem to be using), it looks like this:

args := []string{"example.wasm", "-runtime=wasmtime", "arg1", "arg2"}
envs := []string{"HOME=/", "PWD=/home/test"}

argc, argv, err := wasmexec.SetArgs(instance.Memory, args, envs)
// ...

runFn := instance.GetFunc(store, "run")
// ...

_, err = runFn.Call(store, argc, argv)

Let me know if this works for you.

@prep It works great! Thanks a lot!