From 8e7a288bb4d19f5aaa3b2cbea9b09b485875e9ea Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 30 Oct 2024 11:01:21 +0100 Subject: [PATCH] wasm: use wasip1 API for parameter/env passing Instead of hardcoding the command line parameters and the environment variables in the binary, pass them at runtime to Node.js and use the WASIp1 API to retrieve them in wasm_exec.js. The only real benefit right now is that it becomes possible to change `go.argv` and `go.env` before running a wasm binary. This also changes the syscall package for GOOS=js: it now becomes more like a libc (using wasi-libc), which means error values like `syscall.EEXIST` will actually match the one returned by the relevant libc function. Wasm binary size for packages that import the os package will be increased somewhat. --- main.go | 11 ++-- src/runtime/nonhosted.go | 2 +- src/runtime/runtime_tinygowasm.go | 38 +++++++++++++ src/runtime/runtime_wasip1.go | 42 -------------- src/syscall/env_libc.go | 2 +- src/syscall/errno_other.go | 2 +- .../{errno_wasip1.go => errno_wasmlibc.go} | 2 +- src/syscall/syscall_libc.go | 2 +- src/syscall/syscall_libc_wasi.go | 2 +- src/syscall/syscall_nonhosted.go | 2 +- src/syscall/tables_nonhosted.go | 2 +- targets/wasm_exec.js | 55 ++++++++++++++++++- 12 files changed, 105 insertions(+), 57 deletions(-) rename src/syscall/{errno_wasip1.go => errno_wasmlibc.go} (83%) diff --git a/main.go b/main.go index 254e140cf2..c7c797dc8b 100644 --- a/main.go +++ b/main.go @@ -770,12 +770,11 @@ func Run(pkgName string, options *compileopts.Options, cmdArgs []string) error { // for the given emulator. func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, cmdArgs, environmentVars []string, timeout time.Duration, run func(cmd *exec.Cmd, result builder.BuildResult) error) (builder.BuildResult, error) { // Determine whether we're on a system that supports environment variables - // and command line parameters (operating systems, WASI) or not (baremetal, - // WebAssembly in the browser). If we're on a system without an environment, - // we need to pass command line arguments and environment variables through - // global variables (built into the binary directly) instead of the - // conventional way. - needsEnvInVars := config.GOOS() == "js" + // and command line parameters (operating systems, WASI) or not (baremetal). + // If we're on a system without an environment, we need to pass command line + // arguments and environment variables through global variables (built into + // the binary directly) instead of the conventional way. + needsEnvInVars := false for _, tag := range config.BuildTags() { if tag == "baremetal" { needsEnvInVars = true diff --git a/src/runtime/nonhosted.go b/src/runtime/nonhosted.go index ca5ab4c3c8..680fffcb5b 100644 --- a/src/runtime/nonhosted.go +++ b/src/runtime/nonhosted.go @@ -1,4 +1,4 @@ -//go:build baremetal || js || wasm_unknown +//go:build baremetal || wasm_unknown package runtime diff --git a/src/runtime/runtime_tinygowasm.go b/src/runtime/runtime_tinygowasm.go index f791ffacdf..cfe3fb1547 100644 --- a/src/runtime/runtime_tinygowasm.go +++ b/src/runtime/runtime_tinygowasm.go @@ -29,6 +29,44 @@ func proc_exit(exitcode uint32) //export __stdio_exit func __stdio_exit() +var args []string + +//go:linkname os_runtime_args os.runtime_args +func os_runtime_args() []string { + if args == nil { + // Read the number of args (argc) and the buffer size required to store + // all these args (argv). + var argc, argv_buf_size uint32 + args_sizes_get(&argc, &argv_buf_size) + if argc == 0 { + return nil + } + + // Obtain the command line arguments + argsSlice := make([]unsafe.Pointer, argc) + buf := make([]byte, argv_buf_size) + args_get(&argsSlice[0], unsafe.Pointer(&buf[0])) + + // Convert the array of C strings to an array of Go strings. + args = make([]string, argc) + for i, cstr := range argsSlice { + length := strlen(cstr) + argString := _string{ + length: length, + ptr: (*byte)(cstr), + } + args[i] = *(*string)(unsafe.Pointer(&argString)) + } + } + return args +} + +//go:wasmimport wasi_snapshot_preview1 args_get +func args_get(argv *unsafe.Pointer, argv_buf unsafe.Pointer) (errno uint16) + +//go:wasmimport wasi_snapshot_preview1 args_sizes_get +func args_sizes_get(argc *uint32, argv_buf_size *uint32) (errno uint16) + const ( putcharBufferSize = 120 stdout = 1 diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index ad66b0d860..3605a58ce9 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -2,10 +2,6 @@ package runtime -import ( - "unsafe" -) - type timeUnit int64 // libc constructors @@ -21,38 +17,6 @@ func init() { __wasm_call_ctors() } -var args []string - -//go:linkname os_runtime_args os.runtime_args -func os_runtime_args() []string { - if args == nil { - // Read the number of args (argc) and the buffer size required to store - // all these args (argv). - var argc, argv_buf_size uint32 - args_sizes_get(&argc, &argv_buf_size) - if argc == 0 { - return nil - } - - // Obtain the command line arguments - argsSlice := make([]unsafe.Pointer, argc) - buf := make([]byte, argv_buf_size) - args_get(&argsSlice[0], unsafe.Pointer(&buf[0])) - - // Convert the array of C strings to an array of Go strings. - args = make([]string, argc) - for i, cstr := range argsSlice { - length := strlen(cstr) - argString := _string{ - length: length, - ptr: (*byte)(cstr), - } - args[i] = *(*string)(unsafe.Pointer(&argString)) - } - } - return args -} - func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) } @@ -97,12 +61,6 @@ func beforeExit() { // Implementations of WASI APIs -//go:wasmimport wasi_snapshot_preview1 args_get -func args_get(argv *unsafe.Pointer, argv_buf unsafe.Pointer) (errno uint16) - -//go:wasmimport wasi_snapshot_preview1 args_sizes_get -func args_sizes_get(argc *uint32, argv_buf_size *uint32) (errno uint16) - //go:wasmimport wasi_snapshot_preview1 clock_time_get func clock_time_get(clockid uint32, precision uint64, time *uint64) (errno uint16) diff --git a/src/syscall/env_libc.go b/src/syscall/env_libc.go index 4ad078dc54..b534b527a8 100644 --- a/src/syscall/env_libc.go +++ b/src/syscall/env_libc.go @@ -1,4 +1,4 @@ -//go:build nintendoswitch || wasip1 +//go:build js || nintendoswitch || wasip1 package syscall diff --git a/src/syscall/errno_other.go b/src/syscall/errno_other.go index 3a06ac018f..8c58f5f01a 100644 --- a/src/syscall/errno_other.go +++ b/src/syscall/errno_other.go @@ -1,4 +1,4 @@ -//go:build !wasip1 && !wasip2 +//go:build !js && !wasip1 && !wasip2 package syscall diff --git a/src/syscall/errno_wasip1.go b/src/syscall/errno_wasmlibc.go similarity index 83% rename from src/syscall/errno_wasip1.go rename to src/syscall/errno_wasmlibc.go index c494d7da09..106b192ce4 100644 --- a/src/syscall/errno_wasip1.go +++ b/src/syscall/errno_wasmlibc.go @@ -1,4 +1,4 @@ -//go:build wasip1 +//go:build js || wasip1 package syscall diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 67cf6681f7..03c966683e 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -1,4 +1,4 @@ -//go:build nintendoswitch || wasip1 || wasip2 +//go:build js || nintendoswitch || wasip1 || wasip2 package syscall diff --git a/src/syscall/syscall_libc_wasi.go b/src/syscall/syscall_libc_wasi.go index 06169bb2b9..09536cc0b0 100644 --- a/src/syscall/syscall_libc_wasi.go +++ b/src/syscall/syscall_libc_wasi.go @@ -1,4 +1,4 @@ -//go:build wasip1 || wasip2 +//go:build js || wasip1 || wasip2 package syscall diff --git a/src/syscall/syscall_nonhosted.go b/src/syscall/syscall_nonhosted.go index b56d71af64..0d215af086 100644 --- a/src/syscall/syscall_nonhosted.go +++ b/src/syscall/syscall_nonhosted.go @@ -1,4 +1,4 @@ -//go:build baremetal || js || wasm_unknown +//go:build baremetal || wasm_unknown package syscall diff --git a/src/syscall/tables_nonhosted.go b/src/syscall/tables_nonhosted.go index e66620cbf4..a45834827e 100644 --- a/src/syscall/tables_nonhosted.go +++ b/src/syscall/tables_nonhosted.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build baremetal || nintendoswitch || js || wasm_unknown +//go:build baremetal || nintendoswitch || wasm_unknown package syscall diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index c430cc2b23..f5b76bbab3 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -135,6 +135,8 @@ global.Go = class { constructor() { + this.argv = ["js"]; + this.env = {}; this._callbackTimeouts = new Map(); this._nextCallbackTimeoutID = 1; @@ -235,9 +237,58 @@ return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); } + const storeStringArraySizes = (array, num_ptr, buf_size_ptr) => { + let buf_size = 0; + for (let s of array) { + buf_size += s.length + 1; + } + mem().setUint32(num_ptr, array.length, true); + mem().setUint32(buf_size_ptr, buf_size, true); + } + + const storeStringArray = (array, ptrs_ptr, buf_ptr) => { + for (let s of array) { + // Put string data in buffer. + let data = encoder.encode(s); + let dest = new Uint8Array(this._inst.exports.memory.buffer, buf_ptr, data.length); + dest.set(data); + mem().setUint8(buf_ptr+data.length, 0); + + // Put pointer to buffer in pointers array. + mem().setUint32(ptrs_ptr, buf_ptr, true); + + // Advance to the next element in the array. + ptrs_ptr += 4; + buf_ptr += data.length + 1; + } + } + + const envArray = () => { + let array = []; + for (let [key, value] of Object.entries(this.env)) { + array.push(`${key}=${value}`); + } + return array; + } + const timeOrigin = Date.now() - performance.now(); this.importObject = { wasi_snapshot_preview1: { + args_sizes_get: (argc_ptr, argv_buf_size_ptr) => { + storeStringArraySizes(this.argv, argc_ptr, argv_buf_size_ptr); + return 0; + }, + args_get: (argv_ptr, argv_buf_ptr) => { + storeStringArray(this.argv, argv_ptr, argv_buf_ptr); + return 0; + }, + environ_get: (env_ptr, env_buf_ptr) => { + storeStringArray(envArray(), env_ptr, env_buf_ptr); + }, + environ_sizes_get: (env_ptr, env_buf_size_ptr) => { + storeStringArraySizes(envArray(), env_ptr, env_buf_size_ptr); + return 0; + }, // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { let nwritten = 0; @@ -504,12 +555,14 @@ global.process.versions && !global.process.versions.electron ) { - if (process.argv.length != 3) { + if (process.argv.length < 3) { console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); process.exit(1); } const go = new Go(); + go.argv = process.argv.slice(2); + go.env = process.env; WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { return go.run(result.instance); }).catch((err) => {