diff --git a/RATIONALE.md b/RATIONALE.md index fffaa7bb32..fd3852ddac 100644 --- a/RATIONALE.md +++ b/RATIONALE.md @@ -438,7 +438,7 @@ value (possibly `PWD`). Those unable to control the compiled code should only use absolute paths in configuration. See -* https://github.com/golang/go/blob/go1.19beta1/src/syscall/fs_js.go#L324 +* https://github.com/golang/go/blob/go1.19rc2/src/syscall/fs_js.go#L324 * https://github.com/WebAssembly/wasi-libc/pull/214#issue-673090117 ### FdPrestatDirName diff --git a/examples/wasi/cat_test.go b/examples/wasi/cat_test.go index 1a60fdd4ae..e4a04b05c6 100644 --- a/examples/wasi/cat_test.go +++ b/examples/wasi/cat_test.go @@ -1,10 +1,14 @@ package main import ( + "context" + "io/fs" "testing" + "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/internal/testing/maintester" "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/wasi_snapshot_preview1" ) // Test_main ensures the following will work: @@ -21,3 +25,42 @@ func Test_main(t *testing.T) { }) } } + +func Benchmark_main(b *testing.B) { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig(). + // Enable WebAssembly 2.0 support, which is required for TinyGo 0.24+. + WithWasmCore2()) + defer r.Close(ctx) // This closes everything this Runtime created. + + // Instantiate WASI, which implements system I/O such as console output. + if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { + b.Fatal(err) + } + + // Compile the WebAssembly module using the default configuration. + code, err := r.CompileModule(ctx, catWasmTinyGo, wazero.NewCompileConfig()) + if err != nil { + b.Fatal(err) + } + + rooted, err := fs.Sub(catFS, "testdata") + if err != nil { + b.Fatal(err) + } + config := wazero.NewModuleConfig().WithFS(rooted).WithArgs("cat", "/test.txt") + + b.Run("tinygo cat", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if mod, err := r.InstantiateModule(ctx, code, config); err != nil { + b.Fatal(err) + } else { + mod.Close(ctx) + } + } + }) +} diff --git a/examples/wasm_exec/.gitignore b/examples/wasm_exec/.gitignore new file mode 100644 index 0000000000..24b0c1004b --- /dev/null +++ b/examples/wasm_exec/.gitignore @@ -0,0 +1 @@ +wasi diff --git a/examples/wasm_exec/README.md b/examples/wasm_exec/README.md new file mode 100644 index 0000000000..44847591df --- /dev/null +++ b/examples/wasm_exec/README.md @@ -0,0 +1,4 @@ +## WASI example + +This example shows how to use I/O in your WebAssembly modules using WASI +(WebAssembly System Interface). diff --git a/examples/wasm_exec/cat.go b/examples/wasm_exec/cat.go new file mode 100644 index 0000000000..6d63af8b4a --- /dev/null +++ b/examples/wasm_exec/cat.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "embed" + _ "embed" + "io/fs" + "log" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/sys" + "github.com/tetratelabs/wazero/wasm_exec" +) + +// catFS is an embedded filesystem limited to test.txt +//go:embed testdata/test.txt +var catFS embed.FS + +// catWasm was compiled the TinyGo source testdata/cat.go +//go:embed testdata/cat.wasm +var catWasm []byte + +// main writes an input file to stdout, just like `cat`. +// +// This is a basic introduction to the WebAssembly System Interface (WASI). +// See https://github.com/WebAssembly/WASI +func main() { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntime() + defer r.Close(ctx) // This closes everything this Runtime created. + + // Compile the WebAssembly module using the default configuration. + compiled, err := r.CompileModule(ctx, catWasm, wazero.NewCompileConfig()) + if err != nil { + log.Panicln(err) + } + + we, err := wasm_exec.NewBuilder().Build(ctx, r) + if err != nil { + log.Panicln(err) + } + + // Since wazero uses fs.FS, we can use standard libraries to do things like trim the leading path. + rooted, err := fs.Sub(catFS, "testdata") + if err != nil { + log.Panicln(err) + } + + // Create a configuration for running main, overriding defaults (which discard stdout and has no file system). + config := wazero.NewModuleConfig(). + WithFS(rooted). + WithStdout(os.Stdout). + WithStderr(os.Stderr). + WithArgs("cat", os.Args[1]) + + err = we.Run(ctx, compiled, config) + if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { + log.Panicln(err) + } else if !ok { + log.Panicln(err) + } +} diff --git a/examples/wasm_exec/cat_test.go b/examples/wasm_exec/cat_test.go new file mode 100644 index 0000000000..6a44cbd966 --- /dev/null +++ b/examples/wasm_exec/cat_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "io/fs" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/sys" + "github.com/tetratelabs/wazero/wasm_exec" +) + +// Test_main ensures the following will work: +// +// go run cat.go /test.txt +func Test_main(t *testing.T) { + stdout, stderr := maintester.TestMain(t, main, "cat", "/test.txt") + require.Equal(t, "", stderr) + require.Equal(t, "greet filesystem\n", stdout) +} + +func Benchmark_main(b *testing.B) { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigCompiler()) + defer r.Close(ctx) // This closes everything this Runtime created. + + // Compile the WebAssembly module using the default configuration. + compiled, err := r.CompileModule(ctx, catWasm, wazero.NewCompileConfig()) + if err != nil { + b.Fatal(err) + } + + we, err := wasm_exec.NewBuilder().Build(ctx, r) + + rooted, err := fs.Sub(catFS, "testdata") + if err != nil { + b.Fatal(err) + } + config := wazero.NewModuleConfig().WithFS(rooted).WithArgs("cat", "/test.txt") + + b.Run("go cat", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err = we.Run(ctx, compiled, config) + if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { + b.Fatal(err) + } else if !ok { + b.Fatal(err) + } + } + }) +} diff --git a/examples/wasm_exec/testdata/cat.go b/examples/wasm_exec/testdata/cat.go new file mode 100644 index 0000000000..55e81245f6 --- /dev/null +++ b/examples/wasm_exec/testdata/cat.go @@ -0,0 +1,21 @@ +package main + +import ( + "io/ioutil" + "os" +) + +// main is the same as wasi: "concatenate and print files." +func main() { + // Start at arg[1] because args[0] is the program name. + for i := 1; i < len(os.Args); i++ { + // Intentionally use ioutil.ReadFile instead of os.ReadFile for TinyGo. + bytes, err := ioutil.ReadFile(os.Args[i]) + if err != nil { + os.Exit(1) + } + + // Use write to avoid needing to worry about Windows newlines. + os.Stdout.Write(bytes) + } +} diff --git a/examples/wasm_exec/testdata/cat.wasm b/examples/wasm_exec/testdata/cat.wasm new file mode 100755 index 0000000000..f59fd88260 Binary files /dev/null and b/examples/wasm_exec/testdata/cat.wasm differ diff --git a/examples/wasm_exec/testdata/sub/test.txt b/examples/wasm_exec/testdata/sub/test.txt new file mode 100644 index 0000000000..4b45af5ed2 --- /dev/null +++ b/examples/wasm_exec/testdata/sub/test.txt @@ -0,0 +1 @@ +greet sub dir diff --git a/examples/wasm_exec/testdata/test.txt b/examples/wasm_exec/testdata/test.txt new file mode 100644 index 0000000000..c376dfb132 --- /dev/null +++ b/examples/wasm_exec/testdata/test.txt @@ -0,0 +1 @@ +greet filesystem diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go index 152805b2d8..8cc9a4379a 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -179,6 +179,11 @@ func (f *FunctionInstance) Call(ctx context.Context, params ...uint64) (ret []ui return } +// GlobalVal is an internal hack to get the lower 64 bits of a global. +func (m *CallContext) GlobalVal(idx Index) uint64 { + return m.module.Globals[idx].Val +} + // ExportedGlobal implements the same method as documented on api.Module. func (m *CallContext) ExportedGlobal(name string) api.Global { exp, err := m.module.getExport(name, ExternTypeGlobal) diff --git a/wasm_exec/REFERENCE.md b/wasm_exec/REFERENCE.md new file mode 100644 index 0000000000..89a4764a1a --- /dev/null +++ b/wasm_exec/REFERENCE.md @@ -0,0 +1,176 @@ +# wasm_exec reference + +This package contains imports and state needed by wasm go compiles when +`GOOS=js` and `GOARCH=wasm`. + +## Introduction + +When `GOOS=js` and `GOARCH=wasm`, Go's compiler targets WebAssembly 1.0 Binary +format (%.wasm). + +Ex. +```bash +$ GOOS=js GOARCH=wasm go build -o my.wasm . +``` + +The operating system is "js", but more specifically it is [wasm_exec.js][1]. +This package runs the `%.wasm` just like `wasm_exec.js` would. + +## Identifying wasm compiled by Go + +If you have a `%.wasm` file compiled by Go (via [asm.go][2]), it has a custom +section named "go.buildid". + +You can verify this with wasm-objdump, a part of [wabt][3]: +``` +$ wasm-objdump --section=go.buildid -x my.wasm + +example3.wasm: file format wasm 0x1 + +Section Details: + +Custom: +- name: "go.buildid" +``` + +## Module Exports + +Until [wasmexport][4] is implemented, the [compiled][2] WebAssembly exports are +always the same: + +* "mem" - (memory 265) 265 = data section plus 16MB +* "run" - (func (param $argc i32) (param $argv i32)) the entrypoint +* "resume" - (func) continues work after a timer delay +* "getsp" - (func (result i32)) returns the stack pointer + +## Module Imports + +Go's [compiles][3] all WebAssembly imports in the module "go", and only +functions are imported. + +Except for the "debug" function, all function names are prefixed by their go +package. Here are the defaults: + +* "debug" - is always function index zero, but it has unknown use. +* "runtime.*" - supports system-call like functionality `GOARCH=wasm` +* "syscall/js.*" - supports the JavaScript model `GOOS=js` + +## System Calls + +"syscall/js.*" are host functions for managing the JavaScript object graph, +including functions to make and finalize objects, arrays and numbers +(`js.Value`). + +Each `js.Value` has a `js.ref`, which is either a numeric literal or an object +reference depending on its 64-bit bit pattern. When an object, the first 31 +bits are its identifier. + +There are several pre-defined values with constant `js.ref` patterns. These are +either constants, globals or otherwise needed in initializers. + +For example, the "global" value includes properties like "fs" and "process" +which implement [system calls][7] needed for functions like `os.Getuid`. + +Notably, not all system calls are implemented as some are stubbed by the +compiler to return zero values or `syscall.ENOSYS`. This means not all Go code +compiled to wasm will operate. For example, you cannot launch processes. + +Details beyond this are best looking at the source code of [js.go][5], or its +unit tests. + +## PC_B calling conventions + +The assembly `CallImport` instruction doesn't compile signatures to WebAssembly +function types, invoked by the `call` instruction. + +Instead, the compiler generates the same signature for all functions: a single +parameter of the stack pointer, and invokes them via `call.indirect`. + +Specifically, any function compiled with `CallImport` has the same function +type: `(func (param $sp i32))`. `$sp` is the base memory offset to read and +write parameters to the stack (at 8 byte strides even if the value is 32-bit). + +So, implementors need to read the actual parameters from memory. Similarly, if +there are results, the implementation must write those to memory. + +For example, `func walltime() (sec int64, nsec int32)` writes its results to +memory at offsets `sp+8` and `sp+16` respectively. + +## Go-defined exported functions + +[Several functions][6] differ in calling convention by using WebAssembly type +signatures instead of the single SP parameter summarized above. Functions used +by the host have a "wasm_export_" prefix, which is stripped. For example, +"wasm_export_run" is exported as "run", defined in [rt0_js_wasm.s][7] + +Here is an overview of the Go-defined exported functions: + * "run" - Accepts "argc" and "argv" i32 params and begins the "wasm_pc_f_loop" + * "resume" - Nullary function that resumes execution until it needs an event. + * "getsp" - Returns the i32 stack pointer (SP) + +## User-defined Host Functions + +Users can define their own "go" module function imports by defining a func +without a body in their source and a `%_wasm.s` or `%_js.s` file that uses the +`CallImport` instruction. + +For example, given `func logString(msg string)` and the below assembly: +```assembly +#include "textflag.h" + +TEXT ·logString(SB), NOSPLIT, $0 +CallImport +RET +``` + +If the package was `main`, the WebAssembly function name would be +"main.logString". If it was `util` and your `go.mod` module was +"github.com/user/me", the WebAssembly function name would be +"github.com/user/me/util.logString". + +Regardless of whether the function import was built-in to Go, or defined by an +end user, all imports use `CallImport` conventions. Since these compile to a +signature unrelated to the source, more care is needed implementing the host +side, to ensure the proper count of parameters are read and results written to +the Go stack. + +### (Lack of) Concurrency + +Go generated Wasm is not goroutine safe, so runtimes do not need to implement +concurrency apart from trying to prevent it in API and/or documentation. + +This may seem strange because Go's [function wrapper][9] used for `GOOS=js` is +implemented using locks. For example, seeing this, you may feel the host side +of this code (`_makeFuncWrapper`) should lock its ID namespace as well. + +What makes this difficult to see is that the locks used by `GOOS=js` are +implemented with [atomics][10] defined by `GOARCH=wasm`. + +This codebase is waiting for the "Threads" proposal to finish and be +implemented in Go. However, as of mid-2022, the ["Threads" proposal][11] is +still not finished. Even if it did, it would be at least another release of Go +before it could be usable. + +In summary, runtimes should not allow concurrent use of Wasm generated by +`GOARCH=wasm GOOS=js`. + +## Memory Layout + +Memory layout begins with the "zero page" of size `runtime.minLegalPointer` +(4KB) which matches the `ssa.minZeroPage` in the compiler. It is then followed +by 8KB reserved for args and environment variables. This means the data section +begins at [ld.wasmMinDataAddr][12], offset 12288. + + +[1]: https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js +[2]: https://github.com/golang/go/blob/go1.19rc2/src/cmd/link/internal/wasm/asm.go +[3]: https://github.com/WebAssembly/wabt +[4]: https://github.com/golang/proposal/blob/go1.19rc2/design/42372-wasmexport.md +[5]: https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go +[6]: https://github.com/golang/go/blob/go1.19rc2/src/cmd/internal/obj/wasm/wasmobj.go#L794-L812 +[7]: https://github.com/golang/go/blob/go1.19rc2/src/runtime/rt0_js_wasm.s#L17-L21 +[8]: https://github.com/golang/go/blob/go1.19rc2/src/syscall/syscall_js.go#L292-L306 +[9]: https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/func.go#L41-L44 +[10]: https://github.com/golang/go/blob/go1.19rc2/src/runtime/internal/atomic/atomic_wasm.go#L5-L6 +[11]: https://github.com/WebAssembly/proposals +[12]: https://github.com/golang/go/blob/go1.19rc2/src/cmd/link/internal/ld/data.go#L2457 diff --git a/wasm_exec/args_environ.go b/wasm_exec/args_environ.go new file mode 100644 index 0000000000..0a17df035e --- /dev/null +++ b/wasm_exec/args_environ.go @@ -0,0 +1,61 @@ +package wasm_exec + +import ( + "context" + "errors" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// Constants about memory layout. See REFERENCE.md +const ( + endOfPageZero = uint32(4096) // runtime.minLegalPointer + maxArgsAndEnviron = uint32(8192) // ld.wasmMinDataAddr - runtime.minLegalPointer + wasmMinDataAddr = endOfPageZero + maxArgsAndEnviron // ld.wasmMinDataAddr +) + +// writeArgsAndEnviron writes arguments and environment variables to memory, so +// they can be read by main, Go compiles as the function export "run". +func writeArgsAndEnviron(ctx context.Context, mod api.Module) (argc, argv uint32, err error) { + mem := mod.Memory() + sysCtx := mod.(*wasm.CallContext).Sys + args := sysCtx.Args() + environ := sysCtx.Environ() + + argc = uint32(len(args)) + offset := endOfPageZero + + strPtr := func(val, field string, i int) (ptr uint32) { + // TODO: return err and format "%s[%d], field, i" + ptr = offset + mustWrite(ctx, mem, field, offset, append([]byte(val), 0)) + offset += uint32(len(val) + 1) + if pad := offset % 8; pad != 0 { + offset += 8 - pad + } + return + } + argvPtrs := make([]uint32, 0, len(args)+1+len(environ)+1) + for i, arg := range args { + argvPtrs = append(argvPtrs, strPtr(arg, "args", i)) + } + argvPtrs = append(argvPtrs, 0) + + for i, env := range environ { + argvPtrs = append(argvPtrs, strPtr(env, "env", i)) + } + argvPtrs = append(argvPtrs, 0) + + argv = offset + for _, ptr := range argvPtrs { + // TODO: return err and format "argvPtrs[%d], i" + mustWriteUint64Le(ctx, mem, "argvPtrs[i]", offset, uint64(ptr)) + offset += 8 + } + + if offset >= wasmMinDataAddr { + err = errors.New("total length of command line and environment variables exceeds limit") + } + return +} diff --git a/wasm_exec/compiler_test.go b/wasm_exec/compiler_test.go new file mode 100644 index 0000000000..5b2d1723f3 --- /dev/null +++ b/wasm_exec/compiler_test.go @@ -0,0 +1,138 @@ +package wasm_exec_test + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/internal/wasm/binary" + "github.com/tetratelabs/wazero/sys" + "github.com/tetratelabs/wazero/wasm_exec" +) + +// Test_compileJsWasm ensures the infrastructure to generate wasm on-demand works. +func Test_compileJsWasm(t *testing.T) { + bin, err := compileJsWasm(`package main + +import "os" + +func main() { + os.Exit(1) +}`) + require.NoError(t, err) + + m, err := binary.DecodeModule(bin, wasm.Features20191205, wasm.MemorySizer) + require.NoError(t, err) + // TODO: implement go.buildid custom section and validate it instead. + require.NotNil(t, m.MemorySection) +} + +func Test_compileAndRunJsWasm(t *testing.T) { + stdout, stderr, err := compileAndRunJsWasm(testCtx, `package main + +import "os" + +func main() { + os.Stdout.Write([]byte("stdout")) + os.Stderr.Write([]byte("stderr")) + os.Exit(1) +}`, wazero.NewModuleConfig()) + + require.Equal(t, "stdout", stdout) + require.Equal(t, "stderr", stderr) + require.Error(t, err) + require.Equal(t, uint32(1), err.(*sys.ExitError).ExitCode()) +} + +func compileAndRunJsWasm(ctx context.Context, goSrc string, config wazero.ModuleConfig) (stdout, stderr string, err error) { + bin, compileJsErr := compileJsWasm(goSrc) + if compileJsErr != nil { + err = compileJsErr + return + } + + r := wazero.NewRuntime() + defer r.Close(ctx) + + compiled, compileErr := r.CompileModule(ctx, bin, wazero.NewCompileConfig()) + if compileErr != nil { + err = compileErr + return + } + + we, newErr := wasm_exec.NewBuilder().Build(ctx, r) + if newErr != nil { + err = newErr + return + } + + stdoutBuf, stderrBuf := &bytes.Buffer{}, &bytes.Buffer{} + err = we.Run(ctx, compiled, config.WithStdout(stdoutBuf).WithStderr(stderrBuf)) + stdout = stdoutBuf.String() + stderr = stderrBuf.String() + return +} + +// compileJsWasm allows us to generate a binary with runtime.GOOS=js and runtime.GOARCH=wasm. +func compileJsWasm(goSrc string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + goBin, err := findGoBin() + if err != nil { + return nil, err + } + + workDir, err := os.MkdirTemp("", "") + if err != nil { + return nil, err + } + defer os.RemoveAll(workDir) + + bin := "out.wasm" + goArgs := []string{"build", "-o", bin, "."} + if err = os.WriteFile(filepath.Join(workDir, "main.go"), []byte(goSrc), 0o600); err != nil { + return nil, err + } + + if err = os.WriteFile(filepath.Join(workDir, "go.mod"), + []byte("module github.com/tetratelabs/wazero/wasm_exec/examples\n\ngo 1.17\n"), 0o600); err != nil { + return nil, err + } + + cmd := exec.CommandContext(ctx, goBin, goArgs...) //nolint:gosec + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm") + cmd.Dir = workDir + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("couldn't compile %s: %s\n%w", bin, string(out), err) + } + + binBytes, err := os.ReadFile(filepath.Join(workDir, bin)) //nolint:gosec + if err != nil { + return nil, fmt.Errorf("couldn't compile %s: %w", bin, err) + } + return binBytes, nil +} + +func findGoBin() (string, error) { + binName := "go" + if runtime.GOOS == "windows" { + binName += ".exe" + } + goBin := filepath.Join(runtime.GOROOT(), "bin", binName) + if _, err := os.Stat(goBin); err == nil { + return goBin, nil + } + // Now, search the path + return exec.LookPath(binName) +} diff --git a/wasm_exec/example_test.go b/wasm_exec/example_test.go new file mode 100644 index 0000000000..4f0acaf2bf --- /dev/null +++ b/wasm_exec/example_test.go @@ -0,0 +1,64 @@ +package wasm_exec_test + +import ( + "context" + _ "embed" + "fmt" + "log" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/sys" + "github.com/tetratelabs/wazero/wasm_exec" +) + +// This is an example of how to use `GOARCH=wasm GOOS=js` compiled wasm via a +// sleep function. +// +// See https://github.com/tetratelabs/wazero/tree/main/examples/wasm_exec for another example. +func Example() { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntime() + defer r.Close(ctx) + + // Compile the source as GOARCH=wasm GOOS=js. + bin, err := compileJsWasm(`package main + +import "time" + +func main() { + time.Sleep(time.Duration(1)) +}`) + if err != nil { + log.Panicln(err) + } + + compiled, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig()) + if err != nil { + log.Panicln(err) + } + + // Create a wasm_exec runner. + we, err := wasm_exec.NewBuilder().Build(ctx, r) + if err != nil { + log.Panicln(err) + } + + // Override defaults which discard stdout and fake sleep. + config := wazero.NewModuleConfig(). + WithStdout(os.Stdout). + WithStderr(os.Stderr) + + err = we.Run(ctx, compiled, config) + if exitErr, ok := err.(*sys.ExitError); ok { + // Print the exit code + fmt.Printf("exit_code: %d\n", exitErr.ExitCode()) + } else if !ok { + log.Panicln(err) + } + // Output: + // exit_code: 0 +} diff --git a/wasm_exec/js.go b/wasm_exec/js.go new file mode 100644 index 0000000000..89644a1581 --- /dev/null +++ b/wasm_exec/js.go @@ -0,0 +1,212 @@ +package wasm_exec + +import ( + "os" + + "github.com/tetratelabs/wazero/api" +) + +// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly. +// +// The JavaScript value "undefined" is represented by the value 0. +// A JavaScript number (64-bit float, except 0 and NaN) is represented by its IEEE 754 binary representation. +// All other values are represented as an IEEE 754 binary representation of NaN with bits 0-31 used as +// an ID and bits 32-34 used to differentiate between string, symbol, function and object. +type ref uint64 + +// nanHead are the upper 32 bits of a ref which are set if the value is not encoded as an IEEE 754 number (see above). +const nanHead = 0x7FF80000 + +type constVal struct { + name string + ref +} + +const ( + // the type flags need to be in sync with wasm_exec.js + typeFlagNone = iota + typeFlagObject + typeFlagString + typeFlagSymbol // nolint + typeFlagFunction +) + +func valueRef(id uint32, typeFlag byte) ref { + return (nanHead|ref(typeFlag))<<32 | ref(id) +} + +const ( + // predefined + + idValueNaN uint32 = iota + idValueZero + idValueNull + idValueTrue + idValueFalse + idValueGlobal + idJsGo + + // The below are derived from analyzing `*_js.go` source. + + idObjectConstructor + idArrayConstructor + idJsProcess + idJsFS + idJsFSConstants + idUint8ArrayConstructor + idJsCrypto + idJsDateConstructor + idJsDate + idEOF + nextID +) + +const ( + refValueNaN = (nanHead|ref(typeFlagNone))<<32 | ref(idValueNaN) + refValueZero = (nanHead|ref(typeFlagNone))<<32 | ref(idValueZero) + refValueNull = (nanHead|ref(typeFlagNone))<<32 | ref(idValueNull) + refValueTrue = (nanHead|ref(typeFlagNone))<<32 | ref(idValueTrue) + refValueFalse = (nanHead|ref(typeFlagNone))<<32 | ref(idValueFalse) + refValueGlobal = (nanHead|ref(typeFlagObject))<<32 | ref(idValueGlobal) + refJsGo = (nanHead|ref(typeFlagObject))<<32 | ref(idJsGo) + + refObjectConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idObjectConstructor) + refArrayConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idArrayConstructor) + refJsProcess = (nanHead|ref(typeFlagObject))<<32 | ref(idJsProcess) + refJsFS = (nanHead|ref(typeFlagObject))<<32 | ref(idJsFS) + refJsFSConstants = (nanHead|ref(typeFlagObject))<<32 | ref(idJsFSConstants) + refUint8ArrayConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idUint8ArrayConstructor) + refJsCrypto = (nanHead|ref(typeFlagFunction))<<32 | ref(idJsCrypto) + refJsDateConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idJsDateConstructor) + refJsDate = (nanHead|ref(typeFlagObject))<<32 | ref(idJsDate) + refEOF = (nanHead|ref(typeFlagObject))<<32 | ref(idEOF) +) + +var ( + // Values below are not built-in, but verifiable by looking at Go's source. + // When marked "XX.go init", these are eagerly referenced during syscall.init + + // valueGlobal = js.Global() // js.go init + // + // Here are its properties: + // * js.go + // * objectConstructor = Get("Object") // init + // * arrayConstructor = Get("Array") // init + // * rand_js.go + // * jsCrypto = Get("crypto") // init + // * uint8ArrayConstructor = Get("Uint8Array") // init + // * roundtrip_js.go + // * uint8ArrayConstructor = Get("Uint8Array") // init + // * jsFetchMissing = Get("fetch").IsUndefined() // http init + // * Get("AbortController").New() // http.Roundtrip && "fetch" + // * Get("Object").New() // http.Roundtrip && "fetch" + // * Get("Headers").New() // http.Roundtrip && "fetch" + // * Call("fetch", req.URL.String(), opt) && "fetch" + // * fs_js.go + // * jsProcess = Get("process") // init + // * jsFS = Get("fs") // init + // * uint8ArrayConstructor = Get("Uint8Array") + // * zoneinfo_js.go + // * jsDateConstructor = Get("Date") // time.initLocal + valueGlobal = &constVal{ref: refValueGlobal, name: "global"} + + // jsGo is not a constant + + // objectConstructor is used by js.ValueOf to make `map[string]any`. + objectConstructor = &constVal{ref: refObjectConstructor, name: "Object"} + + // arrayConstructor is used by js.ValueOf to make `[]any`. + arrayConstructor = &constVal{ref: refArrayConstructor, name: "Array"} + + // jsProcess = js.Global().Get("process") // fs_js.go init + // + // Here are its properties: + // * fs_js.go + // * Call("cwd").String() // fs.Open fs.GetCwd + // * Call("chdir", path) // fs.Chdir + // * syscall_js.go + // * Call("getuid").Int() // syscall.Getuid + // * Call("getgid").Int() // syscall.Getgid + // * Call("geteuid").Int() // syscall.Geteuid + // * Call("getgroups") /* array of .Int() */ // syscall.Getgroups + // * Get("pid").Int() // syscall.Getpid + // * Get("ppid").Int() // syscall.Getpid + // * Call("umask", mask /* int */ ).Int() // syscall.Umask + jsProcess = &constVal{ref: refJsProcess, name: "process"} + + // jsFS = js.Global().Get("fs") // fs_js.go init + // + // Here are its properties: + // * jsFSConstants = jsFS.Get("constants") // init + // * jsFD /* Int */, err := fsCall("open", path, flags, perm) // fs.Open + // * stat, err := fsCall("fstat", fd) // fs.Open + // * stat.Call("isDirectory").Bool() + // * dir, err := fsCall("readdir", path) // fs.Open + // * dir.Length(), dir.Index(i).String() + // * _, err := fsCall("close", fd) // fs.Close + // * _, err := fsCall("mkdir", path, perm) // fs.Mkdir + // * jsSt, err := fsCall("stat", path) // fs.Stat + // * jsSt, err := fsCall("lstat", path) // fs.Lstat + // * jsSt, err := fsCall("fstat", fd) // fs.Fstat + // * _, err := fsCall("unlink", path) // fs.Unlink + // * _, err := fsCall("rmdir", path) // fs.Rmdir + // * _, err := fsCall("chmod", path, mode) // fs.Chmod + // * _, err := fsCall("fchmod", fd, mode) // fs.Fchmod + // * _, err := fsCall("chown", path, uint32(uid), uint32(gid)) // fs.Chown + // * _, err := fsCall("fchown", fd, uint32(uid), uint32(gid)) // fs.Fchown + // * _, err := fsCall("lchown", path, uint32(uid), uint32(gid)) // fs.Lchown + // * _, err := fsCall("utimes", path, atime, mtime) // fs.UtimesNano + // * _, err := fsCall("rename", from, to) // fs.Rename + // * _, err := fsCall("truncate", path, length) // fs.Truncate + // * _, err := fsCall("ftruncate", fd, length) // fs.Ftruncate + // * dst, err := fsCall("readlink", path) // fs.Readlink + // * _, err := fsCall("link", path, link) // fs.Link + // * _, err := fsCall("symlink", path, link) // fs.Symlink + // * _, err := fsCall("fsync", fd) // fs.Fsync + // * n, err := fsCall("read", fd, buf, 0, len(b), nil) // fs.Read + // * n, err := fsCall("write", fd, buf, 0, len(b), nil) // fs.Write + // * n, err := fsCall("read", fd, buf, 0, len(b), offset) // fs.Pread + // * n, err := fsCall("write", fd, buf, 0, len(b), offset) // fs.Pwrite + jsFS = &constVal{ref: refJsFS, name: "fs"} + + // jsFSConstants = jsFS Get("constants") // fs_js.go init + jsFSConstants = &constVal{ref: refJsFSConstants, name: "constants"} + + // oWRONLY = jsFSConstants Get("O_WRONLY").Int() // fs_js.go init + oWRONLY = api.EncodeF64(float64(os.O_WRONLY)) + + // oRDWR = jsFSConstants Get("O_RDWR").Int() // fs_js.go init + oRDWR = api.EncodeF64(float64(os.O_RDWR)) + + //o CREAT = jsFSConstants Get("O_CREAT").Int() // fs_js.go init + oCREAT = api.EncodeF64(float64(os.O_CREATE)) + + // oTRUNC = jsFSConstants Get("O_TRUNC").Int() // fs_js.go init + oTRUNC = api.EncodeF64(float64(os.O_TRUNC)) + + // oAPPEND = jsFSConstants Get("O_APPEND").Int() // fs_js.go init + oAPPEND = api.EncodeF64(float64(os.O_APPEND)) + + // oEXCL = jsFSConstants Get("O_EXCL").Int() // fs_js.go init + oEXCL = api.EncodeF64(float64(os.O_EXCL)) + + // uint8ArrayConstructor = js.Global().Get("Uint8Array") + // // fs_js.go, rand_js.go, roundtrip_js.go init + // + // It has only one invocation pattern: `buf := uint8Array.New(len(b))` + uint8ArrayConstructor = &constVal{ref: refUint8ArrayConstructor, name: "Uint8Array"} + + // jsCrypto = js.Global().Get("crypto") // rand_js.go init + // + // It has only one invocation pattern: + // `jsCrypto.Call("getRandomValues", a /* uint8Array */)` + _ = /* jsCrypto */ &constVal{ref: refJsCrypto, name: "crypto"} + + // jsDateConstructor is used inline in zoneinfo_js.go for time.initLocal. + // `New()` returns jsDate. + _ = /* jsDateConstructor */ &constVal{ref: refJsDateConstructor, name: "Date"} + + // jsDate is used inline in zoneinfo_js.go for time.initLocal. + // `.Call("getTimezoneOffset").Int()` returns a timezone offset. + _ = /* jsDate */ &constVal{ref: refJsDate, name: "jsDate"} +) diff --git a/wasm_exec/main.go b/wasm_exec/main.go new file mode 100644 index 0000000000..36d7833bda --- /dev/null +++ b/wasm_exec/main.go @@ -0,0 +1,57 @@ +package wasm_exec + +import ( + "context" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" +) + +// WasmExec allows you to run wasm compiled with `GOARCH=wasm GOOS=js`. +type WasmExec interface { + // Run instantiates a new module and calls the "run" export with the given module config. + Run(context.Context, wazero.CompiledModule, wazero.ModuleConfig) error + + api.Closer +} + +type wasmExec struct{ r wazero.Runtime } + +func newWasmExec(r wazero.Runtime) WasmExec { + return &wasmExec{r} +} + +// Run implements WasmExec.Run +func (e *wasmExec) Run(ctx context.Context, compiled wazero.CompiledModule, mConfig wazero.ModuleConfig) error { + // Instantiate the imports needed by go-compiled wasm. + // TODO: We can't currently share a compiled module because the one here is stateful. + js, err := moduleBuilder(e.r).Instantiate(ctx, e.r) + if err != nil { + return err + } + defer js.Close(ctx) + + // Instantiate the module compiled by go, noting it has no init function. + mod, err := e.r.InstantiateModule(ctx, compiled, mConfig) + if err != nil { + return err + } + defer mod.Close(ctx) + + // Extract the args and env from the module config and write it to memory. + s := &state{values: &values{ids: map[interface{}]uint32{}}} + ctx = context.WithValue(ctx, stateKey{}, s) + argc, argv, err := writeArgsAndEnviron(ctx, mod) + if err != nil { + return err + } + // Invoke the run function. + _, err = mod.ExportedFunction("run").Call(ctx, uint64(argc), uint64(argv)) + return err +} + +// Close implements WasmExec.Close +func (e *wasmExec) Close(context.Context) error { + // currently no-op + return nil +} diff --git a/wasm_exec/runtime.go b/wasm_exec/runtime.go new file mode 100644 index 0000000000..8a11eac945 --- /dev/null +++ b/wasm_exec/runtime.go @@ -0,0 +1,141 @@ +package wasm_exec + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + functionWasmExit = "runtime.wasmExit" + functionWasmWrite = "runtime.wasmWrite" + functionResetMemoryDataView = "runtime.resetMemoryDataView" + functionNanotime1 = "runtime.nanotime1" + functionWalltime = "runtime.walltime" + functionScheduleTimeoutEvent = "runtime.scheduleTimeoutEvent" + functionClearTimeoutEvent = "runtime.clearTimeoutEvent" + functionGetRandomData = "runtime.getRandomData" +) + +// wasmExit implements runtime.wasmExit which supports runtime.exit. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.go#L28 +func wasmExit(ctx context.Context, mod api.Module, sp uint32) { + code := mustReadUint32Le(ctx, mod.Memory(), "code", sp+8) + getState(ctx).clear() + _ = mod.CloseWithExitCode(ctx, code) +} + +// wasmWrite implements runtime.wasmWrite which supports runtime.write and +// runtime.writeErr. It is only known to be used with fd = 2 (stderr). +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/os_js.go#L29 +func wasmWrite(ctx context.Context, mod api.Module, sp uint32) { + fd := mustReadUint64Le(ctx, mod.Memory(), "fd", sp+8) + p := mustReadUint64Le(ctx, mod.Memory(), "p", sp+16) + n := mustReadUint32Le(ctx, mod.Memory(), "n", sp+24) + + var writer io.Writer + + switch fd { + case 1: + writer = mod.(*wasm.CallContext).Sys.Stdout() + case 2: + writer = mod.(*wasm.CallContext).Sys.Stderr() + default: + // Keep things simple by expecting nothing past 2 + panic(fmt.Errorf("unexpected fd %d", fd)) + } + + if _, err := writer.Write(mustRead(ctx, mod.Memory(), "p", uint32(p), n)); err != nil { + panic(fmt.Errorf("error writing p: %w", err)) + } +} + +// resetMemoryDataView signals wasm.OpcodeMemoryGrow happened, indicating any +// cached view of memory should be reset. +// +// See https://github.com/golang/go/blob/9839668b5619f45e293dd40339bf0ac614ea6bee/src/runtime/mem_js.go#L82 +var resetMemoryDataView = &wasm.Func{ + ExportNames: []string{functionResetMemoryDataView}, + Name: functionResetMemoryDataView, + ParamTypes: []wasm.ValueType{wasm.ValueTypeI32}, + ParamNames: []string{parameterSp}, + // TODO: Compiler-based memory.grow callbacks are ignored until we have a generic solution #601 + Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}}, +} + +// nanotime1 implements runtime.nanotime which supports time.Since. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.s#L184 +func nanotime1(ctx context.Context, mod api.Module, sp uint32) { + nanos := uint64(mod.(*wasm.CallContext).Sys.Nanotime(ctx)) + mustWriteUint64Le(ctx, mod.Memory(), "t", sp+8, nanos) +} + +// walltime implements runtime.walltime which supports time.Now. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.s#L188 +func walltime(ctx context.Context, mod api.Module, sp uint32) { + sec, nsec := mod.(*wasm.CallContext).Sys.Walltime(ctx) + mustWriteUint64Le(ctx, mod.Memory(), "sec", sp+8, uint64(sec)) + mustWriteUint64Le(ctx, mod.Memory(), "nsec", sp+16, uint64(nsec)) +} + +// scheduleTimeoutEvent implements runtime.scheduleTimeoutEvent which supports +// runtime.notetsleepg used by runtime.signal_recv. +// +// Unlike other most functions prefixed by "runtime.", this both launches a +// goroutine and invokes code compiled into wasm "resume". +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.s#L192 +func scheduleTimeoutEvent(ctx context.Context, mod api.Module, sp uint32) { + delayMs := mustReadUint64Le(ctx, mod.Memory(), "delay", sp+8) + delay := time.Duration(delayMs) * time.Millisecond + + resume := mod.ExportedFunction("resume") + + // Invoke resume as an anonymous function, to propagate the context. + callResume := func() { + // This may seem like it can occur on a different goroutine, but + // wasmexec is not goroutine-safe, so it won't. + if _, err := resume.Call(ctx); err != nil { + panic(err) + } + } + + id := getState(ctx).scheduleEvent(delay, callResume) + mustWriteUint64Le(ctx, mod.Memory(), "id", sp+16, uint64(id)) +} + +// clearTimeoutEvent implements runtime.clearTimeoutEvent which supports +// runtime.notetsleepg used by runtime.signal_recv. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.s#L196 +func clearTimeoutEvent(ctx context.Context, mod api.Module, sp uint32) { + id := mustReadUint32Le(ctx, mod.Memory(), "id", sp+8) + getState(ctx).clearTimeoutEvent(id) +} + +// getRandomData implements runtime.getRandomData, which initializes the seed +// for runtime.fastrand. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/runtime/sys_wasm.s#L200 +func getRandomData(ctx context.Context, mod api.Module, sp uint32) { + buf := uint32(mustReadUint64Le(ctx, mod.Memory(), "buf", sp+8)) + bufLen := uint32(mustReadUint64Le(ctx, mod.Memory(), "bufLen", sp+16)) + + randSource := mod.(*wasm.CallContext).Sys.RandSource() + + r := mustRead(ctx, mod.Memory(), "r", buf, bufLen) + + if n, err := randSource.Read(r); err != nil { + panic(fmt.Errorf("RandSource.Read(r /* len =%d */) failed: %w", bufLen, err)) + } else if uint32(n) != bufLen { + panic(fmt.Errorf("RandSource.Read(r /* len=%d */) read %d bytes", bufLen, n)) + } +} diff --git a/wasm_exec/state.go b/wasm_exec/state.go new file mode 100644 index 0000000000..54fdc0bc4a --- /dev/null +++ b/wasm_exec/state.go @@ -0,0 +1,66 @@ +package wasm_exec + +import ( + "context" + "time" +) + +// stateKey is a context.Context Value key. The value must be a state pointer. +type stateKey struct{} + +func getState(ctx context.Context) *state { + return ctx.Value(stateKey{}).(*state) +} + +// state holds state used by the "go" imports used by wasm_exec. +// Note: This is module-scoped. +type state struct { + nextCallbackTimeoutID uint32 + scheduledTimeouts map[uint32]*time.Timer + values *values + _pendingEvent *event +} + +func (s *state) clear() { + s.nextCallbackTimeoutID = 0 + for k := range s.scheduledTimeouts { + delete(s.scheduledTimeouts, k) + } + s.values.values = s.values.values[:0] + s.values.goRefCounts = s.values.goRefCounts[:0] + for k := range s.values.ids { + delete(s.values.ids, k) + } + s.values.idPool = s.values.idPool[:0] + s._pendingEvent = nil +} + +// scheduleEvent schedules an event onto another goroutine after d duration and +// returns a handle to remove it (removeEvent). +func (s *state) scheduleEvent(d time.Duration, f func()) uint32 { + id := s.nextCallbackTimeoutID + s.nextCallbackTimeoutID++ + // TODO: this breaks the sandbox (proc.checkTimers is shared), so should + // be substitutable with a different impl. + s.scheduledTimeouts[id] = time.AfterFunc(d, f) + return id +} + +// removeEvent removes an event previously scheduled with scheduleEvent or +// returns nil, if it was already removed. +func (s *state) removeEvent(id uint32) *time.Timer { + t, ok := s.scheduledTimeouts[id] + if ok { + delete(s.scheduledTimeouts, id) + return t + } + return nil +} + +func (s *state) clearTimeoutEvent(id uint32) { + if t := s.removeEvent(id); t != nil { + if !t.Stop() { + <-t.C + } + } +} diff --git a/wasm_exec/syscall.go b/wasm_exec/syscall.go new file mode 100644 index 0000000000..e1a4fca582 --- /dev/null +++ b/wasm_exec/syscall.go @@ -0,0 +1,469 @@ +package wasm_exec + +import ( + "context" + "fmt" + "io" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + functionFinalizeRef = "syscall/js.finalizeRef" + functionStringVal = "syscall/js.stringVal" + functionValueGet = "syscall/js.valueGet" + functionValueSet = "syscall/js.valueSet" + functionValueDelete = "syscall/js.valueDelete" + functionValueIndex = "syscall/js.valueIndex" + functionValueSetIndex = "syscall/js.valueSetIndex" + functionValueCall = "syscall/js.valueCall" + functionValueInvoke = "syscall/js.valueInvoke" + functionValueNew = "syscall/js.valueNew" + functionValueLength = "syscall/js.valueLength" + functionValuePrepareString = "syscall/js.valuePrepareString" + functionValueLoadString = "syscall/js.valueLoadString" + functionValueInstanceOf = "syscall/js.valueInstanceOf" + functionCopyBytesToGo = "syscall/js.copyBytesToGo" + functionCopyBytesToJS = "syscall/js.copyBytesToJS" +) + +// finalizeRef implements js.finalizeRef, which is used as a +// runtime.SetFinalizer on the given reference. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L61 +func finalizeRef(ctx context.Context, mod api.Module, sp uint32) { + // 32-bits are the ID + id := mustReadUint32Le(ctx, mod.Memory(), "r", sp+8) + getState(ctx).values.decrement(id) + panic("TODO: generate code that uses this or stub it as unused") +} + +// stringVal implements js.stringVal, which is used to load the string for +// `js.ValueOf(x)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L212 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L305-L308 +var stringVal = wasm.NewGoFunc( + functionStringVal, functionStringVal, + []string{parameterSp}, + func(ctx context.Context, mod api.Module, sp uint32) { + xAddr := mustReadUint64Le(ctx, mod.Memory(), "xAddr", sp+8) + xLen := mustReadUint64Le(ctx, mod.Memory(), "xLen", sp+16) + x := string(mustRead(ctx, mod.Memory(), "x", uint32(xAddr), uint32(xLen))) + xRef := storeRef(ctx, mod, x) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+24, xRef) + // TODO: this is only used in the cat example: make a unit test + }, +) + +// valueGet implements js.valueGet, which is used to load a js.Value property +// by name, ex. `v.Get("address")`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L295 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L311-L316 +var valueGet = wasm.NewGoFunc( + functionValueGet, functionValueGet, + []string{parameterSp}, + func(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + pAddr := mustReadUint64Le(ctx, mod.Memory(), "pAddr", sp+16) + pLen := mustReadUint64Le(ctx, mod.Memory(), "pLen", sp+24) + v := loadValue(ctx, mod, ref(vRef)) + p := mustRead(ctx, mod.Memory(), "p", uint32(pAddr), uint32(pLen)) + result := reflectGet(ctx, v, string(p)) + xRef := storeRef(ctx, mod, result) + sp = refreshSP(mod) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+32, xRef) + }, +) + +// valueSet implements js.valueSet, which is used to store a js.Value property +// by name, ex. `v.Set("address", a)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L309 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L318-L322 +func valueSet(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + pAddr := mustReadUint64Le(ctx, mod.Memory(), "pAddr", sp+16) + pLen := mustReadUint64Le(ctx, mod.Memory(), "pLen", sp+24) + xRef := mustReadUint64Le(ctx, mod.Memory(), "xRef", sp+32) + v := loadValue(ctx, mod, ref(vRef)) + p := mustRead(ctx, mod.Memory(), "p", uint32(pAddr), uint32(pLen)) + x := loadValue(ctx, mod, ref(xRef)) + reflectSet(ctx, v, string(p), x) +} + +// valueDelete implements js.valueDelete, which is used to delete a js.Value property +// by name, ex. `v.Delete("address")`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L321 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L325-L328 +func valueDelete(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + pAddr := mustReadUint64Le(ctx, mod.Memory(), "pAddr", sp+16) + pLen := mustReadUint64Le(ctx, mod.Memory(), "pLen", sp+24) + v := loadValue(ctx, mod, ref(vRef)) + p := mustRead(ctx, mod.Memory(), "p", uint32(pAddr), uint32(pLen)) + reflectDeleteProperty(v, string(p)) +} + +// valueIndex implements js.valueIndex, which is used to load a js.Value property +// by name, ex. `v.Index(0)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L334 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L331-L334 +func valueIndex(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + i := mustReadUint64Le(ctx, mod.Memory(), "i", sp+16) + v := loadValue(ctx, mod, ref(vRef)) + result := reflectGetIndex(v, uint32(i)) + xRef := storeRef(ctx, mod, result) + sp = refreshSP(mod) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+24, xRef) +} + +// valueSetIndex implements js.valueSetIndex, which is used to store a js.Value property +// by name, ex. `v.SetIndex(0, a)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L348 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L337-L340 +func valueSetIndex(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + i := mustReadUint64Le(ctx, mod.Memory(), "i", sp+16) + xRef := mustReadUint64Le(ctx, mod.Memory(), "xRef", sp+24) + v := loadValue(ctx, mod, ref(vRef)) + x := loadValue(ctx, mod, ref(xRef)) + reflectSetIndex(v, uint32(i), x) +} + +// valueCall implements js.valueCall, which is used to call a js.Value function +// by name, ex. `document.Call("createElement", "div")`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L394 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L343-L358 +func valueCall(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + mAddr := mustReadUint64Le(ctx, mod.Memory(), "mAddr", sp+16) + mLen := mustReadUint64Le(ctx, mod.Memory(), "mLen", sp+24) + argsArray := mustReadUint64Le(ctx, mod.Memory(), "argsArray", sp+32) + argsLen := mustReadUint64Le(ctx, mod.Memory(), "argsLen", sp+40) + + v := loadValue(ctx, mod, ref(vRef)) + propertyKey := string(mustRead(ctx, mod.Memory(), "property", uint32(mAddr), uint32(mLen))) + args := loadSliceOfValues(ctx, mod, uint32(argsArray), uint32(argsLen)) + + var xRef, ok uint64 + if result, err := reflectApply(ctx, mod, v, propertyKey, args); err != nil { + xRef = storeRef(ctx, mod, err) + ok = 0 + } else { + xRef = storeRef(ctx, mod, result) + ok = 1 + } + + sp = refreshSP(mod) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+56, xRef) + mustWriteUint64Le(ctx, mod.Memory(), "ok", sp+64, ok) +} + +// valueInvoke implements js.valueInvoke, which is used to call a js.Value, ex. +// `add.Invoke(1, 2)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L413 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L361-L375 +func valueInvoke(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + argsArray := mustReadUint64Le(ctx, mod.Memory(), "argsArray", sp+16) + argsLen := mustReadUint64Le(ctx, mod.Memory(), "argsLen", sp+24) + + v := loadValue(ctx, mod, ref(vRef)) + args := loadSliceOfValues(ctx, mod, uint32(argsArray), uint32(argsLen)) + + var xRef, ok uint64 + if result, err := reflectApply(ctx, mod, v, nil, args); err != nil { + xRef = storeRef(ctx, mod, err) + ok = 0 + } else { + xRef = storeRef(ctx, mod, result) + ok = 1 + } + + sp = refreshSP(mod) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+40, xRef) + mustWriteUint64Le(ctx, mod.Memory(), "ok", sp+48, ok) +} + +// valueNew implements js.valueNew, which is used to call a js.Value, ex. +// `array.New(2)`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L432 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L380-L391 +func valueNew(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + argsArray := mustReadUint64Le(ctx, mod.Memory(), "argsArray", sp+16) + argsLen := mustReadUint64Le(ctx, mod.Memory(), "argsLen", sp+24) + + v := loadValue(ctx, mod, ref(vRef)) + args := loadSliceOfValues(ctx, mod, uint32(argsArray), uint32(argsLen)) + var xRef, ok uint64 + if result, err := reflectConstruct(v, args); err != nil { + xRef = storeRef(ctx, mod, err) + ok = 0 + } else { + xRef = storeRef(ctx, mod, result) + ok = 1 + } + + sp = refreshSP(mod) + mustWriteUint64Le(ctx, mod.Memory(), "xRef", sp+40, xRef) + mustWriteUint64Le(ctx, mod.Memory(), "ok", sp+48, ok) +} + +// valueLength implements js.valueLength, which is used to load the length +// property of a value, ex. `array.length`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L372 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L396-L397 +func valueLength(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + v := loadValue(ctx, mod, ref(vRef)) + length := uint64(len(toSlice(v))) + mustWriteUint64Le(ctx, mod.Memory(), "length", sp+16, length) +} + +// valuePrepareString implements js.valuePrepareString, which is used to load +// the string for `obString()` (via js.jsString) for string, boolean and +// number types. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L531 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L402-L405 +func valuePrepareString(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + + v := loadValue(ctx, mod, ref(vRef)) + s := valueString(v) + sAddr := storeRef(ctx, mod, s) + sLen := uint64(len(s)) + + mustWriteUint64Le(ctx, mod.Memory(), "sAddr", sp+16, sAddr) + mustWriteUint64Le(ctx, mod.Memory(), "sLen", sp+24, sLen) +} + +// valueLoadString implements js.valueLoadString, which is used copy a string +// value for `obString()`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L533 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L410-L412 +func valueLoadString(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + bAddr := mustReadUint64Le(ctx, mod.Memory(), "bAddr", sp+16) + bLen := mustReadUint64Le(ctx, mod.Memory(), "bLen", sp+24) + + v := loadValue(ctx, mod, ref(vRef)) + s := valueString(v) + b := mustRead(ctx, mod.Memory(), "b", uint32(bAddr), uint32(bLen)) + copy(b, s) +} + +// valueInstanceOf implements js.valueInstanceOf. ex. `array instanceof String`. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L543 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L417-L418 +func valueInstanceOf(ctx context.Context, mod api.Module, sp uint32) { + vRef := mustReadUint64Le(ctx, mod.Memory(), "vRef", sp+8) + tRef := mustReadUint64Le(ctx, mod.Memory(), "tRef", sp+16) + + v := loadValue(ctx, mod, ref(vRef)) + t := loadValue(ctx, mod, ref(tRef)) + var r uint64 + if !instanceOf(v, t) { + r = 1 + } + + mustWriteUint64Le(ctx, mod.Memory(), "r", sp+24, r) +} + +// copyBytesToGo implements js.copyBytesToGo. +// +// Results +// +// * n is the count of bytes written. +// * ok is false if the src was not a uint8Array. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L569 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L424-L433 +func copyBytesToGo(ctx context.Context, mod api.Module, sp uint32) { + dstAddr := mustReadUint64Le(ctx, mod.Memory(), "dstAddr", sp+8) + dstLen := mustReadUint64Le(ctx, mod.Memory(), "dstLen", sp+16) + srcRef := mustReadUint64Le(ctx, mod.Memory(), "srcRef", sp+32) + + dst := mustRead(ctx, mod.Memory(), "dst", uint32(dstAddr), uint32(dstLen)) // nolint + v := loadValue(ctx, mod, ref(srcRef)) + var n, ok uint64 + if src, isBuf := maybeBuf(v); isBuf { + n = uint64(copy(dst, src)) + ok = 1 + } + + mustWriteUint64Le(ctx, mod.Memory(), "n", sp+40, n) + mustWriteUint64Le(ctx, mod.Memory(), "ok", sp+48, ok) +} + +// copyBytesToJS implements js.copyBytesToJS. +// +// Results +// +// * n is the count of bytes written. +// * ok is false if the dst was not a uint8Array. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/syscall/js/js.go#L583 +// https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L438-L448 +func copyBytesToJS(ctx context.Context, mod api.Module, sp uint32) { + dstRef := mustReadUint64Le(ctx, mod.Memory(), "dstRef", sp+8) + srcAddr := mustReadUint64Le(ctx, mod.Memory(), "srcAddr", sp+16) + srcLen := mustReadUint64Le(ctx, mod.Memory(), "srcLen", sp+24) + + src := mustRead(ctx, mod.Memory(), "src", uint32(srcAddr), uint32(srcLen)) // nolint + v := loadValue(ctx, mod, ref(dstRef)) + var n, ok uint64 + if dst, isBuf := maybeBuf(v); isBuf { + n = uint64(copy(dst, src)) + ok = 1 + } + + mustWriteUint64Le(ctx, mod.Memory(), "n", sp+40, n) + mustWriteUint64Le(ctx, mod.Memory(), "ok", sp+48, ok) +} + +// refreshSP refreshes the stack pointer, which is needed prior to storeValue +// when in an operation that can trigger a Go event handler. +// +// See https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L210-L213 +func refreshSP(mod api.Module) uint32 { + // Cheat by reading global[0] directly instead of through a function proxy. + // https://github.com/golang/go/blob/go1.19rc2/src/runtime/rt0_js_wasm.s#L87-L90 + return uint32(mod.(*wasm.CallContext).GlobalVal(0)) +} + +// syscallFstat is like syscall.Fstat +func syscallFstat(ctx context.Context, mod api.Module, fd uint32) (*jsSt, error) { + fsc := mod.(*wasm.CallContext).Sys.FS(ctx) + if f, ok := fsc.OpenedFile(ctx, fd); !ok { + return nil, errorBadFD(fd) + } else if stat, err := f.File.Stat(); err != nil { + return nil, err + } else { + return &jsSt{ + isDir: stat.IsDir(), + dev: 0, // TODO stat.Sys + ino: 0, + mode: uint32(stat.Mode()), + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + size: uint32(stat.Size()), + blksize: 0, + blocks: 0, + atimeMs: 0, + mtimeMs: uint32(stat.ModTime().UnixMilli()), + ctimeMs: 0, + }, nil + } +} + +func errorBadFD(fd uint32) error { + return fmt.Errorf("bad file descriptor: %d", fd) +} + +// syscallClose is like syscall.Close +func syscallClose(ctx context.Context, mod api.Module, fd uint32) (err error) { + fsc := mod.(*wasm.CallContext).Sys.FS(ctx) + if ok := fsc.CloseFile(ctx, fd); !ok { + err = errorBadFD(fd) + } + return +} + +// syscallOpen is like syscall.Open +func syscallOpen(ctx context.Context, mod api.Module, name string, flags, perm uint32) (uint32, error) { + fsc := mod.(*wasm.CallContext).Sys.FS(ctx) + return fsc.OpenFile(ctx, name) +} + +// syscallRead is like syscall.Read +func syscallRead(ctx context.Context, mod api.Module, fd uint32, p []byte) (n uint32, err error) { + if r := fdReader(ctx, mod, fd); r == nil { + err = errorBadFD(fd) + } else if nRead, e := r.Read(p); e == nil || e == io.EOF { + // fs_js.go cannot parse io.EOF so coerce it to nil. + // See https://github.com/golang/go/issues/43913 + n = uint32(nRead) + } else { + err = e + } + return +} + +// syscallWrite is like syscall.Write +func syscallWrite(ctx context.Context, mod api.Module, fd uint32, p []byte) (n uint32, err error) { + if writer := fdWriter(ctx, mod, fd); writer == nil { + err = errorBadFD(fd) + } else if nWritten, e := writer.Write(p); e == nil || e == io.EOF { + // fs_js.go cannot parse io.EOF so coerce it to nil. + // See https://github.com/golang/go/issues/43913 + n = uint32(nWritten) + } else { + err = e + } + return +} + +const ( + fdStdin = iota + fdStdout + fdStderr +) + +// fdReader returns a valid reader for the given file descriptor or nil if ErrnoBadf. +func fdReader(ctx context.Context, mod api.Module, fd uint32) io.Reader { + sysCtx := mod.(*wasm.CallContext).Sys + if fd == fdStdin { + return sysCtx.Stdin() + } else if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok { + return nil + } else { + return f.File + } +} + +// fdWriter returns a valid writer for the given file descriptor or nil if ErrnoBadf. +func fdWriter(ctx context.Context, mod api.Module, fd uint32) io.Writer { + sysCtx := mod.(*wasm.CallContext).Sys + switch fd { + case fdStdout: + return sysCtx.Stdout() + case fdStderr: + return sysCtx.Stderr() + default: + // Check to see if the file descriptor is available + if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil { + return nil + // fs.FS doesn't declare io.Writer, but implementations such as + // os.File implement it. + } else if writer, ok := f.File.(io.Writer); !ok { + return nil + } else { + return writer + } + } +} + +// funcWrapper is the result of go's js.FuncOf ("_makeFuncWrapper" here). +type funcWrapper struct { + s *state + + // id is managed on the Go side an increments (possibly rolling over). + id uint32 +} diff --git a/wasm_exec/wasm_exec.go b/wasm_exec/wasm_exec.go new file mode 100644 index 0000000000..bee260d27a --- /dev/null +++ b/wasm_exec/wasm_exec.go @@ -0,0 +1,580 @@ +// Package wasm_exec contains imports and state needed by wasm go compiles when +// GOOS=js and GOARCH=wasm. +// +// See /wasm_exec/REFERENCE.md for a deeper dive. +package wasm_exec + +import ( + "context" + "fmt" + "io" + "math" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/sys" +) + +// Builder configures the "go" imports used by wasm_exec.js for later use via +// Compile or Instantiate. +type Builder interface { + Build(context.Context, wazero.Runtime) (WasmExec, error) +} + +// NewBuilder returns a new Builder. +func NewBuilder() Builder { + return &builder{} +} + +type builder struct{} + +// Build implements Builder.Build +func (b *builder) Build(ctx context.Context, r wazero.Runtime) (WasmExec, error) { + return newWasmExec(r), nil +} + +const ( + parameterSp = "sp" + functionDebug = "debug" +) + +// moduleBuilder returns a new wazero.ModuleBuilder +func moduleBuilder(r wazero.Runtime) wazero.ModuleBuilder { + return r.NewModuleBuilder("go"). + ExportFunction(functionWasmExit, wasmExit, functionWasmExit, parameterSp). + ExportFunction(functionWasmWrite, wasmWrite, functionWasmWrite, parameterSp). + ExportFunction(resetMemoryDataView.Name, resetMemoryDataView). + ExportFunction(functionNanotime1, nanotime1, functionNanotime1, parameterSp). + ExportFunction(functionWalltime, walltime, functionWalltime, parameterSp). + ExportFunction(functionScheduleTimeoutEvent, scheduleTimeoutEvent, functionScheduleTimeoutEvent, parameterSp). + ExportFunction(functionClearTimeoutEvent, clearTimeoutEvent, functionClearTimeoutEvent, parameterSp). + ExportFunction(functionGetRandomData, getRandomData, functionGetRandomData, parameterSp). + ExportFunction(functionFinalizeRef, finalizeRef, functionFinalizeRef, parameterSp). + ExportFunction(stringVal.Name, stringVal). + ExportFunction(valueGet.Name, valueGet). + ExportFunction(functionValueSet, valueSet, functionValueSet, parameterSp). + ExportFunction(functionValueDelete, valueDelete, functionValueDelete, parameterSp). + ExportFunction(functionValueIndex, valueIndex, functionValueIndex, parameterSp). + ExportFunction(functionValueSetIndex, valueSetIndex, functionValueSetIndex, parameterSp). + ExportFunction(functionValueCall, valueCall, functionValueCall, parameterSp). + ExportFunction(functionValueInvoke, valueInvoke, functionValueInvoke, parameterSp). + ExportFunction(functionValueNew, valueNew, functionValueNew, parameterSp). + ExportFunction(functionValueLength, valueLength, functionValueLength, parameterSp). + ExportFunction(functionValuePrepareString, valuePrepareString, functionValuePrepareString, parameterSp). + ExportFunction(functionValueLoadString, valueLoadString, functionValueLoadString, parameterSp). + ExportFunction(functionValueInstanceOf, valueInstanceOf, functionValueInstanceOf, parameterSp). + ExportFunction(functionCopyBytesToGo, copyBytesToGo, functionCopyBytesToGo, parameterSp). + ExportFunction(functionCopyBytesToJS, copyBytesToJS, functionCopyBytesToJS, parameterSp). + ExportFunction(debug.Name, debug) +} + +// debug has unknown use, so stubbed. +// +// See https://github.com/golang/go/blob/go1.19rc2/src/cmd/link/internal/wasm/asm.go#L133-L138 +var debug = &wasm.Func{ + ExportNames: []string{functionDebug}, + Name: functionDebug, + ParamTypes: []wasm.ValueType{wasm.ValueTypeI32}, + ParamNames: []string{parameterSp}, + Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}}, +} + +// reflectGet implements JavaScript's Reflect.get API. +func reflectGet(ctx context.Context, target interface{}, propertyKey string) interface{} { // nolint + if target == valueGlobal { + switch propertyKey { + case "Object": + return objectConstructor + case "Array": + return arrayConstructor + case "process": + return jsProcess + case "fs": + return jsFS + case "Uint8Array": + return uint8ArrayConstructor + } + } else if target == getState(ctx) { + switch propertyKey { + case "_pendingEvent": + return target.(*state)._pendingEvent + } + } else if target == jsFS { + switch propertyKey { + case "constants": + return jsFSConstants + } + } else if target == io.EOF { + switch propertyKey { + case "code": + return "EOF" + } + } else if s, ok := target.(*jsSt); ok { + switch propertyKey { + case "dev": + return s.dev + case "ino": + return s.ino + case "mode": + return s.mode + case "nlink": + return s.nlink + case "uid": + return s.uid + case "gid": + return s.gid + case "rdev": + return s.rdev + case "size": + return s.size + case "blksize": + return s.blksize + case "blocks": + return s.blocks + case "atimeMs": + return s.atimeMs + case "mtimeMs": + return s.mtimeMs + case "ctimeMs": + return s.ctimeMs + } + } else if target == jsFSConstants { + switch propertyKey { + case "O_WRONLY": + return oWRONLY + case "O_RDWR": + return oRDWR + case "O_CREAT": + return oCREAT + case "O_TRUNC": + return oTRUNC + case "O_APPEND": + return oAPPEND + case "O_EXCL": + return oEXCL + } + } else if e, ok := target.(*event); ok { // syscall_js.handleEvent + switch propertyKey { + case "id": + return e.id + case "this": // ex fs + return e.this + case "args": + return e.args + } + } + panic(fmt.Errorf("TODO: reflectGet(target=%v, propertyKey=%s)", target, propertyKey)) +} + +// reflectGetIndex implements JavaScript's Reflect.get API for an index. +func reflectGetIndex(target interface{}, i uint32) interface{} { // nolint + return toSlice(target)[i] +} + +// reflectSet implements JavaScript's Reflect.set API. +func reflectSet(ctx context.Context, target interface{}, propertyKey string, value interface{}) { // nolint + if target == getState(ctx) { + switch propertyKey { + case "_pendingEvent": + if value == nil { // syscall_js.handleEvent + target.(*state)._pendingEvent = nil + return + } + } + } else if e, ok := target.(*event); ok { // syscall_js.handleEvent + switch propertyKey { + case "result": + e.result = value + return + } + } + panic(fmt.Errorf("TODO: reflectSet(target=%v, propertyKey=%s, value=%v)", target, propertyKey, value)) +} + +// reflectSetIndex implements JavaScript's Reflect.set API for an index. +func reflectSetIndex(target interface{}, i uint32, value interface{}) { // nolint + panic(fmt.Errorf("TODO: reflectSetIndex(target=%v, i=%d, value=%v)", target, i, value)) +} + +// reflectDeleteProperty implements JavaScript's Reflect.deleteProperty API +func reflectDeleteProperty(target interface{}, propertyKey string) { // nolint + panic(fmt.Errorf("TODO: reflectDeleteProperty(target=%v, propertyKey=%s)", target, propertyKey)) +} + +// reflectApply implements JavaScript's Reflect.apply API +func reflectApply( + ctx context.Context, + mod api.Module, + target interface{}, + propertyKey interface{}, + argumentsList []interface{}, +) (interface{}, error) { // nolint + if target == getState(ctx) { + switch propertyKey { + case "_makeFuncWrapper": + return &funcWrapper{s: target.(*state), id: uint32(argumentsList[0].(float64))}, nil + } + } else if target == jsFS { // fs_js.go js.fsCall + // * funcWrapper callback is the last parameter + // * arg0 is error and up to one result in arg1 + switch propertyKey { + case "open": + // jsFD, err := fsCall("open", name, flags, perm) + name := argumentsList[0].(string) + flags := toUint32(argumentsList[1]) // flags are derived from constants like oWRONLY + perm := toUint32(argumentsList[2]) + result := argumentsList[3].(*funcWrapper) + + fd, err := syscallOpen(ctx, mod, name, flags, perm) + result.call(ctx, mod, jsFS, err, fd) // note: error first + + return nil, nil + case "fstat": + // if stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool() + fd := toUint32(argumentsList[0]) + result := argumentsList[1].(*funcWrapper) + + stat, err := syscallFstat(ctx, mod, fd) + result.call(ctx, mod, jsFS, err, stat) // note: error first + + return nil, nil + case "close": + // if stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool() + fd := toUint32(argumentsList[0]) + result := argumentsList[1].(*funcWrapper) + + err := syscallClose(ctx, mod, fd) + result.call(ctx, mod, jsFS, err, true) // note: error first + + return nil, nil + case "read": // syscall.Read, called by src/internal/poll/fd_unix.go poll.Read. + // n, err := fsCall("read", fd, buf, 0, len(b), nil) + fd := toUint32(argumentsList[0]) + buf, ok := maybeBuf(argumentsList[1]) + if !ok { + return nil, fmt.Errorf("arg[1] is %v not a []byte", argumentsList[1]) + } + offset := toUint32(argumentsList[2]) + byteCount := toUint32(argumentsList[3]) + _ /* unknown */ = argumentsList[4] + result := argumentsList[5].(*funcWrapper) + + n, err := syscallRead(ctx, mod, fd, buf[offset:offset+byteCount]) + result.call(ctx, mod, jsFS, err, n) // note: error first + + return nil, nil + case "write": + // n, err := fsCall("write", fd, buf, 0, len(b), nil) + fd := toUint32(argumentsList[0]) + buf, ok := maybeBuf(argumentsList[1]) + if !ok { + return nil, fmt.Errorf("arg[1] is %v not a []byte", argumentsList[1]) + } + offset := toUint32(argumentsList[2]) + byteCount := toUint32(argumentsList[3]) + _ /* unknown */ = argumentsList[4] + result := argumentsList[5].(*funcWrapper) + + n, err := syscallWrite(ctx, mod, fd, buf[offset:offset+byteCount]) + result.call(ctx, mod, jsFS, err, n) // note: error first + + return nil, nil + } + } else if target == jsProcess { + switch propertyKey { + case "cwd": + // cwd := jsProcess.Call("cwd").String() + // TODO + } + } else if stat, ok := target.(*jsSt); ok { + switch propertyKey { + case "isDirectory": + return stat.isDir, nil + } + } + panic(fmt.Errorf("TODO: reflectApply(target=%v, propertyKey=%v, argumentsList=%v)", target, propertyKey, argumentsList)) +} + +func toUint32(arg interface{}) uint32 { + if arg == refValueZero { + return 0 + } else if u, ok := arg.(uint32); ok { + return u + } + return uint32(arg.(float64)) +} + +type event struct { + // funcWrapper.id + id uint32 + this interface{} + args []interface{} + result interface{} +} + +func (f *funcWrapper) call(ctx context.Context, mod api.Module, args ...interface{}) interface{} { + e := &event{ + id: f.id, + this: args[0], + args: args[1:], + } + + f.s._pendingEvent = e // Note: _pendingEvent reference is cleared during resume! + + if _, err := mod.ExportedFunction("resume").Call(ctx); err != nil { + if _, ok := err.(*sys.ExitError); ok { + return nil // allow error-handling to unwind when wasm calls exit due to a panic + } else { + panic(err) + } + } + + return e.result +} + +// reflectConstruct implements JavaScript's Reflect.construct API +func reflectConstruct(target interface{}, argumentsList []interface{}) (interface{}, error) { // nolint + if target == uint8ArrayConstructor { + return make([]byte, uint32(argumentsList[0].(float64))), nil + } + panic(fmt.Errorf("TODO: reflectConstruct(target=%v, argumentsList=%v)", target, argumentsList)) +} + +// valueRef returns 8 bytes to represent either the value or a reference to it. +// Any side effects besides memory must be cleaned up on wasmExit. +// +// See https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L135-L183 +func storeRef(ctx context.Context, mod api.Module, v interface{}) uint64 { // nolint + // allow-list because we control all implementations + if v == nil { + return uint64(refValueNull) + } else if b, ok := v.(bool); ok { + if b { + return uint64(refValueTrue) + } else { + return uint64(refValueFalse) + } + } else if jsV, ok := v.(*constVal); ok { + return uint64(jsV.ref) // constant doesn't need to be stored + } else if u, ok := v.(uint64); ok { + return u // float already encoded as a uint64, doesn't need to be stored. + } else if fn, ok := v.(*funcWrapper); ok { + id := fn.s.values.increment(v) + return uint64(valueRef(id, typeFlagFunction)) + } else if _, ok := v.(*event); ok { + id := getState(ctx).values.increment(v) + return uint64(valueRef(id, typeFlagFunction)) + } else if _, ok := v.(string); ok { + id := getState(ctx).values.increment(v) + return uint64(valueRef(id, typeFlagString)) + } else if _, ok := v.([]interface{}); ok { + id := getState(ctx).values.increment(&v) // []interface{} is not hashable + return uint64(valueRef(id, typeFlagObject)) + } else if _, ok := v.([]byte); ok { + id := getState(ctx).values.increment(&v) // []byte is not hashable + return uint64(valueRef(id, typeFlagObject)) + } else if v == io.EOF { + return uint64(refEOF) + } else if _, ok := v.(*jsSt); ok { + id := getState(ctx).values.increment(v) + return uint64(valueRef(id, typeFlagObject)) + } else if ui, ok := v.(uint32); ok { + if ui == 0 { + return uint64(refValueZero) + } + return api.EncodeF64(float64(ui)) // numbers are encoded as float and passed through as a ref + } + panic(fmt.Errorf("TODO: storeRef(%v)", v)) +} + +type values struct { + // Below is needed to avoid exhausting the ID namespace finalizeRef reclaims + // See https://go-review.googlesource.com/c/go/+/203600 + + values []interface{} // values indexed by ID, nil + goRefCounts []uint32 // recount pair-indexed with values + ids map[interface{}]uint32 // live values + idPool []uint32 // reclaimed IDs (values[i] = nil, goRefCounts[i] nil +} + +func (j *values) get(id uint32) interface{} { + return j.values[id-nextID] +} + +func (j *values) increment(v interface{}) uint32 { + id, ok := j.ids[v] + if !ok { + if len(j.idPool) == 0 { + id, j.values, j.goRefCounts = uint32(len(j.values)), append(j.values, v), append(j.goRefCounts, 0) + } else { + id, j.idPool = j.idPool[len(j.idPool)-1], j.idPool[:len(j.idPool)-1] + j.values[id], j.goRefCounts[id] = v, 0 + } + j.ids[v] = id + } + j.goRefCounts[id]++ + return id + nextID +} + +func (j *values) decrement(id uint32) { + id -= nextID + j.goRefCounts[id]-- + if j.goRefCounts[id] == 0 { + j.values[id] = nil + j.idPool = append(j.idPool, id) + } +} + +var NaN = math.NaN() + +// loadValue reads up to 8 bytes at the memory offset `addr` to return the +// value written by storeValue. +// +// See https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L122-L133 +func loadValue(ctx context.Context, mod api.Module, ref ref) interface{} { // nolint + switch ref { + case refValueNaN: + return NaN + case refValueZero: + return uint32(0) + case refValueNull: + return nil + case refValueTrue: + return true + case refValueFalse: + return false + case refValueGlobal: + return valueGlobal + case refJsGo: + return getState(ctx) + case refObjectConstructor: + return objectConstructor + case refArrayConstructor: + return arrayConstructor + case refJsProcess: + return jsProcess + case refJsFS: + return jsFS + case refJsFSConstants: + return jsFSConstants + case refUint8ArrayConstructor: + return uint8ArrayConstructor + case refEOF: + return io.EOF + default: + if (ref>>32)&nanHead != nanHead { // numbers are passed through as a ref + return api.DecodeF64(uint64(ref)) + } + return getState(ctx).values.get(uint32(ref)) + } +} + +// loadSliceOfValues returns a slice of `len` values at the memory offset +// `addr` +// +// See https://github.com/golang/go/blob/go1.19rc2/misc/wasm/wasm_exec.js#L191-L199 +func loadSliceOfValues(ctx context.Context, mod api.Module, sliceAddr, sliceLen uint32) []interface{} { // nolint + result := make([]interface{}, 0, sliceLen) + for i := uint32(0); i < sliceLen; i++ { // nolint + iRef := mustReadUint64Le(ctx, mod.Memory(), "iRef", sliceAddr+i*8) + result = append(result, loadValue(ctx, mod, ref(iRef))) + } + return result +} + +// valueString returns the string form of JavaScript string, boolean and number types. +func valueString(v interface{}) string { // nolint + if s, ok := v.(string); ok { + return s + } + panic(fmt.Errorf("TODO: valueString(%v)", v)) +} + +// instanceOf returns true if the value is of the given type. +func instanceOf(v, t interface{}) bool { // nolint + panic(fmt.Errorf("TODO: instanceOf(v=%v, t=%v)", v, t)) +} + +// mustRead is like api.Memory except that it panics if the offset and +// byteCount are out of range. +func mustRead(ctx context.Context, mem api.Memory, fieldName string, offset, byteCount uint32) []byte { + buf, ok := mem.Read(ctx, offset, byteCount) + if !ok { + panic(fmt.Errorf("Memory.Read(ctx, %d, %d) out of range of memory size %d reading %s", + offset, byteCount, mem.Size(ctx), fieldName)) + } + return buf +} + +// mustReadUint32Le is like api.Memory except that it panics if the offset +// is out of range. +func mustReadUint32Le(ctx context.Context, mem api.Memory, fieldName string, offset uint32) uint32 { + result, ok := mem.ReadUint32Le(ctx, offset) + if !ok { + panic(fmt.Errorf("Memory.ReadUint64Le(ctx, %d) out of range of memory size %d reading %s", + offset, mem.Size(ctx), fieldName)) + } + return result +} + +// mustReadUint64Le is like api.Memory except that it panics if the offset +// is out of range. +func mustReadUint64Le(ctx context.Context, mem api.Memory, fieldName string, offset uint32) uint64 { + result, ok := mem.ReadUint64Le(ctx, offset) + if !ok { + panic(fmt.Errorf("Memory.ReadUint64Le(ctx, %d) out of range of memory size %d reading %s", + offset, mem.Size(ctx), fieldName)) + } + return result +} + +// mustWrite is like api.Memory except that it panics if the offset +// is out of range. +func mustWrite(ctx context.Context, mem api.Memory, fieldName string, offset uint32, val []byte) { + if ok := mem.Write(ctx, offset, val); !ok { + panic(fmt.Errorf("Memory.Write(ctx, %d, %d) out of range of memory size %d writing %s", + offset, val, mem.Size(ctx), fieldName)) + } +} + +// mustWriteUint64Le is like api.Memory except that it panics if the offset +// is out of range. +func mustWriteUint64Le(ctx context.Context, mem api.Memory, fieldName string, offset uint32, val uint64) { + if ok := mem.WriteUint64Le(ctx, offset, val); !ok { + panic(fmt.Errorf("Memory.WriteUint64Le(ctx, %d, %d) out of range of memory size %d writing %s", + offset, val, mem.Size(ctx), fieldName)) + } +} + +// jsSt is pre-parsed from fs_js.go setStat to avoid thrashin +type jsSt struct { + isDir bool + + dev uint32 + ino uint32 + mode uint32 + nlink uint32 + uid uint32 + gid uint32 + rdev uint32 + size uint32 + blksize uint32 + blocks uint32 + atimeMs uint32 + mtimeMs uint32 + ctimeMs uint32 +} + +func toSlice(v interface{}) []interface{} { + return (*(v.(*interface{}))).([]interface{}) +} + +func maybeBuf(v interface{}) ([]byte, bool) { + if p, ok := v.(*interface{}); ok { + if b, ok := (*(p)).([]byte); ok { + return b, true + } + } + return nil, false +} diff --git a/wasm_exec/wasm_exec_test.go b/wasm_exec/wasm_exec_test.go new file mode 100644 index 0000000000..0cd41ef2d5 --- /dev/null +++ b/wasm_exec/wasm_exec_test.go @@ -0,0 +1,42 @@ +package wasm_exec_test + +import ( + "context" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/sys" +) + +// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. +var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") + +func Test_argsAndEnv(t *testing.T) { + stdout, stderr, err := compileAndRunJsWasm(testCtx, `package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println() + for i, a := range os.Args { + fmt.Println("args", i, "=", a) + } + for i, e := range os.Environ() { + fmt.Println("environ", i, "=", e) + } +}`, wazero.NewModuleConfig().WithArgs("a", "b").WithEnv("c", "d").WithEnv("a", "b")) + + require.Error(t, err) + require.Zero(t, err.(*sys.ExitError).ExitCode()) + require.Equal(t, ` +args 0 = a +args 1 = b +environ 0 = c=d +environ 1 = a=b +`, stdout) + require.Equal(t, "", stderr) +}