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:
- 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"))
}
- Compile it to wasm:
GOOS=js GOARCH=wasm go build -o hello.wasm ./main.go
- Build wasmtime exec runtime
$ cd wasmtimexec/example
$ go build -o wasmtimeexec ./main.go
- 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!