diff --git a/api/wasm.go b/api/wasm.go index e7ea620e..08c5a5a8 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -83,12 +83,17 @@ const ( ValueTypeF32 ValueType = 0x7d // ValueTypeF64 is a 64-bit floating point number. ValueTypeF64 ValueType = 0x7c + // ValueTypeExternref is a externref type. // - // Note: in wazero, externref type value are opaque raw 64-bit pointers, and the ValueTypeExternref type - // in the signature will be translated as uintptr in wazero's API level. - // For example, the import function `(func (import "env" "f") (param externref) (result externref))` can be defined in Go as: + // Note: in wazero, externref type value are opaque raw 64-bit pointers, + // and the ValueTypeExternref type in the signature will be translated 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{}{ // "f": func(externref uintptr) (resultExternRef uintptr) { return }, // }) @@ -222,6 +227,13 @@ type FunctionDefinition interface { // is possible. 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 // function with this signature. // diff --git a/assemblyscript/assemblyscript.go b/assemblyscript/assemblyscript.go index 50b5ce90..ae69027a 100644 --- a/assemblyscript/assemblyscript.go +++ b/assemblyscript/assemblyscript.go @@ -45,9 +45,9 @@ import ( // * To add more functions to the "env" module, use FunctionExporter. // * To instantiate into another wazero.Namespace, use FunctionExporter. func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { - return r.NewModuleBuilder("env"). - ExportFunctions(NewFunctionExporter().ExportFunctions()). - Instantiate(ctx, r) + builder := r.NewModuleBuilder("env") + NewFunctionExporter().ExportFunctions(builder) + return builder.Instantiate(ctx, r) } // 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 // named "env". - ExportFunctions() (nameToGoFunc map[string]interface{}) + ExportFunctions(builder wazero.ModuleBuilder) } // NewFunctionExporter returns a FunctionExporter object with trace disabled. @@ -113,13 +113,13 @@ func (e *functionExporter) WithTraceToStderr() FunctionExporter { } // 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} - return map[string]interface{}{ - "abort": env.abort, - "trace": env.trace, - "seed": env.seed, - } + builder.ExportFunction("abort", env.abort, "~lib/builtins/abort", + "message", "fileName", "lineNumber", "columnNumber") + builder.ExportFunction("trace", env.trace, "~lib/builtins/trace", + "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 diff --git a/assemblyscript/assemblyscript_example_test.go b/assemblyscript/assemblyscript_example_test.go index 56d0d811..b4d2c276 100644 --- a/assemblyscript/assemblyscript_example_test.go +++ b/assemblyscript/assemblyscript_example_test.go @@ -38,8 +38,9 @@ func Example_functionExporter() { ExportFunction("get_int", func() uint32 { return 1 }) // Now, add AssemblyScript special function imports into it. - envBuilder.ExportFunctions(assemblyscript.NewFunctionExporter(). - WithAbortMessageDisabled().ExportFunctions()) + assemblyscript.NewFunctionExporter(). + WithAbortMessageDisabled(). + ExportFunctions(envBuilder) // Output: } diff --git a/assemblyscript/assemblyscript_test.go b/assemblyscript/assemblyscript_test.go index 92d9c59c..945a71be 100644 --- a/assemblyscript/assemblyscript_test.go +++ b/assemblyscript/assemblyscript_test.go @@ -6,12 +6,14 @@ import ( _ "embed" "errors" "io" + "strings" "testing" "testing/iotest" "unicode/utf16" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" + . "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/watzero" "github.com/tetratelabs/wazero/sys" @@ -60,32 +62,37 @@ func TestAbort(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var out, log bytes.Buffer - out := &bytes.Buffer{} + // Set context to one that has an experimental listener + ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) - _, err := r.NewModuleBuilder("env"). - ExportFunctions(tc.exporter.ExportFunctions()). - 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) abortWasm, err := watzero.Wat2Wasm(abortWat) require.NoError(t, err) - code, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig()) + code, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig()) 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) - 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.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode()) 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 messageUTF16 []byte fileNameUTF16 []byte + expectedLog string }{ { name: "bad message", messageUTF16: encodeUTF16("message")[:5], fileNameUTF16: encodeUTF16("filename"), + expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2) +<== () +`, }, { name: "bad filename", messageUTF16: encodeUTF16("message"), 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 t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var out, log bytes.Buffer - _, 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) abortWasm, err := watzero.Wat2Wasm(abortWat) require.NoError(t, err) - compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig()) + compiled, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig()) require.NoError(t, err) - out := &bytes.Buffer{} - exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out) - mod, err := r.InstantiateModule(testCtx, compiled, exporter) + exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(&out) + mod, err := r.InstantiateModule(ctx, compiled, exporter) 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.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. func TestAbort_UnreachableAfter(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var log bytes.Buffer - _, err := r.NewModuleBuilder("env"). - // Disable the abort message as we are passing invalid memory offsets. - ExportFunctions(NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions()). - 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) + + 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) abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort) require.NoError(t, err) - _, err = r.InstantiateModuleFromBinary(testCtx, abortWasm) + _, err = r.InstantiateModuleFromBinary(ctx, abortWasm) require.Error(t, err) 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) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var log bytes.Buffer + + // 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} - _, err := Instantiate(testCtx, r) + _, err := Instantiate(ctx, r) require.NoError(t, err) seedWasm, err := watzero.Wat2Wasm(seedWat) require.NoError(t, err) - code, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig()) + code, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig()) 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) seedFn := mod.ExportedFunction("seed") - res, err := seedFn.Call(testCtx) + _, err = seedFn.Call(ctx) require.NoError(t, err) // 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) { @@ -208,14 +242,14 @@ func TestSeed_error(t *testing.T) { source: bytes.NewReader([]byte{0, 1}), expectedErr: `error reading random seed: unexpected EOF (recovered by wazero) wasm stack trace: - env.seed() f64`, + env.~lib/builtins/seed() f64`, }, { name: "error reading", source: iotest.ErrReader(errors.New("ice cream")), expectedErr: `error reading random seed: ice cream (recovered by wazero) wasm stack trace: - env.seed() f64`, + env.~lib/builtins/seed() f64`, }, } @@ -223,65 +257,84 @@ wasm stack trace: tc := tt t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var log bytes.Buffer - _, 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) seedWasm, err := watzero.Wat2Wasm(seedWat) require.NoError(t, err) - compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig()) + compiled, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig()) require.NoError(t, err) 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) - _, err = mod.ExportedFunction("seed").Call(testCtx) + _, err = mod.ExportedFunction("seed").Call(ctx) require.EqualError(t, err, tc.expectedErr) + require.Equal(t, `==> env.~lib/builtins/seed() +`, log.String()) }) } } func TestTrace(t *testing.T) { 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 { - name string - exporter FunctionExporter - params []uint64 - expected string + name string + exporter FunctionExporter + params []uint64 + expected, expectedLog string }{ { - name: "disabled", - exporter: NewFunctionExporter(), - params: noArgs, - expected: "", + name: "disabled", + exporter: NewFunctionExporter(), + params: noArgs, + expected: "", + expectedLog: noArgsLog, }, { - name: "ToStderr", - exporter: NewFunctionExporter().WithTraceToStderr(), - params: noArgs, - expected: "trace: hello\n", + name: "ToStderr", + exporter: NewFunctionExporter().WithTraceToStderr(), + params: noArgs, + expected: "trace: hello\n", + expectedLog: noArgsLog, }, { - name: "ToStdout - no args", - exporter: NewFunctionExporter().WithTraceToStdout(), - params: noArgs, - expected: "trace: hello\n", + name: "ToStdout - no args", + exporter: NewFunctionExporter().WithTraceToStdout(), + params: noArgs, + expected: "trace: hello\n", + expectedLog: noArgsLog, }, { name: "ToStdout - one arg", exporter: NewFunctionExporter().WithTraceToStdout(), params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0}, 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", exporter: NewFunctionExporter().WithTraceToStdout(), params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0}, 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", @@ -296,6 +349,9 @@ func TestTrace(t *testing.T) { api.EncodeF64(5), }, 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 t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var out, log bytes.Buffer - _, err := r.NewModuleBuilder("env"). - ExportFunctions(tc.exporter.ExportFunctions()). - 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) + + envBuilder := r.NewModuleBuilder("env") + tc.exporter.ExportFunctions(envBuilder) + _, err := envBuilder.Instantiate(ctx, r) require.NoError(t, err) traceWasm, err := watzero.Wat2Wasm(traceWat) require.NoError(t, err) - code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig()) + code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig()) require.NoError(t, err) - out := &bytes.Buffer{} config := wazero.NewModuleConfig() - if tc.name == "ToStderr" { - config.WithStderr(out) + if strings.Contains("ToStderr", tc.name) { + config = config.WithStderr(&out) } 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) 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) - ok = mod.Memory().Write(testCtx, uint32(4), message) + ok = mod.Memory().Write(ctx, uint32(4), message) 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.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{}, expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero) 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", @@ -362,7 +423,7 @@ wasm stack trace: out: &errWriter{err: errors.New("ice cream")}, expectedErr: `ice cream (recovered by wazero) 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 t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) + var log bytes.Buffer - _, err := r.NewModuleBuilder("env"). - ExportFunctions(NewFunctionExporter().WithTraceToStdout().ExportFunctions()). - 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) + + envBuilder := r.NewModuleBuilder("env") + NewFunctionExporter().WithTraceToStdout().ExportFunctions(envBuilder) + _, err := envBuilder.Instantiate(ctx, r) require.NoError(t, err) traceWasm, err := watzero.Wat2Wasm(traceWat) require.NoError(t, err) - compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig()) + compiled, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig()) require.NoError(t, err) 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) - ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message))) + ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(tc.message))) 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) - _, 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.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) { tests := []struct { name string - memory func(api.Memory) + memory func(context.Context, api.Memory) offset int expected, expectedErr string }{ { name: "success", - memory: func(memory api.Memory) { - memory.WriteUint32Le(testCtx, 0, 10) + memory: func(ctx context.Context, memory api.Memory) { + memory.WriteUint32Le(ctx, 0, 10) b := encodeUTF16("hello") - memory.Write(testCtx, 4, b) + memory.Write(ctx, 4, b) }, offset: 4, expected: "hello", }, { name: "can't read size", - memory: func(memory api.Memory) { + memory: func(ctx context.Context, memory api.Memory) { b := encodeUTF16("hello") - memory.Write(testCtx, 0, b) + memory.Write(ctx, 0, b) }, offset: 0, // will attempt to read size from offset -4 expectedErr: "Memory.ReadUint32Le(4294967292) out of range", }, { name: "odd size", - memory: func(memory api.Memory) { - memory.WriteUint32Le(testCtx, 0, 9) + memory: func(ctx context.Context, memory api.Memory) { + memory.WriteUint32Le(ctx, 0, 9) b := encodeUTF16("hello") - memory.Write(testCtx, 4, b) + memory.Write(ctx, 4, b) }, offset: 4, expectedErr: "read an odd number of bytes for utf-16 string: 9", }, { name: "can't read string", - memory: func(memory api.Memory) { - memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value + memory: func(ctx context.Context, memory api.Memory) { + memory.WriteUint32Le(ctx, 0, 10_000_000) // set size to too large value b := encodeUTF16("hello") - memory.Write(testCtx, 4, b) + memory.Write(ctx, 4, b) }, offset: 4, expectedErr: "Memory.Read(4, 10000000) out of range", @@ -459,7 +527,7 @@ func Test_readAssemblyScriptString(t *testing.T) { Instantiate(testCtx, r) require.NoError(t, err) - tc.memory(mod.Memory()) + tc.memory(testCtx, mod.Memory()) s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset)) 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 - ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16))) + ok := mem.WriteUint32Le(ctx, uint32(off), uint32(len(messageUTF16))) require.True(t, ok) off += 4 messageOff := off - ok = mem.Write(testCtx, uint32(off), messageUTF16) + ok = mem.Write(ctx, uint32(off), messageUTF16) require.True(t, ok) 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) off += 4 filenameOff := off - ok = mem.Write(testCtx, uint32(off), fileNameUTF16) + ok = mem.Write(ctx, uint32(off), fileNameUTF16) require.True(t, ok) return messageOff, filenameOff } diff --git a/builder.go b/builder.go index 60bac687..a832922c 100644 --- a/builder.go +++ b/builder.go @@ -10,7 +10,7 @@ import ( "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: // @@ -25,7 +25,8 @@ import ( // ExportFunction("hello", hello). // 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"). // ExportFunction("get_random_string", getRandomString). @@ -37,9 +38,12 @@ import ( // // Notes // -// * ModuleBuilder is mutable. WithXXX functions return the same instance for chaining. -// * WithXXX methods do not return errors, to allow chaining. Any validation errors are deferred until Build. -// * Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build. +// * ModuleBuilder is mutable: each method returns the same instance for +// chaining. +// * 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 { // 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 // - // * name - the name to export. Ex "random_get" - // * goFunc - the `func` to export. + // * exportName - The name to export. Ex "random_get" + // * 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 - // types. This means uint32, uint64, float32 or float64. Up to one result can be returned. + // Ex. + // // 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: // - // addInts := func(x uint32, uint32) uint32 { + // addInts := func(x, y uint32) uint32 { // 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: // - // 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! // 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 - // types in Wasm. The only way to share other data is via writing memory and sharing offsets. + // Ex. This uses an api.Module to reads the parameters from memory. This is + // 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 { // x, _ := m.Memory().ReadUint32Le(ctx, offset) @@ -78,16 +98,17 @@ type ModuleBuilder interface { // 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 { // fn = m.ExportedFunction("__read") // results, err := fn(ctx, offset, byteCount) // --snip-- // // 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(nameToGoFunc map[string]interface{}) ModuleBuilder @@ -200,6 +221,7 @@ type moduleBuilder struct { r *runtime moduleName string nameToGoFunc map[string]interface{} + funcToNames map[string][]string nameToMemory map[string]*wasm.Memory nameToGlobal map[string]*wasm.Global } @@ -210,14 +232,18 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder { r: r, moduleName: moduleName, nameToGoFunc: map[string]interface{}{}, + funcToNames: map[string][]string{}, nameToMemory: map[string]*wasm.Memory{}, nameToGlobal: map[string]*wasm.Global{}, } } // ExportFunction implements ModuleBuilder.ExportFunction -func (b *moduleBuilder) ExportFunction(name string, goFunc interface{}) ModuleBuilder { - b.nameToGoFunc[name] = goFunc +func (b *moduleBuilder) ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder { + b.nameToGoFunc[exportName] = goFunc + if len(names) > 0 { + b.funcToNames[exportName] = names + } 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 { 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 { return nil, err } - return &compiledModule{module: module, compiledEngine: b.r.store.Engine}, nil + return c, nil } // Instantiate implements ModuleBuilder.Instantiate diff --git a/builder_test.go b/builder_test.go index a6b03151..3dbd8646 100644 --- a/builder_test.go +++ b/builder_test.go @@ -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", input: func(r Runtime) ModuleBuilder { diff --git a/config.go b/config.go index 4d239a50..51b5b6ab 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ import ( "time" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/interpreter" "github.com/tetratelabs/wazero/internal/platform" @@ -148,6 +149,7 @@ func NewRuntimeConfig() RuntimeConfig { type runtimeConfig struct { enabledFeatures wasm.Features + isInterpreter bool newEngine func(wasm.Features) wasm.Engine } @@ -179,6 +181,7 @@ func NewRuntimeConfigCompiler() RuntimeConfig { // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly. func NewRuntimeConfigInterpreter() RuntimeConfig { ret := engineLessConfig.clone() + ret.isInterpreter = true ret.newEngine = interpreter.NewEngine return ret } @@ -280,7 +283,8 @@ type compiledModule struct { module *wasm.Module // compiledEngine holds an engine on which `module` is compiled. 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 bool } diff --git a/experimental/experimental_test.go b/experimental/experimental_test.go index 8081544d..2b89d07e 100644 --- a/experimental/experimental_test.go +++ b/experimental/experimental_test.go @@ -1,3 +1,8 @@ // Package experimental_test includes examples for experimental features. When these complete, they'll end up as real // examples in the /examples directory. 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") diff --git a/experimental/listener.go b/experimental/listener.go index b0aa6667..36dca873 100644 --- a/experimental/listener.go +++ b/experimental/listener.go @@ -18,13 +18,7 @@ type FunctionListenerFactoryKey struct{} type FunctionListenerFactory interface { // NewListener returns a FunctionListener for a defined function. If nil is // returned, no listener will be notified. - // - // 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 + NewListener(api.FunctionDefinition) FunctionListener } // FunctionListener can be registered for any function via diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go deleted file mode 100644 index d5af3cee..00000000 --- a/experimental/listener_example_test.go +++ /dev/null @@ -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) -} diff --git a/experimental/listener_test.go b/experimental/listener_test.go new file mode 100644 index 00000000..ba85d745 --- /dev/null +++ b/experimental/listener_test.go @@ -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) +} diff --git a/experimental/log_listener.go b/experimental/log_listener.go new file mode 100644 index 00000000..202a6260 --- /dev/null +++ b/experimental/log_listener.go @@ -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 +} diff --git a/experimental/log_listener_example_test.go b/experimental/log_listener_example_test.go new file mode 100644 index 00000000..9a848418 --- /dev/null +++ b/experimental/log_listener_example_test.go @@ -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 + //<-- () +} diff --git a/experimental/log_listener_test.go b/experimental/log_listener_test.go new file mode 100644 index 00000000..d1ef1244 --- /dev/null +++ b/experimental/log_listener_test.go @@ -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()) + +} diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index 6ebf1776..0a5ce5b2 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -197,7 +197,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { // Trigger relocation of goroutine stack because at this point we have the majority of // goroutine stack unused after recursive call. 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) err = s.Engine.CompileModule(testCtx, hm) diff --git a/internal/wasi_snapshot_preview1/errno.go b/internal/wasi_snapshot_preview1/errno.go new file mode 100644 index 00000000..8dacc266 --- /dev/null +++ b/internal/wasi_snapshot_preview1/errno.go @@ -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", +} diff --git a/internal/wasm/function_definition.go b/internal/wasm/function_definition.go index 30e4c895..281fe790 100644 --- a/internal/wasm/function_definition.go +++ b/internal/wasm/function_definition.go @@ -61,10 +61,13 @@ func (m *Module) BuildFunctionDefinitions() { importFuncIdx++ } + // At the moment, a module can either be solely wasm or host functions. + isHostFunction := m.HostFunctionSection != nil for codeIndex, typeIndex := range m.FunctionSection { m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ - index: Index(codeIndex) + importCount, - funcType: m.TypeSection[typeIndex], + index: Index(codeIndex) + importCount, + funcType: m.TypeSection[typeIndex], + isHostFunction: isHostFunction, }) } @@ -99,14 +102,15 @@ func (m *Module) BuildFunctionDefinitions() { // FunctionDefinition implements api.FunctionDefinition type FunctionDefinition struct { - moduleName string - index Index - name string - debugName string - funcType *FunctionType - importDesc *[2]string - exportNames []string - paramNames []string + moduleName string + index Index + name string + debugName string + isHostFunction bool + funcType *FunctionType + importDesc *[2]string + exportNames []string + paramNames []string } // ModuleName implements the same method as documented on api.FunctionDefinition. @@ -142,6 +146,11 @@ func (f *FunctionDefinition) ExportNames() []string { 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. func (f *FunctionDefinition) ParamNames() []string { return f.paramNames diff --git a/internal/wasm/function_definition_test.go b/internal/wasm/function_definition_test.go index 5a97a71a..408893ff 100644 --- a/internal/wasm/function_definition_test.go +++ b/internal/wasm/function_definition_test.go @@ -1,6 +1,7 @@ package wasm import ( + "reflect" "testing" "github.com/tetratelabs/wazero/api" @@ -9,6 +10,7 @@ import ( func TestModule_BuildFunctionDefinitions(t *testing.T) { nopCode := &Code{nil, []byte{OpcodeEnd}} + fnV := reflect.ValueOf(func() {}) tests := []struct { name string m *Module @@ -26,6 +28,22 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) { 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", m: &Module{ diff --git a/internal/wasm/host.go b/internal/wasm/host.go index f88b406f..a9906839 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -13,6 +13,7 @@ import ( func NewHostModule( moduleName string, nameToGoFunc map[string]interface{}, + funcToNames map[string][]string, nameToMemory map[string]*Memory, nameToGlobal map[string]*Global, enabledFeatures Features, @@ -33,11 +34,12 @@ func NewHostModule( // Check name collision as exports cannot collide on names, regardless of type. for name := range nameToGoFunc { + // manually generate the error message as we don't have debug names yet. 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 { - 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 { @@ -47,7 +49,7 @@ func NewHostModule( } if funcCount > 0 { - if err = addFuncs(m, nameToGoFunc, enabledFeatures); err != nil { + if err = addFuncs(m, nameToGoFunc, funcToNames, enabledFeatures); err != nil { return } } @@ -76,7 +78,12 @@ func (m *Module) IsHostModule() bool { 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)) funcNames := make([]string, 0, funcCount) if m.NameSection == nil { @@ -95,26 +102,32 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Fe sort.Strings(funcNames) for idx := Index(0); idx < funcCount; idx++ { - name := funcNames[idx] - fn := reflect.ValueOf(nameToGoFunc[name]) + exportName := funcNames[idx] + debugName := wasmdebug.FuncName(moduleName, exportName, idx) + fn := reflect.ValueOf(nameToGoFunc[exportName]) _, functionType, err := getFunctionType(&fn, enabledFeatures) 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.HostFunctionSection = append(m.HostFunctionSection, &fn) - m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: name, Index: idx}) - m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name}) - m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ - moduleName: moduleName, - index: idx, - name: name, - debugName: wasmdebug.FuncName(moduleName, name, idx), - funcType: functionType, - exportNames: []string{name}, - paramNames: nil, // TODO - }) + m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: exportName, Index: idx}) + if namesLen > 0 { + m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: names[0]}) + localNames := &NameMapAssoc{Index: idx} + for i, n := range names[1:] { + localNames.NameMap = append(localNames.NameMap, &NameAssoc{Index: Index(i), Name: n}) + } + m.NameSection.LocalNames = append(m.NameSection.LocalNames, localNames) + } else { + m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: exportName}) + } } return nil } diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go index 5ee672a3..7f3c5e34 100644 --- a/internal/wasm/host_test.go +++ b/internal/wasm/host_test.go @@ -181,13 +181,7 @@ func TestNewHostModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := NewHostModule( - tc.moduleName, - tc.nameToGoFunc, - tc.nameToMemory, - tc.nameToGlobal, - Features20191205|FeatureMultiValue, - ) + m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205|FeatureMultiValue) require.NoError(t, e) requireHostModuleEquals(t, tc.expected, m) }) @@ -227,19 +221,19 @@ func TestNewHostModule_Errors(t *testing.T) { { name: "not a function", nameToGoFunc: map[string]interface{}{"fn": t}, - expectedErr: "func[fn] kind != func: ptr", + expectedErr: "func[.fn] kind != func: ptr", }, { name: "function has multiple results", nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }}, 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", nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet}, 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", @@ -255,7 +249,7 @@ func TestNewHostModule_Errors(t *testing.T) { name: "func collides on global name", nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet}, 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 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) }) } diff --git a/internal/wasm/module.go b/internal/wasm/module.go index e453328b..26d83859 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -586,7 +586,7 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo // * This relies on data generated by Module.BuildFunctionDefinitions. // * This is exported for tests that don't call Instantiate, notably only // 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)) if mod.IsHostModule() { for i := range mod.HostFunctionSection { @@ -615,8 +615,8 @@ func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionL f.Idx = d.index f.Type = d.funcType f.definition = d - if fnlf != nil { - f.FunctionListener = fnlf.NewListener(m.Name, d) + if listeners != nil { + f.FunctionListener = listeners[i] } } return diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 8472fa9d..d0611265 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -305,7 +305,7 @@ func (s *Store) Instantiate( module *Module, name string, sys *internalsys.Context, - functionListenerFactory experimentalapi.FunctionListenerFactory, + listeners []experimentalapi.FunctionListener, ) (*CallContext, error) { if ctx == nil { 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. - 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) return nil, err } else { @@ -346,7 +346,7 @@ func (s *Store) instantiate( module *Module, name string, sysCtx *internalsys.Context, - functionListenerFactory experimentalapi.FunctionListenerFactory, + listeners []experimentalapi.FunctionListener, modules map[string]*ModuleInstance, ) (*CallContext, error) { typeIDs, err := s.getFunctionTypeIDs(module.TypeSection) @@ -376,7 +376,7 @@ func (s *Store) instantiate( } 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. m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection) diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 9cea4868..8d73f4ee 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -91,13 +91,7 @@ func TestModuleInstance_Memory(t *testing.T) { func TestStore_Instantiate(t *testing.T) { s, ns := newStore() - m, err := NewHostModule( - "", - map[string]interface{}{"fn": func(api.Module) {}}, - map[string]*Memory{}, - map[string]*Global{}, - Features20191205, - ) + m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205) require.NoError(t, err) sysCtx := sys.DefaultContext(nil) @@ -175,13 +169,7 @@ func TestStore_CloseWithExitCode(t *testing.T) { func TestStore_hammer(t *testing.T) { const importedModuleName = "imported" - m, err := NewHostModule( - importedModuleName, - map[string]interface{}{"fn": func(api.Module) {}}, - map[string]*Memory{}, - map[string]*Global{}, - Features20191205, - ) + m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205) require.NoError(t, err) s, ns := newStore() @@ -235,13 +223,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { const importedModuleName = "imported" const importingModuleName = "test" - m, err := NewHostModule( - importedModuleName, - map[string]interface{}{"fn": func(api.Module) {}}, - map[string]*Memory{}, - map[string]*Global{}, - Features20191205, - ) + m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205) require.NoError(t, err) 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) { - host, err := NewHostModule( - "host", - map[string]interface{}{"host_fn": func(api.Module) {}}, - map[string]*Memory{}, - map[string]*Global{}, - Features20191205, - ) + host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205) require.NoError(t, err) s, ns := newStore() diff --git a/namespace.go b/namespace.go index 8844be60..dbf97e19 100644 --- a/namespace.go +++ b/namespace.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/tetratelabs/wazero/api" - experimentalapi "github.com/tetratelabs/wazero/experimental" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/sys" @@ -80,15 +79,8 @@ func (ns *namespace) InstantiateModule( 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. - 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 there was an error, don't leak the compiled module. if code.closeWithModule { diff --git a/runtime.go b/runtime.go index 1173ae1f..eda89aa4 100644 --- a/runtime.go +++ b/runtime.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/tetratelabs/wazero/api" + experimentalapi "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/wasm" binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" ) @@ -130,6 +131,7 @@ func NewRuntimeWithConfig(rConfig RuntimeConfig) Runtime { store: store, ns: &namespace{store: store, ns: ns}, enabledFeatures: config.enabledFeatures, + isInterpreter: config.isInterpreter, } } @@ -138,15 +140,13 @@ type runtime struct { store *wasm.Store ns *namespace enabledFeatures wasm.Features + isInterpreter bool compiledModules []*compiledModule } // NewNamespace implements Runtime.NewNamespace. func (r *runtime) NewNamespace(ctx context.Context) Namespace { - return &namespace{ - store: r.store, - ns: r.store.NewNamespace(ctx), - } + return &namespace{store: r.store, ns: r.store.NewNamespace(ctx)} } // 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. 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 { return nil, err } - c := &compiledModule{module: internal, compiledEngine: r.store.Engine} r.compiledModules = append(r.compiledModules, c) 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 func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) { if compiled, err := r.CompileModule(ctx, binary, NewCompileConfig()); err != nil { diff --git a/wasi_snapshot_preview1/errno.go b/wasi_snapshot_preview1/errno.go index 83473324..5f430279 100644 --- a/wasi_snapshot_preview1/errno.go +++ b/wasi_snapshot_preview1/errno.go @@ -1,7 +1,7 @@ package wasi_snapshot_preview1 import ( - "fmt" + internalwasi "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1" ) // 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" func ErrnoName(errno Errno) string { - if int(errno) < len(errnoToString) { - return errnoToString[errno] - } - return fmt.Sprintf("errno(%d)", errno) + return internalwasi.ErrnoName(errno) } // 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 ) - -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", -} diff --git a/wasi_snapshot_preview1/wasi.go b/wasi_snapshot_preview1/wasi.go index bbe4b294..d8f57744 100644 --- a/wasi_snapshot_preview1/wasi.go +++ b/wasi_snapshot_preview1/wasi.go @@ -1,9 +1,10 @@ -// Package wasi_snapshot_preview1 contains Go-defined functions to access system calls, such as opening a file, similar -// to Go's x/sys package. These are accessible from WebAssembly-defined functions via importing ModuleName. -// All WASI functions return a single Errno result, which is ErrnoSuccess on success. +// Package wasi_snapshot_preview1 contains Go-defined functions to access +// system calls, such as opening a file, similar to Go's x/sys package. These +// are accessible from WebAssembly-defined functions via importing 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 -// prior to instantiating it. Otherwise, it will error due to missing imports. +// Ex. Call Instantiate before instantiating any wasm binary that imports +// "wasi_snapshot_preview1", Otherwise, it will error due to missing imports. // ctx := context.Background() // r := wazero.NewRuntime() // 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 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 // @@ -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. 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. 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. 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 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 @@ -437,59 +442,102 @@ const ( // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0. 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. -func wasiFunctions() map[string]interface{} { +func exportFunctions(builder wazero.ModuleBuilder) { a := &wasi{} // 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 - return map[string]interface{}{ - functionArgsGet: a.ArgsGet, - functionArgsSizesGet: a.ArgsSizesGet, - functionEnvironGet: a.EnvironGet, - functionEnvironSizesGet: a.EnvironSizesGet, - functionClockResGet: a.ClockResGet, - functionClockTimeGet: a.ClockTimeGet, - functionFdAdvise: a.FdAdvise, - functionFdAllocate: a.FdAllocate, - functionFdClose: a.FdClose, - functionFdDatasync: a.FdDatasync, - functionFdFdstatGet: a.FdFdstatGet, - functionFdFdstatSetFlags: a.FdFdstatSetFlags, - functionFdFdstatSetRights: a.FdFdstatSetRights, - functionFdFilestatGet: a.FdFilestatGet, - functionFdFilestatSetSize: a.FdFilestatSetSize, - functionFdFilestatSetTimes: a.FdFilestatSetTimes, - functionFdPread: a.FdPread, - functionFdPrestatGet: a.FdPrestatGet, - functionFdPrestatDirName: a.FdPrestatDirName, - functionFdPwrite: a.FdPwrite, - functionFdRead: a.FdRead, - functionFdReaddir: a.FdReaddir, - functionFdRenumber: a.FdRenumber, - functionFdSeek: a.FdSeek, - functionFdSync: a.FdSync, - functionFdTell: a.FdTell, - functionFdWrite: a.FdWrite, - functionPathCreateDirectory: a.PathCreateDirectory, - functionPathFilestatGet: a.PathFilestatGet, - functionPathFilestatSetTimes: a.PathFilestatSetTimes, - functionPathLink: a.PathLink, - functionPathOpen: a.PathOpen, - functionPathReadlink: a.PathReadlink, - functionPathRemoveDirectory: a.PathRemoveDirectory, - functionPathRename: a.PathRename, - functionPathSymlink: a.PathSymlink, - functionPathUnlinkFile: a.PathUnlinkFile, - functionPollOneoff: a.PollOneoff, - functionProcExit: a.ProcExit, - functionProcRaise: a.ProcRaise, - functionSchedYield: a.SchedYield, - functionRandomGet: a.RandomGet, - functionSockRecv: a.SockRecv, - functionSockSend: a.SockSend, - functionSockShutdown: a.SockShutdown, - } + builder.ExportFunction(functionArgsGet, a.ArgsGet, + functionArgsGet, "argv", "argv_buf") + builder.ExportFunction(functionArgsSizesGet, a.ArgsSizesGet, + functionArgsSizesGet, "result.argc", "result.argv_buf_size") + builder.ExportFunction(functionEnvironGet, a.EnvironGet, + functionEnvironGet, "environ", "environ_buf") + builder.ExportFunction(functionEnvironSizesGet, a.EnvironSizesGet, + functionEnvironSizesGet, "result.environc", "result.environBufSize") + builder.ExportFunction(functionClockResGet, a.ClockResGet, + functionClockResGet, "id", "result.resolution") + builder.ExportFunction(functionClockTimeGet, a.ClockTimeGet, + functionClockTimeGet, "id", "precision", "result.timestamp") + builder.ExportFunction(functionFdAdvise, a.FdAdvise, + functionFdAdvise, "fd", "offset", "len", "result.advice") + builder.ExportFunction(functionFdAllocate, a.FdAllocate, + functionFdAllocate, "fd", "offset", "len") + builder.ExportFunction(functionFdClose, a.FdClose, + functionFdClose, "fd") + builder.ExportFunction(functionFdDatasync, a.FdDatasync, + functionFdDatasync, "fd") + builder.ExportFunction(functionFdFdstatGet, a.FdFdstatGet, + functionFdFdstatGet, "fd", "result.stat") + builder.ExportFunction(functionFdFdstatSetFlags, a.FdFdstatSetFlags, + functionFdFdstatSetFlags, "fd", "flags") + builder.ExportFunction(functionFdFdstatSetRights, a.FdFdstatSetRights, + functionFdFdstatSetRights, "fd", "fs_rights_base", "fs_rights_inheriting") + builder.ExportFunction(functionFdFilestatGet, a.FdFilestatGet, + functionFdFilestatGet, "fd", "result.buf") + builder.ExportFunction(functionFdFilestatSetSize, a.FdFilestatSetSize, + functionFdFilestatSetSize, "fd", "size") + builder.ExportFunction(functionFdFilestatSetTimes, a.FdFilestatSetTimes, + functionFdFilestatSetTimes, "fd", "atim", "mtim", "fst_flags") + builder.ExportFunction(functionFdPread, a.FdPread, + functionFdPread, "fd", "iovs", "iovs_len", "offset", "result.nread") + builder.ExportFunction(functionFdPrestatGet, a.FdPrestatGet, + functionFdPrestatGet, "fd", "result.prestat") + builder.ExportFunction(functionFdPrestatDirName, a.FdPrestatDirName, + functionFdPrestatDirName, "fd", "path", "path_len") + builder.ExportFunction(functionFdPwrite, a.FdPwrite, + functionFdPwrite, "fd", "iovs", "iovs_len", "offset", "result.nwritten") + builder.ExportFunction(functionFdRead, a.FdRead, + functionFdRead, "fd", "iovs", "iovs_len", "result.size") + builder.ExportFunction(functionFdReaddir, a.FdReaddir, + functionFdReaddir, "fd", "buf", "buf_len", "cookie", "result.bufused") + builder.ExportFunction(functionFdRenumber, a.FdRenumber, + 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).