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:
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package add
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
@@ -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
1
examples/import-go/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
age-calculator
|
||||
38
examples/import-go/README.md
Normal file
38
examples/import-go/README.md
Normal 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).
|
||||
105
examples/import-go/age-calculator.go
Normal file
105
examples/import-go/age-calculator.go
Normal 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)
|
||||
}
|
||||
}
|
||||
24
examples/import-go/age-calculator_test.go
Normal file
24
examples/import-go/age-calculator_test.go
Normal 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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package multiple_results
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package replace_import
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package wasi_example
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
14
wasi/example_test.go
Normal 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:
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user