Skip to content

Commit

Permalink
Adds gojs for Go generated Wasm
Browse files Browse the repository at this point in the history
Signed-off-by: Adrian Cole <adrian@tetrate.io>
  • Loading branch information
Adrian Cole committed Aug 25, 2022
1 parent c00cb1b commit aa2a48e
Show file tree
Hide file tree
Showing 43 changed files with 3,477 additions and 6 deletions.
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ hugo := github.com/gohugoio/hugo@v0.101.0

# Make 3.81 doesn't support '**' globbing: Set explicitly instead of recursion.
all_sources := $(wildcard *.go */*.go */*/*.go */*/*/*.go */*/*/*.go */*/*/*/*.go)
all_testdata := $(wildcard testdata/* */testdata/* */*/testdata/* */*/*/testdata/*)
all_testdata := $(wildcard testdata/* */testdata/* */*/testdata/* */*/testdata/*/* */*/*/testdata/*)
all_testing := $(wildcard internal/testing/* internal/testing/*/* internal/testing/*/*/*)
all_examples := $(wildcard examples/* examples/*/* examples/*/*/*)
all_it := $(wildcard internal/integration_test/* internal/integration_test/*/* internal/integration_test/*/*/*)
Expand Down Expand Up @@ -53,7 +53,16 @@ build.examples.zig: examples/allocation/zig/testdata/greet.wasm
@(cd $(@D); zig build)
@mv $(@D)/zig-out/lib/$(@F) $(@D)

tinygo_sources := $(wildcard examples/*/testdata/*.go examples/*/*/testdata/*.go examples/*/testdata/*/*.go)
go_sources := examples/wasm_exec/testdata/cat.go
.PHONY: build.examples.go
build.examples.go: $(go_sources)
@for f in $^; do \
cd $$(dirname $$f); \
GOARCH=wasm GOOS=js go build -o $$(basename $$f | sed -e 's/\.go/\.wasm/') .; \
cd -; \
done

tinygo_sources := $(filter-out $(go_sources), $(wildcard examples/*/testdata/*.go examples/*/*/testdata/*.go examples/*/testdata/*/*.go))
.PHONY: build.examples.tinygo
build.examples.tinygo: $(tinygo_sources)
@for f in $^; do \
Expand Down
15 changes: 15 additions & 0 deletions examples/gojs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 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.

```bash
$ go run stars.go
wazero has 99999999 stars. Does that include you?
```

Internally, this uses [gojs](../../experimental/gojs/gojs.go), which implements
the custom host functions required by Go.

Note: `GOARCH=wasm GOOS=js` is experimental as is wazero's support of it. For
details, see https://wazero.io/languages/go/.
74 changes: 74 additions & 0 deletions examples/gojs/stars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"context"
"log"
"net/http"
"os"
"os/exec"
"path"
"runtime"

"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() {
// Choose the context to use for function calls.
ctx := context.Background()

// Create a new WebAssembly Runtime.
r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
// WebAssembly 2.0 allows use of gojs.
WithWasmCore2())
defer r.Close(ctx) // This closes everything this Runtime created.

// Combine the above into our baseline config, overriding defaults.
config := wazero.NewModuleConfig().
// By default, I/O streams are discarded, so you won't see output.
WithStdout(os.Stdout).WithStderr(os.Stderr)

// Compile the WebAssembly module using the default configuration.
compiled, err := r.CompileModule(ctx, compileWasm(), wazero.NewCompileConfig())
if err != nil {
log.Panicln(err)
}

// Grant the compiled wasm access to the default HTTP Transport.
ctx = gojs.WithRoundTripper(ctx, http.DefaultTransport)

// Execute the "run" function, which corresponds to "main" in cat/main.go.
err = gojs.Run(ctx, r, compiled, config)
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
log.Panicln(err)
} else if !ok {
log.Panicln(err)
}
}

// compileWasm compiles "stars/main.go" on demand as the binary generated is
// too big (>1MB) to check into the source tree.
func compileWasm() []byte {
cmd := exec.Command("go", "build", "-o", "main.wasm", ".")

_, thisFile, _, ok := runtime.Caller(1)
if !ok {
panic("couldn't read path to current file")
}
cmd.Dir = path.Join(path.Dir(thisFile), "stars")

cmd.Env = append(os.Environ(), "GOARCH=wasm", "GOOS=js")
if out, err := cmd.CombinedOutput(); err != nil {
log.Panicf("go build: %v\n%s", err, out)
}
bin, err := os.ReadFile(path.Join(cmd.Dir, "main.wasm"))
if err != nil {
panic(err)
}
return bin
}
1 change: 1 addition & 0 deletions examples/gojs/stars/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
main.wasm
3 changes: 3 additions & 0 deletions examples/gojs/stars/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/tetratelabs/wazero/examples/gojs/stars

go 1.18
36 changes: 36 additions & 0 deletions examples/gojs/stars/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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?")
}
17 changes: 17 additions & 0 deletions examples/gojs/stars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"testing"

"github.com/tetratelabs/wazero/internal/testing/maintester"
"github.com/tetratelabs/wazero/internal/testing/require"
)

// 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.Contains(t, stdout, "Does that include you?\n")
}
131 changes: 131 additions & 0 deletions experimental/gojs/gojs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Package gojs allows you to run wasm binaries compiled by Go when `GOOS=js`
// and `GOARCH=wasm`.
//
// # Usage
//
// When `GOOS=js` and `GOARCH=wasm`, Go's compiler targets WebAssembly 1.0
// Binary format (%.wasm).
//
// Ex.
//
// GOOS=js GOARCH=wasm go build -o cat.wasm .
//
// After compiling `cat.wasm` with wazero.Runtime's `CompileModule`, run it
// like below:
//
// err = gojs.Run(ctx, r, compiled, config)
// if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
// log.Panicln(err)
// } else if !ok {
// log.Panicln(err)
// }
//
// Under the scenes, the compiled Wasm calls host functions that support the
// runtime.GOOS. This is similar to what is implemented in wasm_exec.js. See
// https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js
//
// # 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. Due to this and the
// relatively high implementation overhead, most will choose TinyGo instead.
package gojs

import (
"context"
"net/http"

"github.com/tetratelabs/wazero"
. "github.com/tetratelabs/wazero/internal/gojs"
)

// 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:
//
// ctx = gojs.WithRoundTripper(ctx, http.DefaultTransport)
// err = gojs.Run(ctx, r, compiled, config)
func WithRoundTripper(ctx context.Context, rt http.RoundTripper) context.Context {
return context.WithValue(ctx, RoundTripperKey{}, rt)
}

// 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 into.
// - compiled: guest binary compiled with `GOARCH=wasm GOOS=js`
// - config: the configuration such as args, env or filesystem to use.
//
// # Example
//
// After compiling your Wasm binary with wazero.Runtime's `CompileModule`, run
// it like below:
//
// err = gojs.Run(ctx, r, compiled, config)
// if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
// log.Panicln(err)
// } else if !ok {
// log.Panicln(err)
// }
//
// Note: Both the host and guest module are closed after being run.
func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, config wazero.ModuleConfig) error {
// Instantiate the imports needed by go-compiled wasm.
js, err := moduleBuilder(r).Instantiate(ctx, r)
if err != nil {
return err
}
defer js.Close(ctx)

// Instantiate the module compiled by go, noting it has no init function.
mod, err := r.InstantiateModule(ctx, compiled, config)
if err != nil {
return err
}
defer mod.Close(ctx)

// Extract the args and env from the module config and write it to memory.
ctx = WithState(ctx)
argc, argv, err := WriteArgsAndEnviron(ctx, mod)
if err != nil {
return err
}
// Invoke the run function.
_, err = mod.ExportedFunction("run").Call(ctx, uint64(argc), uint64(argv))
return err
}

// moduleBuilder returns a new wazero.ModuleBuilder
func moduleBuilder(r wazero.Runtime) wazero.ModuleBuilder {
return r.NewModuleBuilder("go").
ExportFunction(GetRandomData.Name(), GetRandomData).
ExportFunction(Nanotime1.Name(), Nanotime1).
ExportFunction(WasmExit.Name(), WasmExit).
ExportFunction(CopyBytesToJS.Name(), CopyBytesToJS).
ExportFunction(ValueCall.Name(), ValueCall).
ExportFunction(ValueGet.Name(), ValueGet).
ExportFunction(ValueIndex.Name(), ValueIndex).
ExportFunction(ValueLength.Name(), ValueLength).
ExportFunction(ValueNew.Name(), ValueNew).
ExportFunction(ValueSet.Name(), ValueSet).
ExportFunction(WasmWrite.Name(), WasmWrite).
ExportFunction(ResetMemoryDataView.Name, ResetMemoryDataView).
ExportFunction(Walltime.Name(), Walltime).
ExportFunction(ScheduleTimeoutEvent.Name, ScheduleTimeoutEvent).
ExportFunction(ClearTimeoutEvent.Name, ClearTimeoutEvent).
ExportFunction(FinalizeRef.Name(), FinalizeRef).
ExportFunction(StringVal.Name(), StringVal).
ExportFunction(ValueDelete.Name, ValueDelete).
ExportFunction(ValueSetIndex.Name, ValueSetIndex).
ExportFunction(ValueInvoke.Name, ValueInvoke).
ExportFunction(ValuePrepareString.Name(), ValuePrepareString).
ExportFunction(ValueInstanceOf.Name, ValueInstanceOf).
ExportFunction(ValueLoadString.Name(), ValueLoadString).
ExportFunction(CopyBytesToGo.Name(), CopyBytesToGo).
ExportFunction(Debug.Name, Debug)
}
61 changes: 61 additions & 0 deletions internal/gojs/argsenv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package gojs

import (
"context"
"errors"

"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)

// Constants about memory layout. See REFERENCE.md
const (
endOfPageZero = uint32(4096) // runtime.minLegalPointer
maxArgsAndEnviron = uint32(8192) // ld.wasmMinDataAddr - runtime.minLegalPointer
wasmMinDataAddr = endOfPageZero + maxArgsAndEnviron // ld.wasmMinDataAddr
)

// WriteArgsAndEnviron writes arguments and environment variables to memory, so
// they can be read by main, Go compiles as the function export "run".
func WriteArgsAndEnviron(ctx context.Context, mod api.Module) (argc, argv uint32, err error) {
mem := mod.Memory()
sysCtx := mod.(*wasm.CallContext).Sys
args := sysCtx.Args()
environ := sysCtx.Environ()

argc = uint32(len(args))
offset := endOfPageZero

strPtr := func(val, field string, i int) (ptr uint32) {
// TODO: return err and format "%s[%d], field, i"
ptr = offset
mustWrite(ctx, mem, field, offset, append([]byte(val), 0))
offset += uint32(len(val) + 1)
if pad := offset % 8; pad != 0 {
offset += 8 - pad
}
return
}
argvPtrs := make([]uint32, 0, len(args)+1+len(environ)+1)
for i, arg := range args {
argvPtrs = append(argvPtrs, strPtr(arg, "args", i))
}
argvPtrs = append(argvPtrs, 0)

for i, env := range environ {
argvPtrs = append(argvPtrs, strPtr(env, "env", i))
}
argvPtrs = append(argvPtrs, 0)

argv = offset
for _, ptr := range argvPtrs {
// TODO: return err and format "argvPtrs[%d], i"
mustWriteUint64Le(ctx, mem, "argvPtrs[i]", offset, uint64(ptr))
offset += 8
}

if offset >= wasmMinDataAddr {
err = errors.New("total length of command line and environment variables exceeds limit")
}
return
}
25 changes: 25 additions & 0 deletions internal/gojs/argsenv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gojs_test

import (
_ "embed"
"testing"

"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
)

//go:embed testdata/argsenv/main.go
var argsenvGo string

func Test_argsAndEnv(t *testing.T) {
stdout, stderr, err := compileAndRunJsWasm(testCtx, t, argsenvGo, wazero.NewModuleConfig().WithArgs("prog", "a", "b").WithEnv("c", "d").WithEnv("a", "b"))

require.EqualError(t, err, `module "" closed with exit_code(0)`)
require.Zero(t, stderr)
require.Equal(t, `
args 0 = a
args 1 = b
environ 0 = c=d
environ 1 = a=b
`, stdout)
}
Loading

0 comments on commit aa2a48e

Please sign in to comment.