From 7498ad335f62d17eeda66d9bf5bc3cc73846fec0 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Wed, 5 Jul 2023 16:56:18 +0800 Subject: [PATCH] gojs: drops HTTP support to be compatible with Go 1.21 (#1557) Signed-off-by: Adrian Cole --- RATIONALE.md | 4 +- cmd/wazero/testdata/cat/.gitignore | 2 +- cmd/wazero/wazero.go | 16 +-- cmd/wazero/wazero_test.go | 25 ++-- experimental/gojs/README.md | 8 +- experimental/gojs/example/README.md | 17 +-- experimental/gojs/example/cat.go | 65 ++++++++++ experimental/gojs/example/cat/.gitignore | 2 + experimental/gojs/example/cat/main.go | 19 +++ experimental/gojs/example/cat_test.go | 46 +++++++ experimental/gojs/example/stars.go | 83 ------------ experimental/gojs/example/stars/go.mod | 3 - experimental/gojs/example/stars/main.go | 36 ------ experimental/gojs/example/stars_test.go | 97 -------------- experimental/gojs/gojs.go | 75 ++++++----- experimental/logging/log_listener.go | 2 +- imports/README.md | 2 +- internal/fstest/fstest.go | 4 +- internal/gojs/argsenv_test.go | 2 +- internal/gojs/builtin.go | 28 ++-- internal/gojs/compiler_test.go | 29 +++-- internal/gojs/config/config.go | 3 - internal/gojs/crypto_test.go | 4 +- internal/gojs/custom/names.go | 2 +- internal/gojs/custom/process.go | 6 + internal/gojs/fs_test.go | 6 +- internal/gojs/goos/goos.go | 22 ++-- internal/gojs/http.go | 156 ----------------------- internal/gojs/http_test.go | 55 -------- internal/gojs/misc_test.go | 26 ++-- internal/gojs/process_test.go | 2 +- internal/gojs/run/gojs.go | 6 + internal/gojs/state.go | 7 +- internal/gojs/syscall.go | 5 - internal/gojs/testdata/http/main.go | 32 ----- internal/gojs/testdata/main.go | 3 - internal/gojs/time_test.go | 9 +- internal/gojs/values/values_test.go | 4 +- site/content/languages/_index.md | 2 +- site/content/languages/go.md | 24 ++-- 40 files changed, 303 insertions(+), 636 deletions(-) create mode 100644 experimental/gojs/example/cat.go create mode 100644 experimental/gojs/example/cat/.gitignore create mode 100644 experimental/gojs/example/cat/main.go create mode 100644 experimental/gojs/example/cat_test.go delete mode 100644 experimental/gojs/example/stars.go delete mode 100644 experimental/gojs/example/stars/go.mod delete mode 100644 experimental/gojs/example/stars/main.go delete mode 100644 experimental/gojs/example/stars_test.go delete mode 100644 internal/gojs/http.go delete mode 100644 internal/gojs/http_test.go delete mode 100644 internal/gojs/testdata/http/main.go diff --git a/RATIONALE.md b/RATIONALE.md index 487fab5c..7bc05405 100644 --- a/RATIONALE.md +++ b/RATIONALE.md @@ -531,7 +531,7 @@ In short, wazero defined system configuration in `ModuleConfig`, not a WASI type one spec to another with minimal impact. This has other helpful benefits, as centralized resources are simpler to close coherently (ex via `Module.Close`). -In reflection, this worked well as more ABI became usable in wazero. For example, `GOARCH=wasm GOOS=js` code uses the +In reflection, this worked well as more ABI became usable in wazero. For example, `GOOS=js GOARCH=wasm` code uses the same `ModuleConfig` (and `FSConfig`) WASI uses, and in compatible ways. ### Background on `ModuleConfig` design @@ -664,7 +664,7 @@ WASI is an abstraction over syscalls. For example, the signature of `fs.Open` does not permit use of flags. This creates conflict on what default behaviors to take when Go implemented `os.DirFS`. On the other hand, `path_open` can pass flags, and in fact tests require them to be honored in specific ways. This -extends beyond WASI as even `GOARCH=wasm GOOS=js` compiled code requires +extends beyond WASI as even `GOOS=js GOARCH=wasm` compiled code requires certain flags passed to `os.OpenFile` which are impossible to pass due to the signature of `fs.FS`. diff --git a/cmd/wazero/testdata/cat/.gitignore b/cmd/wazero/testdata/cat/.gitignore index bd1435d6..344ca55c 100644 --- a/cmd/wazero/testdata/cat/.gitignore +++ b/cmd/wazero/testdata/cat/.gitignore @@ -1,2 +1,2 @@ -# GOARCH=wasm GOOS=js binaries are too huge to check-in +# GOOS=js GOARCH=wasm binaries are too huge to check-in cat-go.wasm diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index 93c24f67..e8956db8 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -316,16 +316,16 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { conf = conf.WithEnv(env[i], env[i+1]) } - code, err := rt.CompileModule(ctx, wasm) + guest, err := rt.CompileModule(ctx, wasm) if err != nil { fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err) return 1 } - switch detectImports(code.ImportedFunctions()) { + switch detectImports(guest.ImportedFunctions()) { case modeWasi: wasi_snapshot_preview1.MustInstantiate(ctx, rt) - _, err = rt.InstantiateModule(ctx, code, conf) + _, err = rt.InstantiateModule(ctx, guest, conf) case modeWasiUnstable: // Instantiate the current WASI functions under the wasi_unstable // instead of wasi_snapshot_preview1. @@ -334,7 +334,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { _, err = wasiBuilder.Instantiate(ctx) if err == nil { // Instantiate our binary, but using the old import names. - _, err = rt.InstantiateModule(ctx, code, conf) + _, err = rt.InstantiateModule(ctx, guest, conf) } case modeGo: // Fail fast on multiple mounts with the deprecated GOOS=js. @@ -345,7 +345,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { return 1 } - gojs.MustInstantiate(ctx, rt) + gojs.MustInstantiate(ctx, rt, guest) config := gojs.NewConfig(conf).WithOSUser() @@ -359,9 +359,9 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { config = config.WithOSWorkdir() } - err = gojs.Run(ctx, rt, code, config) + err = gojs.Run(ctx, rt, guest, config) case modeDefault: - _, err = rt.InstantiateModule(ctx, code, conf) + _, err = rt.InstantiateModule(ctx, guest, conf) } if err != nil { @@ -469,7 +469,7 @@ func detectImports(imports []api.FunctionDefinition) importMode { return modeWasi case "wasi_unstable": return modeWasiUnstable - case "go": + case "go", "gojs": return modeGo } } diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go index 67464356..f75dd50c 100644 --- a/cmd/wazero/wazero_test.go +++ b/cmd/wazero/wazero_test.go @@ -39,7 +39,7 @@ var wasmWasiFd []byte //go:embed testdata/wasi_random_get.wasm var wasmWasiRandomGet []byte -// wasmCatGo is compiled on demand with `GOARCH=wasm GOOS=js` +// wasmCatGo is compiled on demand with `GOOS=js GOARCH=wasm` var wasmCatGo []byte //go:embed testdata/cat/cat-tinygo.wasm @@ -57,7 +57,7 @@ func TestMain(m *testing.M) { // Notably our scratch containers don't have go, so don't fail tests. if err := compileGoJS(); err != nil { - log.Println("main: Skipping GOARCH=wasm GOOS=js tests due to:", err) + log.Println("main: Skipping GOOS=js GOARCH=wasm tests due to:", err) os.Exit(0) } os.Exit(m.Run()) @@ -370,14 +370,14 @@ func TestRun(t *testing.T) { `, }, { - name: "GOARCH=wasm GOOS=js", + name: "GOOS=js GOARCH=wasm", wasm: wasmCatGo, wazeroOpts: []string{fmt.Sprintf("--mount=%s:/", bearDir)}, wasmArgs: []string{"/bear.txt"}, expectedStdout: "pooh\n", }, { - name: "GOARCH=wasm GOOS=js workdir", + name: "GOOS=js GOARCH=wasm workdir", wasm: wasmCatGo, wazeroOpts: []string{ // --mount=X:\:/ on Windows, --mount=/:/ everywhere else @@ -388,14 +388,14 @@ func TestRun(t *testing.T) { expectedStdout: "pooh\n", }, { - name: "GOARCH=wasm GOOS=js readonly", + name: "GOOS=js GOARCH=wasm readonly", wasm: wasmCatGo, wazeroOpts: []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)}, wasmArgs: []string{"/bear.txt"}, expectedStdout: "pooh\n", }, { - name: "GOARCH=wasm GOOS=js hostlogging=proc", + name: "GOOS=js GOARCH=wasm hostlogging=proc", wasm: wasmCatGo, wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/:ro", bearDir)}, wasmArgs: []string{"/not-bear.txt"}, @@ -405,7 +405,7 @@ func TestRun(t *testing.T) { expectedExitCode: 1, }, { - name: "GOARCH=wasm GOOS=js hostlogging=filesystem", + name: "GOOS=js GOARCH=wasm hostlogging=filesystem", wasm: wasmCatGo, wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/", bearDir)}, wasmArgs: []string{"/bear.txt"}, @@ -425,7 +425,7 @@ func TestRun(t *testing.T) { `, bearMode, bearMtime), }, { - name: "GOARCH=wasm GOOS=js not root mount", + name: "GOOS=js GOARCH=wasm not root mount", wasm: wasmCatGo, wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)}, wasmArgs: []string{"/not-bear.txt"}, @@ -531,7 +531,7 @@ Consider switching to GOOS=wasip1. } cryptoTest := test{ - name: "GOARCH=wasm GOOS=js hostlogging=filesystem,random", + name: "GOOS=js GOARCH=wasm hostlogging=filesystem,random", wasm: wasmCatGo, wazeroOpts: []string{"--hostlogging=filesystem,random"}, wasmArgs: []string{"/bear.txt"}, @@ -682,7 +682,7 @@ func Test_detectImports(t *testing.T) { mode: modeWasiUnstable, }, { - message: "GOARCH=wasm GOOS=js", + message: "GOOS=js GOARCH=wasm", imports: []api.FunctionDefinition{ importer{internalapi.WazeroOnlyType{}, "go", "syscall/js.valueCall"}, }, @@ -813,7 +813,10 @@ func runMain(t *testing.T, workdir string, args []string) (int, string, string) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) exitCode := doMain(stdout, stderr) - return exitCode, stdout.String(), stderr.String() + + // Handle "go" -> "gojs" module rename in Go 1.21 + stderrString := strings.ReplaceAll(stderr.String(), "==> gojs", "==> go") + return exitCode, stdout.String(), stderrString } // compileGoJS compiles "testdata/cat/cat.go" on demand as the binary generated diff --git a/experimental/gojs/README.md b/experimental/gojs/README.md index b608b576..0cab3a4c 100644 --- a/experimental/gojs/README.md +++ b/experimental/gojs/README.md @@ -9,15 +9,15 @@ a `%.wasm` file compiled by Go. This is similar to what is implemented in ## Example -wazero includes an [example](example) that makes HTTP client requests. +wazero includes an [example](example) that implements the `cat` utility. ## Experimental Go defines js "EXPERIMENTAL... exempt from the Go compatibility promise." Accordingly, wazero cannot guarantee this will work from release to release, -or that usage will be relatively free of bugs. Moreover, [`GOOS=wasip1`][2] will be shipped -in Go 1.21, and once that's available in two releases wazero will remove this -package. +or that usage will be relatively free of bugs. Moreover, [`GOOS=wasip1`][2] +will be shipped in Go 1.21. wazero will remove this package after Go 1.22 is +released. Due to these concerns and the relatively high implementation overhead, most will choose TinyGo instead of gojs. diff --git a/experimental/gojs/example/README.md b/experimental/gojs/example/README.md index ff069ac6..e0870a3d 100644 --- a/experimental/gojs/example/README.md +++ b/experimental/gojs/example/README.md @@ -1,21 +1,18 @@ ## gojs example -This shows how to use Wasm built by go using `GOARCH=wasm GOOS=js`. Notably, -this shows an interesting feature this supports, HTTP client requests. +This shows how to use Wasm built by go using `GOOS=js GOARCH=wasm`. Notably, +this uses filesystem support. ```bash -$ cd stars -$ GOARCH=wasm GOOS=js GOWASM=satconv,signext go build -o main.wasm . -$ cd .. -$ go run stars.go -wazero has 9999999 stars. Does that include you? +$ go run cat.go /test.txt +greet filesystem ``` -Internally, this uses [gojs](../gojs.go), which implements the custom host +Internally, this uses [gojs](../README.md), which implements the custom host functions required by Go. Notes: -* `GOARCH=wasm GOOS=js` is experimental as is wazero's support of it. For - details, see https://wazero.io/languages/go/. +* `GOOS=js GOARCH=wasm` wazero be removed after Go 1.22 is released. Please + switch to `GOOS=wasip1 GOARCH=wasm` released in Go 1.21. * `GOWASM=satconv,signext` enables features in WebAssembly Core Specification 2.0. diff --git a/experimental/gojs/example/cat.go b/experimental/gojs/example/cat.go new file mode 100644 index 00000000..fbed5f40 --- /dev/null +++ b/experimental/gojs/example/cat.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "log" + "os" + "path" + "testing/fstest" + "time" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/experimental/gojs" +) + +// main invokes Wasm compiled via `GOOS=js GOARCH=wasm`, which writes an input +// file to stdout, just like `cat`. +// +// This shows how to integrate a filesystem with wasm using gojs. +func main() { + // Read the binary compiled with `GOOS=js GOARCH=wasm`. + bin, err := os.ReadFile(path.Join("cat", "main.wasm")) + if err != nil { + log.Panicln(err) + } + + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntime(ctx) + defer r.Close(ctx) // This closes everything this Runtime created. + + // Compile the wasm binary to machine code. + start := time.Now() + guest, err := r.CompileModule(ctx, bin) + if err != nil { + log.Panicln(err) + } + compilationTime := time.Since(start).Milliseconds() + log.Printf("CompileModule took %dms", compilationTime) + + // Instantiate the host functions needed by the guest. + start = time.Now() + gojs.MustInstantiate(ctx, r, guest) + instantiateTime := time.Since(start).Milliseconds() + log.Printf("gojs.MustInstantiate took %dms", instantiateTime) + + fakeFilesystem := fstest.MapFS{"test.txt": {Data: []byte("greet filesystem\n")}} + + // Create the sandbox configuration used by the guest. + guestConfig := wazero.NewModuleConfig(). + // By default, I/O streams are discarded and there's no file system. + WithStdout(os.Stdout).WithStderr(os.Stderr). + WithFS(fakeFilesystem). + WithArgs("gojs", os.Args[1]) // only what's in the filesystem will work! + + // Execute the "run" function, which corresponds to "main" in stars/main.go. + start = time.Now() + err = gojs.Run(ctx, r, guest, gojs.NewConfig(guestConfig)) + runTime := time.Since(start).Milliseconds() + log.Printf("gojs.Run took %dms", runTime) + if err != nil { + log.Panicln(err) + } +} diff --git a/experimental/gojs/example/cat/.gitignore b/experimental/gojs/example/cat/.gitignore new file mode 100644 index 00000000..c6698dea --- /dev/null +++ b/experimental/gojs/example/cat/.gitignore @@ -0,0 +1,2 @@ +# GOOS=js GOARCH=wasm binaries are too huge to check-in +main.wasm diff --git a/experimental/gojs/example/cat/main.go b/experimental/gojs/example/cat/main.go new file mode 100644 index 00000000..0888df56 --- /dev/null +++ b/experimental/gojs/example/cat/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "os" +) + +// main runs cat: 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++ { + bytes, err := os.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/experimental/gojs/example/cat_test.go b/experimental/gojs/example/cat_test.go new file mode 100644 index 00000000..76668aff --- /dev/null +++ b/experimental/gojs/example/cat_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// 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) +} + +// TestMain compiles the wasm on-demand, which uses the runner's Go as opposed +// to a binary checked in, which would be pinned to one version. This is +// separate from Test_main to show that compilation doesn't dominate the +// execution time. +func TestMain(m *testing.M) { + // Notably our scratch containers don't have go, so don't fail tests. + if err := compileFromGo(); err != nil { + log.Println("Skipping tests due to:", err) + os.Exit(0) + } + os.Exit(m.Run()) +} + +// compileFromGo compiles "stars/main.go" on demand as the binary generated is +// too big (>7MB) to check into the source tree. +func compileFromGo() error { + cmd := exec.Command("go", "build", "-o", "main.wasm", ".") + cmd.Dir = "cat" + cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js", "GOWASM=satconv,signext") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("go build: %v\n%s", err, out) + } + return nil +} diff --git a/experimental/gojs/example/stars.go b/experimental/gojs/example/stars.go deleted file mode 100644 index ae446032..00000000 --- a/experimental/gojs/example/stars.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "context" - "io" - "log" - "net/http" - "os" - "path" - "strings" - "time" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/experimental/gojs" - "github.com/tetratelabs/wazero/sys" -) - -// main invokes Wasm compiled via `GOARCH=wasm GOOS=js`, which reports the star -// count of wazero. -// -// This shows how to integrate an HTTP client with wasm using gojs. -func main() { - ctx := context.Background() - - // Create a new WebAssembly Runtime. - r := wazero.NewRuntime(ctx) - defer r.Close(ctx) // This closes everything this Runtime created. - - // Add the host functions used by `GOARCH=wasm GOOS=js` - start := time.Now() - gojs.MustInstantiate(ctx, r) - - goJsInstantiate := time.Since(start).Milliseconds() - log.Printf("gojs.MustInstantiate took %dms", goJsInstantiate) - - // Combine the above into our baseline config, overriding defaults. - moduleConfig := wazero.NewModuleConfig(). - // By default, I/O streams are discarded, so you won't see output. - WithStdout(os.Stdout).WithStderr(os.Stderr) - - bin, err := os.ReadFile(path.Join("stars", "main.wasm")) - if err != nil { - log.Panicln(err) - } - - // Compile the WebAssembly module using the default configuration. - start = time.Now() - compiled, err := r.CompileModule(ctx, bin) - if err != nil { - log.Panicln(err) - } - compilationTime := time.Since(start).Milliseconds() - log.Printf("CompileModule took %dms", compilationTime) - - // Instead of making real HTTP calls, return fake data. - config := gojs.NewConfig(moduleConfig).WithRoundTripper(&fakeGitHub{}) - - // Execute the "run" function, which corresponds to "main" in stars/main.go. - start = time.Now() - err = gojs.Run(ctx, r, compiled, config) - runTime := time.Since(start).Milliseconds() - log.Printf("gojs.Run took %dms", runTime) - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - log.Panicln(err) - } else if !ok { - log.Panicln(err) - } -} - -// compile-time check to ensure fakeGitHub implements http.RoundTripper -var _ http.RoundTripper = &fakeGitHub{} - -type fakeGitHub struct{} - -func (f *fakeGitHub) RoundTrip(*http.Request) (*http.Response, error) { - fakeResponse := `{"stargazers_count": 9999999}` - return &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(strings.NewReader(fakeResponse)), - ContentLength: int64(len(fakeResponse)), - }, nil -} diff --git a/experimental/gojs/example/stars/go.mod b/experimental/gojs/example/stars/go.mod deleted file mode 100644 index 38f9ff15..00000000 --- a/experimental/gojs/example/stars/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/tetratelabs/wazero/examples/gojs/stars - -go 1.18 diff --git a/experimental/gojs/example/stars/main.go b/experimental/gojs/example/stars/main.go deleted file mode 100644 index 9345a8b7..00000000 --- a/experimental/gojs/example/stars/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" -) - -const gitHubRepoAPI = "https://api.github.com/repos/tetratelabs/wazero" - -type gitHubRepo struct { - Stars int `json:"stargazers_count"` -} - -func main() { - req, err := http.NewRequest("GET", gitHubRepoAPI, nil) - if err != nil { - panic(err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - b, _ := io.ReadAll(resp.Body) - panic("GitHub lookup failed: " + string(b)) - } - - var repo gitHubRepo - json.NewDecoder(resp.Body).Decode(&repo) - fmt.Println("wazero has", repo.Stars, "stars. Does that include you?") -} diff --git a/experimental/gojs/example/stars_test.go b/experimental/gojs/example/stars_test.go deleted file mode 100644 index 26fcf683..00000000 --- a/experimental/gojs/example/stars_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "os/exec" - "path" - "testing" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/experimental/gojs" - "github.com/tetratelabs/wazero/internal/platform" - "github.com/tetratelabs/wazero/internal/testing/maintester" - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/sys" -) - -// Test_main ensures the following will work: -// -// go run stars.go -func Test_main(t *testing.T) { - stdout, stderr := maintester.TestMain(t, main, "stars") - require.Equal(t, "", stderr) - require.Equal(t, "wazero has 9999999 stars. Does that include you?\n", stdout) -} - -// TestMain compiles the wasm on-demand, which uses the runner's Go as opposed -// to a binary checked in, which would be pinned to one version. This is -// separate from Test_main to show that compilation doesn't dominate the -// execution time. -func TestMain(m *testing.M) { - // Notably our scratch containers don't have go, so don't fail tests. - if err := compileFromGo(); err != nil { - log.Println("Skipping tests due to:", err) - os.Exit(0) - } - os.Exit(m.Run()) -} - -// compileFromGo compiles "stars/main.go" on demand as the binary generated is -// too big (>7MB) to check into the source tree. -func compileFromGo() error { - cmd := exec.Command("go", "build", "-o", "main.wasm", ".") - cmd.Dir = "stars" - cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js", "GOWASM=satconv,signext") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("go build: %v\n%s", err, out) - } - return nil -} - -// Benchmark_main is in the example for GOOS=js to re-use compilation caching -// infrastructure. This is only used to sporadically check the impact of -// internal changes as in general, it is known that GOOS=js will be slow due to -// JavaScript emulation. -func Benchmark_main(b *testing.B) { - // Don't benchmark with interpreter as we know it will be slow. - if !platform.CompilerSupported() { - b.Skip() - } - - ctx := context.Background() - - // Create a new WebAssembly Runtime. - r := wazero.NewRuntime(ctx) - defer r.Close(ctx) // This closes everything this Runtime created. - - bin, err := os.ReadFile(path.Join("stars", "main.wasm")) - if err != nil { - b.Fatal(err) - } - compiled, err := r.CompileModule(ctx, bin) - if err != nil { - b.Fatal(err) - } - - // Add the imports needed for `GOARCH=wasm GOOS=js` - gojs.MustInstantiate(ctx, r) - - // Instead of making real HTTP calls, return fake data. - cfg := gojs.NewConfig(wazero.NewModuleConfig()). - WithRoundTripper(&fakeGitHub{}) - - b.Run("gojs.Run", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - err = gojs.Run(ctx, r, compiled, cfg) - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - b.Fatal(err) - } else if !ok { - b.Fatal(err) - } - } - }) -} diff --git a/experimental/gojs/gojs.go b/experimental/gojs/gojs.go index 67f85566..f1c828f6 100644 --- a/experimental/gojs/gojs.go +++ b/experimental/gojs/gojs.go @@ -1,5 +1,5 @@ // Package gojs allows you to run wasm binaries compiled by Go when -// `GOARCH=wasm GOOS=js`. See https://wazero.io/languages/go/ for more. +// `GOOS=js GOARCH=wasm`. See https://wazero.io/languages/go/ for more. // // # Experimental // @@ -15,7 +15,7 @@ package gojs import ( "context" - "net/http" + "errors" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" @@ -27,33 +27,57 @@ import ( // MustInstantiate calls Instantiate or panics on error. // -// This is a simpler function for those who know the module "go" is not -// already instantiated, and don't need to unload it. -func MustInstantiate(ctx context.Context, r wazero.Runtime) { - if _, err := Instantiate(ctx, r); err != nil { +// This is a simpler function for those who know host functions are not already +// instantiated, and don't need to unload them separate from the runtime. +func MustInstantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) { + if _, err := Instantiate(ctx, r, guest); err != nil { panic(err) } } -// Instantiate instantiates the "go" module, used by `GOARCH=wasm GOOS=js`, -// into the runtime. +// Instantiate detects and instantiates host functions for wasm compiled with +// `GOOS=js GOARCH=wasm`. `guest` must be a result of `r.CompileModule`. // // # Notes // // - Failure cases are documented on wazero.Runtime InstantiateModule. // - Closing the wazero.Runtime has the same effect as closing the result. -// - To add more functions to the "env" module, use FunctionExporter. -func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { - builder := r.NewHostModuleBuilder("go") +// - To add more functions to `goModule`, use FunctionExporter. +func Instantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) (api.Closer, error) { + goModule, err := detectGoModule(guest.ImportedFunctions()) + if err != nil { + return nil, err + } + builder := r.NewHostModuleBuilder(goModule) NewFunctionExporter().ExportFunctions(builder) return builder.Instantiate(ctx) } -// FunctionExporter configures the functions in the "go" module used by -// `GOARCH=wasm GOOS=js`. +// detectGoModule is needed because the module name defining host functions for +// `GOOS=js GOARCH=wasm` was renamed from "go" to "gojs" in Go 1.21. We can't +// use the version that compiles wazero because it could be different from what +// compiled the guest. +// +// See https://github.com/golang/go/commit/02411bcd7c8eda9c694a5755aff0a516d4983952 +func detectGoModule(imports []api.FunctionDefinition) (string, error) { + for _, f := range imports { + moduleName, _, _ := f.Import() + switch moduleName { + case "go", "gojs": + return moduleName, nil + } + } + return "", errors.New("guest wasn't compiled with GOOS=js GOARCH=wasm") +} + +// FunctionExporter builds host functions for wasm compiled with +// `GOOS=js GOARCH=wasm`. type FunctionExporter interface { - // ExportFunctions builds functions to export with a - // wazero.HostModuleBuilder named "go". + // ExportFunctions builds functions to an existing host module builder. + // + // This should be named "go" or "gojs", depending on the version of Go the + // guest was compiled with. The module name changed from "go" to "gojs" in + // Go 1.21. ExportFunctions(wazero.HostModuleBuilder) } @@ -122,16 +146,6 @@ type Config interface { // // Note: This has no effect on windows. WithOSUser() Config - - // WithRoundTripper sets the http.RoundTripper used to Run Wasm. - // - // For example, if the code compiled via `GOARCH=wasm GOOS=js` uses - // http.RoundTripper, you can avoid failures by assigning an implementation - // like so: - // - // err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig). - // WithRoundTripper(ctx, http.DefaultTransport)) - WithRoundTripper(http.RoundTripper) Config } // NewConfig returns a Config that can be used for configuring module instantiation. @@ -162,20 +176,13 @@ func (c *cfg) WithOSUser() Config { return ret } -// WithRoundTripper implements Config.WithRoundTripper -func (c *cfg) WithRoundTripper(rt http.RoundTripper) Config { - ret := c.clone() - ret.internal.Rt = rt - return ret -} - // Run instantiates a new module and calls "run" with the given config. // // # Parameters // // - ctx: context to use when instantiating the module and calling "run". // - r: runtime to instantiate both the host and guest (compiled) module in. -// - compiled: guest binary compiled with `GOARCH=wasm GOOS=js` +// - compiled: guest binary compiled with `GOOS=js GOARCH=wasm` // - config: the Config to use including wazero.ModuleConfig or extensions of // it. // @@ -200,7 +207,7 @@ func (c *cfg) WithRoundTripper(rt http.RoundTripper) Config { // // # Notes // -// - Wasm generated by `GOARCH=wasm GOOS=js` is very slow to compile: Use +// - Wasm generated by `GOOS=js GOARCH=wasm` is very slow to compile: Use // wazero.RuntimeConfig with wazero.CompilationCache when re-running the // same binary. // - The guest module is closed after being run. diff --git a/experimental/logging/log_listener.go b/experimental/logging/log_listener.go index 88c4cc13..04e6d7a4 100644 --- a/experimental/logging/log_listener.go +++ b/experimental/logging/log_listener.go @@ -115,7 +115,7 @@ func (f *loggingListenerFactory) NewFunctionListener(fnd api.FunctionDefinition) return nil } pSampler, pLoggers, rLoggers = wasilogging.Config(fnd) - case "go": + case "go", "gojs": if !gologging.IsInLogScope(fnd, f.scopes) { return nil } diff --git a/imports/README.md b/imports/README.md index 080ea53b..b9da880d 100644 --- a/imports/README.md +++ b/imports/README.md @@ -35,5 +35,5 @@ module in the compiled `%.wasm` file. To support any of these, wazero users can invoke `wasi_snapshot_preview1.Instantiate` on their `wazero.Runtime`. Other times, host imports are either completely compiler-specific, such as the -case with `GOARCH=wasm GOOS=js`, or coexist alongside WASI, such as the case +case with `GOOS=js GOARCH=wasm`, or coexist alongside WASI, such as the case with Emscripten. diff --git a/internal/fstest/fstest.go b/internal/fstest/fstest.go index 5a337ab7..0c4f8d49 100644 --- a/internal/fstest/fstest.go +++ b/internal/fstest/fstest.go @@ -1,5 +1,5 @@ // Package fstest defines filesystem test cases that help validate host -// functions implementing WASI and `GOARCH=wasm GOOS=js`. Tests are defined +// functions implementing WASI and `GOOS=js GOARCH=wasm`. Tests are defined // here to reduce duplication and drift. // // Here's an example using this inside code that compiles to wasm. @@ -15,7 +15,7 @@ // for example, gojs, sysfs or wasi_snapshot_preview1. // // This package must have no dependencies. Otherwise, compiling this with -// TinyGo or `GOARCH=wasm GOOS=js` can become bloated or complicated. +// TinyGo or `GOOS=js GOARCH=wasm` can become bloated or complicated. package fstest import ( diff --git a/internal/gojs/argsenv_test.go b/internal/gojs/argsenv_test.go index d0ea2a0c..99592b51 100644 --- a/internal/gojs/argsenv_test.go +++ b/internal/gojs/argsenv_test.go @@ -15,8 +15,8 @@ func Test_argsAndEnv(t *testing.T) { return moduleConfig.WithEnv("c", "d").WithEnv("a", "b"), config.NewConfig() }) - require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) + require.NoError(t, err) require.Equal(t, ` args 0 = test args 1 = argsenv diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go index e69e807f..f71a201b 100644 --- a/internal/gojs/builtin.go +++ b/internal/gojs/builtin.go @@ -14,26 +14,18 @@ func newJsGlobal(config *config.Config) *jsVal { cwd: config.Workdir, umask: config.Umask, } - rt := config.Rt - - if config.Rt != nil { - fetchProperty = goos.RefHttpFetch - } return newJsVal(goos.RefValueGlobal, "global"). addProperties(map[string]interface{}{ - "Object": objectConstructor, - "Array": arrayConstructor, - "crypto": jsCrypto, - "Uint8Array": uint8ArrayConstructor, - "fetch": fetchProperty, - "AbortController": goos.Undefined, - "Headers": headersConstructor, - "process": newJsProcess(uid, gid, euid, groups, proc), - "fs": newJsFs(proc), - "Date": jsDateConstructor, - }). - addFunction("fetch", &httpFetch{rt}) + "Object": objectConstructor, + "Array": arrayConstructor, + "crypto": jsCrypto, + "Uint8Array": uint8ArrayConstructor, + "fetch": fetchProperty, + "process": newJsProcess(uid, gid, euid, groups, proc), + "fs": newJsFs(proc), + "Date": jsDateConstructor, + }) } var ( @@ -51,7 +43,7 @@ var ( arrayConstructor = newJsVal(goos.RefArrayConstructor, "Array") // uint8ArrayConstructor = js.Global().Get("Uint8Array") - // // fs_js.go, rand_js.go, roundtrip_js.go init + // // fs_js.go, rand_js.go init // // It has only one invocation pattern: `buf := uint8Array.New(len(b))` uint8ArrayConstructor = newJsVal(goos.RefUint8ArrayConstructor, "Uint8Array") diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index 6eeadf72..f50ef895 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -12,6 +12,7 @@ import ( "path/filepath" "reflect" "runtime" + "strings" "testing" "time" @@ -41,30 +42,27 @@ func compileAndRun(ctx context.Context, arg string, config newConfig) (stdout, s } func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config newConfig) (stdout, stderr string, err error) { - var stdoutBuf, stderrBuf bytes.Buffer - - builder := r.NewHostModuleBuilder("go") - gojs.NewFunctionExporter().ExportFunctions(builder) - if _, err = builder.Instantiate(ctx); err != nil { - return - } - // Note: this hits the file cache. - compiled, err := r.CompileModule(testCtx, testBin) - if err != nil { + var guest wazero.CompiledModule + if guest, err = r.CompileModule(testCtx, testBin); err != nil { log.Panicln(err) } + if _, err = gojs.Instantiate(ctx, r, guest); err != nil { + return + } + + var stdoutBuf, stderrBuf bytes.Buffer mc, c := config(wazero.NewModuleConfig(). WithStdout(&stdoutBuf). WithStderr(&stderrBuf). WithArgs("test", arg)) var s *internalgojs.State - s, err = run.RunAndReturnState(ctx, r, compiled, mc, c) + s, err = run.RunAndReturnState(ctx, r, guest, mc, c) if err == nil { - if !reflect.DeepEqual(s, internalgojs.NewState(c)) { - log.Panicf("unexpected state: %v\n", s) + if want, have := internalgojs.NewState(c), s; !reflect.DeepEqual(want, have) { + log.Panicf("unexpected state: want %#v, have %#v", want, have) } } @@ -169,3 +167,8 @@ func findGoBin() (string, error) { // Now, search the path return exec.LookPath(binName) } + +// logString handles the "go" -> "gojs" module rename in Go 1.21 +func logString(log bytes.Buffer) string { + return strings.ReplaceAll(log.String(), "==> gojs", "==> go") +} diff --git a/internal/gojs/config/config.go b/internal/gojs/config/config.go index 4fb23935..2bafdd85 100644 --- a/internal/gojs/config/config.go +++ b/internal/gojs/config/config.go @@ -4,7 +4,6 @@ package config import ( "fmt" - "net/http" "os" "path/filepath" "runtime" @@ -23,7 +22,6 @@ type Config struct { // Workdir is the actual working directory value. Workdir string Umask uint32 - Rt http.RoundTripper } func NewConfig() *Config { @@ -36,7 +34,6 @@ func NewConfig() *Config { Groups: []int{0}, Workdir: "/", Umask: uint32(0o0022), - Rt: nil, } } diff --git a/internal/gojs/crypto_test.go b/internal/gojs/crypto_test.go index 8e1d46c8..d496ae55 100644 --- a/internal/gojs/crypto_test.go +++ b/internal/gojs/crypto_test.go @@ -20,7 +20,7 @@ func Test_crypto(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "crypto", defaultConfig) require.Zero(t, stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, `7a0c9f9f0d `, stdout) require.Equal(t, `==> go.runtime.getRandomData(r_len=32) @@ -29,5 +29,5 @@ func Test_crypto(t *testing.T) { <== ==> go.syscall/js.valueCall(crypto.getRandomValues(r_len=5)) <== (n=5) -`, log.String()) +`, logString(log)) } diff --git a/internal/gojs/custom/names.go b/internal/gojs/custom/names.go index 9c39122e..d74a6631 100644 --- a/internal/gojs/custom/names.go +++ b/internal/gojs/custom/names.go @@ -1,5 +1,5 @@ // Package custom is similar to the WebAssembly Custom Sections. These are -// needed because `GOARCH=wasm GOOS=js` functions aren't defined naturally +// needed because `GOOS=js GOARCH=wasm` functions aren't defined naturally // in WebAssembly. For example, every function has a single parameter "sp", // which implicitly maps to stack parameters in this package. package custom diff --git a/internal/gojs/custom/process.go b/internal/gojs/custom/process.go index c288a618..d55b1cb8 100644 --- a/internal/gojs/custom/process.go +++ b/internal/gojs/custom/process.go @@ -2,6 +2,7 @@ package custom const ( NameProcess = "process" + NameProcessArgv0 = "argv0" NameProcessCwd = "cwd" NameProcessChdir = "chdir" NameProcessGetuid = "getuid" @@ -15,6 +16,11 @@ const ( // Results here are those set to the current event object, but effectively are // results of the host function. var ProcessNameSection = map[string]*Names{ + NameProcessArgv0: { + Name: NameProcessArgv0, + ParamNames: []string{}, + ResultNames: []string{"argv0"}, + }, NameProcessCwd: { Name: NameProcessCwd, ParamNames: []string{}, diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go index 5fc3674f..2fe8bf64 100644 --- a/internal/gojs/fs_test.go +++ b/internal/gojs/fs_test.go @@ -20,7 +20,7 @@ func Test_fs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, `sub mode drwxr-xr-x /animals.txt mode -rw-r--r-- animals.txt mode -rw-r--r-- @@ -50,7 +50,7 @@ func Test_testfs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Zero(t, stdout) } @@ -67,7 +67,7 @@ func Test_writefs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) if platform.CompilerSupported() { // Note: as of Go 1.19, only the Sec field is set on update in fs_js.go. diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go index e46d93be..8121eae7 100644 --- a/internal/gojs/goos/goos.go +++ b/internal/gojs/goos/goos.go @@ -48,8 +48,6 @@ const ( IdJsCrypto IdJsDateConstructor IdJsDate - IdHttpFetch - IdHttpHeaders NextID ) @@ -63,17 +61,15 @@ const ( 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) - RefHttpFetch = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpFetch) - RefHttpHeadersConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpHeaders) + 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) ) type TypeFlag byte diff --git a/internal/gojs/http.go b/internal/gojs/http.go deleted file mode 100644 index 13130dfe..00000000 --- a/internal/gojs/http.go +++ /dev/null @@ -1,156 +0,0 @@ -package gojs - -import ( - "context" - "fmt" - "io" - "net/http" - "net/textproto" - "sort" - - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/internal/gojs/goos" -) - -// headersConstructor = Get("Headers").New() // http.Roundtrip && "fetch" -var headersConstructor = newJsVal(goos.RefHttpHeadersConstructor, "Headers") - -// httpFetch implements jsFn for http.RoundTripper -// -// Reference in roundtrip_js.go init -// -// jsFetchMissing = js.Global().Get("fetch").IsUndefined() -// -// In http.Transport RoundTrip, this returns a promise -// -// fetchPromise := js.Global().Call("fetch", req.URL.String(), opt) -type httpFetch struct{ rt http.RoundTripper } - -func (h *httpFetch) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) { - rt := h.rt - if rt == nil { - panic("unexpected to reach here without roundtripper as property is nil checked") - } - url := args[0].(string) - properties := args[1].(*object).properties - req, err := http.NewRequestWithContext(ctx, properties["method"].(string), url, nil) - if err != nil { - return nil, err - } - // TODO: headers properties[headers] - v := &fetchPromise{rt: rt, req: req} - return v, nil -} - -type fetchPromise struct { - rt http.RoundTripper - req *http.Request -} - -// call implements jsCall.call -func (p *fetchPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) { - if method == "then" { - if res, err := p.rt.RoundTrip(p.req); err != nil { - failure := args[1].(funcWrapper) - // HTTP is at the GOOS=js abstraction, so we can return any error. - return failure.invoke(ctx, mod, this, err) - } else { - success := args[0].(funcWrapper) - return success.invoke(ctx, mod, this, &fetchResult{res: res}) - } - } - panic(fmt.Sprintf("TODO: fetchPromise.%s", method)) -} - -type fetchResult struct { - res *http.Response -} - -// Get implements the same method as documented on goos.GetFunction -func (s *fetchResult) Get(propertyKey string) interface{} { - switch propertyKey { - case "headers": - names := make([]string, 0, len(s.res.Header)) - for k := range s.res.Header { - names = append(names, k) - } - // Sort names for consistent iteration - sort.Strings(names) - h := &headers{names: names, headers: s.res.Header} - return h - case "body": - // return undefined as arrayPromise is more complicated than an array. - return goos.Undefined - case "status": - return uint32(s.res.StatusCode) - } - panic(fmt.Sprintf("TODO: get fetchResult.%s", propertyKey)) -} - -// call implements jsCall.call -func (s *fetchResult) call(_ context.Context, _ api.Module, _ goos.Ref, method string, _ ...interface{}) (interface{}, error) { - switch method { - case "arrayBuffer": - v := &arrayPromise{reader: s.res.Body} - return v, nil - } - panic(fmt.Sprintf("TODO: call fetchResult.%s", method)) -} - -type headers struct { - headers http.Header - names []string - i int -} - -// Get implements the same method as documented on goos.GetFunction -func (h *headers) Get(propertyKey string) interface{} { - switch propertyKey { - case "done": - return h.i == len(h.names) - case "value": - name := h.names[h.i] - value := h.headers.Get(name) - h.i++ - return &objectArray{[]interface{}{name, value}} - } - panic(fmt.Sprintf("TODO: get headers.%s", propertyKey)) -} - -// call implements jsCall.call -func (h *headers) call(_ context.Context, _ api.Module, _ goos.Ref, method string, args ...interface{}) (interface{}, error) { - switch method { - case "entries": - // Sort names for consistent iteration - sort.Strings(h.names) - return h, nil - case "next": - return h, nil - case "append": - name := textproto.CanonicalMIMEHeaderKey(args[0].(string)) - value := args[1].(string) - h.names = append(h.names, name) - h.headers.Add(name, value) - return nil, nil - } - panic(fmt.Sprintf("TODO: call headers.%s", method)) -} - -type arrayPromise struct { - reader io.ReadCloser -} - -// call implements jsCall.call -func (p *arrayPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) { - switch method { - case "then": - defer p.reader.Close() - if b, err := io.ReadAll(p.reader); err != nil { - // HTTP is at the GOOS=js abstraction, so we can return any error. - return args[1].(funcWrapper).invoke(ctx, mod, this, err) - } else { - return args[0].(funcWrapper).invoke(ctx, mod, this, goos.WrapByteArray(b)) - } - } - panic(fmt.Sprintf("TODO: call arrayPromise.%s", method)) -} diff --git a/internal/gojs/http_test.go b/internal/gojs/http_test.go deleted file mode 100644 index 5ba1e261..00000000 --- a/internal/gojs/http_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package gojs_test - -import ( - "errors" - "io" - "net/http" - "strings" - "testing" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/internal/gojs/config" - "github.com/tetratelabs/wazero/internal/testing/require" -) - -type roundTripperFunc func(r *http.Request) (*http.Response, error) - -func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { - return f(r) -} - -func Test_http(t *testing.T) { - t.Parallel() - - rt := roundTripperFunc(func(req *http.Request) (*http.Response, error) { - if req.URL.Path == "/error" { - return nil, errors.New("error") - } - if req.Body != nil { - require.Equal(t, http.MethodPost, req.Method) - bytes, err := io.ReadAll(req.Body) - require.NoError(t, err) - require.Equal(t, "ice cream", string(bytes)) - } - return &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Header: http.Header{"Custom": {"1"}}, - Body: io.NopCloser(strings.NewReader("abcdef")), - ContentLength: 6, - }, nil - }) - - stdout, stderr, err := compileAndRun(testCtx, "http", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) { - config := config.NewConfig() - config.Rt = rt - return moduleConfig.WithEnv("BASE_URL", "http://host"), config - }) - - require.EqualError(t, err, `module closed with exit_code(0)`) - require.Zero(t, stderr) - require.Equal(t, `Get "http://host/error": net/http: fetch() failed: error -1 -abcdef -`, stdout) -} diff --git a/internal/gojs/misc_test.go b/internal/gojs/misc_test.go index 6de70c6b..b8454697 100644 --- a/internal/gojs/misc_test.go +++ b/internal/gojs/misc_test.go @@ -23,12 +23,12 @@ func Test_exit(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "exit", defaultConfig) - require.EqualError(t, err, `module closed with exit_code(255)`) require.Zero(t, stderr) + require.EqualError(t, err, `module closed with exit_code(255)`) require.Zero(t, stdout) require.Equal(t, `==> go.runtime.wasmExit(code=255) <== -`, log.String()) // Note: gojs doesn't panic on exit, so you see "<==" +`, logString(log)) // Note: gojs doesn't panic on exit, so you see "<==" } func Test_goroutine(t *testing.T) { @@ -36,8 +36,8 @@ func Test_goroutine(t *testing.T) { stdout, stderr, err := compileAndRun(testCtx, "goroutine", defaultConfig) - require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) + require.NoError(t, err) require.Equal(t, `producer consumer `, stdout) @@ -52,12 +52,12 @@ func Test_mem(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "mem", defaultConfig) - require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) + require.NoError(t, err) require.Zero(t, stdout) // The memory view is reset at least once. - require.Contains(t, log.String(), `==> go.runtime.resetMemoryDataView() + require.Contains(t, logString(log), `==> go.runtime.resetMemoryDataView() <== `) } @@ -71,7 +71,7 @@ func Test_stdio(t *testing.T) { }) require.Equal(t, "stderr 6\n", stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, "stdout 6\n", stdout) } @@ -89,17 +89,13 @@ func Test_stdio_large(t *testing.T) { return defaultConfig(moduleConfig.WithStdin(bytes.NewReader(input))) }) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, fmt.Sprintf("stderr %d\n", size), stderr) require.Equal(t, fmt.Sprintf("stdout %d\n", size), stdout) - // We can't predict the precise ms the timeout event will be, so we partial match. - require.Contains(t, log.String(), `==> go.runtime.scheduleTimeoutEvent(ms=`) - require.Contains(t, log.String(), `<== (id=1)`) - // There may be another timeout event between the first and its clear. - require.Contains(t, log.String(), `==> go.runtime.clearTimeoutEvent(id=1) -<== -`) + // There's no guarantee of a timeout event (in Go 1.21 there isn't), so we + // don't verify this. gojs is in maintenance mode until it is removed after + // Go 1.22 is out. } func Test_gc(t *testing.T) { @@ -107,7 +103,7 @@ func Test_gc(t *testing.T) { stdout, stderr, err := compileAndRun(testCtx, "gc", defaultConfig) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, "", stderr) require.Equal(t, "before gc\nafter gc\n", stdout) } diff --git a/internal/gojs/process_test.go b/internal/gojs/process_test.go index efa6cb49..9965c1e5 100644 --- a/internal/gojs/process_test.go +++ b/internal/gojs/process_test.go @@ -18,7 +18,7 @@ func Test_process(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module closed with exit_code(0)`) + require.NoError(t, err) require.Equal(t, `syscall.Getpid()=1 syscall.Getppid()=0 syscall.Getuid()=0 diff --git a/internal/gojs/run/gojs.go b/internal/gojs/run/gojs.go index 1f9745a1..687c7666 100644 --- a/internal/gojs/run/gojs.go +++ b/internal/gojs/run/gojs.go @@ -8,6 +8,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/internal/gojs" "github.com/tetratelabs/wazero/internal/gojs/config" + "github.com/tetratelabs/wazero/sys" ) func RunAndReturnState( @@ -40,5 +41,10 @@ func RunAndReturnState( // Invoke the run function. _, err = mod.ExportedFunction("run").Call(ctx, uint64(argc), uint64(argv)) + if se, ok := err.(*sys.ExitError); ok { + if se.ExitCode() == 0 { // Don't err on success. + err = nil + } + } return s, err } diff --git a/internal/gojs/state.go b/internal/gojs/state.go index 57ba3ea1..a0ac6c92 100644 --- a/internal/gojs/state.go +++ b/internal/gojs/state.go @@ -13,6 +13,7 @@ import ( func NewState(config *config.Config) *State { return &State{ + config: config, values: values.NewValues(), valueGlobal: newJsGlobal(config), _nextCallbackTimeoutID: 1, @@ -100,8 +101,6 @@ func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint return jsDateConstructor case goos.RefJsDate: return jsDate - case goos.RefHttpHeadersConstructor: - return headersConstructor default: if f, ok := ref.ParseFloat(); ok { // numbers are passed through as a Ref return f @@ -169,6 +168,7 @@ func toFloatRef(f float64) goos.Ref { // State holds state used by the "go" imports used by gojs. // Note: This is module-scoped. type State struct { + config *config.Config values *values.Values _pendingEvent *event // _lastEvent was the last _pendingEvent value @@ -208,11 +208,12 @@ func (s *State) close() { } // Reset all state recursively to their initial values. This allows our // unit tests to check we closed everything. - s._scheduledTimeouts = map[uint32]chan bool{} s.values.Reset() s._pendingEvent = nil s._lastEvent = nil + s.valueGlobal = newJsGlobal(s.config) s._nextCallbackTimeoutID = 1 + s._scheduledTimeouts = map[uint32]chan bool{} } func toInt64(arg interface{}) int64 { diff --git a/internal/gojs/syscall.go b/internal/gojs/syscall.go index 8a8dca15..6594caf8 100644 --- a/internal/gojs/syscall.go +++ b/internal/gojs/syscall.go @@ -3,7 +3,6 @@ package gojs import ( "context" "fmt" - "net/http" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/gojs/custom" @@ -215,10 +214,6 @@ func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) { result := &object{properties: map[string]interface{}{}} res = storeValue(ctx, result) ok = true - case goos.RefHttpHeadersConstructor: - result := &headers{headers: http.Header{}} - res = storeValue(ctx, result) - ok = true case goos.RefJsDateConstructor: res = goos.RefJsDate ok = true diff --git a/internal/gojs/testdata/http/main.go b/internal/gojs/testdata/http/main.go deleted file mode 100644 index fadde528..00000000 --- a/internal/gojs/testdata/http/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package http - -import ( - "fmt" - "io" - "log" - "net/http" - "os" - "strings" -) - -func Main() { - url := os.Getenv("BASE_URL") - res, err := http.Get(url + "/error") - if err == nil { - log.Panicln(err) - } - fmt.Println(err) - - res, err = http.Post(url, "text/plain", io.NopCloser(strings.NewReader("ice cream"))) - if err != nil { - log.Panicln(err) - } - body, err := io.ReadAll(res.Body) - if err != nil { - log.Panicln(err) - } - res.Body.Close() - - fmt.Println(res.Header.Get("Custom")) - fmt.Println(string(body)) -} diff --git a/internal/gojs/testdata/main.go b/internal/gojs/testdata/main.go index 9c66072f..a389d4e4 100644 --- a/internal/gojs/testdata/main.go +++ b/internal/gojs/testdata/main.go @@ -9,7 +9,6 @@ import ( "github.com/tetratelabs/wazero/internal/gojs/testdata/fs" "github.com/tetratelabs/wazero/internal/gojs/testdata/gc" "github.com/tetratelabs/wazero/internal/gojs/testdata/goroutine" - "github.com/tetratelabs/wazero/internal/gojs/testdata/http" "github.com/tetratelabs/wazero/internal/gojs/testdata/mem" "github.com/tetratelabs/wazero/internal/gojs/testdata/process" "github.com/tetratelabs/wazero/internal/gojs/testdata/stdio" @@ -31,8 +30,6 @@ func main() { fs.Main() case "gc": gc.Main() - case "http": - http.Main() case "goroutine": goroutine.Main() case "mem": diff --git a/internal/gojs/time_test.go b/internal/gojs/time_test.go index 5784ec89..eb7502d1 100644 --- a/internal/gojs/time_test.go +++ b/internal/gojs/time_test.go @@ -19,20 +19,21 @@ func Test_time(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "time", defaultConfig) - require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) + require.NoError(t, err) require.Equal(t, `Local 1ms `, stdout) // To avoid multiple similar assertions, just check three functions we // expect were called. - require.Contains(t, log.String(), `==> go.runtime.nanotime1() + logString := logString(log) + require.Contains(t, logString, `==> go.runtime.nanotime1() <== (nsec=0)`) - require.Contains(t, log.String(), `==> go.runtime.walltime() + require.Contains(t, logString, `==> go.runtime.walltime() <== (sec=1640995200,nsec=0) `) - require.Contains(t, log.String(), `==> go.syscall/js.valueCall(Date.getTimezoneOffset()) + require.Contains(t, logString, `==> go.syscall/js.valueCall(Date.getTimezoneOffset()) <== (tz=0) `) } diff --git a/internal/gojs/values/values_test.go b/internal/gojs/values/values_test.go index 552683e2..1b84cf32 100644 --- a/internal/gojs/values/values_test.go +++ b/internal/gojs/values/values_test.go @@ -15,7 +15,7 @@ func Test_Values(t *testing.T) { err := require.CapturePanic(func() { _ = vs.Get(goos.NextID) }) - require.EqualError(t, err, "id 18 is out of range 0") + require.Contains(t, err.Error(), "is out of range 0") v1 := "foo" id1 := vs.Increment(v1) @@ -43,7 +43,7 @@ func Test_Values(t *testing.T) { err = require.CapturePanic(func() { _ = vs.Get(id1) }) - require.EqualError(t, err, "value for 18 was nil") + require.Contains(t, err.Error(), "was nil") // Since the ID is no longer in use, we should be able to revive it. require.Equal(t, id1, vs.Increment(v1)) diff --git a/site/content/languages/_index.md b/site/content/languages/_index.md index 1f0a8346..d513b12c 100644 --- a/site/content/languages/_index.md +++ b/site/content/languages/_index.md @@ -17,7 +17,7 @@ e.g. If your source is in Go, you might compile it with TinyGo. Below are notes wazero contributed so far, in alphabetical order by language. -* [Go]({{< relref "/go.md" >}}) e.g. `GOARCH=wasm GOOS=js go build -o X.wasm X.go` +* [Go]({{< relref "/go.md" >}}) e.g. `GOOS=js GOARCH=wasm go build -o X.wasm X.go` * [TinyGo]({{< relref "/tinygo.md" >}}) e.g. `tinygo build -o X.wasm -target=wasi X.go` * [Rust]({{< relref "/rust.md" >}}) e.g. `rustc -o X.wasm --target wasm32-wasi X.rs` * [Zig]({{< relref "/zig.md" >}}) e.g. `zig build-exe X.zig -target wasm32-wasi` diff --git a/site/content/languages/go.md b/site/content/languages/go.md index a5eb73a5..31d1ce08 100644 --- a/site/content/languages/go.md +++ b/site/content/languages/go.md @@ -4,12 +4,12 @@ title = "Go" ## Introduction -When `GOARCH=wasm GOOS=js`, Go's compiler targets WebAssembly Binary format +When `GOOS=js GOARCH=wasm`, Go's compiler targets WebAssembly Binary format (%.wasm). Here's a typical compilation command: ```bash -$ GOARCH=wasm GOOS=js go build -o my.wasm . +$ GOOS=js GOARCH=wasm go build -o my.wasm . ``` The operating system is "js", but more specifically it is [wasm_exec.js][1]. @@ -36,7 +36,7 @@ features. ## WebAssembly Features -`GOARCH=wasm GOOS=js` uses instructions in [WebAssembly Core Specification 1.0] +`GOOS=js GOARCH=wasm` uses instructions in [WebAssembly Core Specification 1.0] [15] unless `GOWASM` includes features added afterwards. Here are the valid [GOWASM values][16]: @@ -53,7 +53,7 @@ Please read our overview of WebAssembly and limitations in both language features and library choices when developing your software. -`GOARCH=wasm GOOS=js` has a custom ABI which supports a subset of features in +`GOOS=js GOARCH=wasm` has a custom ABI which supports a subset of features in the Go standard library. Notably, the host can implement time, crypto, file system and HTTP client functions. Even where implemented, certain operations will have no effect for reasons like ignoring HTTP request properties or fake @@ -61,7 +61,7 @@ values returned (such as the pid). When not supported, many functions return `syscall.ENOSYS` errors, or the string form: "not implemented on js". Here are the more notable parts of Go which will not work when compiled via -`GOARCH=wasm GOOS=js`, resulting in `syscall.ENOSYS` errors: +`GOOS=js GOARCH=wasm`, resulting in `syscall.ENOSYS` errors: * Raw network access. e.g. `net.Bind` * File descriptor control (`fnctl`). e.g. `syscall.Pipe` * Arbitrary syscalls. Ex `syscall.Syscall` @@ -119,13 +119,13 @@ Digging deeper, you'll notice the [atomics][10] defined by `GOARCH=wasm` are not actually implemented with locks, rather it is awaiting the ["Threads" proposal][11]. -In summary, while goroutines are supported in `GOARCH=wasm GOOS=js`, they won't +In summary, while goroutines are supported in `GOOS=js GOARCH=wasm`, they won't be able to run in parallel until the WebAssembly Specification includes atomics and Go's compiler is updated to use them. ## Error handling -There are several `js.Value` used to implement `GOARCH=wasm GOOS=js` including +There are several `js.Value` used to implement `GOOS=js GOARCH=wasm` including the global, file system, HTTP round tripping, processes, etc. All of these have functions that may return an error on `js.Value.Call`. @@ -266,7 +266,7 @@ go that require version matching. Build a go binary from source to avoid these: ```bash $ cd src -$ GOARCH=wasm GOOS=js ./make.bash +$ GOOS=js GOARCH=wasm ./make.bash Building Go cmd/dist using /usr/local/go. (go1.19 darwin/amd64) Building Go toolchain1 using /usr/local/go. --snip-- @@ -298,18 +298,18 @@ like wazero. In other words, go can't run the wasm it just built. Instead, Now, you should be all set and can iterate similar to normal Go development. The main thing to keep in mind is where files are, and remember to set -`GOARCH=wasm GOOS=js` when running go commands. +`GOOS=js GOARCH=wasm` when running go commands. For example, if you fixed something in the `syscall/js` package (`${GOROOT}/src/syscall/js`), test it like so: ```bash -$ GOARCH=wasm GOOS=js go test syscall/js +$ GOOS=js GOARCH=wasm go test syscall/js ok syscall/js 1.093s ``` ### Notes -Here are some notes about testing `GOARCH=wasm GOOS=js` +Here are some notes about testing `GOOS=js GOARCH=wasm` #### Skipped tests @@ -326,7 +326,7 @@ like launching subprocesses on wasm, which won't likely ever support that. #### Filesystem access `TestStat` tries to read `/etc/passwd` due to a [runtime.GOOS default][21]. -As `GOARCH=wasm GOOS=js` is a virtualized operating system, this may not make +As `GOOS=js GOARCH=wasm` is a virtualized operating system, this may not make sense, because it has no files representing an operating system. Moreover, as of Go 1.19, tests don't pass through any configuration to hint at