Skip to content

Commit

Permalink
wasm: use wasip1 API for parameter/env passing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl committed Oct 30, 2024
1 parent 0edeaf6 commit 8e7a288
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 57 deletions.
11 changes: 5 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/nonhosted.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown
//go:build baremetal || wasm_unknown

package runtime

Expand Down
38 changes: 38 additions & 0 deletions src/runtime/runtime_tinygowasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 0 additions & 42 deletions src/runtime/runtime_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

package runtime

import (
"unsafe"
)

type timeUnit int64

// libc constructors
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/env_libc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1
//go:build js || nintendoswitch || wasip1

package syscall

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/errno_other.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !wasip1 && !wasip2
//go:build !js && !wasip1 && !wasip2

package syscall

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build wasip1
//go:build js || wasip1

package syscall

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/syscall_libc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1 || wasip2
//go:build js || nintendoswitch || wasip1 || wasip2

package syscall

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/syscall_libc_wasi.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build wasip1 || wasip2
//go:build js || wasip1 || wasip2

package syscall

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/syscall_nonhosted.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown
//go:build baremetal || wasm_unknown

package syscall

Expand Down
2 changes: 1 addition & 1 deletion src/syscall/tables_nonhosted.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 54 additions & 1 deletion targets/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@

global.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit 8e7a288

Please sign in to comment.