Adds function names to host functions and improves logging listener (#697)

This improves the experimental logging listener to show parameter name
and values like so:

```
--> ._start.command_export()
        	--> .__wasm_call_ctors()
        		--> .__wasilibc_initialize_environ()
        			==> wasi_snapshot_preview1.environ_sizes_get(result.environc=1048572,result.environBufSize=1048568)
        			<== ESUCCESS
        		<-- ()
        		==> wasi_snapshot_preview1.fd_prestat_get(fd=3,result.prestat=1048568)
        		<== ESUCCESS
        		--> .dlmalloc(2)
        			--> .sbrk(0)
        			<-- (1114112)
        		<-- (1060080)
--snip--
```

The convention `==>` implies it was a host function call
(def.IsHostFunction). This also improves the lifecycle by creating
listeners during compile. Finally, this backfills param names for
assemblyscript and wasi.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-07-14 16:43:25 +08:00
committed by GitHub
parent 0ae4254f21
commit 040736caac
27 changed files with 1221 additions and 499 deletions

View File

@@ -83,12 +83,17 @@ const (
ValueTypeF32 ValueType = 0x7d ValueTypeF32 ValueType = 0x7d
// ValueTypeF64 is a 64-bit floating point number. // ValueTypeF64 is a 64-bit floating point number.
ValueTypeF64 ValueType = 0x7c ValueTypeF64 ValueType = 0x7c
// ValueTypeExternref is a externref type. // ValueTypeExternref is a externref type.
// //
// Note: in wazero, externref type value are opaque raw 64-bit pointers, and the ValueTypeExternref type // Note: in wazero, externref type value are opaque raw 64-bit pointers,
// in the signature will be translated as uintptr in wazero's API level. // and the ValueTypeExternref type in the signature will be translated as
// For example, the import function `(func (import "env" "f") (param externref) (result externref))` can be defined in Go as: // uintptr in wazero's API level.
// //
// For example, given the import function:
// (func (import "env" "f") (param externref) (result externref))
//
// This can be defined in Go as:
// r.NewModuleBuilder("env").ExportFunctions(map[string]interface{}{ // r.NewModuleBuilder("env").ExportFunctions(map[string]interface{}{
// "f": func(externref uintptr) (resultExternRef uintptr) { return }, // "f": func(externref uintptr) (resultExternRef uintptr) { return },
// }) // })
@@ -222,6 +227,13 @@ type FunctionDefinition interface {
// is possible. // is possible.
ExportNames() []string ExportNames() []string
// IsHostFunction returns true if the function was implemented by the
// embedder (ex via wazero.ModuleBuilder) instead of a wasm binary.
//
// Note: Host functions can be non-deterministic or cause side effects.
// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A0
IsHostFunction() bool
// ParamTypes are the possibly empty sequence of value types accepted by a // ParamTypes are the possibly empty sequence of value types accepted by a
// function with this signature. // function with this signature.
// //

View File

@@ -45,9 +45,9 @@ import (
// * To add more functions to the "env" module, use FunctionExporter. // * To add more functions to the "env" module, use FunctionExporter.
// * To instantiate into another wazero.Namespace, use FunctionExporter. // * To instantiate into another wazero.Namespace, use FunctionExporter.
func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
return r.NewModuleBuilder("env"). builder := r.NewModuleBuilder("env")
ExportFunctions(NewFunctionExporter().ExportFunctions()). NewFunctionExporter().ExportFunctions(builder)
Instantiate(ctx, r) return builder.Instantiate(ctx, r)
} }
// FunctionExporter configures the functions in the "env" module used by // FunctionExporter configures the functions in the "env" module used by
@@ -70,7 +70,7 @@ type FunctionExporter interface {
// ExportFunctions builds functions to export with a wazero.ModuleBuilder // ExportFunctions builds functions to export with a wazero.ModuleBuilder
// named "env". // named "env".
ExportFunctions() (nameToGoFunc map[string]interface{}) ExportFunctions(builder wazero.ModuleBuilder)
} }
// NewFunctionExporter returns a FunctionExporter object with trace disabled. // NewFunctionExporter returns a FunctionExporter object with trace disabled.
@@ -113,13 +113,13 @@ func (e *functionExporter) WithTraceToStderr() FunctionExporter {
} }
// ExportFunctions implements FunctionExporter.ExportFunctions // ExportFunctions implements FunctionExporter.ExportFunctions
func (e *functionExporter) ExportFunctions() (nameToGoFunc map[string]interface{}) { func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode} env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode}
return map[string]interface{}{ builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
"abort": env.abort, "message", "fileName", "lineNumber", "columnNumber")
"trace": env.trace, builder.ExportFunction("trace", env.trace, "~lib/builtins/trace",
"seed": env.seed, "message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4")
} builder.ExportFunction("seed", env.seed, "~lib/builtins/seed")
} }
// assemblyScript includes "Special imports" only used In AssemblyScript when a // assemblyScript includes "Special imports" only used In AssemblyScript when a

View File

@@ -38,8 +38,9 @@ func Example_functionExporter() {
ExportFunction("get_int", func() uint32 { return 1 }) ExportFunction("get_int", func() uint32 { return 1 })
// Now, add AssemblyScript special function imports into it. // Now, add AssemblyScript special function imports into it.
envBuilder.ExportFunctions(assemblyscript.NewFunctionExporter(). assemblyscript.NewFunctionExporter().
WithAbortMessageDisabled().ExportFunctions()) WithAbortMessageDisabled().
ExportFunctions(envBuilder)
// Output: // Output:
} }

View File

@@ -6,12 +6,14 @@ import (
_ "embed" _ "embed"
"errors" "errors"
"io" "io"
"strings"
"testing" "testing"
"testing/iotest" "testing/iotest"
"unicode/utf16" "unicode/utf16"
"github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
. "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/watzero" "github.com/tetratelabs/wazero/internal/watzero"
"github.com/tetratelabs/wazero/sys" "github.com/tetratelabs/wazero/sys"
@@ -60,32 +62,37 @@ func TestAbort(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime() var out, log bytes.Buffer
defer r.Close(testCtx)
out := &bytes.Buffer{} // Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
_, err := r.NewModuleBuilder("env"). r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
ExportFunctions(tc.exporter.ExportFunctions()). defer r.Close(ctx)
Instantiate(testCtx, r)
envBuilder := r.NewModuleBuilder("env")
tc.exporter.ExportFunctions(envBuilder)
_, err := envBuilder.Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(abortWat) abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err) require.NoError(t, err)
code, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig()) code, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStderr(out)) mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStderr(&out))
require.NoError(t, err) require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename")) messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2) _, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.Error(t, err) require.Error(t, err)
require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode()) require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
require.Equal(t, tc.expected, out.String()) require.Equal(t, tc.expected, out.String())
require.Equal(t, `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
`, log.String())
}) })
} }
} }
@@ -95,16 +102,23 @@ func TestAbort_Error(t *testing.T) {
name string name string
messageUTF16 []byte messageUTF16 []byte
fileNameUTF16 []byte fileNameUTF16 []byte
expectedLog string
}{ }{
{ {
name: "bad message", name: "bad message",
messageUTF16: encodeUTF16("message")[:5], messageUTF16: encodeUTF16("message")[:5],
fileNameUTF16: encodeUTF16("filename"), fileNameUTF16: encodeUTF16("filename"),
expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
<== ()
`,
}, },
{ {
name: "bad filename", name: "bad filename",
messageUTF16: encodeUTF16("message"), messageUTF16: encodeUTF16("message"),
fileNameUTF16: encodeUTF16("filename")[:5], fileNameUTF16: encodeUTF16("filename")[:5],
expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
<== ()
`,
}, },
} }
@@ -112,28 +126,33 @@ func TestAbort_Error(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime() var out, log bytes.Buffer
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r) // Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
_, err := Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(abortWat) abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err) require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig()) compiled, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
out := &bytes.Buffer{} exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(&out)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out) mod, err := r.InstantiateModule(ctx, compiled, exporter)
mod, err := r.InstantiateModule(testCtx, compiled, exporter)
require.NoError(t, err) require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16) messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2) _, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "", out.String()) // nothing output if strings fail to read. require.Equal(t, "", out.String()) // nothing output if strings fail to read.
require.Equal(t, tc.expectedLog, log.String())
}) })
} }
} }
@@ -153,48 +172,63 @@ var unreachableAfterAbort = `(module
// TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked. // TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked.
func TestAbort_UnreachableAfter(t *testing.T) { func TestAbort_UnreachableAfter(t *testing.T) {
r := wazero.NewRuntime() var log bytes.Buffer
defer r.Close(testCtx)
_, err := r.NewModuleBuilder("env"). // Set context to one that has an experimental listener
// Disable the abort message as we are passing invalid memory offsets. ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
ExportFunctions(NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions()).
Instantiate(testCtx, r) r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
envBuilder := r.NewModuleBuilder("env")
// Disable the abort message as we are passing invalid memory offsets.
NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions(envBuilder)
_, err := envBuilder.Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort) abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort)
require.NoError(t, err) require.NoError(t, err)
_, err = r.InstantiateModuleFromBinary(testCtx, abortWasm) _, err = r.InstantiateModuleFromBinary(ctx, abortWasm)
require.Error(t, err) require.Error(t, err)
require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode()) require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
require.Equal(t, `--> .main()
==> env.~lib/builtins/abort(message=0,fileName=0,lineNumber=0,columnNumber=0)
`, log.String())
} }
func TestSeed(t *testing.T) { func TestSeed(t *testing.T) {
r := wazero.NewRuntime() var log bytes.Buffer
defer r.Close(testCtx)
// Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
seed := []byte{0, 1, 2, 3, 4, 5, 6, 7} seed := []byte{0, 1, 2, 3, 4, 5, 6, 7}
_, err := Instantiate(testCtx, r) _, err := Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat) seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err) require.NoError(t, err)
code, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig()) code, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed))) mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
require.NoError(t, err) require.NoError(t, err)
seedFn := mod.ExportedFunction("seed") seedFn := mod.ExportedFunction("seed")
res, err := seedFn.Call(testCtx) _, err = seedFn.Call(ctx)
require.NoError(t, err) require.NoError(t, err)
// If this test doesn't break, the seed is deterministic. // If this test doesn't break, the seed is deterministic.
require.Equal(t, uint64(506097522914230528), res[0]) require.Equal(t, `==> env.~lib/builtins/seed()
<== (7.949928895127363e-275)
`, log.String())
} }
func TestSeed_error(t *testing.T) { func TestSeed_error(t *testing.T) {
@@ -208,14 +242,14 @@ func TestSeed_error(t *testing.T) {
source: bytes.NewReader([]byte{0, 1}), source: bytes.NewReader([]byte{0, 1}),
expectedErr: `error reading random seed: unexpected EOF (recovered by wazero) expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
wasm stack trace: wasm stack trace:
env.seed() f64`, env.~lib/builtins/seed() f64`,
}, },
{ {
name: "error reading", name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")), source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading random seed: ice cream (recovered by wazero) expectedErr: `error reading random seed: ice cream (recovered by wazero)
wasm stack trace: wasm stack trace:
env.seed() f64`, env.~lib/builtins/seed() f64`,
}, },
} }
@@ -223,65 +257,84 @@ wasm stack trace:
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime() var log bytes.Buffer
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r) // Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
_, err := Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat) seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err) require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig()) compiled, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source) exporter := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
mod, err := r.InstantiateModule(testCtx, compiled, exporter) mod, err := r.InstantiateModule(ctx, compiled, exporter)
require.NoError(t, err) require.NoError(t, err)
_, err = mod.ExportedFunction("seed").Call(testCtx) _, err = mod.ExportedFunction("seed").Call(ctx)
require.EqualError(t, err, tc.expectedErr) require.EqualError(t, err, tc.expectedErr)
require.Equal(t, `==> env.~lib/builtins/seed()
`, log.String())
}) })
} }
} }
func TestTrace(t *testing.T) { func TestTrace(t *testing.T) {
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0} noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
noArgsLog := `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
<== ()
`
tests := []struct { tests := []struct {
name string name string
exporter FunctionExporter exporter FunctionExporter
params []uint64 params []uint64
expected string expected, expectedLog string
}{ }{
{ {
name: "disabled", name: "disabled",
exporter: NewFunctionExporter(), exporter: NewFunctionExporter(),
params: noArgs, params: noArgs,
expected: "", expected: "",
expectedLog: noArgsLog,
}, },
{ {
name: "ToStderr", name: "ToStderr",
exporter: NewFunctionExporter().WithTraceToStderr(), exporter: NewFunctionExporter().WithTraceToStderr(),
params: noArgs, params: noArgs,
expected: "trace: hello\n", expected: "trace: hello\n",
expectedLog: noArgsLog,
}, },
{ {
name: "ToStdout - no args", name: "ToStdout - no args",
exporter: NewFunctionExporter().WithTraceToStdout(), exporter: NewFunctionExporter().WithTraceToStdout(),
params: noArgs, params: noArgs,
expected: "trace: hello\n", expected: "trace: hello\n",
expectedLog: noArgsLog,
}, },
{ {
name: "ToStdout - one arg", name: "ToStdout - one arg",
exporter: NewFunctionExporter().WithTraceToStdout(), exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0}, params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
expected: "trace: hello 1\n", expected: "trace: hello 1\n",
expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
<== ()
`,
}, },
{ {
name: "ToStdout - two args", name: "ToStdout - two args",
exporter: NewFunctionExporter().WithTraceToStdout(), exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0}, params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
expected: "trace: hello 1,2\n", expected: "trace: hello 1,2\n",
expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
<== ()
`,
}, },
{ {
name: "ToStdout - five args", name: "ToStdout - five args",
@@ -296,6 +349,9 @@ func TestTrace(t *testing.T) {
api.EncodeF64(5), api.EncodeF64(5),
}, },
expected: "trace: hello 1,2,3.3,4.4,5\n", expected: "trace: hello 1,2,3.3,4.4,5\n",
expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
<== ()
`,
}, },
} }
@@ -303,40 +359,45 @@ func TestTrace(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime() var out, log bytes.Buffer
defer r.Close(testCtx)
_, err := r.NewModuleBuilder("env"). // Set context to one that has an experimental listener
ExportFunctions(tc.exporter.ExportFunctions()). ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
Instantiate(testCtx, r)
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
envBuilder := r.NewModuleBuilder("env")
tc.exporter.ExportFunctions(envBuilder)
_, err := envBuilder.Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat) traceWasm, err := watzero.Wat2Wasm(traceWat)
require.NoError(t, err) require.NoError(t, err)
code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig()) code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
out := &bytes.Buffer{}
config := wazero.NewModuleConfig() config := wazero.NewModuleConfig()
if tc.name == "ToStderr" { if strings.Contains("ToStderr", tc.name) {
config.WithStderr(out) config = config.WithStderr(&out)
} else { } else {
config.WithStdout(out) config = config.WithStdout(&out)
} }
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStdout(out).WithStderr(out)) mod, err := r.InstantiateModule(ctx, code, config)
require.NoError(t, err) require.NoError(t, err)
message := encodeUTF16("hello") message := encodeUTF16("hello")
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message))) ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(message)))
require.True(t, ok) require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), message) ok = mod.Memory().Write(ctx, uint32(4), message)
require.True(t, ok) require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, tc.params...) _, err = mod.ExportedFunction("trace").Call(ctx, tc.params...)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.expected, out.String()) require.Equal(t, tc.expected, out.String())
require.Equal(t, tc.expectedLog, log.String())
}) })
} }
} }
@@ -354,7 +415,7 @@ func TestTrace_error(t *testing.T) {
out: &bytes.Buffer{}, out: &bytes.Buffer{},
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero) expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
wasm stack trace: wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`, env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
}, },
{ {
name: "error writing", name: "error writing",
@@ -362,7 +423,7 @@ wasm stack trace:
out: &errWriter{err: errors.New("ice cream")}, out: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream (recovered by wazero) expectedErr: `ice cream (recovered by wazero)
wasm stack trace: wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`, env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
}, },
} }
@@ -370,31 +431,38 @@ wasm stack trace:
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime() var log bytes.Buffer
defer r.Close(testCtx)
_, err := r.NewModuleBuilder("env"). // Set context to one that has an experimental listener
ExportFunctions(NewFunctionExporter().WithTraceToStdout().ExportFunctions()). ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
Instantiate(testCtx, r)
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
envBuilder := r.NewModuleBuilder("env")
NewFunctionExporter().WithTraceToStdout().ExportFunctions(envBuilder)
_, err := envBuilder.Instantiate(ctx, r)
require.NoError(t, err) require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat) traceWasm, err := watzero.Wat2Wasm(traceWat)
require.NoError(t, err) require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig()) compiled, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err) require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out) exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
mod, err := r.InstantiateModule(testCtx, compiled, exporter) mod, err := r.InstantiateModule(ctx, compiled, exporter)
require.NoError(t, err) require.NoError(t, err)
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message))) ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(tc.message)))
require.True(t, ok) require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), tc.message) ok = mod.Memory().Write(ctx, uint32(4), tc.message)
require.True(t, ok) require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0) _, err = mod.ExportedFunction("trace").Call(ctx, 4, 0, 0, 0, 0, 0, 0)
require.EqualError(t, err, tc.expectedErr) require.EqualError(t, err, tc.expectedErr)
require.Equal(t, `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
`, log.String())
}) })
} }
} }
@@ -402,45 +470,45 @@ wasm stack trace:
func Test_readAssemblyScriptString(t *testing.T) { func Test_readAssemblyScriptString(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
memory func(api.Memory) memory func(context.Context, api.Memory)
offset int offset int
expected, expectedErr string expected, expectedErr string
}{ }{
{ {
name: "success", name: "success",
memory: func(memory api.Memory) { memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10) memory.WriteUint32Le(ctx, 0, 10)
b := encodeUTF16("hello") b := encodeUTF16("hello")
memory.Write(testCtx, 4, b) memory.Write(ctx, 4, b)
}, },
offset: 4, offset: 4,
expected: "hello", expected: "hello",
}, },
{ {
name: "can't read size", name: "can't read size",
memory: func(memory api.Memory) { memory: func(ctx context.Context, memory api.Memory) {
b := encodeUTF16("hello") b := encodeUTF16("hello")
memory.Write(testCtx, 0, b) memory.Write(ctx, 0, b)
}, },
offset: 0, // will attempt to read size from offset -4 offset: 0, // will attempt to read size from offset -4
expectedErr: "Memory.ReadUint32Le(4294967292) out of range", expectedErr: "Memory.ReadUint32Le(4294967292) out of range",
}, },
{ {
name: "odd size", name: "odd size",
memory: func(memory api.Memory) { memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 9) memory.WriteUint32Le(ctx, 0, 9)
b := encodeUTF16("hello") b := encodeUTF16("hello")
memory.Write(testCtx, 4, b) memory.Write(ctx, 4, b)
}, },
offset: 4, offset: 4,
expectedErr: "read an odd number of bytes for utf-16 string: 9", expectedErr: "read an odd number of bytes for utf-16 string: 9",
}, },
{ {
name: "can't read string", name: "can't read string",
memory: func(memory api.Memory) { memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value memory.WriteUint32Le(ctx, 0, 10_000_000) // set size to too large value
b := encodeUTF16("hello") b := encodeUTF16("hello")
memory.Write(testCtx, 4, b) memory.Write(ctx, 4, b)
}, },
offset: 4, offset: 4,
expectedErr: "Memory.Read(4, 10000000) out of range", expectedErr: "Memory.Read(4, 10000000) out of range",
@@ -459,7 +527,7 @@ func Test_readAssemblyScriptString(t *testing.T) {
Instantiate(testCtx, r) Instantiate(testCtx, r)
require.NoError(t, err) require.NoError(t, err)
tc.memory(mod.Memory()) tc.memory(testCtx, mod.Memory())
s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset)) s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset))
if tc.expectedErr != "" { if tc.expectedErr != "" {
@@ -472,20 +540,20 @@ func Test_readAssemblyScriptString(t *testing.T) {
} }
} }
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) { func writeAbortMessageAndFileName(ctx context.Context, t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
off := 0 off := 0
ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16))) ok := mem.WriteUint32Le(ctx, uint32(off), uint32(len(messageUTF16)))
require.True(t, ok) require.True(t, ok)
off += 4 off += 4
messageOff := off messageOff := off
ok = mem.Write(testCtx, uint32(off), messageUTF16) ok = mem.Write(ctx, uint32(off), messageUTF16)
require.True(t, ok) require.True(t, ok)
off += len(messageUTF16) off += len(messageUTF16)
ok = mem.WriteUint32Le(testCtx, uint32(off), uint32(len(fileNameUTF16))) ok = mem.WriteUint32Le(ctx, uint32(off), uint32(len(fileNameUTF16)))
require.True(t, ok) require.True(t, ok)
off += 4 off += 4
filenameOff := off filenameOff := off
ok = mem.Write(testCtx, uint32(off), fileNameUTF16) ok = mem.Write(ctx, uint32(off), fileNameUTF16)
require.True(t, ok) require.True(t, ok)
return messageOff, filenameOff return messageOff, filenameOff
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
) )
// ModuleBuilder is a way to define a WebAssembly 1.0 (20191205) in Go. // ModuleBuilder is a way to define a WebAssembly Module in Go.
// //
// Ex. Below defines and instantiates a module named "env" with one function: // Ex. Below defines and instantiates a module named "env" with one function:
// //
@@ -25,7 +25,8 @@ import (
// ExportFunction("hello", hello). // ExportFunction("hello", hello).
// Instantiate(ctx, r) // Instantiate(ctx, r)
// //
// If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex. // If the same module may be instantiated multiple times, it is more efficient
// to separate steps. Ex.
// //
// compiled, _ := r.NewModuleBuilder("env"). // compiled, _ := r.NewModuleBuilder("env").
// ExportFunction("get_random_string", getRandomString). // ExportFunction("get_random_string", getRandomString).
@@ -37,9 +38,12 @@ import (
// //
// Notes // Notes
// //
// * ModuleBuilder is mutable. WithXXX functions return the same instance for chaining. // * ModuleBuilder is mutable: each method returns the same instance for
// * WithXXX methods do not return errors, to allow chaining. Any validation errors are deferred until Build. // chaining.
// * Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build. // * methods do not return errors, to allow chaining. Any validation errors
// are deferred until Compile.
// * Insertion order is not retained. Anything defined by this builder is
// sorted lexicographically on Compile.
type ModuleBuilder interface { type ModuleBuilder interface {
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs. // Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
@@ -48,29 +52,45 @@ type ModuleBuilder interface {
// //
// Parameters // Parameters
// //
// * name - the name to export. Ex "random_get" // * exportName - The name to export. Ex "random_get"
// * goFunc - the `func` to export. // * goFunc - The `func` to export.
// * names - If present, the first is the api.FunctionDefinition name.
// If any follow, they must match the count of goFunc's parameters.
// //
// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (20191205) value // Ex.
// types. This means uint32, uint64, float32 or float64. Up to one result can be returned. // // Just export the function, and use "abort" in stack traces.
// builder.ExportFunction("abort", env.abort)
// // Ensure "~lib/builtins/abort" is used in stack traces.
// builder.ExportFunction("abort", env.abort, "~lib/builtins/abort")
// // Allow function listeners to know the param names for logging, etc.
// builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
// "message", "fileName", "lineNumber", "columnNumber")
//
// Valid Signature
//
// Noting a context exception described later, all parameters or result
// types must match WebAssembly 1.0 (20191205) value types. This means
// uint32, uint64, float32 or float64. Up to one result can be returned.
// //
// Ex. This is a valid host function: // Ex. This is a valid host function:
// //
// addInts := func(x uint32, uint32) uint32 { // addInts := func(x, y uint32) uint32 {
// return x + y // return x + y
// } // }
// //
// Host functions may also have an initial parameter (param[0]) of type context.Context or api.Module. // Host functions may also have an initial parameter (param[0]) of type
// context.Context or api.Module.
// //
// Ex. This uses a Go Context: // Ex. This uses a Go Context:
// //
// addInts := func(ctx context.Context, x uint32, uint32) uint32 { // addInts := func(ctx context.Context, x, y uint32) uint32 {
// // add a little extra if we put some in the context! // // add a little extra if we put some in the context!
// return x + y + ctx.Value(extraKey).(uint32) // return x + y + ctx.Value(extraKey).(uint32)
// } // }
// //
// Ex. This uses an api.Module to reads the parameters from memory. This is important because there are only numeric // Ex. This uses an api.Module to reads the parameters from memory. This is
// types in Wasm. The only way to share other data is via writing memory and sharing offsets. // important because there are only numeric types in Wasm. The only way to
// share other data is via writing memory and sharing offsets.
// //
// addInts := func(ctx context.Context, m api.Module, offset uint32) uint32 { // addInts := func(ctx context.Context, m api.Module, offset uint32) uint32 {
// x, _ := m.Memory().ReadUint32Le(ctx, offset) // x, _ := m.Memory().ReadUint32Le(ctx, offset)
@@ -78,16 +98,17 @@ type ModuleBuilder interface {
// return x + y // return x + y
// } // }
// //
// If both parameters exist, they must be in order at positions zero and one. // If both parameters exist, they must be in order at positions 0 and 1.
// //
// Ex. This uses propagates context properly when calling other functions exported in the api.Module: // Ex. This uses propagates context properly when calling other functions
// exported in the api.Module:
// callRead := func(ctx context.Context, m api.Module, offset, byteCount uint32) uint32 { // callRead := func(ctx context.Context, m api.Module, offset, byteCount uint32) uint32 {
// fn = m.ExportedFunction("__read") // fn = m.ExportedFunction("__read")
// results, err := fn(ctx, offset, byteCount) // results, err := fn(ctx, offset, byteCount)
// --snip-- // --snip--
// //
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2
ExportFunction(name string, goFunc interface{}) ModuleBuilder ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder
// ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map. // ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map.
ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder
@@ -200,6 +221,7 @@ type moduleBuilder struct {
r *runtime r *runtime
moduleName string moduleName string
nameToGoFunc map[string]interface{} nameToGoFunc map[string]interface{}
funcToNames map[string][]string
nameToMemory map[string]*wasm.Memory nameToMemory map[string]*wasm.Memory
nameToGlobal map[string]*wasm.Global nameToGlobal map[string]*wasm.Global
} }
@@ -210,14 +232,18 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
r: r, r: r,
moduleName: moduleName, moduleName: moduleName,
nameToGoFunc: map[string]interface{}{}, nameToGoFunc: map[string]interface{}{},
funcToNames: map[string][]string{},
nameToMemory: map[string]*wasm.Memory{}, nameToMemory: map[string]*wasm.Memory{},
nameToGlobal: map[string]*wasm.Global{}, nameToGlobal: map[string]*wasm.Global{},
} }
} }
// ExportFunction implements ModuleBuilder.ExportFunction // ExportFunction implements ModuleBuilder.ExportFunction
func (b *moduleBuilder) ExportFunction(name string, goFunc interface{}) ModuleBuilder { func (b *moduleBuilder) ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder {
b.nameToGoFunc[name] = goFunc b.nameToGoFunc[exportName] = goFunc
if len(names) > 0 {
b.funcToNames[exportName] = names
}
return b return b
} }
@@ -295,16 +321,21 @@ func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (Com
} }
} }
module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures) module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
if c.listeners, err = buildListeners(ctx, b.r, module); err != nil {
return nil, err
}
if err = b.r.store.Engine.CompileModule(ctx, module); err != nil { if err = b.r.store.Engine.CompileModule(ctx, module); err != nil {
return nil, err return nil, err
} }
return &compiledModule{module: module, compiledEngine: b.r.store.Engine}, nil return c, nil
} }
// Instantiate implements ModuleBuilder.Instantiate // Instantiate implements ModuleBuilder.Instantiate

View File

@@ -63,6 +63,26 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
}, },
}, },
}, },
{
name: "ExportFunction with names",
input: func(r Runtime) ModuleBuilder {
return r.NewModuleBuilder("").ExportFunction("1", uint32_uint32, "get", "x")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
},
FunctionSection: []wasm.Index{0},
HostFunctionSection: []*reflect.Value{&fnUint32_uint32},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
LocalNames: []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
},
},
},
{ {
name: "ExportFunction overwrites existing", name: "ExportFunction overwrites existing",
input: func(r Runtime) ModuleBuilder { input: func(r Runtime) ModuleBuilder {

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/compiler"
"github.com/tetratelabs/wazero/internal/engine/interpreter" "github.com/tetratelabs/wazero/internal/engine/interpreter"
"github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/platform"
@@ -148,6 +149,7 @@ func NewRuntimeConfig() RuntimeConfig {
type runtimeConfig struct { type runtimeConfig struct {
enabledFeatures wasm.Features enabledFeatures wasm.Features
isInterpreter bool
newEngine func(wasm.Features) wasm.Engine newEngine func(wasm.Features) wasm.Engine
} }
@@ -179,6 +181,7 @@ func NewRuntimeConfigCompiler() RuntimeConfig {
// NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly. // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly.
func NewRuntimeConfigInterpreter() RuntimeConfig { func NewRuntimeConfigInterpreter() RuntimeConfig {
ret := engineLessConfig.clone() ret := engineLessConfig.clone()
ret.isInterpreter = true
ret.newEngine = interpreter.NewEngine ret.newEngine = interpreter.NewEngine
return ret return ret
} }
@@ -280,7 +283,8 @@ type compiledModule struct {
module *wasm.Module module *wasm.Module
// compiledEngine holds an engine on which `module` is compiled. // compiledEngine holds an engine on which `module` is compiled.
compiledEngine wasm.Engine compiledEngine wasm.Engine
// listeners are present if the code was compiled with a listener
listeners []experimental.FunctionListener
// closeWithModule prevents leaking compiled code when a module is compiled implicitly. // closeWithModule prevents leaking compiled code when a module is compiled implicitly.
closeWithModule bool closeWithModule bool
} }

View File

@@ -1,3 +1,8 @@
// Package experimental_test includes examples for experimental features. When these complete, they'll end up as real // Package experimental_test includes examples for experimental features. When these complete, they'll end up as real
// examples in the /examples directory. // examples in the /examples directory.
package experimental_test package experimental_test
import "context"
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")

View File

@@ -18,13 +18,7 @@ type FunctionListenerFactoryKey struct{}
type FunctionListenerFactory interface { type FunctionListenerFactory interface {
// NewListener returns a FunctionListener for a defined function. If nil is // NewListener returns a FunctionListener for a defined function. If nil is
// returned, no listener will be notified. // returned, no listener will be notified.
// NewListener(api.FunctionDefinition) FunctionListener
// Params
//
// * moduleName - the name this listener was instantiated with, and might
// vary for the same binary.
// * definition - metadata about this function parsed from its Wasm binary.
NewListener(moduleName string, definition api.FunctionDefinition) FunctionListener
} }
// FunctionListener can be registered for any function via // FunctionListener can be registered for any function via

View File

@@ -1,121 +0,0 @@
package experimental_test
import (
"context"
_ "embed"
"fmt"
"io"
"log"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)
// listenerWasm was generated by the following:
// cd testdata; wat2wasm --debug-names listener.wat
//go:embed testdata/listener.wasm
var listenerWasm []byte
// compile-time check to ensure loggerFactory implements experimental.FunctionListenerFactory.
var _ experimental.FunctionListenerFactory = &loggerFactory{}
// loggerFactory implements experimental.FunctionListenerFactory to log all function calls to the console.
type loggerFactory struct{}
// NewListener implements the same method as documented on experimental.FunctionListener.
func (f *loggerFactory) NewListener(moduleName string, fnd api.FunctionDefinition) experimental.FunctionListener {
return &logger{funcName: []byte(moduleName + "." + funcName(fnd))}
}
// nestLevelKey holds state between logger.Before and logger.After to ensure call depth is reflected.
type nestLevelKey struct{}
// logger implements experimental.FunctionListener to log entrance and exit of each function call.
type logger struct{ funcName []byte }
// Before logs to stdout the module and function name, prefixed with '>>' and indented based on the call nesting level.
func (l *logger) Before(ctx context.Context, _ []uint64) context.Context {
nestLevel, _ := ctx.Value(nestLevelKey{}).(int)
l.writeIndented(os.Stdout, true, nestLevel+1)
// Increase the next nesting level.
return context.WithValue(ctx, nestLevelKey{}, nestLevel+1)
}
// After logs to stdout the module and function name, prefixed with '<<' and indented based on the call nesting level.
func (l *logger) After(ctx context.Context, _ error, _ []uint64) {
// Note: We use the nest level directly even though it is the "next" nesting level.
// This works because our indent of zero nesting is one tab.
l.writeIndented(os.Stdout, false, ctx.Value(nestLevelKey{}).(int))
}
// funcName returns the name in priority order: first export name, module-defined name, numeric index.
func funcName(fnd api.FunctionDefinition) string {
if len(fnd.ExportNames()) > 0 {
return fnd.ExportNames()[0]
}
if fnd.Name() != "" {
return fnd.Name()
}
return fmt.Sprintf("[%d]", fnd.Index())
}
// This is a very basic integration of listener. The main goal is to show how it is configured.
func Example_listener() {
// Set context to one that has an experimental listener
ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, &loggerFactory{})
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx) // This closes everything this Runtime created.
if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
log.Panicln(err)
}
// Compile the WebAssembly module using the default configuration.
code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
if err != nil {
log.Panicln(err)
}
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout))
if err != nil {
log.Panicln(err)
}
_, err = mod.ExportedFunction("rand").Call(ctx, 4)
if err != nil {
log.Panicln(err)
}
// We should see the same function called twice: directly and indirectly.
// Output:
//>> listener.rand
//>> wasi_snapshot_preview1.random_get
//<< wasi_snapshot_preview1.random_get
//>> wasi_snapshot_preview1.random_get
//<< wasi_snapshot_preview1.random_get
//<< listener.rand
}
// writeIndented writes an indented message like this: ">>\t\t\t$indentLevel$funcName\n"
func (l *logger) writeIndented(writer io.Writer, before bool, indentLevel int) {
var message = make([]byte, 0, 2+indentLevel+len(l.funcName)+1)
if before {
message = append(message, '>', '>')
} else { // after
message = append(message, '<', '<')
}
for i := 0; i < indentLevel; i++ {
message = append(message, '\t')
}
message = append(message, l.funcName...)
message = append(message, '\n')
_, _ = writer.Write(message)
}

View File

@@ -0,0 +1,73 @@
package experimental_test
import (
"context"
_ "embed"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
. "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasm/binary"
)
// compile-time check to ensure recorder implements FunctionListenerFactory
var _ FunctionListenerFactory = &recorder{}
type recorder struct {
m map[string]struct{}
}
func (r *recorder) NewListener(definition api.FunctionDefinition) FunctionListener {
r.m[definition.Name()] = struct{}{}
return nil
}
func TestFunctionListenerFactory(t *testing.T) {
// Set context to one that has an experimental listener
factory := &recorder{map[string]struct{}{}}
ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, factory)
// Define a module with two functions
bin := binary.EncodeModule(&wasm.Module{
TypeSection: []*wasm.FunctionType{{}},
ImportSection: []*wasm.Import{{}},
FunctionSection: []wasm.Index{0, 0},
CodeSection: []*wasm.Code{
{Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}},
},
NameSection: &wasm.NameSection{
ModuleName: "test",
FunctionNames: wasm.NameMap{
{Index: 0, Name: "import"}, // should skip
{Index: 1, Name: "fn1"},
{Index: 2, Name: "fn2"},
},
},
})
if platform.CompilerSupported() {
t.Run("fails on compile if compiler", func(t *testing.T) {
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigCompiler())
defer r.Close(testCtx) // This closes everything this Runtime created.
_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
require.EqualError(t, err,
"context includes a FunctionListenerFactoryKey, which is only supported in the interpreter")
})
}
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx) // This closes everything this Runtime created.
_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
require.NoError(t, err)
// Ensure each function was converted to a listener eagerly
require.Equal(t, map[string]struct{}{
"fn1": {},
"fn2": {},
}, factory.m)
}

View File

@@ -0,0 +1,155 @@
package experimental
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
)
// NewLoggingListenerFactory implements FunctionListenerFactory to log all
// function calls to the writer.
func NewLoggingListenerFactory(writer io.Writer) FunctionListenerFactory {
return &loggingListenerFactory{writer}
}
type loggingListenerFactory struct{ writer io.Writer }
// NewListener implements the same method as documented on
// experimental.FunctionListener.
func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) FunctionListener {
return &loggingListener{writer: f.writer, fnd: fnd, isWasi: fnd.ModuleName() == "wasi_snapshot_preview1"}
}
// nestLevelKey holds state between logger.Before and loggingListener.After to ensure
// call depth is reflected.
type nestLevelKey struct{}
// loggingListener implements experimental.FunctionListener to log entrance and exit
// of each function call.
type loggingListener struct {
writer io.Writer
fnd api.FunctionDefinition
isWasi bool
}
// Before logs to stdout the module and function name, prefixed with '-->' and
// indented based on the call nesting level.
func (l *loggingListener) Before(ctx context.Context, vals []uint64) context.Context {
nestLevel, _ := ctx.Value(nestLevelKey{}).(int)
l.writeIndented(true, nil, vals, nestLevel+1)
// Increase the next nesting level.
return context.WithValue(ctx, nestLevelKey{}, nestLevel+1)
}
// After logs to stdout the module and function name, prefixed with '<--' and
// indented based on the call nesting level.
func (l *loggingListener) After(ctx context.Context, err error, vals []uint64) {
// Note: We use the nest level directly even though it is the "next" nesting level.
// This works because our indent of zero nesting is one tab.
l.writeIndented(false, err, vals, ctx.Value(nestLevelKey{}).(int))
}
// writeIndented writes an indented message like this: "-->\t\t\t$indentLevel$funcName\n"
func (l *loggingListener) writeIndented(before bool, err error, vals []uint64, indentLevel int) {
var message strings.Builder
for i := 1; i < indentLevel; i++ {
message.WriteByte('\t')
}
if before {
if l.fnd.IsHostFunction() {
message.WriteString("==> ")
} else {
message.WriteString("--> ")
}
l.writeFuncEnter(&message, vals)
} else { // after
if l.fnd.IsHostFunction() {
message.WriteString("<== ")
} else {
message.WriteString("<-- ")
}
l.writeFuncExit(&message, err, vals)
}
message.WriteByte('\n')
_, _ = l.writer.Write([]byte(message.String()))
}
func (l *loggingListener) writeFuncEnter(message *strings.Builder, vals []uint64) {
valLen := len(vals)
message.WriteString(l.fnd.DebugName())
message.WriteByte('(')
switch valLen {
case 0:
default:
i := l.writeParam(message, 0, vals)
for i < valLen {
message.WriteByte(',')
i = l.writeParam(message, i, vals)
}
}
message.WriteByte(')')
}
func (l *loggingListener) writeFuncExit(message *strings.Builder, err error, vals []uint64) {
if err != nil {
message.WriteString("error: ")
message.WriteString(err.Error())
return
} else if l.isWasi {
message.WriteString(wasi_snapshot_preview1.ErrnoName(uint32(vals[0])))
return
}
valLen := len(vals)
message.WriteByte('(')
switch valLen {
case 0:
default:
i := l.writeResult(message, 0, vals)
for i < valLen {
message.WriteByte(',')
i = l.writeResult(message, i, vals)
}
}
message.WriteByte(')')
}
func (l *loggingListener) writeResult(message *strings.Builder, i int, vals []uint64) int {
return l.writeVal(message, l.fnd.ResultTypes()[i], i, vals)
}
func (l *loggingListener) writeParam(message *strings.Builder, i int, vals []uint64) int {
if len(l.fnd.ParamNames()) > 0 {
message.WriteString(l.fnd.ParamNames()[i])
message.WriteByte('=')
}
return l.writeVal(message, l.fnd.ParamTypes()[i], i, vals)
}
func (l *loggingListener) writeVal(message *strings.Builder, t api.ValueType, i int, vals []uint64) int {
v := vals[i]
i++
switch t {
case api.ValueTypeI32:
message.WriteString(strconv.FormatUint(uint64(uint32(v)), 10))
case api.ValueTypeI64:
message.WriteString(strconv.FormatUint(v, 10))
case api.ValueTypeF32:
message.WriteString(strconv.FormatFloat(float64(api.DecodeF32(v)), 'g', -1, 32))
case api.ValueTypeF64:
message.WriteString(strconv.FormatFloat(api.DecodeF64(v), 'g', -1, 64))
case 0x7b: // wasm.ValueTypeV128
message.WriteString(fmt.Sprintf("%016x%016x", v, vals[i])) // fixed-width hex
i++
case api.ValueTypeExternref, 0x70: // wasm.ValueTypeFuncref
message.WriteString(fmt.Sprintf("%016x", v)) // fixed-width hex
}
return i
}

View File

@@ -0,0 +1,56 @@
package experimental_test
import (
"context"
_ "embed"
"log"
"os"
"github.com/tetratelabs/wazero"
. "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)
// listenerWasm was generated by the following:
// cd testdata; wat2wasm --debug-names listener.wat
//go:embed testdata/listener.wasm
var listenerWasm []byte
// This is a very basic integration of listener. The main goal is to show how it is configured.
func Example_newLoggingListenerFactory() {
// Set context to one that has an experimental listener
ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, NewLoggingListenerFactory(os.Stdout))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx) // This closes everything this Runtime created.
if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
log.Panicln(err)
}
// Compile the WebAssembly module using the default configuration.
code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
if err != nil {
log.Panicln(err)
}
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout))
if err != nil {
log.Panicln(err)
}
_, err = mod.ExportedFunction("rand").Call(ctx, 4)
if err != nil {
log.Panicln(err)
}
// We should see the same function called twice: directly and indirectly.
// Output:
//--> listener.rand(len=4)
// ==> wasi_snapshot_preview1.random_get(buf=4,buf_len=4)
// <== ESUCCESS
// ==> wasi_snapshot_preview1.random_get(buf=8,buf_len=4)
// <== ESUCCESS
//<-- ()
}

View File

@@ -0,0 +1,336 @@
package experimental_test
import (
"bytes"
"io"
"math"
"reflect"
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)
func Test_loggingListener(t *testing.T) {
wasiFuncName := "random_get"
wasiFuncType := &wasm.FunctionType{
Params: []api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
Results: []api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
}
wasiParamNames := []string{"buf", "buf_len"}
wasiParams := []uint64{0, 8}
tests := []struct {
name string
moduleName, funcName string
functype *wasm.FunctionType
isHostFunc bool
paramNames []string
params, results []uint64
err error
expected string
}{
{
name: "v_v",
functype: &wasm.FunctionType{},
expected: `--> test.fn()
<-- ()
`,
},
{
name: "error",
functype: &wasm.FunctionType{},
err: io.EOF,
expected: `--> test.fn()
<-- error: EOF
`,
},
{
name: "host",
functype: &wasm.FunctionType{},
isHostFunc: true,
expected: `==> test.fn()
<== ()
`,
},
{
name: "wasi",
functype: wasiFuncType,
moduleName: wasi_snapshot_preview1.ModuleName,
funcName: wasiFuncName,
paramNames: wasiParamNames,
isHostFunc: true,
params: wasiParams,
results: []uint64{uint64(wasi_snapshot_preview1.ErrnoSuccess)},
expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
<== ESUCCESS
`,
},
{
name: "wasi errno",
functype: wasiFuncType,
moduleName: wasi_snapshot_preview1.ModuleName,
funcName: wasiFuncName,
paramNames: wasiParamNames,
isHostFunc: true,
params: wasiParams,
results: []uint64{uint64(wasi_snapshot_preview1.ErrnoFault)},
expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
<== EFAULT
`,
},
{
name: "wasi error",
functype: wasiFuncType,
moduleName: wasi_snapshot_preview1.ModuleName,
funcName: wasiFuncName,
paramNames: wasiParamNames,
isHostFunc: true,
params: wasiParams,
err: io.EOF, // not possible as we coerce errors to numbers, but test anyway!
expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
<== error: EOF
`,
},
{
name: "i32",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI32}},
params: []uint64{math.MaxUint32},
expected: `--> test.fn(4294967295)
<-- ()
`,
},
{
name: "i32 named",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI32}},
params: []uint64{math.MaxUint32},
paramNames: []string{"x"},
expected: `--> test.fn(x=4294967295)
<-- ()
`,
},
{
name: "i64",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI64}},
params: []uint64{math.MaxUint64},
expected: `--> test.fn(18446744073709551615)
<-- ()
`,
},
{
name: "i64 named",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI64}},
params: []uint64{math.MaxUint64},
paramNames: []string{"x"},
expected: `--> test.fn(x=18446744073709551615)
<-- ()
`,
},
{
name: "f32",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF32}},
params: []uint64{api.EncodeF32(math.MaxFloat32)},
expected: `--> test.fn(3.4028235e+38)
<-- ()
`,
},
{
name: "f32 named",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF32}},
params: []uint64{api.EncodeF32(math.MaxFloat32)},
paramNames: []string{"x"},
expected: `--> test.fn(x=3.4028235e+38)
<-- ()
`,
},
{
name: "f64",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF64}},
params: []uint64{api.EncodeF64(math.MaxFloat64)},
expected: `--> test.fn(1.7976931348623157e+308)
<-- ()
`,
},
{
name: "f64 named",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF64}},
params: []uint64{api.EncodeF64(math.MaxFloat64)},
paramNames: []string{"x"},
expected: `--> test.fn(x=1.7976931348623157e+308)
<-- ()
`,
},
{
name: "externref",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeExternref}},
params: []uint64{0},
expected: `--> test.fn(0000000000000000)
<-- ()
`,
},
{
name: "externref named",
functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeExternref}},
params: []uint64{0},
paramNames: []string{"x"},
expected: `--> test.fn(x=0000000000000000)
<-- ()
`,
},
{
name: "v128",
functype: &wasm.FunctionType{Params: []api.ValueType{0x7b}},
params: []uint64{0, 1},
expected: `--> test.fn(00000000000000000000000000000001)
<-- ()
`,
},
{
name: "v128 named",
functype: &wasm.FunctionType{Params: []api.ValueType{0x7b}},
params: []uint64{0, 1},
paramNames: []string{"x"},
expected: `--> test.fn(x=00000000000000000000000000000001)
<-- ()
`,
},
{
name: "funcref",
functype: &wasm.FunctionType{Params: []api.ValueType{0x70}},
params: []uint64{0},
expected: `--> test.fn(0000000000000000)
<-- ()
`,
},
{
name: "funcref named",
functype: &wasm.FunctionType{Params: []api.ValueType{0x70}},
params: []uint64{0},
paramNames: []string{"x"},
expected: `--> test.fn(x=0000000000000000)
<-- ()
`,
},
{
name: "no params, one result",
functype: &wasm.FunctionType{Results: []api.ValueType{api.ValueTypeI32}},
results: []uint64{math.MaxUint32},
expected: `--> test.fn()
<-- (4294967295)
`,
},
{
name: "one param, one result",
functype: &wasm.FunctionType{
Params: []api.ValueType{api.ValueTypeI32},
Results: []api.ValueType{api.ValueTypeF32},
},
params: []uint64{math.MaxUint32},
results: []uint64{api.EncodeF32(math.MaxFloat32)},
expected: `--> test.fn(4294967295)
<-- (3.4028235e+38)
`,
},
{
name: "two params, two results",
functype: &wasm.FunctionType{
Params: []api.ValueType{api.ValueTypeI32, api.ValueTypeI64},
Results: []api.ValueType{api.ValueTypeF32, api.ValueTypeF64},
},
params: []uint64{math.MaxUint32, math.MaxUint64},
results: []uint64{api.EncodeF32(math.MaxFloat32), api.EncodeF64(math.MaxFloat64)},
expected: `--> test.fn(4294967295,18446744073709551615)
<-- (3.4028235e+38,1.7976931348623157e+308)
`,
},
{
name: "two params, two results named",
functype: &wasm.FunctionType{
Params: []api.ValueType{api.ValueTypeI32, api.ValueTypeI64},
Results: []api.ValueType{api.ValueTypeF32, api.ValueTypeF64},
},
params: []uint64{math.MaxUint32, math.MaxUint64},
paramNames: []string{"x", "y"},
results: []uint64{api.EncodeF32(math.MaxFloat32), api.EncodeF64(math.MaxFloat64)},
expected: `--> test.fn(x=4294967295,y=18446744073709551615)
<-- (3.4028235e+38,1.7976931348623157e+308)
`,
},
}
out := bytes.NewBuffer(nil)
lf := experimental.NewLoggingListenerFactory(out)
fnV := reflect.ValueOf(func() {})
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
if tc.moduleName == "" {
tc.moduleName = "test"
}
if tc.funcName == "" {
tc.funcName = "fn"
}
m := &wasm.Module{
TypeSection: []*wasm.FunctionType{tc.functype},
FunctionSection: []wasm.Index{0},
NameSection: &wasm.NameSection{
ModuleName: tc.moduleName,
FunctionNames: wasm.NameMap{{Name: tc.funcName}},
LocalNames: wasm.IndirectNameMap{{NameMap: toNameMap(tc.paramNames)}},
},
}
if tc.isHostFunc {
m.HostFunctionSection = []*reflect.Value{&fnV}
}
m.BuildFunctionDefinitions()
l := lf.NewListener(m.FunctionDefinitionSection[0])
out.Reset()
ctx := l.Before(testCtx, tc.params)
l.After(ctx, tc.err, tc.results)
require.Equal(t, tc.expected, out.String())
})
}
}
func toNameMap(names []string) wasm.NameMap {
if len(names) == 0 {
return nil
}
var ret wasm.NameMap
for i, n := range names {
ret = append(ret, &wasm.NameAssoc{Index: wasm.Index(i), Name: n})
}
return ret
}
func Test_loggingListener_indentation(t *testing.T) {
out := bytes.NewBuffer(nil)
lf := experimental.NewLoggingListenerFactory(out)
m := &wasm.Module{
TypeSection: []*wasm.FunctionType{{}},
FunctionSection: []wasm.Index{0, 0},
NameSection: &wasm.NameSection{
ModuleName: "test",
FunctionNames: wasm.NameMap{{Index: 0, Name: "fn1"}, {Index: 1, Name: "fn2"}},
},
}
m.BuildFunctionDefinitions()
l1 := lf.NewListener(m.FunctionDefinitionSection[0])
l2 := lf.NewListener(m.FunctionDefinitionSection[1])
ctx := l1.Before(testCtx, []uint64{})
ctx1 := l2.Before(ctx, []uint64{})
l2.After(ctx1, nil, []uint64{})
l1.After(ctx, nil, []uint64{})
require.Equal(t, `--> test.fn1()
--> test.fn2()
<-- ()
<-- ()
`, out.String())
}

View File

@@ -197,7 +197,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
// Trigger relocation of goroutine stack because at this point we have the majority of // Trigger relocation of goroutine stack because at this point we have the majority of
// goroutine stack unused after recursive call. // goroutine stack unused after recursive call.
runtime.GC() runtime.GC()
}}, map[string]*wasm.Memory{}, map[string]*wasm.Global{}, enabledFeatures) }}, nil, map[string]*wasm.Memory{}, map[string]*wasm.Global{}, enabledFeatures)
require.NoError(t, err) require.NoError(t, err)
err = s.Engine.CompileModule(testCtx, hm) err = s.Engine.CompileModule(testCtx, hm)

View File

@@ -0,0 +1,95 @@
// Package wasi_snapshot_preview1 is an internal helper to remove package
// cycles re-using errno
package wasi_snapshot_preview1
import (
"fmt"
)
// ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG"
func ErrnoName(errno uint32) string {
if int(errno) < len(errnoToString) {
return errnoToString[errno]
}
return fmt.Sprintf("errno(%d)", errno)
}
var errnoToString = [...]string{
"ESUCCESS",
"E2BIG",
"EACCES",
"EADDRINUSE",
"EADDRNOTAVAIL",
"EAFNOSUPPORT",
"EAGAIN",
"EALREADY",
"EBADF",
"EBADMSG",
"EBUSY",
"ECANCELED",
"ECHILD",
"ECONNABORTED",
"ECONNREFUSED",
"ECONNRESET",
"EDEADLK",
"EDESTADDRREQ",
"EDOM",
"EDQUOT",
"EEXIST",
"EFAULT",
"EFBIG",
"EHOSTUNREACH",
"EIDRM",
"EILSEQ",
"EINPROGRESS",
"EINTR",
"EINVAL",
"EIO",
"EISCONN",
"EISDIR",
"ELOOP",
"EMFILE",
"EMLINK",
"EMSGSIZE",
"EMULTIHOP",
"ENAMETOOLONG",
"ENETDOWN",
"ENETRESET",
"ENETUNREACH",
"ENFILE",
"ENOBUFS",
"ENODEV",
"ENOENT",
"ENOEXEC",
"ENOLCK",
"ENOLINK",
"ENOMEM",
"ENOMSG",
"ENOPROTOOPT",
"ENOSPC",
"ENOSYS",
"ENOTCONN",
"ENOTDIR",
"ENOTEMPTY",
"ENOTRECOVERABLE",
"ENOTSOCK",
"ENOTSUP",
"ENOTTY",
"ENXIO",
"EOVERFLOW",
"EOWNERDEAD",
"EPERM",
"EPIPE",
"EPROTO",
"EPROTONOSUPPORT",
"EPROTOTYPE",
"ERANGE",
"EROFS",
"ESPIPE",
"ESRCH",
"ESTALE",
"ETIMEDOUT",
"ETXTBSY",
"EXDEV",
"ENOTCAPABLE",
}

View File

@@ -61,10 +61,13 @@ func (m *Module) BuildFunctionDefinitions() {
importFuncIdx++ importFuncIdx++
} }
// At the moment, a module can either be solely wasm or host functions.
isHostFunction := m.HostFunctionSection != nil
for codeIndex, typeIndex := range m.FunctionSection { for codeIndex, typeIndex := range m.FunctionSection {
m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{
index: Index(codeIndex) + importCount, index: Index(codeIndex) + importCount,
funcType: m.TypeSection[typeIndex], funcType: m.TypeSection[typeIndex],
isHostFunction: isHostFunction,
}) })
} }
@@ -99,14 +102,15 @@ func (m *Module) BuildFunctionDefinitions() {
// FunctionDefinition implements api.FunctionDefinition // FunctionDefinition implements api.FunctionDefinition
type FunctionDefinition struct { type FunctionDefinition struct {
moduleName string moduleName string
index Index index Index
name string name string
debugName string debugName string
funcType *FunctionType isHostFunction bool
importDesc *[2]string funcType *FunctionType
exportNames []string importDesc *[2]string
paramNames []string exportNames []string
paramNames []string
} }
// ModuleName implements the same method as documented on api.FunctionDefinition. // ModuleName implements the same method as documented on api.FunctionDefinition.
@@ -142,6 +146,11 @@ func (f *FunctionDefinition) ExportNames() []string {
return f.exportNames return f.exportNames
} }
// IsHostFunction implements the same method as documented on api.FunctionDefinition.
func (f *FunctionDefinition) IsHostFunction() bool {
return f.isHostFunction
}
// ParamNames implements the same method as documented on api.FunctionDefinition. // ParamNames implements the same method as documented on api.FunctionDefinition.
func (f *FunctionDefinition) ParamNames() []string { func (f *FunctionDefinition) ParamNames() []string {
return f.paramNames return f.paramNames

View File

@@ -1,6 +1,7 @@
package wasm package wasm
import ( import (
"reflect"
"testing" "testing"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
@@ -9,6 +10,7 @@ import (
func TestModule_BuildFunctionDefinitions(t *testing.T) { func TestModule_BuildFunctionDefinitions(t *testing.T) {
nopCode := &Code{nil, []byte{OpcodeEnd}} nopCode := &Code{nil, []byte{OpcodeEnd}}
fnV := reflect.ValueOf(func() {})
tests := []struct { tests := []struct {
name string name string
m *Module m *Module
@@ -26,6 +28,22 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
GlobalSection: []*Global{{}}, GlobalSection: []*Global{{}},
}, },
}, },
{
name: "host func",
m: &Module{
TypeSection: []*FunctionType{v_v},
FunctionSection: []Index{0},
HostFunctionSection: []*reflect.Value{&fnV},
},
expected: []*FunctionDefinition{
{
index: 0,
debugName: ".$0",
isHostFunction: true,
funcType: v_v,
},
},
},
{ {
name: "without imports", name: "without imports",
m: &Module{ m: &Module{

View File

@@ -13,6 +13,7 @@ import (
func NewHostModule( func NewHostModule(
moduleName string, moduleName string,
nameToGoFunc map[string]interface{}, nameToGoFunc map[string]interface{},
funcToNames map[string][]string,
nameToMemory map[string]*Memory, nameToMemory map[string]*Memory,
nameToGlobal map[string]*Global, nameToGlobal map[string]*Global,
enabledFeatures Features, enabledFeatures Features,
@@ -33,11 +34,12 @@ func NewHostModule(
// Check name collision as exports cannot collide on names, regardless of type. // Check name collision as exports cannot collide on names, regardless of type.
for name := range nameToGoFunc { for name := range nameToGoFunc {
// manually generate the error message as we don't have debug names yet.
if _, ok := nameToMemory[name]; ok { if _, ok := nameToMemory[name]; ok {
return nil, fmt.Errorf("func[%s] exports the same name as a memory", name) return nil, fmt.Errorf("func[%s.%s] exports the same name as a memory", moduleName, name)
} }
if _, ok := nameToGlobal[name]; ok { if _, ok := nameToGlobal[name]; ok {
return nil, fmt.Errorf("func[%s] exports the same name as a global", name) return nil, fmt.Errorf("func[%s.%s] exports the same name as a global", moduleName, name)
} }
} }
for name := range nameToMemory { for name := range nameToMemory {
@@ -47,7 +49,7 @@ func NewHostModule(
} }
if funcCount > 0 { if funcCount > 0 {
if err = addFuncs(m, nameToGoFunc, enabledFeatures); err != nil { if err = addFuncs(m, nameToGoFunc, funcToNames, enabledFeatures); err != nil {
return return
} }
} }
@@ -76,7 +78,12 @@ func (m *Module) IsHostModule() bool {
return len(m.HostFunctionSection) > 0 return len(m.HostFunctionSection) > 0
} }
func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Features) error { func addFuncs(
m *Module,
nameToGoFunc map[string]interface{},
funcToNames map[string][]string,
enabledFeatures Features,
) error {
funcCount := uint32(len(nameToGoFunc)) funcCount := uint32(len(nameToGoFunc))
funcNames := make([]string, 0, funcCount) funcNames := make([]string, 0, funcCount)
if m.NameSection == nil { if m.NameSection == nil {
@@ -95,26 +102,32 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Fe
sort.Strings(funcNames) sort.Strings(funcNames)
for idx := Index(0); idx < funcCount; idx++ { for idx := Index(0); idx < funcCount; idx++ {
name := funcNames[idx] exportName := funcNames[idx]
fn := reflect.ValueOf(nameToGoFunc[name]) debugName := wasmdebug.FuncName(moduleName, exportName, idx)
fn := reflect.ValueOf(nameToGoFunc[exportName])
_, functionType, err := getFunctionType(&fn, enabledFeatures) _, functionType, err := getFunctionType(&fn, enabledFeatures)
if err != nil { if err != nil {
return fmt.Errorf("func[%s] %w", name, err) return fmt.Errorf("func[%s] %w", debugName, err)
}
names := funcToNames[exportName]
namesLen := len(names)
if namesLen > 1 && namesLen-1 != len(functionType.Params) {
return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(functionType.Params))
} }
m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType)) m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType))
m.HostFunctionSection = append(m.HostFunctionSection, &fn) m.HostFunctionSection = append(m.HostFunctionSection, &fn)
m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: name, Index: idx}) m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: exportName, Index: idx})
m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name}) if namesLen > 0 {
m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: names[0]})
moduleName: moduleName, localNames := &NameMapAssoc{Index: idx}
index: idx, for i, n := range names[1:] {
name: name, localNames.NameMap = append(localNames.NameMap, &NameAssoc{Index: Index(i), Name: n})
debugName: wasmdebug.FuncName(moduleName, name, idx), }
funcType: functionType, m.NameSection.LocalNames = append(m.NameSection.LocalNames, localNames)
exportNames: []string{name}, } else {
paramNames: nil, // TODO m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: exportName})
}) }
} }
return nil return nil
} }

View File

@@ -181,13 +181,7 @@ func TestNewHostModule(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
m, e := NewHostModule( m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205|FeatureMultiValue)
tc.moduleName,
tc.nameToGoFunc,
tc.nameToMemory,
tc.nameToGlobal,
Features20191205|FeatureMultiValue,
)
require.NoError(t, e) require.NoError(t, e)
requireHostModuleEquals(t, tc.expected, m) requireHostModuleEquals(t, tc.expected, m)
}) })
@@ -227,19 +221,19 @@ func TestNewHostModule_Errors(t *testing.T) {
{ {
name: "not a function", name: "not a function",
nameToGoFunc: map[string]interface{}{"fn": t}, nameToGoFunc: map[string]interface{}{"fn": t},
expectedErr: "func[fn] kind != func: ptr", expectedErr: "func[.fn] kind != func: ptr",
}, },
{ {
name: "function has multiple results", name: "function has multiple results",
nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }}, nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }},
nameToMemory: map[string]*Memory{"mem": {Min: 1, Max: 1}}, nameToMemory: map[string]*Memory{"mem": {Min: 1, Max: 1}},
expectedErr: "func[fn] multiple result types invalid as feature \"multi-value\" is disabled", expectedErr: "func[.fn] multiple result types invalid as feature \"multi-value\" is disabled",
}, },
{ {
name: "func collides on memory name", name: "func collides on memory name",
nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet}, nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
nameToMemory: map[string]*Memory{"fn": {Min: 1, Max: 1}}, nameToMemory: map[string]*Memory{"fn": {Min: 1, Max: 1}},
expectedErr: "func[fn] exports the same name as a memory", expectedErr: "func[.fn] exports the same name as a memory",
}, },
{ {
name: "multiple memories", name: "multiple memories",
@@ -255,7 +249,7 @@ func TestNewHostModule_Errors(t *testing.T) {
name: "func collides on global name", name: "func collides on global name",
nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet}, nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
nameToGlobal: map[string]*Global{"fn": {}}, nameToGlobal: map[string]*Global{"fn": {}},
expectedErr: "func[fn] exports the same name as a global", expectedErr: "func[.fn] exports the same name as a global",
}, },
} }
@@ -263,7 +257,7 @@ func TestNewHostModule_Errors(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal, Features20191205) _, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205)
require.EqualError(t, e, tc.expectedErr) require.EqualError(t, e, tc.expectedErr)
}) })
} }

View File

@@ -586,7 +586,7 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo
// * This relies on data generated by Module.BuildFunctionDefinitions. // * This relies on data generated by Module.BuildFunctionDefinitions.
// * This is exported for tests that don't call Instantiate, notably only // * This is exported for tests that don't call Instantiate, notably only
// enginetest.go. // enginetest.go.
func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionListenerFactory) (fns []*FunctionInstance) { func (m *ModuleInstance) BuildFunctions(mod *Module, listeners []experimental.FunctionListener) (fns []*FunctionInstance) {
fns = make([]*FunctionInstance, 0, len(mod.FunctionDefinitionSection)) fns = make([]*FunctionInstance, 0, len(mod.FunctionDefinitionSection))
if mod.IsHostModule() { if mod.IsHostModule() {
for i := range mod.HostFunctionSection { for i := range mod.HostFunctionSection {
@@ -615,8 +615,8 @@ func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionL
f.Idx = d.index f.Idx = d.index
f.Type = d.funcType f.Type = d.funcType
f.definition = d f.definition = d
if fnlf != nil { if listeners != nil {
f.FunctionListener = fnlf.NewListener(m.Name, d) f.FunctionListener = listeners[i]
} }
} }
return return

View File

@@ -305,7 +305,7 @@ func (s *Store) Instantiate(
module *Module, module *Module,
name string, name string,
sys *internalsys.Context, sys *internalsys.Context,
functionListenerFactory experimentalapi.FunctionListenerFactory, listeners []experimentalapi.FunctionListener,
) (*CallContext, error) { ) (*CallContext, error) {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
@@ -329,7 +329,7 @@ func (s *Store) Instantiate(
} }
// Instantiate the module and add it to the namespace so that other modules can import it. // Instantiate the module and add it to the namespace so that other modules can import it.
if callCtx, err := s.instantiate(ctx, ns, module, name, sys, functionListenerFactory, importedModules); err != nil { if callCtx, err := s.instantiate(ctx, ns, module, name, sys, listeners, importedModules); err != nil {
ns.deleteModule(name) ns.deleteModule(name)
return nil, err return nil, err
} else { } else {
@@ -346,7 +346,7 @@ func (s *Store) instantiate(
module *Module, module *Module,
name string, name string,
sysCtx *internalsys.Context, sysCtx *internalsys.Context,
functionListenerFactory experimentalapi.FunctionListenerFactory, listeners []experimentalapi.FunctionListener,
modules map[string]*ModuleInstance, modules map[string]*ModuleInstance,
) (*CallContext, error) { ) (*CallContext, error) {
typeIDs, err := s.getFunctionTypeIDs(module.TypeSection) typeIDs, err := s.getFunctionTypeIDs(module.TypeSection)
@@ -376,7 +376,7 @@ func (s *Store) instantiate(
} }
m := &ModuleInstance{Name: name, TypeIDs: typeIDs} m := &ModuleInstance{Name: name, TypeIDs: typeIDs}
functions := m.BuildFunctions(module, functionListenerFactory) functions := m.BuildFunctions(module, listeners)
// Now we have all instances from imports and local ones, so ready to create a new ModuleInstance. // Now we have all instances from imports and local ones, so ready to create a new ModuleInstance.
m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection) m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection)

View File

@@ -91,13 +91,7 @@ func TestModuleInstance_Memory(t *testing.T) {
func TestStore_Instantiate(t *testing.T) { func TestStore_Instantiate(t *testing.T) {
s, ns := newStore() s, ns := newStore()
m, err := NewHostModule( m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
"",
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err) require.NoError(t, err)
sysCtx := sys.DefaultContext(nil) sysCtx := sys.DefaultContext(nil)
@@ -175,13 +169,7 @@ func TestStore_CloseWithExitCode(t *testing.T) {
func TestStore_hammer(t *testing.T) { func TestStore_hammer(t *testing.T) {
const importedModuleName = "imported" const importedModuleName = "imported"
m, err := NewHostModule( m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
importedModuleName,
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err) require.NoError(t, err)
s, ns := newStore() s, ns := newStore()
@@ -235,13 +223,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
const importedModuleName = "imported" const importedModuleName = "imported"
const importingModuleName = "test" const importingModuleName = "test"
m, err := NewHostModule( m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
importedModuleName,
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err) require.NoError(t, err)
t.Run("Fails if module name already in use", func(t *testing.T) { t.Run("Fails if module name already in use", func(t *testing.T) {
@@ -332,13 +314,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
} }
func TestCallContext_ExportedFunction(t *testing.T) { func TestCallContext_ExportedFunction(t *testing.T) {
host, err := NewHostModule( host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
"host",
map[string]interface{}{"host_fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err) require.NoError(t, err)
s, ns := newStore() s, ns := newStore()

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
experimentalapi "github.com/tetratelabs/wazero/experimental"
internalsys "github.com/tetratelabs/wazero/internal/sys" internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys" "github.com/tetratelabs/wazero/sys"
@@ -80,15 +79,8 @@ func (ns *namespace) InstantiateModule(
name = code.module.NameSection.ModuleName name = code.module.NameSection.ModuleName
} }
var functionListenerFactory experimentalapi.FunctionListenerFactory
if ctx != nil { // Test to see if internal code are using an experimental feature.
if fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{}); fnlf != nil {
functionListenerFactory = fnlf.(experimentalapi.FunctionListenerFactory)
}
}
// Instantiate the module in the appropriate namespace. // Instantiate the module in the appropriate namespace.
mod, err = ns.store.Instantiate(ctx, ns.ns, code.module, name, sysCtx, functionListenerFactory) mod, err = ns.store.Instantiate(ctx, ns.ns, code.module, name, sysCtx, code.listeners)
if err != nil { if err != nil {
// If there was an error, don't leak the compiled module. // If there was an error, don't leak the compiled module.
if code.closeWithModule { if code.closeWithModule {

View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
experimentalapi "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
) )
@@ -130,6 +131,7 @@ func NewRuntimeWithConfig(rConfig RuntimeConfig) Runtime {
store: store, store: store,
ns: &namespace{store: store, ns: ns}, ns: &namespace{store: store, ns: ns},
enabledFeatures: config.enabledFeatures, enabledFeatures: config.enabledFeatures,
isInterpreter: config.isInterpreter,
} }
} }
@@ -138,15 +140,13 @@ type runtime struct {
store *wasm.Store store *wasm.Store
ns *namespace ns *namespace
enabledFeatures wasm.Features enabledFeatures wasm.Features
isInterpreter bool
compiledModules []*compiledModule compiledModules []*compiledModule
} }
// NewNamespace implements Runtime.NewNamespace. // NewNamespace implements Runtime.NewNamespace.
func (r *runtime) NewNamespace(ctx context.Context) Namespace { func (r *runtime) NewNamespace(ctx context.Context) Namespace {
return &namespace{ return &namespace{store: r.store, ns: r.store.NewNamespace(ctx)}
store: r.store,
ns: r.store.NewNamespace(ctx),
}
} }
// Module implements Namespace.Module embedded by Runtime. // Module implements Namespace.Module embedded by Runtime.
@@ -179,15 +179,41 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte, cConfig Comp
// Now that the module is validated, cache the function definitions. // Now that the module is validated, cache the function definitions.
internal.BuildFunctionDefinitions() internal.BuildFunctionDefinitions()
c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
if c.listeners, err = buildListeners(ctx, r, internal); err != nil {
return nil, err
}
if err = r.store.Engine.CompileModule(ctx, internal); err != nil { if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
return nil, err return nil, err
} }
c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
r.compiledModules = append(r.compiledModules, c) r.compiledModules = append(r.compiledModules, c)
return c, nil return c, nil
} }
func buildListeners(ctx context.Context, r *runtime, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
if ctx == nil {
return nil, nil
}
// Test to see if internal code are using an experimental feature.
fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{})
if fnlf == nil {
return nil, nil
}
if !r.isInterpreter {
return nil, errors.New("context includes a FunctionListenerFactoryKey, which is only supported in the interpreter")
}
factory := fnlf.(experimentalapi.FunctionListenerFactory)
importCount := internal.ImportFuncCount()
listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection))
for i := 0; i < len(listeners); i++ {
listeners[i] = factory.NewListener(internal.FunctionDefinitionSection[uint32(i)+importCount])
}
return listeners, nil
}
// InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary // InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) { func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
if compiled, err := r.CompileModule(ctx, binary, NewCompileConfig()); err != nil { if compiled, err := r.CompileModule(ctx, binary, NewCompileConfig()); err != nil {

View File

@@ -1,7 +1,7 @@
package wasi_snapshot_preview1 package wasi_snapshot_preview1
import ( import (
"fmt" internalwasi "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
) )
// Errno are the error codes returned by WASI functions. // Errno are the error codes returned by WASI functions.
@@ -17,10 +17,7 @@ type Errno = uint32 // neither uint16 nor an alias for parity with wasm.ValueTyp
// ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG" // ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG"
func ErrnoName(errno Errno) string { func ErrnoName(errno Errno) string {
if int(errno) < len(errnoToString) { return internalwasi.ErrnoName(errno)
return errnoToString[errno]
}
return fmt.Sprintf("errno(%d)", errno)
} }
// Note: Below prefers POSIX symbol names over WASI ones, even if the docs are from WASI. // Note: Below prefers POSIX symbol names over WASI ones, even if the docs are from WASI.
@@ -182,83 +179,3 @@ const (
// ErrnoNotcapable Extension: Capabilities insufficient. // ErrnoNotcapable Extension: Capabilities insufficient.
ErrnoNotcapable ErrnoNotcapable
) )
var errnoToString = [...]string{
ErrnoSuccess: "ESUCCESS",
Errno2big: "E2BIG",
ErrnoAcces: "EACCES",
ErrnoAddrinuse: "EADDRINUSE",
ErrnoAddrnotavail: "EADDRNOTAVAIL",
ErrnoAfnosupport: "EAFNOSUPPORT",
ErrnoAgain: "EAGAIN",
ErrnoAlready: "EALREADY",
ErrnoBadf: "EBADF",
ErrnoBadmsg: "EBADMSG",
ErrnoBusy: "EBUSY",
ErrnoCanceled: "ECANCELED",
ErrnoChild: "ECHILD",
ErrnoConnaborted: "ECONNABORTED",
ErrnoConnrefused: "ECONNREFUSED",
ErrnoConnreset: "ECONNRESET",
ErrnoDeadlk: "EDEADLK",
ErrnoDestaddrreq: "EDESTADDRREQ",
ErrnoDom: "EDOM",
ErrnoDquot: "EDQUOT",
ErrnoExist: "EEXIST",
ErrnoFault: "EFAULT",
ErrnoFbig: "EFBIG",
ErrnoHostunreach: "EHOSTUNREACH",
ErrnoIdrm: "EIDRM",
ErrnoIlseq: "EILSEQ",
ErrnoInprogress: "EINPROGRESS",
ErrnoIntr: "EINTR",
ErrnoInval: "EINVAL",
ErrnoIo: "EIO",
ErrnoIsconn: "EISCONN",
ErrnoIsdir: "EISDIR",
ErrnoLoop: "ELOOP",
ErrnoMfile: "EMFILE",
ErrnoMlink: "EMLINK",
ErrnoMsgsize: "EMSGSIZE",
ErrnoMultihop: "EMULTIHOP",
ErrnoNametoolong: "ENAMETOOLONG",
ErrnoNetdown: "ENETDOWN",
ErrnoNetreset: "ENETRESET",
ErrnoNetunreach: "ENETUNREACH",
ErrnoNfile: "ENFILE",
ErrnoNobufs: "ENOBUFS",
ErrnoNodev: "ENODEV",
ErrnoNoent: "ENOENT",
ErrnoNoexec: "ENOEXEC",
ErrnoNolck: "ENOLCK",
ErrnoNolink: "ENOLINK",
ErrnoNomem: "ENOMEM",
ErrnoNomsg: "ENOMSG",
ErrnoNoprotoopt: "ENOPROTOOPT",
ErrnoNospc: "ENOSPC",
ErrnoNosys: "ENOSYS",
ErrnoNotconn: "ENOTCONN",
ErrnoNotdir: "ENOTDIR",
ErrnoNotempty: "ENOTEMPTY",
ErrnoNotrecoverable: "ENOTRECOVERABLE",
ErrnoNotsock: "ENOTSOCK",
ErrnoNotsup: "ENOTSUP",
ErrnoNotty: "ENOTTY",
ErrnoNxio: "ENXIO",
ErrnoOverflow: "EOVERFLOW",
ErrnoOwnerdead: "EOWNERDEAD",
ErrnoPerm: "EPERM",
ErrnoPipe: "EPIPE",
ErrnoProto: "EPROTO",
ErrnoProtonosupport: "EPROTONOSUPPORT",
ErrnoPrototype: "EPROTOTYPE",
ErrnoRange: "ERANGE",
ErrnoRofs: "EROFS",
ErrnoSpipe: "ESPIPE",
ErrnoSrch: "ESRCH",
ErrnoStale: "ESTALE",
ErrnoTimedout: "ETIMEDOUT",
ErrnoTxtbsy: "ETXTBSY",
ErrnoXdev: "EXDEV",
ErrnoNotcapable: "ENOTCAPABLE",
}

View File

@@ -1,9 +1,10 @@
// Package wasi_snapshot_preview1 contains Go-defined functions to access system calls, such as opening a file, similar // Package wasi_snapshot_preview1 contains Go-defined functions to access
// to Go's x/sys package. These are accessible from WebAssembly-defined functions via importing ModuleName. // system calls, such as opening a file, similar to Go's x/sys package. These
// All WASI functions return a single Errno result, which is ErrnoSuccess on success. // are accessible from WebAssembly-defined functions via importing ModuleName.
// All WASI functions return a single Errno result: ErrnoSuccess on success.
// //
// Ex. If your source (%.wasm binary) includes an import "wasi_snapshot_preview1", call Instantiate // Ex. Call Instantiate before instantiating any wasm binary that imports
// prior to instantiating it. Otherwise, it will error due to missing imports. // "wasi_snapshot_preview1", Otherwise, it will error due to missing imports.
// ctx := context.Background() // ctx := context.Background()
// r := wazero.NewRuntime() // r := wazero.NewRuntime()
// defer r.Close(ctx) // This closes everything this Runtime created. // defer r.Close(ctx) // This closes everything this Runtime created.
@@ -31,7 +32,8 @@ import (
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
const ModuleName = "wasi_snapshot_preview1" const ModuleName = "wasi_snapshot_preview1"
// Instantiate instantiates the ModuleName module into the runtime default namespace. // Instantiate instantiates the ModuleName module into the runtime default
// namespace.
// //
// Notes // Notes
// //
@@ -44,12 +46,13 @@ func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
// Builder configures the ModuleName module for later use via Compile or Instantiate. // Builder configures the ModuleName module for later use via Compile or Instantiate.
type Builder interface { type Builder interface {
// Compile compiles the ModuleName module that can instantiated in any namespace (wazero.Namespace). // Compile compiles the ModuleName module that can instantiated in any
// namespace (wazero.Namespace).
// //
// Note: This has the same effect as the same function on wazero.ModuleBuilder. // Note: This has the same effect as the same function on wazero.ModuleBuilder.
Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error) Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error)
// Instantiate instantiates the ModuleName module into the provided namespace. // Instantiate instantiates the ModuleName module into the given namespace.
// //
// Note: This has the same effect as the same function on wazero.ModuleBuilder. // Note: This has the same effect as the same function on wazero.ModuleBuilder.
Instantiate(context.Context, wazero.Namespace) (api.Closer, error) Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
@@ -64,7 +67,9 @@ type builder struct{ r wazero.Runtime }
// moduleBuilder returns a new wazero.ModuleBuilder for ModuleName // moduleBuilder returns a new wazero.ModuleBuilder for ModuleName
func (b *builder) moduleBuilder() wazero.ModuleBuilder { func (b *builder) moduleBuilder() wazero.ModuleBuilder {
return b.r.NewModuleBuilder(ModuleName).ExportFunctions(wasiFunctions()) builder := b.r.NewModuleBuilder(ModuleName)
exportFunctions(builder)
return builder
} }
// Compile implements Builder.Compile // Compile implements Builder.Compile
@@ -437,59 +442,102 @@ const (
// See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0. // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
type wasi struct{} type wasi struct{}
// wasiFunctions returns all go functions that implement wasi. // exportFunctions adds all go functions that implement wasi.
// These should be exported in the module named ModuleName. // These should be exported in the module named ModuleName.
func wasiFunctions() map[string]interface{} { func exportFunctions(builder wazero.ModuleBuilder) {
a := &wasi{} a := &wasi{}
// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that. // Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
return map[string]interface{}{ builder.ExportFunction(functionArgsGet, a.ArgsGet,
functionArgsGet: a.ArgsGet, functionArgsGet, "argv", "argv_buf")
functionArgsSizesGet: a.ArgsSizesGet, builder.ExportFunction(functionArgsSizesGet, a.ArgsSizesGet,
functionEnvironGet: a.EnvironGet, functionArgsSizesGet, "result.argc", "result.argv_buf_size")
functionEnvironSizesGet: a.EnvironSizesGet, builder.ExportFunction(functionEnvironGet, a.EnvironGet,
functionClockResGet: a.ClockResGet, functionEnvironGet, "environ", "environ_buf")
functionClockTimeGet: a.ClockTimeGet, builder.ExportFunction(functionEnvironSizesGet, a.EnvironSizesGet,
functionFdAdvise: a.FdAdvise, functionEnvironSizesGet, "result.environc", "result.environBufSize")
functionFdAllocate: a.FdAllocate, builder.ExportFunction(functionClockResGet, a.ClockResGet,
functionFdClose: a.FdClose, functionClockResGet, "id", "result.resolution")
functionFdDatasync: a.FdDatasync, builder.ExportFunction(functionClockTimeGet, a.ClockTimeGet,
functionFdFdstatGet: a.FdFdstatGet, functionClockTimeGet, "id", "precision", "result.timestamp")
functionFdFdstatSetFlags: a.FdFdstatSetFlags, builder.ExportFunction(functionFdAdvise, a.FdAdvise,
functionFdFdstatSetRights: a.FdFdstatSetRights, functionFdAdvise, "fd", "offset", "len", "result.advice")
functionFdFilestatGet: a.FdFilestatGet, builder.ExportFunction(functionFdAllocate, a.FdAllocate,
functionFdFilestatSetSize: a.FdFilestatSetSize, functionFdAllocate, "fd", "offset", "len")
functionFdFilestatSetTimes: a.FdFilestatSetTimes, builder.ExportFunction(functionFdClose, a.FdClose,
functionFdPread: a.FdPread, functionFdClose, "fd")
functionFdPrestatGet: a.FdPrestatGet, builder.ExportFunction(functionFdDatasync, a.FdDatasync,
functionFdPrestatDirName: a.FdPrestatDirName, functionFdDatasync, "fd")
functionFdPwrite: a.FdPwrite, builder.ExportFunction(functionFdFdstatGet, a.FdFdstatGet,
functionFdRead: a.FdRead, functionFdFdstatGet, "fd", "result.stat")
functionFdReaddir: a.FdReaddir, builder.ExportFunction(functionFdFdstatSetFlags, a.FdFdstatSetFlags,
functionFdRenumber: a.FdRenumber, functionFdFdstatSetFlags, "fd", "flags")
functionFdSeek: a.FdSeek, builder.ExportFunction(functionFdFdstatSetRights, a.FdFdstatSetRights,
functionFdSync: a.FdSync, functionFdFdstatSetRights, "fd", "fs_rights_base", "fs_rights_inheriting")
functionFdTell: a.FdTell, builder.ExportFunction(functionFdFilestatGet, a.FdFilestatGet,
functionFdWrite: a.FdWrite, functionFdFilestatGet, "fd", "result.buf")
functionPathCreateDirectory: a.PathCreateDirectory, builder.ExportFunction(functionFdFilestatSetSize, a.FdFilestatSetSize,
functionPathFilestatGet: a.PathFilestatGet, functionFdFilestatSetSize, "fd", "size")
functionPathFilestatSetTimes: a.PathFilestatSetTimes, builder.ExportFunction(functionFdFilestatSetTimes, a.FdFilestatSetTimes,
functionPathLink: a.PathLink, functionFdFilestatSetTimes, "fd", "atim", "mtim", "fst_flags")
functionPathOpen: a.PathOpen, builder.ExportFunction(functionFdPread, a.FdPread,
functionPathReadlink: a.PathReadlink, functionFdPread, "fd", "iovs", "iovs_len", "offset", "result.nread")
functionPathRemoveDirectory: a.PathRemoveDirectory, builder.ExportFunction(functionFdPrestatGet, a.FdPrestatGet,
functionPathRename: a.PathRename, functionFdPrestatGet, "fd", "result.prestat")
functionPathSymlink: a.PathSymlink, builder.ExportFunction(functionFdPrestatDirName, a.FdPrestatDirName,
functionPathUnlinkFile: a.PathUnlinkFile, functionFdPrestatDirName, "fd", "path", "path_len")
functionPollOneoff: a.PollOneoff, builder.ExportFunction(functionFdPwrite, a.FdPwrite,
functionProcExit: a.ProcExit, functionFdPwrite, "fd", "iovs", "iovs_len", "offset", "result.nwritten")
functionProcRaise: a.ProcRaise, builder.ExportFunction(functionFdRead, a.FdRead,
functionSchedYield: a.SchedYield, functionFdRead, "fd", "iovs", "iovs_len", "result.size")
functionRandomGet: a.RandomGet, builder.ExportFunction(functionFdReaddir, a.FdReaddir,
functionSockRecv: a.SockRecv, functionFdReaddir, "fd", "buf", "buf_len", "cookie", "result.bufused")
functionSockSend: a.SockSend, builder.ExportFunction(functionFdRenumber, a.FdRenumber,
functionSockShutdown: a.SockShutdown, functionFdRenumber, "fd", "to")
} builder.ExportFunction(functionFdSeek, a.FdSeek,
functionFdSeek, "fd", "offset", "whence", "result.newoffset")
builder.ExportFunction(functionFdSync, a.FdSync,
functionFdSync, "fd")
builder.ExportFunction(functionFdTell, a.FdTell,
functionFdTell, "fd", "result.offset")
builder.ExportFunction(functionFdWrite, a.FdWrite,
functionFdWrite, "fd", "iovs", "iovs_len", "result.size")
builder.ExportFunction(functionPathCreateDirectory, a.PathCreateDirectory,
functionPathCreateDirectory, "fd", "path", "path_len")
builder.ExportFunction(functionPathFilestatGet, a.PathFilestatGet,
functionPathFilestatGet, "fd", "flags", "path", "path_len", "result.buf")
builder.ExportFunction(functionPathFilestatSetTimes, a.PathFilestatSetTimes,
functionPathFilestatSetTimes, "fd", "flags", "path", "path_len", "atim", "mtim", "fst_flags")
builder.ExportFunction(functionPathLink, a.PathLink,
functionPathLink, "old_fd", "old_flags", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
builder.ExportFunction(functionPathOpen, a.PathOpen,
functionPathOpen, "fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd")
builder.ExportFunction(functionPathReadlink, a.PathReadlink,
functionPathReadlink, "fd", "path", "path_len", "buf", "buf_len", "result.bufused")
builder.ExportFunction(functionPathRemoveDirectory, a.PathRemoveDirectory,
functionPathRemoveDirectory, "fd", "path", "path_len")
builder.ExportFunction(functionPathRename, a.PathRename,
functionPathRename, "fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
builder.ExportFunction(functionPathSymlink, a.PathSymlink,
functionPathSymlink, "old_path", "old_path_len", "fd", "new_path", "new_path_len")
builder.ExportFunction(functionPathUnlinkFile, a.PathUnlinkFile,
functionPathUnlinkFile, "fd", "path", "path_len")
builder.ExportFunction(functionPollOneoff, a.PollOneoff,
functionPollOneoff, "in", "out", "nsubscriptions", "result.nevents")
builder.ExportFunction(functionProcExit, a.ProcExit,
functionProcExit, "rval")
builder.ExportFunction(functionProcRaise, a.ProcRaise,
functionProcRaise, "sig")
builder.ExportFunction(functionSchedYield, a.SchedYield,
functionSchedYield)
builder.ExportFunction(functionRandomGet, a.RandomGet,
functionRandomGet, "buf", "buf_len")
builder.ExportFunction(functionSockRecv, a.SockRecv,
functionSockRecv, "fd", "ri_data", "ri_data_count", "ri_flags", "result.ro_datalen", "result.ro_flags")
builder.ExportFunction(functionSockSend, a.SockSend,
functionSockSend, "fd", "si_data", "si_data_count", "si_flags", "result.so_datalen")
builder.ExportFunction(functionSockShutdown, a.SockShutdown,
functionSockShutdown, "fd", "how")
} }
// ArgsGet is the WASI function that reads command-line argument data (WithArgs). // ArgsGet is the WASI function that reads command-line argument data (WithArgs).