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:
@@ -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`.
|
||||
|
||||
|
||||
2
cmd/wazero/testdata/cat/.gitignore
vendored
2
cmd/wazero/testdata/cat/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
65
experimental/gojs/example/cat.go
Normal file
65
experimental/gojs/example/cat.go
Normal 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)
|
||||
}
|
||||
}
|
||||
2
experimental/gojs/example/cat/.gitignore
vendored
Normal file
2
experimental/gojs/example/cat/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# GOOS=js GOARCH=wasm binaries are too huge to check-in
|
||||
main.wasm
|
||||
19
experimental/gojs/example/cat/main.go
Normal file
19
experimental/gojs/example/cat/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
46
experimental/gojs/example/cat_test.go
Normal file
46
experimental/gojs/example/cat_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module github.com/tetratelabs/wazero/examples/gojs/stars
|
||||
|
||||
go 1.18
|
||||
@@ -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?")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
32
internal/gojs/testdata/http/main.go
vendored
32
internal/gojs/testdata/http/main.go
vendored
@@ -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))
|
||||
}
|
||||
3
internal/gojs/testdata/main.go
vendored
3
internal/gojs/testdata/main.go
vendored
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user