diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml index 755bc9f1..eb159ada 100644 --- a/.github/workflows/examples.yaml +++ b/.github/workflows/examples.yaml @@ -4,13 +4,13 @@ on: branches: [main] paths: - '.github/workflows/examples.yaml' - - 'examples/wasm/*.go' + - 'examples/*/testdata/*.go' - 'Makefile' push: branches: [main] paths: - '.github/workflows/examples.yaml' - - 'examples/wasm/*.go' + - 'examples/*/testdata/*.go' - 'Makefile' jobs: diff --git a/Makefile b/Makefile index 475c2ad5..a9c96359 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ bench_testdata_dir := tests/bench/testdata build.bench: tinygo build -o $(bench_testdata_dir)/case.wasm -scheduler=none -target=wasi $(bench_testdata_dir)/case.go -wasi_testdata_dir := ./examples/testdata +wasi_testdata_dir := ./examples/*/testdata .PHONY: build.examples build.examples: @find $(wasi_testdata_dir) -type f -name "*.go" | xargs -Ip /bin/sh -c 'tinygo build -o $$(echo p | sed -e 's/\.go/\.wasm/') -scheduler=none -target=wasi p' diff --git a/README.md b/README.md index aa6b3f43..1926abf1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ Import wazero and extend your Go application with code written in any language! ## Example -Here's an example of using wazero to invoke a factorial function: +The best way to learn this and other features you get with wazero is by trying one of our [examples](examples). +For the impatient, here's how invoking a factorial function looks in wazero: + ```golang func main() { // Read a WebAssembly binary containing an exported "fac" function. @@ -55,9 +57,6 @@ defer module.Close() ... ``` -The best way to learn this and other features you get with wazero is by trying -[examples](examples). - ## Runtime There are two runtime configurations supported in wazero, where _JIT_ is default: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..83c12880 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,10 @@ +## wazero examples + +The following example projects can help you practice WebAssembly with wazero: + +* [basic](basic) - how to use WebAssembly and Go-defined functions. +* [multiple-results](multiple-results) - how to return more than one result from WebAssembly or Go-defined functions. +* [replace-import](replace-import) - how to override a module name hard-coded in a WebAssembly module. +* [wasi](wasi) - how to use I/O in your WebAssembly modules using WASI (WebAssembly System Interface). + +Please [open an issue](https://github.com/tetratelabs/wazero/issues/new) if you would like to see another example. diff --git a/examples/add_test.go b/examples/add_test.go deleted file mode 100644 index ddcab30b..00000000 --- a/examples/add_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package examples - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" -) - -// Test_AddInt shows how you can define a function in text format and have it compiled inline. -// See https://github.com/summerwind/the-art-of-webassembly-go/blob/main/chapter1/addint/addint.wat -func Test_AddInt(t *testing.T) { - module, err := wazero.NewRuntime().InstantiateModuleFromCode([]byte(`(module $test - (func $addInt ;; TODO: function module (export "AddInt") - (param $value_1 i32) (param $value_2 i32) - (result i32) - local.get 0 ;; TODO: instruction variables $value_1 - local.get 1 ;; TODO: instruction variables $value_2 - i32.add - ) - (export "AddInt" (func $addInt)) -)`)) - require.NoError(t, err) - defer module.Close() - - addInt := module.ExportedFunction("AddInt") - - for _, c := range []struct { - value1, value2, expected uint64 // i32i32_i32 sig, but wasm.ExportedFunction params and results are uint64 - }{ - {value1: 1, value2: 2, expected: 3}, - {value1: 5, value2: 5, expected: 10}, - } { - results, err := addInt.Call(nil, c.value1, c.value2) - require.NoError(t, err) - require.Equal(t, c.expected, results[0]) - } -} diff --git a/examples/basic/.gitignore b/examples/basic/.gitignore new file mode 100644 index 00000000..76d4bb83 --- /dev/null +++ b/examples/basic/.gitignore @@ -0,0 +1 @@ +add diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 00000000..3d95042b --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,3 @@ +## Basic example + +This example shows how to use WebAssembly and Go-defined functions with wazero. diff --git a/examples/basic/add.go b/examples/basic/add.go new file mode 100644 index 00000000..d2fd4fd7 --- /dev/null +++ b/examples/basic/add.go @@ -0,0 +1,69 @@ +package main + +import ( + _ "embed" + "fmt" + "log" + "os" + "strconv" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" +) + +// main implements a basic function in both Go and WebAssembly. +func main() { + // Create a new WebAssembly Runtime. + r := wazero.NewRuntime() + + // Add a module to the runtime named "wasm/math" which exports one function "add", implemented in WebAssembly. + wasm, err := r.InstantiateModuleFromCode([]byte(`(module $wasm/math + (func $add (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add + ) + (export "add" (func $add)) +)`)) + if err != nil { + log.Fatal(err) + } + defer wasm.Close() + + // Add a module to the runtime named "host/math" which exports one function "add", implemented in Go. + host, err := r.NewModuleBuilder("host/math"). + ExportFunction("add", func(v1, v2 uint32) uint32 { + return v1 + v2 + }).Instantiate() + if err != nil { + log.Fatal(err) + } + defer host.Close() + + // Read two args to add. + x, y := readTwoArgs() + + // Call the same function in both modules and print the results to the console. + for _, mod := range []api.Module{wasm, host} { + add := mod.ExportedFunction("add") + results, err := add.Call(nil, x, y) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s: %d + %d = %d\n", mod.Name(), x, y, results[0]) + } +} + +func readTwoArgs() (uint64, uint64) { + x, err := strconv.ParseUint(os.Args[1], 10, 64) + if err != nil { + log.Fatalf("invalid arg %v: %v", os.Args[1], err) + } + + y, err := strconv.ParseUint(os.Args[2], 10, 64) + if err != nil { + log.Fatalf("invalid arg %v: %v", os.Args[2], err) + } + return x, y +} diff --git a/examples/basic/add_test.go b/examples/basic/add_test.go new file mode 100644 index 00000000..1133b196 --- /dev/null +++ b/examples/basic/add_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" +) + +// ExampleMain ensures the following will work: +// +// go build add.go +// ./add 7 9 +func ExampleMain() { + + // Save the old os.Args and replace with our example input. + oldArgs := os.Args + os.Args = []string{"add", "7", "9"} + defer func() { os.Args = oldArgs }() + + main() + + // Output: + // wasm/math: 7 + 9 = 16 + // host/math: 7 + 9 = 16 +} diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go deleted file mode 100644 index f2a95464..00000000 --- a/examples/fibonacci_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package examples - -import ( - _ "embed" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/wasi" -) - -// fibWasm was compiled from TinyGo testdata/fibonacci.go -//go:embed testdata/fibonacci.wasm -var fibWasm []byte // TODO: implement this in text format as it is less distracting setup - -func Test_fibonacci(t *testing.T) { - r := wazero.NewRuntime() - - // Note: fibonacci.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command. - wm, err := wasi.InstantiateSnapshotPreview1(r) - require.NoError(t, err) - defer wm.Close() - - module, err := r.InstantiateModuleFromCode(fibWasm) - require.NoError(t, err) - defer module.Close() - - fibonacci := module.ExportedFunction("fibonacci") - - for _, c := range []struct { - input, expected uint64 // i32_i32 sig, but wasm.ExportedFunction params and results are uint64 - }{ - {input: 20, expected: 6765}, - {input: 10, expected: 55}, - {input: 5, expected: 5}, - } { - results, err := fibonacci.Call(nil, c.input) - require.NoError(t, err) - require.Equal(t, c.expected, results[0]) - } -} diff --git a/examples/file_system_test.go b/examples/file_system_test.go deleted file mode 100644 index 29c7cd49..00000000 --- a/examples/file_system_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package examples - -import ( - "bytes" - "embed" - _ "embed" - "io/fs" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/wasi" -) - -// catFS is an embedded filesystem limited to cat.go -//go:embed testdata/cat.go -var catFS embed.FS - -// catGo is the TinyGo source -//go:embed testdata/cat.go -var catGo []byte - -// catWasm was compiled from catGo -//go:embed testdata/cat.wasm -var catWasm []byte - -// Test_Cat writes the input file to stdout, just like `cat`. -// -// This is a basic introduction to the WebAssembly System Interface (WASI). -// See https://github.com/WebAssembly/WASI -func Test_Cat(t *testing.T) { - r := wazero.NewRuntime() - - // First, configure where the WebAssembly Module (Wasm) console outputs to (stdout). - stdoutBuf := bytes.NewBuffer(nil) - - // Since wazero uses fs.FS, we can use standard libraries to do things like trim the leading path. - rooted, err := fs.Sub(catFS, "testdata") - require.NoError(t, err) - - // Combine the above into our baseline config, overriding defaults (which discard stdout and have no file system). - config := wazero.NewModuleConfig().WithStdout(stdoutBuf).WithFS(rooted) - - // Instantiate WASI, which implements system I/O such as console output. - wm, err := wasi.InstantiateSnapshotPreview1(r) - require.NoError(t, err) - defer wm.Close() - - // InstantiateModuleFromCodeWithConfig runs the "_start" function which is what TinyGo compiles "main" to. - // * Set the program name (arg[0]) to "cat" and add args to write "cat.go" to stdout twice. - // * We use both "/cat.go" and "./cat.go" because WithFS by default maps the workdir "." to "/". - cat, err := r.InstantiateModuleFromCodeWithConfig(catWasm, config.WithArgs("cat", "/cat.go", "./cat.go")) - require.NoError(t, err) - defer cat.Close() - - // We expect the WebAssembly function wrote "cat.go" twice! - require.Equal(t, append(catGo, catGo...), stdoutBuf.Bytes()) -} diff --git a/examples/host_func_test.go b/examples/host_func_test.go deleted file mode 100644 index 683bccff..00000000 --- a/examples/host_func_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package examples - -import ( - "bytes" - "context" - "crypto/rand" - _ "embed" - "encoding/base64" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/wasi" -) - -type testKey struct{} - -// hostFuncWasm was compiled from TinyGo testdata/host_func.go -//go:embed testdata/host_func.wasm -var hostFuncWasm []byte - -func Test_hostFunc(t *testing.T) { - // The function for allocating the in-Wasm memory region. - // We resolve this function after main module instantiation. - allocateInWasmBuffer := func(api.Module, uint32) uint32 { - panic("unimplemented") - } - - var expectedBase64String string - - // Host-side implementation of get_random_string on Wasm import. - getRandomBytes := func(ctx api.Module, retBufPtr uint32, retBufSize uint32) { - // Assert that context values passed in from CallFunctionContext are accessible. - contextValue := ctx.Context().Value(testKey{}).(int64) - require.Equal(t, int64(12345), contextValue) - - const bufferSize = 10 - offset := allocateInWasmBuffer(ctx, bufferSize) - - // Store the address info to the memory. - require.True(t, ctx.Memory().WriteUint32Le(retBufPtr, offset)) - require.True(t, ctx.Memory().WriteUint32Le(retBufSize, uint32(bufferSize))) - - // Now store the random values in the region. - b, ok := ctx.Memory().Read(offset, bufferSize) - require.True(t, ok) - - n, err := rand.Read(b) - require.NoError(t, err) - require.Equal(t, bufferSize, n) - - expectedBase64String = base64.StdEncoding.EncodeToString(b) - } - - r := wazero.NewRuntime() - - _, err := r.NewModuleBuilder("env").ExportFunction("get_random_bytes", getRandomBytes).Instantiate() - require.NoError(t, err) - - // Configure stdout (console) to write to a buffer. - stdout := bytes.NewBuffer(nil) - config := wazero.NewModuleConfig().WithStdout(stdout) - - // Instantiate WASI, which implements system I/O such as console output. - wm, err := wasi.InstantiateSnapshotPreview1(r) - require.NoError(t, err) - defer wm.Close() - - // InstantiateModuleFromCodeWithConfig runs the "_start" function which is what TinyGo compiles "main" to. - module, err := r.InstantiateModuleFromCodeWithConfig(hostFuncWasm, config) - require.NoError(t, err) - defer module.Close() - - allocateInWasmBufferFn := module.ExportedFunction("allocate_buffer") - require.NotNil(t, allocateInWasmBufferFn) - - // Implement the function pointer. This mainly shows how you can decouple a module function dependency. - allocateInWasmBuffer = func(ctx api.Module, size uint32) uint32 { - res, err := allocateInWasmBufferFn.Call(ctx, uint64(size)) - require.NoError(t, err) - return uint32(res[0]) - } - - // Set a context variable that should be available in a wasm.Function. - ctx := context.WithValue(context.Background(), testKey{}, int64(12345)) - - // Invoke a module-defined function that depends on a host function import - _, err = module.ExportedFunction("base64").Call(module.WithContext(ctx)) - require.NoError(t, err) - - // Verify that in-Wasm calculated base64 string matches the one calculated in native Go. - require.Equal(t, expectedBase64String, strings.TrimSpace(stdout.String())) -} diff --git a/examples/multiple-results/.gitignore b/examples/multiple-results/.gitignore new file mode 100644 index 00000000..769cedca --- /dev/null +++ b/examples/multiple-results/.gitignore @@ -0,0 +1 @@ +multiple-results diff --git a/examples/multiple-results/README.md b/examples/multiple-results/README.md new file mode 100644 index 00000000..33633e60 --- /dev/null +++ b/examples/multiple-results/README.md @@ -0,0 +1,3 @@ +## Multiple results example + +This example shows how to return more than one result from WebAssembly or Go-defined functions. diff --git a/examples/results_test.go b/examples/multiple-results/multiple-results.go similarity index 68% rename from examples/results_test.go rename to examples/multiple-results/multiple-results.go index 45d6dd5f..b2277545 100644 --- a/examples/results_test.go +++ b/examples/multiple-results/multiple-results.go @@ -1,38 +1,75 @@ -package examples +package main import ( - "testing" - - "github.com/stretchr/testify/require" + _ "embed" + "fmt" + "log" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" ) -// Test_MultiReturn_V1_0 implements functions with multiple returns values, using an approach portable with any -// WebAssembly 1.0 (20191205) runtime. +// main implements functions with multiple returns values, using both an approach portable with any WebAssembly 1.0 +// (20191205) runtime, as well one dependent on the "multiple-results" feature. // -// Note: This is the same approach used by WASI snapshot-01! +// The portable approach uses parameters to return additional results. The parameter value is a memory offset to write +// the next value. This is the same approach used by WASI snapshot-01! +// * resultOffsetWasmFunctions +// * resultOffsetHostFunctions // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md -func Test_MultipleResults(t *testing.T) { - r := wazero.NewRuntime() +// +// Another approach is to enable the "multiple-results" feature. While "multiple-results" is not yet a W3C recommendation, most +// WebAssembly runtimes support it by default. +// * multiValueWasmFunctions +// * multiValueHostFunctions +// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md +func main() { + // Create a portable WebAssembly Runtime. + runtime := wazero.NewRuntime() - // Instantiate a module that illustrates multiple results using functions defined as WebAssembly instructions. - wasm, err := resultOffsetWasmFunctions(r) - require.NoError(t, err) + // Add a module that uses offset parameters for multiple results, with functions defined in WebAssembly. + wasm, err := resultOffsetWasmFunctions(runtime) + if err != nil { + log.Fatal(err) + } defer wasm.Close() - // Instantiate a module that illustrates multiple results using functions defined in Go. - host, err := resultOffsetHostFunctions(r) - require.NoError(t, err) - defer wasm.Close() + // Add a module that uses offset parameters for multiple results, with functions defined in Go. + host, err := resultOffsetHostFunctions(runtime) + if err != nil { + log.Fatal(err) + } + defer host.Close() - // Prove both implementations have the same effects! - for _, mod := range []api.Module{wasm, host} { - callGetNumber := mod.ExportedFunction("call_get_age") - results, err := callGetNumber.Call(nil) - require.NoError(t, err) - require.Equal(t, []uint64{37}, results) + // wazero enables only W3C recommended features by default. Opt-in to other features like so: + runtimeWithMultiValue := wazero.NewRuntimeWithConfig( + wazero.NewRuntimeConfig().WithFeatureMultiValue(true), + // ^^ Note: You can enable all features via WithFinishedFeatures. + ) + + // Add a module that uses multiple results values, with functions defined in WebAssembly. + wasmWithMultiValue, err := multiValueWasmFunctions(runtimeWithMultiValue) + if err != nil { + log.Fatal(err) + } + defer wasmWithMultiValue.Close() + + // Add a module that uses multiple results values, with functions defined in Go. + hostWithMultiValue, err := multiValueHostFunctions(runtimeWithMultiValue) + if err != nil { + log.Fatal(err) + } + defer hostWithMultiValue.Close() + + // Call the same function in all modules and print the results to the console. + for _, mod := range []api.Module{wasm, host, wasmWithMultiValue, hostWithMultiValue} { + getAge := mod.ExportedFunction("call_get_age") + results, err := getAge.Call(nil) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s: age=%d\n", mod.Name(), results[0]) } } @@ -96,38 +133,7 @@ func resultOffsetHostFunctions(r wazero.Runtime) (api.Module, error) { }).Instantiate() } -// Test_MultipleResults_MultiValue implements functions with multiple returns values naturally, due to enabling the -// "multi-value" feature. -// -// Note: While "multi-value" is not yet a W3C recommendation, most WebAssembly runtimes support it by default. -// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md -func Test_MultipleResults_MultiValue(t *testing.T) { - // wazero enables only W3C recommended features by default. Opt-in to other features like so: - r := wazero.NewRuntimeWithConfig( - wazero.NewRuntimeConfig().WithFeatureMultiValue(true), - // ^^ Note: You can enable all features via WithFinishedFeatures. - ) - - // Instantiate a module that illustrates multi-value functions defined as WebAssembly instructions. - wasm, err := multiValueWasmFunctions(r) - require.NoError(t, err) - defer wasm.Close() - - // Instantiate a module that illustrates multi-value functions using functions defined in Go. - host, err := multiValueHostFunctions(r) - require.NoError(t, err) - defer wasm.Close() - - // Prove both implementations have the same effects! - for _, mod := range []api.Module{wasm, host} { - callGetNumber := mod.ExportedFunction("call_get_age") - results, err := callGetNumber.Call(nil) - require.NoError(t, err) - require.Equal(t, []uint64{37}, results) - } -} - -// multiValueWasmFunctions defines Wasm functions that illustrate multiple results using the "multi-value" feature. +// multiValueWasmFunctions defines Wasm functions that illustrate multiple results using the "multiple-results" feature. func multiValueWasmFunctions(r wazero.Runtime) (api.Module, error) { return r.InstantiateModuleFromCode([]byte(`(module $multi-value/wasm @@ -148,7 +154,7 @@ func multiValueWasmFunctions(r wazero.Runtime) (api.Module, error) { )`)) } -// multiValueHostFunctions defines Wasm functions that illustrate multiple results using the "multi-value" feature. +// multiValueHostFunctions defines Wasm functions that illustrate multiple results using the "multiple-results" feature. func multiValueHostFunctions(r wazero.Runtime) (api.Module, error) { return r.NewModuleBuilder("multi-value/host"). // Define a function that returns two results diff --git a/examples/multiple-results/multiple-results_test.go b/examples/multiple-results/multiple-results_test.go new file mode 100644 index 00000000..131dfe1f --- /dev/null +++ b/examples/multiple-results/multiple-results_test.go @@ -0,0 +1,16 @@ +package main + +// ExampleMain ensures the following will work: +// +// go build multiple-results.go +// ./multiple-results +func ExampleMain() { + + main() + + // Output: + // result-offset/wasm: age=37 + // result-offset/host: age=37 + // multi-value/wasm: age=37 + // multi-value/host: age=37 +} diff --git a/examples/replace-import/.gitignore b/examples/replace-import/.gitignore new file mode 100644 index 00000000..2885c81e --- /dev/null +++ b/examples/replace-import/.gitignore @@ -0,0 +1 @@ +replace-import diff --git a/examples/replace-import/README.md b/examples/replace-import/README.md new file mode 100644 index 00000000..3adf135b --- /dev/null +++ b/examples/replace-import/README.md @@ -0,0 +1,3 @@ +## Replace import example + +This example shows how to override a module name hard-coded in a WebAssembly module. diff --git a/examples/replace_test.go b/examples/replace-import/replace-import.go similarity index 69% rename from examples/replace_test.go rename to examples/replace-import/replace-import.go index ad780072..3793e32f 100644 --- a/examples/replace_test.go +++ b/examples/replace-import/replace-import.go @@ -1,16 +1,16 @@ -package examples +package main import ( - "testing" - - "github.com/stretchr/testify/require" + _ "embed" + "fmt" + "log" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" ) -// Test_Replace shows how you can replace a module import when it doesn't match instantiated modules. -func Test_Replace(t *testing.T) { +// main shows how you can replace a module import when it doesn't match instantiated modules. +func main() { r := wazero.NewRuntime() // Instantiate a function that closes the module under "assemblyscript.abort". @@ -18,25 +18,30 @@ func Test_Replace(t *testing.T) { ExportFunction("abort", func(m api.Module, messageOffset, fileNameOffset, line, col uint32) { _ = m.CloseWithExitCode(255) }).Instantiate() - require.NoError(t, err) + if err != nil { + log.Fatal(err) + } defer host.Close() // Compile code that needs the function "env.abort". - code, err := r.CompileModule([]byte(`(module + code, err := r.CompileModule([]byte(`(module $needs-import (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (export "abort" (func 0)) ;; exports the import for testing )`)) - require.NoError(t, err) + if err != nil { + log.Fatal(err) + } // Instantiate the module, replacing the import "env.abort" with "assemblyscript.abort". mod, err := r.InstantiateModuleWithConfig(code, wazero.NewModuleConfig(). - WithName(t.Name()). WithImport("env", "abort", "assemblyscript", "abort")) - require.NoError(t, err) + if err != nil { + log.Fatal(err) + } defer mod.Close() // Since the above worked, the exported function closes the module. _, err = mod.ExportedFunction("abort").Call(nil, 0, 0, 0, 0) - require.EqualError(t, err, `module "Test_Replace" closed with exit_code(255)`) + fmt.Println(err) } diff --git a/examples/replace-import/replace-import_test.go b/examples/replace-import/replace-import_test.go new file mode 100644 index 00000000..224d364f --- /dev/null +++ b/examples/replace-import/replace-import_test.go @@ -0,0 +1,13 @@ +package main + +// ExampleMain ensures the following will work: +// +// go build replace-import.go +// ./replace-import +func ExampleMain() { + + main() + + // Output: + // module "needs-import" closed with exit_code(255) +} diff --git a/examples/simple_test.go b/examples/simple_test.go deleted file mode 100644 index 46d71e80..00000000 --- a/examples/simple_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package examples - -import ( - "bytes" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" -) - -// Test_Simple implements a basic function in go: hello. This is imported as the Wasm name "$hello" and run on start. -func Test_Simple(t *testing.T) { - stdout := new(bytes.Buffer) - hello := func() { - _, _ = fmt.Fprintln(stdout, "hello!") - } - - r := wazero.NewRuntime() - - // Host functions can be exported as any module name, including the empty string. - host, err := r.NewModuleBuilder("").ExportFunction("hello", hello).Instantiate() - require.NoError(t, err) - defer host.Close() - - // The "hello" function was imported as $hello in Wasm. Since it was marked as the start - // function, it is invoked on instantiation. Ensure that worked: "hello" was called! - mod, err := r.InstantiateModuleFromCode([]byte(`(module $test - (import "" "hello" (func $hello)) - (start $hello) -)`)) - require.NoError(t, err) - defer mod.Close() - - require.Equal(t, "hello!\n", stdout.String()) -} diff --git a/examples/stdio_test.go b/examples/stdio_test.go deleted file mode 100644 index 83b5f55e..00000000 --- a/examples/stdio_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package examples - -import ( - "bytes" - _ "embed" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/wasi" -) - -// stdioWasm was compiled from TinyGo testdata/stdio.go -//go:embed testdata/stdio.wasm -var stdioWasm []byte - -func Test_stdio(t *testing.T) { - r := wazero.NewRuntime() - - // Configure standard I/O (ex stdout) to write to buffers instead of no-op. - stdinBuf := bytes.NewBuffer([]byte("WASI\n")) - stdoutBuf := bytes.NewBuffer(nil) - stderrBuf := bytes.NewBuffer(nil) - config := wazero.NewModuleConfig().WithStdin(stdinBuf).WithStdout(stdoutBuf).WithStderr(stderrBuf) - - // Instantiate WASI, which implements system I/O such as console output. - wm, err := wasi.InstantiateSnapshotPreview1(r) - require.NoError(t, err) - defer wm.Close() - - // InstantiateModuleFromCodeWithConfig runs the "_start" function which is what TinyGo compiles "main" to. - module, err := r.InstantiateModuleFromCodeWithConfig(stdioWasm, config) - require.NoError(t, err) - defer module.Close() - - require.Equal(t, "Hello, WASI!", strings.TrimSpace(stdoutBuf.String())) - require.Equal(t, "Error Message", strings.TrimSpace(stderrBuf.String())) -} diff --git a/examples/testdata/fibonacci.go b/examples/testdata/fibonacci.go deleted file mode 100644 index a539dd25..00000000 --- a/examples/testdata/fibonacci.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -func main() {} - -//export fibonacci -func fibonacci(in uint32) uint32 { - if in <= 1 { - return in - } - return fibonacci(in-1) + fibonacci(in-2) -} diff --git a/examples/testdata/fibonacci.wasm b/examples/testdata/fibonacci.wasm deleted file mode 100755 index 97e8a30e..00000000 Binary files a/examples/testdata/fibonacci.wasm and /dev/null differ diff --git a/examples/testdata/host_func.go b/examples/testdata/host_func.go deleted file mode 100644 index 70debd8f..00000000 --- a/examples/testdata/host_func.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "reflect" - "unsafe" -) - -func main() {} - -//export allocate_buffer -func allocateBuffer(size uint32) *byte { - // Allocate the in-Wasm memory region and returns its pointer to hosts. - // The region is supposed to store random bytes generated in hosts. - buf := make([]byte, size) - return &buf[0] -} - -// Note: Export on a function without an implementation is a Wasm import that defaults to the module "env". -//export get_random_bytes -func get_random_bytes(retBufPtr **byte, retBufSize *int) - -// Get random bytes from the host. -func getRandomBytes() []byte { - var bufPtr *byte - var bufSize int - get_random_bytes(&bufPtr, &bufSize) - //nolint - return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(bufPtr)), - Len: uintptr(bufSize), - Cap: uintptr(bufSize), - })) -} - -//export base64 -func base64OnString() { - // Get random bytes from the host and - // do base64 encoding them for given times. - buf := getRandomBytes() - encoded := base64.StdEncoding.EncodeToString(buf) - fmt.Println(encoded) -} diff --git a/examples/testdata/host_func.wasm b/examples/testdata/host_func.wasm deleted file mode 100755 index 9ed7e7eb..00000000 Binary files a/examples/testdata/host_func.wasm and /dev/null differ diff --git a/examples/testdata/stdio.go b/examples/testdata/stdio.go deleted file mode 100644 index 9231968f..00000000 --- a/examples/testdata/stdio.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "strings" -) - -func main() { - s := bufio.NewScanner(os.Stdin) - for s.Scan() { - line := s.Text() - if _, err := fmt.Printf("Hello, %s!\n", strings.TrimSpace(line)); err != nil { - os.Exit(1) - } - - if _, err := fmt.Fprintln(os.Stderr, "Error Message"); err != nil { - os.Exit(1) - } - } -} diff --git a/examples/testdata/stdio.wasm b/examples/testdata/stdio.wasm deleted file mode 100755 index 4460b3c1..00000000 Binary files a/examples/testdata/stdio.wasm and /dev/null differ diff --git a/examples/wasi/.gitignore b/examples/wasi/.gitignore new file mode 100644 index 00000000..24b0c100 --- /dev/null +++ b/examples/wasi/.gitignore @@ -0,0 +1 @@ +wasi diff --git a/examples/wasi/README.md b/examples/wasi/README.md new file mode 100644 index 00000000..73e2f3d2 --- /dev/null +++ b/examples/wasi/README.md @@ -0,0 +1,3 @@ +## WASI example + +This example shows how to use I/O in your WebAssembly modules using WASI (WebAssembly System Interface). diff --git a/examples/wasi/cat.go b/examples/wasi/cat.go new file mode 100644 index 00000000..f28a67e6 --- /dev/null +++ b/examples/wasi/cat.go @@ -0,0 +1,53 @@ +package main + +import ( + "embed" + _ "embed" + "io/fs" + "log" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/wasi" +) + +// catFS is an embedded filesystem limited to test.txt +//go:embed testdata/test.txt +var catFS embed.FS + +// catWasm was compiled the TinyGo source testdata/cat.go +//go:embed testdata/cat.wasm +var catWasm []byte + +// main writes an input file to stdout, just like `cat`. +// +// This is a basic introduction to the WebAssembly System Interface (WASI). +// See https://github.com/WebAssembly/WASI +func main() { + r := wazero.NewRuntime() + + // Since wazero uses fs.FS, we can use standard libraries to do things like trim the leading path. + rooted, err := fs.Sub(catFS, "testdata") + if err != nil { + log.Fatal(err) + } + + // Combine the above into our baseline config, overriding defaults (which discard stdout and have no file system). + config := wazero.NewModuleConfig().WithStdout(os.Stdout).WithFS(rooted) + + // Instantiate WASI, which implements system I/O such as console output. + wm, err := wasi.InstantiateSnapshotPreview1(r) + if err != nil { + log.Fatal(err) + } + defer wm.Close() + + // InstantiateModuleFromCodeWithConfig runs the "_start" function which is what TinyGo compiles "main" to. + // * Set the program name (arg[0]) to "wasi" and add args to write "test.txt" to stdout twice. + // * We use "/test.txt" or "./test.txt" because WithFS by default maps the workdir "." to "/". + cat, err := r.InstantiateModuleFromCodeWithConfig(catWasm, config.WithArgs("wasi", os.Args[1])) + if err != nil { + log.Fatal(err) + } + defer cat.Close() +} diff --git a/examples/wasi/cat_test.go b/examples/wasi/cat_test.go new file mode 100644 index 00000000..d8866f72 --- /dev/null +++ b/examples/wasi/cat_test.go @@ -0,0 +1,20 @@ +package main + +import "os" + +// ExampleMain ensures the following will work: +// +// go build cat.go +// ./cat ./test.txt +func ExampleMain() { + + // Save the old os.Args and replace with our example input. + oldArgs := os.Args + os.Args = []string{"cat", "./test.txt"} + defer func() { os.Args = oldArgs }() + + main() + + // Output: + // hello filesystem +} diff --git a/examples/testdata/cat.go b/examples/wasi/testdata/cat.go similarity index 87% rename from examples/testdata/cat.go rename to examples/wasi/testdata/cat.go index f296403e..55e81245 100644 --- a/examples/testdata/cat.go +++ b/examples/wasi/testdata/cat.go @@ -5,7 +5,7 @@ import ( "os" ) -// main is the same as cat: "concatenate and print files." +// main is the same as wasi: "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++ { diff --git a/examples/testdata/cat.wasm b/examples/wasi/testdata/cat.wasm similarity index 96% rename from examples/testdata/cat.wasm rename to examples/wasi/testdata/cat.wasm index 33e296f1..103cde34 100755 Binary files a/examples/testdata/cat.wasm and b/examples/wasi/testdata/cat.wasm differ diff --git a/examples/wasi/testdata/test.txt b/examples/wasi/testdata/test.txt new file mode 100644 index 00000000..d67ef599 --- /dev/null +++ b/examples/wasi/testdata/test.txt @@ -0,0 +1 @@ +hello filesystem diff --git a/examples/wasi_test.go b/examples/wasi_test.go deleted file mode 100644 index 50b7c046..00000000 --- a/examples/wasi_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package examples - -import ( - "bytes" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/wasi" -) - -func Test_WASI(t *testing.T) { - // built-in WASI function to write a random value to memory - randomGet := func(ctx api.Module, buf, bufLen uint32) wasi.Errno { - panic("unimplemented") - } - - stdout := new(bytes.Buffer) - random := func(ctx api.Module) { - // Write 8 random bytes to memory using WASI. - errno := randomGet(ctx, 0, 8) - require.Equal(t, wasi.ErrnoSuccess, errno) - - // Read them back and print it in hex! - random, ok := ctx.Memory().ReadUint64Le(0) - require.True(t, ok) - _, _ = fmt.Fprintf(stdout, "random: %x\n", random) - } - - r := wazero.NewRuntime() - - // Host functions can be exported as any module name, including the empty string. - host, err := r.NewModuleBuilder("").ExportFunction("random", random).Instantiate() - require.NoError(t, err) - defer host.Close() - - // Configure WASI and implement the function to use it - wm, err := wasi.InstantiateSnapshotPreview1(r) - require.NoError(t, err) - defer wm.Close() - - randomGetFn := wm.ExportedFunction("random_get") - - // Implement the function pointer. This mainly shows how you can decouple a host function dependency. - randomGet = func(ctx api.Module, buf, bufLen uint32) wasi.Errno { - res, err := randomGetFn.Call(ctx, uint64(buf), uint64(bufLen)) - require.NoError(t, err) - return wasi.Errno(res[0]) - } - - // The "random" function was imported as $random in Wasm. Since it was marked as the start - // function, it is invoked on instantiation. Ensure that worked: "random" was called! - mod, err := r.InstantiateModuleFromCode([]byte(`(module $wasi - (import "wasi_snapshot_preview1" "random_get" - (func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32))) - (import "" "random" (func $random)) - (memory 1) - (start $random) -)`)) - require.NoError(t, err) - defer mod.Close() - - require.Contains(t, stdout.String(), "random: ") -}