From 106f96b066b4ea4544bdd00da971441c4b5c9953 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Fri, 15 Apr 2022 09:31:52 +0800 Subject: [PATCH] Adds import-go example (#466) This shows how to define, export and import functions written in Go. Fixes #464 Signed-off-by: Adrian Cole Co-authored-by: Takeshi Yoneda --- examples/README.md | 3 +- examples/basic/README.md | 2 +- examples/basic/add.go | 2 +- examples/basic/add_test.go | 5 +- examples/import-go/.gitignore | 1 + examples/import-go/README.md | 38 +++++++ examples/import-go/age-calculator.go | 105 ++++++++++++++++++ examples/import-go/age-calculator_test.go | 24 ++++ examples/multiple-results/multiple-results.go | 2 +- .../multiple-results/multiple-results_test.go | 5 +- examples/replace-import/replace-import.go | 2 +- .../replace-import/replace-import_test.go | 5 +- examples/wasi/cat.go | 2 +- examples/wasi/cat_test.go | 5 +- internal/wasm/func_validation.go | 2 +- internal/wasm/text/func_parser.go | 3 + internal/wasm/text/func_parser_test.go | 14 +++ wasi/example_test.go | 14 +++ wasi/usage_test.go | 6 +- wasi/wasi.go | 8 +- 20 files changed, 222 insertions(+), 26 deletions(-) create mode 100644 examples/import-go/.gitignore create mode 100644 examples/import-go/README.md create mode 100644 examples/import-go/age-calculator.go create mode 100644 examples/import-go/age-calculator_test.go create mode 100644 wasi/example_test.go diff --git a/examples/README.md b/examples/README.md index 83c12880..730031dd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,7 +2,8 @@ The following example projects can help you practice WebAssembly with wazero: -* [basic](basic) - how to use WebAssembly and Go-defined functions. +* [basic](basic) - how to use both WebAssembly and Go-defined functions. +* [import-go](import-go) - how to define, import and call a Go-defined function from a WebAssembly-defined function. * [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). diff --git a/examples/basic/README.md b/examples/basic/README.md index 3d95042b..174b26a2 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,3 +1,3 @@ ## Basic example -This example shows how to use WebAssembly and Go-defined functions with wazero. +This example shows how to use both WebAssembly and Go-defined functions with wazero. diff --git a/examples/basic/add.go b/examples/basic/add.go index d2fd4fd7..0a229418 100644 --- a/examples/basic/add.go +++ b/examples/basic/add.go @@ -1,4 +1,4 @@ -package main +package add import ( _ "embed" diff --git a/examples/basic/add_test.go b/examples/basic/add_test.go index 4e56c1a6..00164866 100644 --- a/examples/basic/add_test.go +++ b/examples/basic/add_test.go @@ -1,4 +1,4 @@ -package main +package add import ( "os" @@ -6,8 +6,7 @@ import ( // Example_main ensures the following will work: // -// go build add.go -// ./add 7 9 +// go run add.go 7 9 func Example_main() { // Save the old os.Args and replace with our example input. diff --git a/examples/import-go/.gitignore b/examples/import-go/.gitignore new file mode 100644 index 00000000..8d7abcc7 --- /dev/null +++ b/examples/import-go/.gitignore @@ -0,0 +1 @@ +age-calculator diff --git a/examples/import-go/README.md b/examples/import-go/README.md new file mode 100644 index 00000000..b1ed9d56 --- /dev/null +++ b/examples/import-go/README.md @@ -0,0 +1,38 @@ +## Import go func example + +This example shows how to define, import and call a Go-defined function from a WebAssembly-defined function. + +If the current year is 2022, and we give the argument 2000, [age-calculator.go](age-calculator.go) should output 22. +```bash +$ go run age-calculator.go 2000 +println >> 21 +log_i32 >> 21 +``` + +### Background + +WebAssembly has neither a mechanism to get the current year, nor one to print to the console, so we define these in Go. +Similar to Go, WebAssembly functions are namespaced, into modules instead of packages. Just like Go, only exported +functions can be imported into another module. What you'll learn in [age-calculator.go](age-calculator.go), is how to +export functions using [ModuleBuilder](https://pkg.go.dev/github.com/tetratelabs/wazero#ModuleBuilder) and how a +WebAssembly module defined in its [text format](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0) +imports it. This only uses the text format for demonstration purposes, to show you what's going on. It is likely, you +will use another language to compile a Wasm (WebAssembly Module) binary, such as TinyGo. Regardless of how wasm is +produced, the export/import mechanics are the same! + +### Where next? + +The following examples continue the path of learning about importing and exporting functions with wazero: + +#### [WebAssembly System Interface (WASI)](../wasi) + +This uses an ad-hoc Go-defined function to print to the console. There is an emerging specification to standardize +system calls (similar to Go's [x/sys](https://pkg.go.dev/golang.org/x/sys/unix)) called WebAssembly System Interface +[(WASI)](https://github.com/WebAssembly/WASI). While this is not yet a W3C standard, wazero includes a +[wasi package](https://pkg.go.dev/github.com/tetratelabs/wazero/wasi). + +#### [Replace Import](../replace-import) + +You may use WebAssembly modules that have imports that don't match your ideal packaging structure. wazero allows you to +replace imports with different module names as needed, on a function granularity using +[ModuleConfig.WithImport](https://pkg.go.dev/github.com/tetratelabs/wazero#ModuleConfig.WithImport). diff --git a/examples/import-go/age-calculator.go b/examples/import-go/age-calculator.go new file mode 100644 index 00000000..0d080cc1 --- /dev/null +++ b/examples/import-go/age-calculator.go @@ -0,0 +1,105 @@ +package age_calculator + +import ( + _ "embed" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/tetratelabs/wazero" +) + +// main shows how to define, import and call a Go-defined function from a +// WebAssembly-defined function. +// +// See README.md for a full description. +func main() { + r := wazero.NewRuntime() + + // Instantiate a module named "env" that exports functions to get the + // current year and log to the console. + // + // Note: As noted on ExportFunction documentation, function signatures are + // constrained to a subset of numeric types. + // Note: "env" is a module name conventionally used for arbitrary + // host-defined functions, but any name would do. + env, err := r.NewModuleBuilder("env"). + ExportFunction("log_i32", func(v uint32) { + fmt.Println("log_i32 >>", v) + }). + ExportFunction("current_year", func() uint32 { + if envYear, err := strconv.ParseUint(os.Getenv("CURRENT_YEAR"), 10, 64); err == nil { + return uint32(envYear) // Allow env-override to prevent annual test maintenance! + } + return uint32(time.Now().Year()) + }). + Instantiate() + if err != nil { + log.Fatal(err) + } + defer env.Close() + + // Instantiate a module named "age-calculator" that imports functions + // defined in "env". + // + // Note: The import syntax in both Text and Binary format is the same + // regardless of if the function was defined in Go or WebAssembly. + ageCalculator, err := r.InstantiateModuleFromCode([]byte(` +;; Define the optional module name. '$' prefixing is a part of the text format. +(module $age-calculator + + ;; In WebAssembly, you don't import an entire module, rather each function. + ;; This imports the functions and gives them names which are easier to read + ;; than the alternative (zero-based index). + ;; + ;; Note: Importing unused functions is not an error in WebAssembly. + (import "env" "log_i32" (func $log (param i32))) + (import "env" "current_year" (func $year (result i32))) + + ;; get_age looks up the current year and subtracts the input from it. + ;; Note: The stack begins empty and anything left must match the result type. + (func $get_age (param $year_born i32) (result i32) + ;; stack: [] + call $year ;; stack: [$year.result] + local.get 0 ;; stack: [$year.result, $year_born] + i32.sub ;; stack: [$year.result-$year_born] + ) + ;; export allows api.Module to return this via ExportedFunction("get_age") + (export "get_age" (func $get_age)) + + ;; log_age + (func $log_age (param $year_born i32) + ;; stack: [] + local.get 0 ;; stack: [$year_born] + call $get_age ;; stack: [$get_age.result] + call $log ;; stack: [] + ) + (export "log_age" (func $log_age)) +)`)) + // ^^ Note: wazero's text compiler is incomplete #59. We are using it anyway to keep this example dependency free. + if err != nil { + log.Fatal(err) + } + defer ageCalculator.Close() + + // Read the birthYear from the arguments to main + birthYear, err := strconv.ParseUint(os.Args[1], 10, 64) + if err != nil { + log.Fatalf("invalid arg %v: %v", os.Args[1], err) + } + + // First, try calling the "get_age" function and printing to the console externally. + results, err := ageCalculator.ExportedFunction("get_age").Call(nil, birthYear) + if err != nil { + log.Fatal(err) + } + fmt.Println("println >>", results[0]) + + // First, try calling the "log_age" function and printing to the console externally. + _, err = ageCalculator.ExportedFunction("log_age").Call(nil, birthYear) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/import-go/age-calculator_test.go b/examples/import-go/age-calculator_test.go new file mode 100644 index 00000000..a7c04d3c --- /dev/null +++ b/examples/import-go/age-calculator_test.go @@ -0,0 +1,24 @@ +package age_calculator + +import "os" + +// Example_main ensures the following will work: +// +// go run age-calculator.go 2000 +func Example_main() { + + // Save the old os.Args and replace with our example input. + oldArgs := os.Args + _ = os.Setenv("CURRENT_YEAR", "2021") + os.Args = []string{"age-calculator", "2000"} + defer func() { + os.Args = oldArgs + _ = os.Unsetenv("CURRENT_YEAR") + }() + + main() + + // Output: + // println >> 21 + // log_i32 >> 21 +} diff --git a/examples/multiple-results/multiple-results.go b/examples/multiple-results/multiple-results.go index b2277545..e5f041dd 100644 --- a/examples/multiple-results/multiple-results.go +++ b/examples/multiple-results/multiple-results.go @@ -1,4 +1,4 @@ -package main +package multiple_results import ( _ "embed" diff --git a/examples/multiple-results/multiple-results_test.go b/examples/multiple-results/multiple-results_test.go index 10395c61..36fd2952 100644 --- a/examples/multiple-results/multiple-results_test.go +++ b/examples/multiple-results/multiple-results_test.go @@ -1,9 +1,8 @@ -package main +package multiple_results // Example_main ensures the following will work: // -// go build multiple-results.go -// ./multiple-results +// go run multiple-results.go func Example_main() { main() diff --git a/examples/replace-import/replace-import.go b/examples/replace-import/replace-import.go index 2c299b3f..e46554ab 100644 --- a/examples/replace-import/replace-import.go +++ b/examples/replace-import/replace-import.go @@ -1,4 +1,4 @@ -package main +package replace_import import ( _ "embed" diff --git a/examples/replace-import/replace-import_test.go b/examples/replace-import/replace-import_test.go index 847b13de..d365bc7a 100644 --- a/examples/replace-import/replace-import_test.go +++ b/examples/replace-import/replace-import_test.go @@ -1,9 +1,8 @@ -package main +package replace_import // Example_main ensures the following will work: // -// go build replace-import.go -// ./replace-import +// go run replace-import.go func Example_main() { main() diff --git a/examples/wasi/cat.go b/examples/wasi/cat.go index f28a67e6..6c5a7691 100644 --- a/examples/wasi/cat.go +++ b/examples/wasi/cat.go @@ -1,4 +1,4 @@ -package main +package wasi_example import ( "embed" diff --git a/examples/wasi/cat_test.go b/examples/wasi/cat_test.go index 42b7a61a..d57a2dbe 100644 --- a/examples/wasi/cat_test.go +++ b/examples/wasi/cat_test.go @@ -1,11 +1,10 @@ -package main +package wasi_example import "os" // Example_main ensures the following will work: // -// go build cat.go -// ./cat ./test.txt +// go run cat.go ./test.txt func Example_main() { // Save the old os.Args and replace with our example input. diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index cbb02a7c..43a107eb 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -480,7 +480,7 @@ func (m *Module) validateFunctionWithMaxStackValues( funcType := types[functions[index]] for i := 0; i < len(funcType.Params); i++ { if err := valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil { - return fmt.Errorf("type mismatch on %s operation param type", OpcodeCallName) + return fmt.Errorf("type mismatch on %s operation param type: %v", OpcodeCallName, err) } } for _, exp := range funcType.Results { diff --git a/internal/wasm/text/func_parser.go b/internal/wasm/text/func_parser.go index b7d8da4c..2062df62 100644 --- a/internal/wasm/text/func_parser.go +++ b/internal/wasm/text/func_parser.go @@ -152,6 +152,9 @@ func (p *funcParser) beginInstruction(tokenBytes []byte) (next tokenParser, err case wasm.OpcodeI32AddName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric opCode = wasm.OpcodeI32Add next = p.beginFieldOrInstruction + case wasm.OpcodeI32SubName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric + opCode = wasm.OpcodeI32Sub + next = p.beginFieldOrInstruction case wasm.OpcodeI32ConstName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric opCode = wasm.OpcodeI32Const next = p.parseI32 diff --git a/internal/wasm/text/func_parser_test.go b/internal/wasm/text/func_parser_test.go index fe4d7a23..b36ae3bd 100644 --- a/internal/wasm/text/func_parser_test.go +++ b/internal/wasm/text/func_parser_test.go @@ -48,6 +48,20 @@ func TestFuncParser(t *testing.T) { source: "(func i32.const 306)", expected: &wasm.Code{Body: []byte{wasm.OpcodeI32Const, 0xb2, 0x02, wasm.OpcodeEnd}}, }, + { + name: "i32.add", + source: "(func i32.const 2 i32.const 1 i32.add)", + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeI32Const, 0x02, wasm.OpcodeI32Const, 0x01, wasm.OpcodeI32Add, wasm.OpcodeEnd, + }}, + }, + { + name: "i32.sub", + source: "(func i32.const 2 i32.const 1 i32.sub)", + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeI32Const, 0x02, wasm.OpcodeI32Const, 0x01, wasm.OpcodeI32Sub, wasm.OpcodeEnd, + }}, + }, { name: "i64.const", source: "(func i64.const 356)", diff --git a/wasi/example_test.go b/wasi/example_test.go new file mode 100644 index 00000000..43196bf4 --- /dev/null +++ b/wasi/example_test.go @@ -0,0 +1,14 @@ +package wasi + +import "github.com/tetratelabs/wazero" + +var r = wazero.NewRuntime() + +// Example_InstantiateSnapshotPreview1 shows how to instantiate ModuleSnapshotPreview1, which allows other modules to +// import functions such as "wasi_snapshot_preview1" "fd_write". +func Example_instantiateSnapshotPreview1() { + wm, _ := InstantiateSnapshotPreview1(r) + defer wm.Close() + + // Output: +} diff --git a/wasi/usage_test.go b/wasi/usage_test.go index e77f36cd..a70930c8 100644 --- a/wasi/usage_test.go +++ b/wasi/usage_test.go @@ -24,13 +24,13 @@ func TestInstantiateModuleWithConfig(t *testing.T) { require.NoError(t, err) defer wm.Close() - code, err := r.CompileModule(wasiArg) + compiled, err := r.CompileModule(wasiArg) require.NoError(t, err) - defer code.Close() + defer compiled.Close() // Re-use the same module many times. for _, tc := range []string{"a", "b", "c"} { - mod, err := r.InstantiateModuleWithConfig(code, sys.WithArgs(tc).WithName(tc)) + mod, err := r.InstantiateModuleWithConfig(compiled, sys.WithArgs(tc).WithName(tc)) require.NoError(t, err) // Ensure the scoped configuration applied. As the args are null-terminated, we append zero (NUL). diff --git a/wasi/wasi.go b/wasi/wasi.go index 24d9988c..2524d6c5 100644 --- a/wasi/wasi.go +++ b/wasi/wasi.go @@ -1,3 +1,7 @@ +// Package wasi contains Go-defined functions to access system calls, such as opening a file, similar to Go's x/sys +// package. These are accessible from WebAssembly-defined functions via importing ModuleSnapshotPreview1. +// +// See https://github.com/WebAssembly/WASI package wasi import ( @@ -22,10 +26,6 @@ const ( // InstantiateSnapshotPreview1 instantiates ModuleSnapshotPreview1, so that other modules can import them. // -// Ex. After you configure like this, other modules can import functions like "wasi_snapshot_preview1" "fd_write". -// wm, _ := wasi.InstantiateSnapshotPreview1(r) -// defer wm.Close() -// // Note: All WASI functions return a single Errno result, ErrnoSuccess on success. func InstantiateSnapshotPreview1(r wazero.Runtime) (api.Module, error) { _, fns := snapshotPreview1Functions()