gojs: drops HTTP support to be compatible with Go 1.21 (#1557)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-07-05 16:56:18 +08:00
committed by GitHub
parent c2ee2242f6
commit 7498ad335f
40 changed files with 303 additions and 636 deletions

View File

@@ -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`.

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -0,0 +1,2 @@
# GOOS=js GOARCH=wasm binaries are too huge to check-in
main.wasm

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,3 +0,0 @@
module github.com/tetratelabs/wazero/examples/gojs/stars
go 1.18

View File

@@ -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?")
}

View File

@@ -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)
}
}
})
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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 (

View File

@@ -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

View File

@@ -14,11 +14,6 @@ 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{}{
@@ -27,13 +22,10 @@ func newJsGlobal(config *config.Config) *jsVal {
"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})
})
}
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")

View File

@@ -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")
}

View File

@@ -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,
}
}

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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{},

View File

@@ -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.

View File

@@ -48,8 +48,6 @@ const (
IdJsCrypto
IdJsDateConstructor
IdJsDate
IdHttpFetch
IdHttpHeaders
NextID
)
@@ -72,8 +70,6 @@ const (
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)
)
type TypeFlag byte

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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":

View File

@@ -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)
`)
}

View File

@@ -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))

View File

@@ -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`

View File

@@ -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