-
Notifications
You must be signed in to change notification settings - Fork 267
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Adrian Cole <adrian@tetrate.io>
- Loading branch information
Adrian Cole
committed
Aug 25, 2022
1 parent
c00cb1b
commit aa2a48e
Showing
43 changed files
with
3,477 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
main.wasm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.