Adds import-go example (#466)

This shows how to define, export and import functions written in Go.

Fixes #464

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-15 09:31:52 +08:00
committed by GitHub
parent 556cfa1967
commit 106f96b066
20 changed files with 222 additions and 26 deletions

View File

@@ -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).

View File

@@ -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.

View File

@@ -1,4 +1,4 @@
package main
package add
import (
_ "embed"

View File

@@ -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.

1
examples/import-go/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
age-calculator

View File

@@ -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).

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
package main
package multiple_results
import (
_ "embed"

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
package main
package replace_import
import (
_ "embed"

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
package main
package wasi_example
import (
"embed"

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)",

14
wasi/example_test.go Normal file
View File

@@ -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:
}

View File

@@ -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).

View File

@@ -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()