Refactors host function tests to stub with wasm (#710)

This refactors host functions with no-op or constant returns to be
implemented with wasm instead of the host function bridge. This allows
better performance.

This also breaks up and makes WASI tests consistent, in a way that shows
parameter name drifts easier.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-07-21 15:49:55 +08:00
committed by GitHub
parent 303b14e67c
commit b98a11e9c3
36 changed files with 3342 additions and 3839 deletions

View File

@@ -18,7 +18,9 @@
// these functions.
//
// See wasi_snapshot_preview1.Instantiate and
// https://www.assemblyscript.org/concepts.html#special-imports
// * https://www.assemblyscript.org/concepts.html#special-imports
// * https://www.assemblyscript.org/concepts.html#targeting-wasi
// * https://www.assemblyscript.org/compiler.html#compiler-options
package assemblyscript
import (
@@ -114,24 +116,26 @@ func (e *functionExporter) WithTraceToStderr() FunctionExporter {
// ExportFunctions implements FunctionExporter.ExportFunctions
func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode}
builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
var abortFn fnAbort
if e.abortMessageDisabled {
abortFn = abort
} else {
abortFn = abortWithMessage
}
var traceFn interface{}
switch e.traceMode {
case traceDisabled:
traceFn = traceNoop
case traceStdout:
traceFn = traceToStdout
case traceStderr:
traceFn = traceToStderr
}
builder.ExportFunction("abort", abortFn, "~lib/builtins/abort",
"message", "fileName", "lineNumber", "columnNumber")
builder.ExportFunction("trace", env.trace, "~lib/builtins/trace",
builder.ExportFunction("trace", traceFn, "~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
// user didn't add `import "wasi"` to their entry file.
//
// See https://www.assemblyscript.org/concepts.html#special-imports
// See https://www.assemblyscript.org/concepts.html#targeting-wasi
// See https://www.assemblyscript.org/compiler.html#compiler-options
// See https://github.com/AssemblyScript/assemblyscript/issues/1562
type assemblyscript struct {
abortMessageDisabled bool
traceMode traceMode
builder.ExportFunction("seed", seed, "~lib/builtins/seed")
}
// abort is called on unrecoverable errors. This is typically present in Wasm
@@ -146,27 +150,25 @@ type assemblyscript struct {
// (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18
func (a *assemblyscript) abort(
ctx context.Context,
mod api.Module,
message uint32,
fileName uint32,
lineNumber uint32,
columnNumber uint32,
) {
if !a.abortMessageDisabled {
sysCtx := mod.(*wasm.CallContext).Sys
msg, err := readAssemblyScriptString(ctx, mod, message)
if err != nil {
return
}
fn, err := readAssemblyScriptString(ctx, mod, fileName)
if err != nil {
return
}
_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
}
type fnAbort func(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
)
// abortWithMessage implements fnAbort
func abortWithMessage(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
) {
sysCtx := mod.(*wasm.CallContext).Sys
msg := requireAssemblyScriptString(ctx, mod, "message", message)
fn := requireAssemblyScriptString(ctx, mod, "fileName", fileName)
_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
abort(ctx, mod, message, fileName, lineNumber, columnNumber)
}
// abortWithMessage implements fnAbort ignoring the message.
func abort(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
) {
// AssemblyScript expects the exit code to be 255
// See https://github.com/AssemblyScript/assemblyscript/blob/v0.20.13/tests/compiler/wasi/abort.js#L14
exitCode := uint32(255)
@@ -178,7 +180,27 @@ func (a *assemblyscript) abort(
panic(sys.NewExitError(mod.Name(), exitCode))
}
// trace implements the same named function in AssemblyScript. Ex.
// traceNoop implements trace in wasm to avoid host call overhead on no-op.
var traceNoop = &wasm.Func{
Type: wasm.MustFunctionType(traceToStdout),
Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
}
// traceToStdout implements trace to the configured Stdout.
func traceToStdout(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
) {
traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stdout())
}
// traceToStdout implements trace to the configured Stderr.
func traceToStderr(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
) {
traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stderr())
}
// traceTo implements the function "trace" in AssemblyScript. Ex.
// trace('Hello World!')
//
// Here's the import in a user's module that ends up using this, in WebAssembly
@@ -186,26 +208,13 @@ func (a *assemblyscript) abort(
// (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
func (a *assemblyscript) trace(
func traceTo(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
writer io.Writer,
) {
var writer io.Writer
switch a.traceMode {
case traceDisabled:
return
case traceStdout:
writer = mod.(*wasm.CallContext).Sys.Stdout()
case traceStderr:
writer = mod.(*wasm.CallContext).Sys.Stderr()
}
msg, err := readAssemblyScriptString(ctx, mod, message)
if err != nil {
panic(err)
}
var ret strings.Builder
ret.WriteString("trace: ")
ret.WriteString(msg)
ret.WriteString(requireAssemblyScriptString(ctx, mod, "message", message))
if nArgs >= 1 {
ret.WriteString(" ")
ret.WriteString(formatFloat(arg0))
@@ -227,8 +236,7 @@ func (a *assemblyscript) trace(
ret.WriteString(formatFloat(arg4))
}
ret.WriteByte('\n')
_, err = writer.Write([]byte(ret.String()))
if err != nil {
if _, err := writer.Write([]byte(ret.String())); err != nil {
panic(err)
}
}
@@ -245,7 +253,7 @@ func formatFloat(f float64) string {
// (import "env" "seed" (func $~lib/builtins/seed (result f64)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111
func (a *assemblyscript) seed(mod api.Module) float64 {
func seed(mod api.Module) float64 {
randSource := mod.(*wasm.CallContext).Sys.RandSource()
v, err := ieee754.DecodeFloat64(randSource)
if err != nil {
@@ -254,21 +262,21 @@ func (a *assemblyscript) seed(mod api.Module) float64 {
return v
}
// readAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
func readAssemblyScriptString(ctx context.Context, mod api.Module, offset uint32) (string, error) {
// requireAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
func requireAssemblyScriptString(ctx context.Context, mod api.Module, fieldName string, offset uint32) string {
// Length is four bytes before pointer.
byteCount, ok := mod.Memory().ReadUint32Le(ctx, offset-4)
if !ok {
return "", fmt.Errorf("Memory.ReadUint32Le(%d) out of range", offset-4)
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
if byteCount%2 != 0 {
return "", fmt.Errorf("read an odd number of bytes for utf-16 string: %d", byteCount)
panic(fmt.Errorf("invalid UTF-16 reading %s", fieldName))
}
buf, ok := mod.Memory().Read(ctx, offset, byteCount)
if !ok {
return "", fmt.Errorf("Memory.Read(%d, %d) out of range", offset, byteCount)
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return decodeUTF16(buf), nil
return decodeUTF16(buf)
}
func decodeUTF16(b []byte) string {

View File

@@ -19,40 +19,29 @@ import (
"github.com/tetratelabs/wazero/sys"
)
var abortWat = `(module
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
(memory 1 1)
(export "abort" (func 0))
)`
var seedWat = `(module
(import "env" "seed" (func $~lib/builtins/seed (result f64)))
(memory 1 1)
(export "seed" (func 0))
)`
var traceWat = `(module
(import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
(memory 1 1)
(export "trace" (func 0))
)`
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
func TestAbort(t *testing.T) {
var stderr bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
defer r.Close(testCtx)
tests := []struct {
name string
abortFn fnAbort
exporter FunctionExporter
expected string
}{
{
name: "enabled",
abortFn: abortWithMessage,
exporter: NewFunctionExporter(),
expected: "message at filename:1:2\n",
},
{
name: "disabled",
abortFn: abort,
exporter: NewFunctionExporter().WithAbortMessageDisabled(),
expected: "",
},
@@ -62,42 +51,26 @@ func TestAbort(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var out, log bytes.Buffer
defer stderr.Reset()
// Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
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(ctx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStderr(&out))
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
_, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2)
err := require.CapturePanic(func() {
tc.abortFn(testCtx, mod, messageOff, 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())
require.Equal(t, tc.expected, stderr.String())
})
}
}
func TestAbort_Error(t *testing.T) {
var stderr bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
defer r.Close(testCtx)
tests := []struct {
name string
messageUTF16 []byte
@@ -126,109 +99,25 @@ func TestAbort_Error(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var out, log bytes.Buffer
defer stderr.Reset()
// Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
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(ctx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(&out)
mod, err := r.InstantiateModule(ctx, compiled, exporter)
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, 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())
// Since abort panics, any opcodes afterwards cannot be reached.
_ = require.CapturePanic(func() {
abortWithMessage(testCtx, mod, messageOff, filenameOff, 1, 2)
})
require.Equal(t, "", stderr.String()) // nothing output if strings fail to read.
})
}
}
var unreachableAfterAbort = `(module
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
(func $main
i32.const 0
i32.const 0
i32.const 0
i32.const 0
call $~lib/builtins/abort
unreachable ;; If abort doesn't panic, this code is reached.
)
(start $main)
)`
// TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked.
func TestAbort_UnreachableAfter(t *testing.T) {
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)
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(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) {
var log bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().
WithRandSource(bytes.NewReader([]byte{0, 1, 2, 3, 4, 5, 6, 7})))
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}
_, err := Instantiate(ctx, r)
require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err)
code, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
require.NoError(t, err)
seedFn := mod.ExportedFunction("seed")
_, err = seedFn.Call(ctx)
require.NoError(t, err)
// If this test doesn't break, the seed is deterministic.
require.Equal(t, `==> env.~lib/builtins/seed()
<== (7.949928895127363e-275)
`, log.String())
require.Equal(t, 7.949928895127363e-275, seed(mod))
}
func TestSeed_error(t *testing.T) {
@@ -240,16 +129,12 @@ func TestSeed_error(t *testing.T) {
{
name: "not 8 bytes",
source: bytes.NewReader([]byte{0, 1}),
expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
wasm stack trace:
env.~lib/builtins/seed() f64`,
expectedErr: `error reading random seed: unexpected EOF`,
},
{
name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading random seed: ice cream (recovered by wazero)
wasm stack trace:
env.~lib/builtins/seed() f64`,
expectedErr: `error reading random seed: ice cream`,
},
}
@@ -257,36 +142,17 @@ wasm stack trace:
tc := tt
t.Run(tc.name, func(t *testing.T) {
var log bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().WithRandSource(tc.source))
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)
_, err := Instantiate(ctx, r)
require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err)
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(ctx, compiled, exporter)
require.NoError(t, err)
_, err = mod.ExportedFunction("seed").Call(ctx)
err := require.CapturePanic(func() { seed(mod) })
require.EqualError(t, err, tc.expectedErr)
require.Equal(t, `==> env.~lib/builtins/seed()
`, log.String())
})
}
}
func TestTrace(t *testing.T) {
// TestFunctionExporter_Trace ensures the trace output is according to configuration.
func TestFunctionExporter_Trace(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)
<== ()
@@ -302,7 +168,8 @@ func TestTrace(t *testing.T) {
exporter: NewFunctionExporter(),
params: noArgs,
expected: "",
expectedLog: noArgsLog,
// expect no host call since it is disabled. ==> is host and --> is wasm.
expectedLog: strings.ReplaceAll(noArgsLog, "==", "--"),
},
{
name: "ToStderr",
@@ -359,10 +226,10 @@ func TestTrace(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var out, log bytes.Buffer
var stderr, functionLog bytes.Buffer
// Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&functionLog))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
defer r.Close(ctx)
@@ -372,7 +239,11 @@ func TestTrace(t *testing.T) {
_, err := envBuilder.Instantiate(ctx, r)
require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat)
traceWasm, err := watzero.Wat2Wasm(`(module
(import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
(memory 1 1)
(export "trace" (func 0))
)`)
require.NoError(t, err)
code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
@@ -380,9 +251,9 @@ func TestTrace(t *testing.T) {
config := wazero.NewModuleConfig()
if strings.Contains("ToStderr", tc.name) {
config = config.WithStderr(&out)
config = config.WithStderr(&stderr)
} else {
config = config.WithStdout(&out)
config = config.WithStdout(&stderr)
}
mod, err := r.InstantiateModule(ctx, code, config)
@@ -396,8 +267,8 @@ func TestTrace(t *testing.T) {
_, 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())
require.Equal(t, tc.expected, stderr.String())
require.Equal(t, tc.expectedLog, functionLog.String())
})
}
}
@@ -406,24 +277,20 @@ func TestTrace_error(t *testing.T) {
tests := []struct {
name string
message []byte
out io.Writer
stderr io.Writer
expectedErr string
}{
{
name: "not 8 bytes",
message: encodeUTF16("hello")[:5],
out: &bytes.Buffer{},
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
wasm stack trace:
env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
stderr: &bytes.Buffer{},
expectedErr: `invalid UTF-16 reading message`,
},
{
name: "error writing",
message: encodeUTF16("hello"),
out: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream (recovered by wazero)
wasm stack trace:
env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
stderr: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream`,
},
}
@@ -431,43 +298,27 @@ wasm stack trace:
tc := tt
t.Run(tc.name, func(t *testing.T) {
var log bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(tc.stderr))
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)
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(ctx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
mod, err := r.InstantiateModule(ctx, compiled, exporter)
require.NoError(t, err)
ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(tc.message)))
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
require.True(t, ok)
ok = mod.Memory().Write(ctx, uint32(4), tc.message)
ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(ctx, 4, 0, 0, 0, 0, 0, 0)
err := require.CapturePanic(func() {
traceToStderr(testCtx, mod, 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())
})
}
}
func Test_readAssemblyScriptString(t *testing.T) {
func Test_requireAssemblyScriptString(t *testing.T) {
var stderr bytes.Buffer
mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
defer r.Close(testCtx)
tests := []struct {
name string
memory func(context.Context, api.Memory)
@@ -476,42 +327,42 @@ func Test_readAssemblyScriptString(t *testing.T) {
}{
{
name: "success",
memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(ctx, 0, 10)
memory: func(testCtx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10)
b := encodeUTF16("hello")
memory.Write(ctx, 4, b)
memory.Write(testCtx, 4, b)
},
offset: 4,
expected: "hello",
},
{
name: "can't read size",
memory: func(ctx context.Context, memory api.Memory) {
memory: func(testCtx context.Context, memory api.Memory) {
b := encodeUTF16("hello")
memory.Write(ctx, 0, b)
memory.Write(testCtx, 0, b)
},
offset: 0, // will attempt to read size from offset -4
expectedErr: "Memory.ReadUint32Le(4294967292) out of range",
expectedErr: "out of memory reading message",
},
{
name: "odd size",
memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(ctx, 0, 9)
memory: func(testCtx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 9)
b := encodeUTF16("hello")
memory.Write(ctx, 4, b)
memory.Write(testCtx, 4, b)
},
offset: 4,
expectedErr: "read an odd number of bytes for utf-16 string: 9",
expectedErr: "invalid UTF-16 reading message",
},
{
name: "can't read string",
memory: func(ctx context.Context, memory api.Memory) {
memory.WriteUint32Le(ctx, 0, 10_000_000) // set size to too large value
memory: func(testCtx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value
b := encodeUTF16("hello")
memory.Write(ctx, 4, b)
memory.Write(testCtx, 4, b)
},
offset: 4,
expectedErr: "Memory.Read(4, 10000000) out of range",
expectedErr: "out of memory reading message",
},
}
@@ -519,41 +370,35 @@ func Test_readAssemblyScriptString(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
mod, err := r.NewModuleBuilder("mod").
ExportMemory("memory", 1).
Instantiate(testCtx, r)
require.NoError(t, err)
tc.memory(testCtx, mod.Memory())
s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset))
if tc.expectedErr != "" {
err := require.CapturePanic(func() {
_ = requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset))
})
require.EqualError(t, err, tc.expectedErr)
} else {
require.NoError(t, err)
s := requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset))
require.Equal(t, tc.expected, s)
}
})
}
}
func writeAbortMessageAndFileName(ctx context.Context, t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
off := 0
ok := mem.WriteUint32Le(ctx, uint32(off), uint32(len(messageUTF16)))
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (uint32, uint32) {
off := uint32(0)
ok := mem.WriteUint32Le(testCtx, off, uint32(len(messageUTF16)))
require.True(t, ok)
off += 4
messageOff := off
ok = mem.Write(ctx, uint32(off), messageUTF16)
ok = mem.Write(testCtx, off, messageUTF16)
require.True(t, ok)
off += len(messageUTF16)
ok = mem.WriteUint32Le(ctx, uint32(off), uint32(len(fileNameUTF16)))
off += uint32(len(messageUTF16))
ok = mem.WriteUint32Le(testCtx, off, uint32(len(fileNameUTF16)))
require.True(t, ok)
off += 4
filenameOff := off
ok = mem.Write(ctx, uint32(off), fileNameUTF16)
ok = mem.Write(testCtx, off, fileNameUTF16)
require.True(t, ok)
return messageOff, filenameOff
}
@@ -575,3 +420,16 @@ type errWriter struct {
func (w *errWriter) Write([]byte) (int, error) {
return 0, w.err
}
func requireModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer) {
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
compiled, err := r.NewModuleBuilder(t.Name()).
ExportMemoryWithMax("memory", 1, 1).
Compile(testCtx, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
return mod, r
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
// Instantiate instantiates the "env" module used by Emscripten into the
@@ -50,14 +51,10 @@ type functionExporter struct{}
// ExportFunctions implements FunctionExporter.ExportFunctions
func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
env := &emscripten{}
builder.ExportFunction("emscripten_notify_memory_growth", env.emscriptenNotifyMemoryGrowth,
builder.ExportFunction("emscripten_notify_memory_growth", emscriptenNotifyMemoryGrowth,
"emscripten_notify_memory_growth", "memory_index")
}
// emscripten implements common imports used by standalone wasm.
type emscripten struct{}
// emscriptenNotifyMemoryGrowth is called when wasm is compiled with
// `-s ALLOW_MEMORY_GROWTH` and a "memory.grow" instruction succeeded.
// The memoryIndex parameter will be zero until "multi-memory" is implemented.
@@ -73,4 +70,7 @@ type emscripten struct{}
//
// See https://github.com/emscripten-core/emscripten/blob/3.1.16/system/lib/standalone/standalone.c#L118
// and https://emscripten.org/docs/api_reference/emscripten.h.html#abi-functions
func (a *emscripten) emscriptenNotifyMemoryGrowth(uint32) {}
var emscriptenNotifyMemoryGrowth = &wasm.Func{
Type: &wasm.FunctionType{Params: []wasm.ValueType{wasm.ValueTypeI32}},
Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
}

View File

@@ -20,6 +20,7 @@ var growWasm []byte
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
// TestGrow is an integration test until we have an Emscripten example.
func TestGrow(t *testing.T) {
var log bytes.Buffer
@@ -40,6 +41,6 @@ func TestGrow(t *testing.T) {
require.Error(t, err)
require.Zero(t, err.(*sys.ExitError).ExitCode())
// We expect the memory no-op memory growth hook to be invoked.
require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)")
// We expect the memory no-op memory growth hook to be invoked as wasm.
require.Contains(t, log.String(), "--> env.emscripten_notify_memory_growth(memory_index=0)")
}

View File

@@ -261,8 +261,8 @@ func Test_loggingListener(t *testing.T) {
},
}
out := bytes.NewBuffer(nil)
lf := experimental.NewLoggingListenerFactory(out)
var out bytes.Buffer
lf := experimental.NewLoggingListenerFactory(&out)
fnV := reflect.ValueOf(func() {})
for _, tt := range tests {
tc := tt

View File

@@ -7,6 +7,7 @@ import (
"testing"
"unsafe"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/enginetest"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -22,12 +23,17 @@ var et = &engineTester{}
// engineTester implements enginetest.EngineTester.
type engineTester struct{}
// NewEngine implements enginetest.EngineTester NewEngine.
// ListenerFactory implements the same method as documented on enginetest.EngineTester.
func (e *engineTester) ListenerFactory() experimental.FunctionListenerFactory {
return nil
}
// NewEngine implements the same method as documented on enginetest.EngineTester.
func (e *engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return newEngine(enabledFeatures)
}
// InitTables implements enginetest.EngineTester InitTables.
// InitTables implements the same method as documented on enginetest.EngineTester.
func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int, tableInits []wasm.TableInitEntry) [][]wasm.Reference {
references := make([][]wasm.Reference, len(tableIndexToLen))
for tableIndex, l := range tableIndexToLen {
@@ -44,7 +50,7 @@ func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.
return references
}
// CompiledFunctionPointerValue implements enginetest.EngineTester CompiledFunctionPointerValue.
// CompiledFunctionPointerValue implements the same method as documented on enginetest.EngineTester.
func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 {
internal := me.(*moduleEngine)
return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex])))
@@ -55,7 +61,7 @@ func TestCompiler_Engine_NewModuleEngine(t *testing.T) {
enginetest.RunTestEngine_NewModuleEngine(t, et)
}
func TestInterpreter_Engine_InitializeFuncrefGlobals(t *testing.T) {
func TestCompiler_Engine_InitializeFuncrefGlobals(t *testing.T) {
enginetest.RunTestEngine_InitializeFuncrefGlobals(t, et)
}

View File

@@ -111,18 +111,13 @@ func (ce *callEngine) popValue() (v uint64) {
return
}
// peekValues peeks api.ValueType values from the stack and returns them in reverse order.
// peekValues peeks api.ValueType values from the stack and returns them.
func (ce *callEngine) peekValues(count int) []uint64 {
if count == 0 {
return nil
}
stackLen := len(ce.stack)
peeked := ce.stack[stackLen-count : stackLen]
values := make([]uint64, 0, count)
for i := count - 1; i >= 0; i-- {
values = append(values, peeked[i])
}
return values
return ce.stack[stackLen-count : stackLen]
}
func (ce *callEngine) drop(r *wazeroir.InclusiveRange) {

View File

@@ -1,6 +1,7 @@
package interpreter
import (
"bytes"
"context"
"fmt"
"math"
@@ -8,6 +9,7 @@ import (
"testing"
"unsafe"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/buildoptions"
"github.com/tetratelabs/wazero/internal/testing/enginetest"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -24,7 +26,7 @@ func TestInterpreter_peekValues(t *testing.T) {
ce.stack = []uint64{5, 4, 3, 2, 1}
require.Nil(t, ce.peekValues(0))
require.Equal(t, []uint64{1, 2}, ce.peekValues(2))
require.Equal(t, []uint64{2, 1}, ce.peekValues(2))
}
func TestInterpreter_CallEngine_PushFrame(t *testing.T) {
@@ -62,10 +64,16 @@ func TestInterpreter_CallEngine_PushFrame_StackOverflow(t *testing.T) {
// et is used for tests defined in the enginetest package.
var et = &engineTester{}
var functionLog bytes.Buffer
var listenerFactory = experimental.NewLoggingListenerFactory(&functionLog)
// engineTester implements enginetest.EngineTester.
type engineTester struct{}
func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory {
return listenerFactory
}
// NewEngine implements enginetest.EngineTester NewEngine.
func (e engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return NewEngine(enabledFeatures)
@@ -107,15 +115,96 @@ func TestInterpreter_Engine_NewModuleEngine_InitTable(t *testing.T) {
}
func TestInterpreter_ModuleEngine_Call(t *testing.T) {
defer functionLog.Reset()
enginetest.RunTestModuleEngine_Call(t, et)
require.Equal(t, `--> .$0(1,2)
<-- (1,2)
`, functionLog.String())
}
func TestInterpreter_ModuleEngine_Call_HostFn(t *testing.T) {
defer functionLog.Reset()
enginetest.RunTestModuleEngine_Call_HostFn(t, et)
require.Equal(t, `==> .$0(3)
<== (3)
--> imported.wasm_div_by(1)
<-- (1)
==> host.host_div_by(1)
<== (1)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
--> importing.call_import->call->host_div_by(1)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
<-- (1)
`, functionLog.String())
}
func TestInterpreter_ModuleEngine_Call_Errors(t *testing.T) {
defer functionLog.Reset()
enginetest.RunTestModuleEngine_Call_Errors(t, et)
// TODO: Currently, the listener doesn't get notified on errors as they are
// implemented with panic. This means the end hooks aren't make resulting
// in dangling logs like this:
// ==> host.host_div_by(4294967295)
// instead of seeing a return like
// <== DivByZero
require.Equal(t, `==> host.host_div_by(1)
<== (1)
==> host.host_div_by(1)
<== (1)
--> imported.wasm_div_by(1)
<-- (1)
--> imported.wasm_div_by(1)
<-- (1)
--> imported.wasm_div_by(0)
--> imported.wasm_div_by(1)
<-- (1)
==> host.host_div_by(4294967295)
==> host.host_div_by(1)
<== (1)
==> host.host_div_by(0)
==> host.host_div_by(1)
<== (1)
--> imported.call->host_div_by(4294967295)
==> host.host_div_by(4294967295)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
--> importing.call_import->call->host_div_by(0)
--> imported.call->host_div_by(0)
==> host.host_div_by(0)
--> importing.call_import->call->host_div_by(1)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
<-- (1)
--> importing.call_import->call->host_div_by(4294967295)
--> imported.call->host_div_by(4294967295)
==> host.host_div_by(4294967295)
--> importing.call_import->call->host_div_by(1)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
<-- (1)
--> importing.call_import->call->host_div_by(0)
--> imported.call->host_div_by(0)
==> host.host_div_by(0)
--> importing.call_import->call->host_div_by(1)
--> imported.call->host_div_by(1)
==> host.host_div_by(1)
<== (1)
<-- (1)
<-- (1)
`, functionLog.String())
}
func TestInterpreter_ModuleEngine_Memory(t *testing.T) {

View File

@@ -14,6 +14,16 @@ import (
"github.com/tetratelabs/wazero/sys"
)
func TestContext_FS(t *testing.T) {
sysCtx := DefaultContext(testfs.FS{})
require.Equal(t, NewFSContext(testfs.FS{}), sysCtx.FS(testCtx))
// can override to something else
fsc := NewFSContext(testfs.FS{"foo": &testfs.File{}})
require.Equal(t, fsc, sysCtx.FS(context.WithValue(testCtx, FSKey{}, fsc)))
}
func TestDefaultSysContext(t *testing.T) {
sysCtx, err := NewContext(
0, // max

View File

@@ -24,6 +24,7 @@ import (
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -37,9 +38,13 @@ var (
type EngineTester interface {
NewEngine(enabledFeatures wasm.Features) wasm.Engine
ListenerFactory() experimental.FunctionListenerFactory
// InitTables returns expected table contents ([]wasm.Reference) per table.
InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int,
tableInits []wasm.TableInitEntry) [][]wasm.Reference
// CompiledFunctionPointerValue returns the opaque compiledFunction's pointer for the `funcIndex`.
CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64
}
@@ -81,7 +86,7 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) {
// To use the function, we first need to add it to a module.
instance := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
fns := instance.BuildFunctions(m, nil)
fns := instance.BuildFunctions(m, buildListeners(et.ListenerFactory(), m))
me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil)
require.NoError(t, err)
@@ -106,14 +111,24 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) {
}
func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
e := et.NewEngine(wasm.Features20191205)
e := et.NewEngine(wasm.Features20220419)
// Define a basic function which defines one parameter. This is used to test results when incorrect arity is used.
// Define a basic function which defines two parameters and two results.
// This is used to test results when incorrect arity is used.
i64 := wasm.ValueTypeI64
m := &wasm.Module{
TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}, ParamNumInUint64: 1, ResultNumInUint64: 1}},
TypeSection: []*wasm.FunctionType{
{
Params: []wasm.ValueType{i64, i64},
Results: []wasm.ValueType{i64, i64},
ParamNumInUint64: 2,
ResultNumInUint64: 2,
},
},
FunctionSection: []uint32{0},
CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{wasm.ValueTypeI64}}},
CodeSection: []*wasm.Code{
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeEnd}},
},
}
m.BuildFunctionDefinitions()
err := e.CompileModule(testCtx, m)
@@ -121,7 +136,7 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
// To use the function, we first need to add it to a module.
module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
module.Functions = module.BuildFunctions(m, nil)
module.Functions = module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m))
// Compile the module
me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions, nil, nil)
@@ -130,18 +145,18 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
// Ensure the base case doesn't fail: A single parameter should work as that matches the function signature.
fn := module.Functions[0]
results, err := me.Call(testCtx, module.CallCtx, fn, 3)
results, err := me.Call(testCtx, module.CallCtx, fn, 1, 2)
require.NoError(t, err)
require.Equal(t, uint64(3), results[0])
require.Equal(t, []uint64{1, 2}, results)
t.Run("errs when not enough parameters", func(t *testing.T) {
_, err := me.Call(testCtx, module.CallCtx, fn)
require.EqualError(t, err, "expected 1 params, but passed 0")
require.EqualError(t, err, "expected 2 params, but passed 0")
})
t.Run("errs when too many parameters", func(t *testing.T) {
_, err := me.Call(testCtx, module.CallCtx, fn, 1, 2)
require.EqualError(t, err, "expected 1 params, but passed 2")
_, err := me.Call(testCtx, module.CallCtx, fn, 1, 2, 3)
require.EqualError(t, err, "expected 2 params, but passed 3")
})
}
@@ -186,7 +201,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
require.NoError(t, err)
module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
fns := module.BuildFunctions(m, nil)
fns := module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m))
var func1, func2 = uint32(2), uint32(1)
tableInits := []wasm.TableInitEntry{
@@ -221,7 +236,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
require.NoError(t, err)
imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
importedFunctions := imported.BuildFunctions(importedModule, nil)
importedFunctions := imported.BuildFunctions(importedModule, buildListeners(et.ListenerFactory(), importedModule))
// Imported functions are compiled before the importing module is instantiated.
importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil)
@@ -245,7 +260,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
}
importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
fns := importing.BuildFunctions(importingModule, nil)
fns := importing.BuildFunctions(importingModule, buildListeners(et.ListenerFactory(), importingModule))
importingMe, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, fns, tables, tableInits)
require.NoError(t, err)
@@ -272,7 +287,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
err := e.CompileModule(testCtx, importedModule)
require.NoError(t, err)
imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
importedFunctions := imported.BuildFunctions(importedModule, nil)
importedFunctions := imported.BuildFunctions(importedModule, buildListeners(et.ListenerFactory(), importedModule))
// Imported functions are compiled before the importing module is instantiated.
importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil)
@@ -292,7 +307,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
require.NoError(t, err)
importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}}
fns := importing.BuildFunctions(importingModule, nil)
fns := importing.BuildFunctions(importingModule, buildListeners(et.ListenerFactory(), importingModule))
var func1, func2 = uint32(0), uint32(4)
tableInits := []wasm.TableInitEntry{
@@ -341,7 +356,7 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester
_, ns := wasm.NewStore(features, e)
modCtx := wasm.NewCallContext(ns, module, nil)
fns := module.BuildFunctions(m, nil)
fns := module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m))
me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil)
require.NoError(t, err)
@@ -359,7 +374,7 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
e := et.NewEngine(wasm.Features20191205)
host, imported, importing, close := setupCallTests(t, e)
host, imported, importing, close := setupCallTests(t, e, et.ListenerFactory())
defer close()
// Ensure the base case doesn't fail: A single parameter should work as that matches the function signature.
@@ -405,7 +420,7 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
e := et.NewEngine(wasm.Features20191205)
host, imported, importing, close := setupCallTests(t, e)
host, imported, importing, close := setupCallTests(t, e, et.ListenerFactory())
defer close()
tests := []struct {
@@ -591,7 +606,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
var memory api.Memory = module.Memory
// To use functions, we need to instantiate them (associate them with a ModuleInstance).
module.Functions = module.BuildFunctions(m, nil)
module.Functions = module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m))
module.BuildExports(m.ExportSection)
grow, init := module.Functions[0], module.Functions[1]
@@ -666,7 +681,7 @@ func divBy(d uint32) uint32 {
return 1 / d // go panics if d == 0
}
func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) {
func setupCallTests(t *testing.T, e wasm.Engine, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) {
i32 := wasm.ValueTypeI32
ft := &wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1}
@@ -686,7 +701,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo
err := e.CompileModule(testCtx, hostModule)
require.NoError(t, err)
host := &wasm.ModuleInstance{Name: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}}
host.Functions = host.BuildFunctions(hostModule, nil)
host.Functions = host.BuildFunctions(hostModule, buildListeners(fnlf, hostModule))
host.BuildExports(hostModule.ExportSection)
hostFn := host.Exports[hostFnName].Function
@@ -721,7 +736,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo
require.NoError(t, err)
imported := &wasm.ModuleInstance{Name: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}}
importedFunctions := imported.BuildFunctions(importedModule, nil)
importedFunctions := imported.BuildFunctions(importedModule, buildListeners(fnlf, importedModule))
imported.Functions = append([]*wasm.FunctionInstance{hostFn}, importedFunctions...)
imported.BuildExports(importedModule.ExportSection)
callHostFn := imported.Exports[callHostFnName].Function
@@ -754,7 +769,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo
// Add the exported function.
importing := &wasm.ModuleInstance{Name: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}}
importingFunctions := importing.BuildFunctions(importingModule, nil)
importingFunctions := importing.BuildFunctions(importingModule, buildListeners(fnlf, importingModule))
importing.Functions = append([]*wasm.FunctionInstance{callHostFn}, importingFunctions...)
importing.BuildExports(importingModule.ExportSection)
@@ -782,3 +797,15 @@ func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
// callEngineModuleContextModuleInstanceAddressOffset
module.CallCtx = wasm.NewCallContext(nil, module, nil)
}
func buildListeners(factory experimental.FunctionListenerFactory, m *wasm.Module) []experimental.FunctionListener {
if factory == nil || len(m.FunctionSection) == 0 {
return nil
}
listeners := make([]experimental.FunctionListener, len(m.FunctionSection))
importCount := m.ImportFuncCount()
for i := 0; i < len(listeners); i++ {
listeners[i] = factory.NewListener(m.FunctionDefinitionSection[uint32(i)+importCount])
}
return listeners
}

View File

@@ -148,8 +148,19 @@ func newModuleVal(m api.Module) reflect.Value {
return val
}
// MustFunctionType returns the function type corresponding to the function
// signature or panics if invalid.
func MustFunctionType(fn interface{}) *FunctionType {
fnV := reflect.ValueOf(fn)
_, ft, err := getFunctionType(&fnV)
if err != nil {
panic(err)
}
return ft
}
// getFunctionType returns the function type corresponding to the function signature or errs if invalid.
func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKind, ft *FunctionType, err error) {
func getFunctionType(fn *reflect.Value) (fk FunctionKind, ft *FunctionType, err error) {
p := fn.Type()
if fn.Kind() != reflect.Func {
@@ -169,14 +180,6 @@ func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKi
rCount := p.NumOut()
if rCount > 1 {
// Guard >1.0 feature multi-value
if err = enabledFeatures.Require(FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return
}
}
ft = &FunctionType{Params: make([]ValueType, p.NumIn()-pOffset), Results: make([]ValueType, rCount)}
ft.CacheNumInUint64()

View File

@@ -87,7 +87,7 @@ func TestGetFunctionType(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rVal := reflect.ValueOf(tc.inputFunc)
fk, ft, err := getFunctionType(&rVal, Features20191205|FeatureMultiValue)
fk, ft, err := getFunctionType(&rVal)
require.NoError(t, err)
require.Equal(t, tc.expectedKind, fk)
require.Equal(t, tc.expectedType, ft)
@@ -122,11 +122,6 @@ func TestGetFunctionTypeErrors(t *testing.T) {
input: func() error { return nil },
expectedErr: "result[0] is an error, which is unsupported",
},
{
name: "multiple results - multi-value not enabled",
input: func() (uint64, uint32) { return 0, 0 },
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "multiple context types",
input: func(api.Module, context.Context) error { return nil },
@@ -149,7 +144,7 @@ func TestGetFunctionTypeErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rVal := reflect.ValueOf(tc.input)
_, _, err := getFunctionType(&rVal, Features20191205)
_, _, err := getFunctionType(&rVal)
require.EqualError(t, err, tc.expectedErr)
})
}
@@ -254,7 +249,7 @@ func TestPopGoFuncParams(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
goFunc := reflect.ValueOf(tc.inputFunc)
fk, _, err := getFunctionType(&goFunc, Features20220419)
fk, _, err := getFunctionType(&goFunc)
require.NoError(t, err)
vals := PopGoFuncParams(&FunctionInstance{Kind: fk, GoFunc: &goFunc}, (&stack{stackVals}).pop)
@@ -407,7 +402,7 @@ func TestCallGoFunc(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
goFunc := reflect.ValueOf(tc.inputFunc)
fk, _, err := getFunctionType(&goFunc, Features20220419)
fk, _, err := getFunctionType(&goFunc)
require.NoError(t, err)
results := CallGoFunc(testCtx, callCtx, &FunctionInstance{Kind: fk, GoFunc: &goFunc}, tc.inputParams)

View File

@@ -9,6 +9,16 @@ import (
"github.com/tetratelabs/wazero/internal/wasmdebug"
)
// Func is a function with an inlined type, typically used for NewHostModule.
type Func struct {
// Type is the equivalent function in the SectionIDType.
// This will resolve to an existing or new element.
Type *FunctionType
// Code is the equivalent function in the SectionIDCode.
Code *Code
}
// NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small.
func NewHostModule(
moduleName string,
@@ -79,7 +89,7 @@ func addFuncs(
nameToGoFunc map[string]interface{},
funcToNames map[string][]string,
enabledFeatures Features,
) error {
) (err error) {
funcCount := uint32(len(nameToGoFunc))
funcNames := make([]string, 0, funcCount)
if m.NameSection == nil {
@@ -100,19 +110,36 @@ func addFuncs(
for idx := Index(0); idx < funcCount; idx++ {
exportName := funcNames[idx]
debugName := wasmdebug.FuncName(moduleName, exportName, idx)
fn := reflect.ValueOf(nameToGoFunc[exportName])
_, functionType, err := getFunctionType(&fn, enabledFeatures)
gf := nameToGoFunc[exportName]
var ft *FunctionType
if hf, ok := gf.(*Func); ok {
ft = hf.Type
m.CodeSection = append(m.CodeSection, hf.Code)
} else {
fn := reflect.ValueOf(gf)
_, ft, err = getFunctionType(&fn)
if err != nil {
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.CodeSection = append(m.CodeSection, &Code{GoFunc: &fn})
}
m.FunctionSection = append(m.FunctionSection, m.maybeAddType(ft))
names := funcToNames[exportName]
namesLen := len(names)
if namesLen > 1 && namesLen-1 != len(ft.Params) {
return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(ft.Params))
}
if len(ft.Results) > 1 {
// Guard >1.0 feature multi-value
if err = enabledFeatures.Require(FeatureMultiValue); err != nil {
err = fmt.Errorf("func[%s] multiple result types invalid as %v", debugName, err)
return
}
}
m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType))
m.CodeSection = append(m.CodeSection, &Code{GoFunc: &fn})
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]})

View File

@@ -248,7 +248,7 @@ const (
// OpcodeRefFunc pushes a funcref value whose index equals the immediate to this opcode.
// This is defined in the reference-types proposal, but necessary for FeatureBulkMemoryOperations as well.
//
// Currently only supported in the constant expression in element segments.
// Currently, this is only supported in the constant expression in element segments.
OpcodeRefFunc = 0xd2
// Below are toggled with FeatureSignExtensionOps

View File

@@ -0,0 +1,92 @@
package wasi_snapshot_preview1
import (
"context"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
functionArgsGet = "args_get"
functionArgsSizesGet = "args_sizes_get"
)
// argsGet is the WASI function named functionArgsGet that reads command-line
// argument data.
//
// Parameters
//
// * argv: offset to begin writing argument offsets in uint32 little-endian
// encoding to api.Memory
// * argsSizesGet result argc * 4 bytes are written to this offset
// * argvBuf: offset to write the null terminated arguments to api.Memory
// * argsSizesGet result argv_buf_size bytes are written to this offset
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoFault: there is not enough memory to write results
//
// For example, if argsSizesGet wrote argc=2 and argvBufSize=5 for arguments:
// "a" and "bc" parameters argv=7 and argvBuf=1, this function writes the below
// to api.Memory:
//
// argvBufSize uint32le uint32le
// +----------------+ +--------+ +--------+
// | | | | | |
// []byte{?, 'a', 0, 'b', 'c', 0, ?, 1, 0, 0, 0, 3, 0, 0, 0, ?}
// argvBuf --^ ^ ^
// argv --| |
// offset that begins "a" --+ |
// offset that begins "bc" --+
//
// See argsSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
func argsGet(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Args(), argv, argvBuf)
}
// argsSizesGet is the WASI function named functionArgsSizesGet that reads
// command-line argument sizes.
//
// Parameters
//
// * resultArgc: offset to write the argument count to api.Memory
// * resultArgvBufSize: offset to write the null-terminated argument length to
// api.Memory
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoFault: there is not enough memory to write results
//
// For example, if args are "a", "bc" and parameters resultArgc=1 and
// resultArgvBufSize=6, this function writes the below to api.Memory:
//
// uint32le uint32le
// +--------+ +--------+
// | | | |
// []byte{?, 2, 0, 0, 0, ?, 5, 0, 0, 0, ?}
// resultArgc --^ ^
// 2 args --+ |
// resultArgvBufSize --|
// len([]byte{'a',0,'b',c',0}) --+
//
// See argsGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
func argsSizesGet(ctx context.Context, mod api.Module, resultArgc, resultArgvBufSize uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
mem := mod.Memory()
if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) {
return ErrnoFault
}
if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) {
return ErrnoFault
}
return ErrnoSuccess
}

View File

@@ -0,0 +1,191 @@
package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func Test_argsGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc"))
defer r.Close(testCtx)
argv := uint32(7) // arbitrary offset
argvBuf := uint32(1) // arbitrary offset
expectedMemory := []byte{
'?', // argvBuf is after this
'a', 0, 'b', 'c', 0, // null terminated "a", "bc"
'?', // argv is after this
1, 0, 0, 0, // little endian-encoded offset of "a"
3, 0, 0, 0, // little endian-encoded offset of "bc"
'?', // stopped after encoding
}
maskMemory(t, testCtx, mod, len(expectedMemory))
// Invoke argsGet and check the memory side effects.
requireErrno(t, ErrnoSuccess, mod, functionArgsGet, uint64(argv), uint64(argvBuf))
require.Equal(t, `
==> wasi_snapshot_preview1.args_get(argv=7,argv_buf=1)
<== ESUCCESS
`, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
}
func Test_argsGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc"))
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here.
tests := []struct {
name string
argv, argvBuf uint32
expectedLog string
}{
{
name: "out-of-memory argv",
argv: memorySize,
argvBuf: validAddress,
expectedLog: `
==> wasi_snapshot_preview1.args_get(argv=65536,argv_buf=0)
<== EFAULT
`,
},
{
name: "out-of-memory argvBuf",
argv: validAddress,
argvBuf: memorySize,
expectedLog: `
==> wasi_snapshot_preview1.args_get(argv=0,argv_buf=65536)
<== EFAULT
`,
},
{
name: "argv exceeds the maximum valid address by 1",
// 4*argCount is the size of the result of the pointers to args, 4 is the size of uint32
argv: memorySize - 4*2 + 1,
argvBuf: validAddress,
expectedLog: `
==> wasi_snapshot_preview1.args_get(argv=65529,argv_buf=0)
<== EFAULT
`,
},
{
name: "argvBuf exceeds the maximum valid address by 1",
argv: validAddress,
// "a", "bc" size = size of "a0bc0" = 5
argvBuf: memorySize - 5 + 1,
expectedLog: `
==> wasi_snapshot_preview1.args_get(argv=0,argv_buf=65532)
<== EFAULT
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
requireErrno(t, ErrnoFault, mod, functionArgsGet, uint64(tc.argv), uint64(tc.argvBuf))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func Test_argsSizesGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc"))
defer r.Close(testCtx)
resultArgc := uint32(1) // arbitrary offset
resultArgvBufSize := uint32(6) // arbitrary offset
expectedMemory := []byte{
'?', // resultArgc is after this
0x2, 0x0, 0x0, 0x0, // little endian-encoded arg count
'?', // resultArgvBufSize is after this
0x5, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
'?', // stopped after encoding
}
maskMemory(t, testCtx, mod, len(expectedMemory))
// Invoke argsSizesGet and check the memory side effects.
requireErrno(t, ErrnoSuccess, mod, functionArgsSizesGet, uint64(resultArgc), uint64(resultArgvBufSize))
require.Equal(t, `
==> wasi_snapshot_preview1.args_sizes_get(result.argc=1,result.argv_buf_size=6)
<== ESUCCESS
`, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
}
func Test_argsSizesGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc"))
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here.
tests := []struct {
name string
argc, argvBufSize uint32
expectedLog string
}{
{
name: "out-of-memory argc",
argc: memorySize,
argvBufSize: validAddress,
expectedLog: `
==> wasi_snapshot_preview1.args_sizes_get(result.argc=65536,result.argv_buf_size=0)
<== EFAULT
`,
},
{
name: "out-of-memory argvBufSize",
argc: validAddress,
argvBufSize: memorySize,
expectedLog: `
==> wasi_snapshot_preview1.args_sizes_get(result.argc=0,result.argv_buf_size=65536)
<== EFAULT
`,
},
{
name: "argc exceeds the maximum valid address by 1",
argc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args
argvBufSize: validAddress,
expectedLog: `
==> wasi_snapshot_preview1.args_sizes_get(result.argc=65533,result.argv_buf_size=0)
<== EFAULT
`,
},
{
name: "argvBufSize exceeds the maximum valid size by 1",
argc: validAddress,
argvBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le
expectedLog: `
==> wasi_snapshot_preview1.args_sizes_get(result.argc=0,result.argv_buf_size=65533)
<== EFAULT
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
requireErrno(t, ErrnoFault, mod, functionArgsSizesGet, uint64(tc.argc), uint64(tc.argvBufSize))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}

View File

@@ -9,42 +9,40 @@ import (
)
const (
// functionClockResGet returns the resolution of a clock.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
functionClockResGet = "clock_res_get"
// importClockResGet is the WebAssembly 1.0 Text format import of functionClockResGet.
importClockResGet = `(import "wasi_snapshot_preview1" "clock_res_get"
(func $wasi.clock_res_get (param $id i32) (param $result.resolution i32) (result (;errno;) i32)))`
// functionClockTimeGet returns the time value of a clock.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
functionClockTimeGet = "clock_time_get"
// importClockTimeGet is the WebAssembly 1.0 Text format import of functionClockTimeGet.
importClockTimeGet = `(import "wasi_snapshot_preview1" "clock_time_get"
(func $wasi.clock_time_get (param $id i32) (param $precision i64) (param $result.timestamp i32) (result (;errno;) i32)))`
)
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clockid-enumu32
const (
// clockIDRealtime is the clock ID named "realtime" associated with sys.Walltime
// clockIDRealtime is the name ID named "realtime" like sys.Walltime
clockIDRealtime = iota
// clockIDMonotonic is the clock ID named "monotonic" with sys.Nanotime
// clockIDMonotonic is the name ID named "monotonic" like sys.Nanotime
clockIDMonotonic
// clockIDProcessCputime is the unsupported clock ID named "process_cputime_id"
// clockIDProcessCputime is the unsupported "process_cputime_id"
clockIDProcessCputime
// clockIDThreadCputime is the unsupported clock ID named "thread_cputime_id"
// clockIDThreadCputime is the unsupported "thread_cputime_id"
clockIDThreadCputime
)
// ClockResGet is the WASI function named functionClockResGet that returns the resolution of time values returned by ClockTimeGet.
// clockResGet is the WASI function named functionClockResGet that returns the
// resolution of time values returned by clockTimeGet.
//
// * id - The clock id for which to return the time.
// * resultResolution - the offset to write the resolution to mod.Memory
// * the resolution is an uint64 little-endian encoding.
// Parameters
//
// For example, if the resolution is 100ns, this function writes the below to `mod.Memory`:
// * id: clock ID to use
// * resultResolution: offset to write the resolution to api.Memory
// * the resolution is an uint64 little-endian encoding
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoNotsup: the clock ID is not supported.
// * ErrnoInval: the clock ID is invalid.
// * ErrnoFault: there is not enough memory to write results
//
// For example, if the resolution is 100ns, this function writes the below to
// api.Memory:
//
// uint64le
// +-------------------------------------+
@@ -52,11 +50,10 @@ const (
// []byte{?, 0x64, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ?}
// resultResolution --^
//
// Note: importClockResGet shows this signature in the WebAssembly 1.0 Text Format.
// Note: This is similar to `clock_getres` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
// See https://linux.die.net/man/3/clock_getres
func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno {
func clockResGet(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
var resolution uint64 // ns
@@ -66,8 +63,9 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul
case clockIDMonotonic:
resolution = uint64(sysCtx.NanotimeResolution())
case clockIDProcessCputime, clockIDThreadCputime:
// Similar to many other runtimes, we only support realtime and monotonic clocks. Other types
// are slated to be removed from the next version of WASI.
// Similar to many other runtimes, we only support realtime and
// monotonic clocks. Other types are slated to be removed from the next
// version of WASI.
return ErrnoNotsup
default:
return ErrnoInval
@@ -78,15 +76,27 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul
return ErrnoSuccess
}
// ClockTimeGet is the WASI function named functionClockTimeGet that returns the time value of a clock (time.Now).
// clockTimeGet is the WASI function named functionClockTimeGet that returns
// the time value of a name (time.Now).
//
// * id - The clock id for which to return the time.
// * precision - The maximum lag (exclusive) that the returned time value may have, compared to its actual value.
// * resultTimestamp - the offset to write the timestamp to mod.Memory
// * the timestamp is epoch nanoseconds encoded as a uint64 little-endian encoding.
// Parameters
//
// For example, if time.Now returned exactly midnight UTC 2022-01-01 (1640995200000000000), and
// parameters resultTimestamp=1, this function writes the below to `mod.Memory`:
// * id: clock ID to use
// * precision: maximum lag (exclusive) that the returned time value may have,
// compared to its actual value
// * resultTimestamp: offset to write the timestamp to api.Memory
// * the timestamp is epoch nanos encoded as a little-endian uint64
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoNotsup: the clock ID is not supported.
// * ErrnoInval: the clock ID is invalid.
// * ErrnoFault: there is not enough memory to write results
//
// For example, if time.Now returned exactly midnight UTC 2022-01-01
// (1640995200000000000), and parameters resultTimestamp=1, this function
// writes the below to api.Memory:
//
// uint64le
// +------------------------------------------+
@@ -94,11 +104,10 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul
// []byte{?, 0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, ?}
// resultTimestamp --^
//
// Note: importClockTimeGet shows this signature in the WebAssembly 1.0 Text Format.
// Note: This is similar to `clock_gettime` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
// See https://linux.die.net/man/3/clock_gettime
func (a *wasi) ClockTimeGet(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
func clockTimeGet(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
// TODO: precision is currently ignored.
sysCtx := mod.(*wasm.CallContext).Sys
@@ -110,8 +119,9 @@ func (a *wasi) ClockTimeGet(ctx context.Context, mod api.Module, id uint32, prec
case clockIDMonotonic:
val = uint64(sysCtx.Nanotime(ctx))
case clockIDProcessCputime, clockIDThreadCputime:
// Similar to many other runtimes, we only support realtime and monotonic clocks. Other types
// are slated to be removed from the next version of WASI.
// Similar to many other runtimes, we only support realtime and
// monotonic clocks. Other types are slated to be removed from the next
// version of WASI.
return ErrnoNotsup
default:
return ErrnoInval

View File

@@ -2,19 +2,15 @@ package wasi_snapshot_preview1
import (
_ "embed"
"fmt"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
// Test_ClockResGet only tests it is stubbed for GrainLang per #271
func Test_ClockResGet(t *testing.T) {
mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil)
defer mod.Close(testCtx)
resultResolution := uint32(1) // arbitrary offset
func Test_clockResGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
expectedMemoryMicro := []byte{
'?', // resultResolution is after this
@@ -30,56 +26,41 @@ func Test_ClockResGet(t *testing.T) {
tests := []struct {
name string
clockID uint64
clockID uint32
expectedMemory []byte
invocation func(clockID uint64) Errno
expectedLog string
}{
{
name: "wasi.ClockResGet",
clockID: 0,
name: "Realtime",
clockID: clockIDRealtime,
expectedMemory: expectedMemoryMicro,
invocation: func(clockID uint64) Errno {
return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution)
},
expectedLog: `
==> wasi_snapshot_preview1.clock_res_get(id=0,result.resolution=1)
<== ESUCCESS
`,
},
{
name: "wasi.ClockResGet",
clockID: 1,
name: "Monotonic",
clockID: clockIDMonotonic,
expectedMemory: expectedMemoryNano,
invocation: func(clockID uint64) Errno {
return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution)
},
},
{
name: functionClockResGet,
clockID: 0,
expectedMemory: expectedMemoryMicro,
invocation: func(clockID uint64) Errno {
results, err := fn.Call(testCtx, clockID, uint64(resultResolution))
require.NoError(t, err)
return Errno(results[0]) // results[0] is the errno
},
},
{
name: functionClockResGet,
clockID: 1,
expectedMemory: expectedMemoryNano,
invocation: func(clockID uint64) Errno {
results, err := fn.Call(testCtx, clockID, uint64(resultResolution))
require.NoError(t, err)
return Errno(results[0]) // results[0] is the errno
},
expectedLog: `
==> wasi_snapshot_preview1.clock_res_get(id=1,result.resolution=1)
<== ESUCCESS
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(fmt.Sprintf("%v/clockID=%v", tc.name, tc.clockID), func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
maskMemory(t, testCtx, mod, len(tc.expectedMemory))
errno := tc.invocation(tc.clockID)
require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno))
resultResolution := uint32(1) // arbitrary offset
requireErrno(t, ErrnoSuccess, mod, functionClockResGet, uint64(tc.clockID), uint64(resultResolution))
require.Equal(t, tc.expectedLog, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory)))
require.True(t, ok)
@@ -88,30 +69,42 @@ func Test_ClockResGet(t *testing.T) {
}
}
func Test_ClockResGet_Unsupported(t *testing.T) {
resultResolution := uint32(1) // arbitrary offset
mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil)
defer mod.Close(testCtx)
func Test_clockResGet_Unsupported(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
tests := []struct {
name string
clockID uint64
clockID uint32
expectedErrno Errno
expectedLog string
}{
{
name: "process cputime",
clockID: 2,
expectedErrno: ErrnoNotsup,
expectedLog: `
==> wasi_snapshot_preview1.clock_res_get(id=2,result.resolution=1)
<== ENOTSUP
`,
},
{
name: "thread cputime",
clockID: 3,
expectedErrno: ErrnoNotsup,
expectedLog: `
==> wasi_snapshot_preview1.clock_res_get(id=3,result.resolution=1)
<== ENOTSUP
`,
},
{
name: "undefined",
clockID: 100,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.clock_res_get(id=100,result.resolution=1)
<== EINVAL
`,
},
}
@@ -119,100 +112,80 @@ func Test_ClockResGet_Unsupported(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := fn.Call(testCtx, tc.clockID, uint64(resultResolution))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
defer log.Reset()
resultResolution := uint32(1) // arbitrary offset
requireErrno(t, tc.expectedErrno, mod, functionClockResGet, uint64(tc.clockID), uint64(resultResolution))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func Test_ClockTimeGet(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
func Test_clockTimeGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
clocks := []struct {
clock string
id uint32
tests := []struct {
name string
clockID uint32
expectedMemory []byte
expectedLog string
}{
{
clock: "Realtime",
id: clockIDRealtime,
name: "Realtime",
clockID: clockIDRealtime,
expectedMemory: []byte{
'?', // resultTimestamp is after this
0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos
'?', // stopped after encoding
},
expectedLog: `
==> wasi_snapshot_preview1.clock_time_get(id=0,precision=0,result.timestamp=1)
<== ESUCCESS
`,
},
{
clock: "Monotonic",
id: clockIDMonotonic,
name: "Monotonic",
clockID: clockIDMonotonic,
expectedMemory: []byte{
'?', // resultTimestamp is after this
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // fake nanotime starts at zero
'?', // stopped after encoding
},
},
}
for _, c := range clocks {
cc := c
t.Run(cc.clock, func(t *testing.T) {
tests := []struct {
name string
invocation func() Errno
}{
{
name: "wasi.ClockTimeGet",
invocation: func() Errno {
return a.ClockTimeGet(testCtx, mod, cc.id, 0 /* TODO: precision */, resultTimestamp)
},
},
{
name: functionClockTimeGet,
invocation: func() Errno {
results, err := fn.Call(testCtx, uint64(cc.id), 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
return errno
},
expectedLog: `
==> wasi_snapshot_preview1.clock_time_get(id=1,precision=0,result.timestamp=1)
<== ESUCCESS
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
// Reset the fake clock
sysCtx, err := newSysContext(nil, nil, nil)
require.NoError(t, err)
mod.(*wasm.CallContext).Sys = sysCtx
defer log.Reset()
maskMemory(t, testCtx, mod, len(cc.expectedMemory))
maskMemory(t, testCtx, mod, len(tc.expectedMemory))
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
resultTimestamp := uint32(1) // arbitrary offset
requireErrno(t, ErrnoSuccess, mod, functionClockTimeGet, uint64(tc.clockID), 0 /* TODO: precision */, uint64(resultTimestamp))
require.Equal(t, tc.expectedLog, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(cc.expectedMemory)))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory)))
require.True(t, ok)
require.Equal(t, cc.expectedMemory, actual)
})
}
require.Equal(t, tc.expectedMemory, actual)
})
}
}
func Test_ClockTimeGet_Unsupported(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
func Test_clockTimeGet_Unsupported(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
tests := []struct {
name string
clockID uint64
clockID uint32
expectedErrno Errno
expectedLog string
}{
{
name: "process cputime",
@@ -235,24 +208,26 @@ func Test_ClockTimeGet_Unsupported(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := fn.Call(testCtx, tc.clockID, 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
defer log.Reset()
resultTimestamp := uint32(1) // arbitrary offset
errno := clockTimeGet(testCtx, mod, tc.clockID, 0 /* TODO: precision */, resultTimestamp)
require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
require.Equal(t, tc.expectedLog, log.String())
})
}
}
func Test_ClockTimeGet_Errors(t *testing.T) {
mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
func Test_clockTimeGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
tests := []struct {
name string
resultTimestamp uint32
argvBufSize uint32
resultTimestamp, argvBufSize uint32
expectedLog string
}{
{
name: "resultTimestamp out-of-memory",
@@ -269,10 +244,11 @@ func Test_ClockTimeGet_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := fn.Call(testCtx, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
defer log.Reset()
errno := clockTimeGet(testCtx, mod, 0 /* TODO: id */, 0 /* TODO: precision */, tc.resultTimestamp)
require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
require.Equal(t, tc.expectedLog, log.String())
})
}
}

View File

@@ -0,0 +1,95 @@
package wasi_snapshot_preview1
import (
"context"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
functionEnvironGet = "environ_get"
functionEnvironSizesGet = "environ_sizes_get"
)
// environGet is the WASI function named functionEnvironGet that reads
// environment variables.
//
// Parameters
//
// * environ: offset to begin writing environment offsets in uint32
// little-endian encoding to api.Memory
// * environSizesGet result environc * 4 bytes are written to this offset
// * environBuf: offset to write the null-terminated variables to api.Memory
// * the format is like os.Environ: null-terminated "key=val" entries
// * environSizesGet result environBufSize bytes are written to this offset
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoFault: there is not enough memory to write results
//
// For example, if environSizesGet wrote environc=2 and environBufSize=9 for
// environment variables: "a=b", "b=cd" and parameters environ=11 and
// environBuf=1, this function writes the below to api.Memory:
//
// environBufSize uint32le uint32le
// +------------------------------------+ +--------+ +--------+
// | | | | | |
// []byte{?, 'a', '=', 'b', 0, 'b', '=', 'c', 'd', 0, ?, 1, 0, 0, 0, 5, 0, 0, 0, ?}
// environBuf --^ ^ ^
// environ offset for "a=b" --+ |
// environ offset for "b=cd" --+
//
// See environSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
func environGet(ctx context.Context, mod api.Module, environ uint32, environBuf uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Environ(), environ, environBuf)
}
// environSizesGet is the WASI function named functionEnvironSizesGet that
// reads environment variable sizes.
//
// Parameters
//
// * resultEnvironc: offset to write the count of environment variables to
// api.Memory
// * resultEnvironBufSize: offset to write the null-terminated environment
// variable length to api.Memory
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoFault: there is not enough memory to write results
//
// For example, if environ are "a=b","b=cd" and parameters resultEnvironc=1 and
// resultEnvironBufSize=6, this function writes the below to api.Memory:
//
// uint32le uint32le
// +--------+ +--------+
// | | | |
// []byte{?, 2, 0, 0, 0, ?, 9, 0, 0, 0, ?}
// resultEnvironc --^ ^
// 2 variables --+ |
// resultEnvironBufSize --|
// len([]byte{'a','=','b',0, |
// 'b','=','c','d',0}) --+
//
// See environGet
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
// and https://en.wikipedia.org/wiki/Null-terminated_string
func environSizesGet(ctx context.Context, mod api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
mem := mod.Memory()
if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) {
return ErrnoFault
}
if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) {
return ErrnoFault
}
return ErrnoSuccess
}

View File

@@ -0,0 +1,164 @@
package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func Test_environGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithEnv("a", "b").WithEnv("b", "cd"))
defer r.Close(testCtx)
resultEnviron := uint32(11) // arbitrary offset
resultEnvironBuf := uint32(1) // arbitrary offset
expectedMemory := []byte{
'?', // environBuf is after this
'a', '=', 'b', 0, // null terminated "a=b",
'b', '=', 'c', 'd', 0, // null terminated "b=cd"
'?', // environ is after this
1, 0, 0, 0, // little endian-encoded offset of "a=b"
5, 0, 0, 0, // little endian-encoded offset of "b=cd"
'?', // stopped after encoding
}
maskMemory(t, testCtx, mod, len(expectedMemory))
// Invoke environGet and check the memory side effects.
requireErrno(t, ErrnoSuccess, mod, functionEnvironGet, uint64(resultEnviron), uint64(resultEnvironBuf))
require.Equal(t, `
==> wasi_snapshot_preview1.environ_get(environ=11,environ_buf=1)
<== ESUCCESS
`, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
}
func Test_environGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithEnv("a", "bc").WithEnv("b", "cd"))
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here.
tests := []struct {
name string
environ, environBuf uint32
expectedLog string
}{
{
name: "out-of-memory environPtr",
environ: memorySize,
environBuf: validAddress,
},
{
name: "out-of-memory environBufPtr",
environ: validAddress,
environBuf: memorySize,
},
{
name: "environPtr exceeds the maximum valid address by 1",
// 4*envCount is the expected length for environPtr, 4 is the size of uint32
environ: memorySize - 4*2 + 1,
environBuf: validAddress,
},
{
name: "environBufPtr exceeds the maximum valid address by 1",
environ: validAddress,
// "a=bc", "b=cd" size = size of "a=bc0b=cd0" = 10
environBuf: memorySize - 10 + 1,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
errno := environGet(testCtx, mod, tc.environ, tc.environBuf)
require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
require.Equal(t, tc.expectedLog, log.String())
})
}
}
func Test_environSizesGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithEnv("a", "b").WithEnv("b", "cd"))
defer r.Close(testCtx)
resultEnvironc := uint32(1) // arbitrary offset
resultEnvironBufSize := uint32(6) // arbitrary offset
expectedMemory := []byte{
'?', // resultEnvironc is after this
0x2, 0x0, 0x0, 0x0, // little endian-encoded environment variable count
'?', // resultEnvironBufSize is after this
0x9, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
'?', // stopped after encoding
}
maskMemory(t, testCtx, mod, len(expectedMemory))
// Invoke environSizesGet and check the memory side effects.
requireErrno(t, ErrnoSuccess, mod, functionEnvironSizesGet, uint64(resultEnvironc), uint64(resultEnvironBufSize))
require.Equal(t, `
==> wasi_snapshot_preview1.environ_sizes_get(result.environc=1,result.environBufSize=6)
<== ESUCCESS
`, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
}
func Test_environSizesGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithEnv("a", "b").WithEnv("b", "cd"))
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here.
tests := []struct {
name string
environc, environBufSize uint32
expectedLog string
}{
{
name: "out-of-memory environCountPtr",
environc: memorySize,
environBufSize: validAddress,
},
{
name: "out-of-memory environBufSizePtr",
environc: validAddress,
environBufSize: memorySize,
},
{
name: "environCountPtr exceeds the maximum valid address by 1",
environc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of environ
environBufSize: validAddress,
},
{
name: "environBufSizePtr exceeds the maximum valid size by 1",
environc: validAddress,
environBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
errno := environSizesGet(testCtx, mod, tc.environc, tc.environBufSize)
require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
require.Equal(t, tc.expectedLog, log.String())
})
}
}

View File

@@ -44,7 +44,7 @@ func Example() {
// Override default configuration (which discards stdout).
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithName("wasi-demo"))
if mod != nil {
defer mod.Close(ctx)
defer r.Close(ctx)
}
// Note: Most compilers do not exit the module after running "_start", unless

View File

@@ -0,0 +1,678 @@
package wasi_snapshot_preview1
import (
"context"
"errors"
"io"
"io/fs"
"github.com/tetratelabs/wazero/api"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
functionFdAdvise = "fd_advise"
functionFdAllocate = "fd_allocate"
functionFdClose = "fd_close"
functionFdDatasync = "fd_datasync"
functionFdFdstatGet = "fd_fdstat_get"
functionFdFdstatSetFlags = "fd_fdstat_set_flags"
functionFdFdstatSetRights = "fd_fdstat_set_rights"
functionFdFilestatGet = "fd_filestat_get"
functionFdFilestatSetSize = "fd_filestat_set_size"
functionFdFilestatSetTimes = "fd_filestat_set_times"
functionFdPread = "fd_pread"
functionFdPrestatGet = "fd_prestat_get"
functionFdPrestatDirName = "fd_prestat_dir_name"
functionFdPwrite = "fd_pwrite"
functionFdRead = "fd_read"
functionFdReaddir = "fd_readdir"
functionFdRenumber = "fd_renumber"
functionFdSeek = "fd_seek"
functionFdSync = "fd_sync"
functionFdTell = "fd_tell"
functionFdWrite = "fd_write"
functionPathCreateDirectory = "path_create_directory"
functionPathFilestatGet = "path_filestat_get"
functionPathFilestatSetTimes = "path_filestat_set_times"
functionPathLink = "path_link"
functionPathOpen = "path_open"
functionPathReadlink = "path_readlink"
functionPathRemoveDirectory = "path_remove_directory"
functionPathRename = "path_rename"
functionPathSymlink = "path_symlink"
functionPathUnlinkFile = "path_unlink_file"
)
// fdAdvise is the WASI function named functionFdAdvise which provides file
// advisory information on a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_advisefd-fd-offset-filesize-len-filesize-advice-advice---errno
var fdAdvise = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271.
// fdAllocate is the WASI function named functionFdAllocate which forces the
// allocation of space in a file.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_allocatefd-fd-offset-filesize-len-filesize---errno
var fdAllocate = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271.
// fdClose is the WASI function named functionFdClose which closes a file
// descriptor.
//
// Parameters
//
// * fd: file descriptor to close
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: the fd was not open.
//
// Note: This is similar to `close` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
// and https://linux.die.net/man/3/close
func fdClose(ctx context.Context, mod api.Module, fd uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
if ok := sysCtx.FS(ctx).CloseFile(ctx, fd); !ok {
return ErrnoBadf
}
return ErrnoSuccess
}
// fdDatasync is the WASI function named functionFdDatasync which synchronizes
// the data of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno
var fdDatasync = stubFunction(i32) // stubbed for GrainLang per #271.
// fdFdstatGet is the WASI function named functionFdFdstatGet which returns the
// attributes of a file descriptor.
//
// Parameters
//
// * fd: file descriptor to get the fdstat attributes data
// * resultFdstat: offset to write the result fdstat data
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `resultFdstat` points to an offset out of memory
//
// fdstat byte layout is 24-byte size, with the following fields:
// * fs_filetype 1 byte, to indicate the file type
// * fs_flags 2 bytes, to indicate the file descriptor flag
// * 5 pad bytes
// * fs_right_base 8 bytes, to indicate the current rights of the fd
// * fs_right_inheriting 8 bytes, to indicate the maximum rights of the fd
//
// For example, with a file corresponding with `fd` was a directory (=3) opened
// with `fd_read` right (=1) and no fs_flags (=0), parameter resultFdstat=1,
// this function writes the below to api.Memory:
//
// uint16le padding uint64le uint64le
// uint8 --+ +--+ +-----------+ +--------------------+ +--------------------+
// | | | | | | | | |
// []byte{?, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0}
// resultFdstat --^ ^-- fs_flags ^-- fs_right_base ^-- fs_right_inheriting
// |
// +-- fs_filetype
//
// Note: fdFdstatGet returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as
// well as additional fields.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat
// and https://linux.die.net/man/3/fsync
func fdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
if _, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
return ErrnoBadf
}
// TODO: actually write the fdstat!
return ErrnoSuccess
}
// fdFdstatSetFlags is the WASI function named functionFdFdstatSetFlags which
// adjusts the flags associated with a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_flagsfd-fd-flags-fdflags---errnoand is stubbed for GrainLang per #271
var fdFdstatSetFlags = stubFunction(i32, i32) // stubbed for GrainLang per #271.
// fdFdstatSetRights is the WASI function named functionFdFdstatSetRights which
// adjusts the rights associated with a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_rightsfd-fd-fs_rights_base-rights-fs_rights_inheriting-rights---errno
//
// Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
var fdFdstatSetRights = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271.
// fdFilestatGet is the WASI function named functionFdFilestatGet which returns
// the attributes of an open file.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
var fdFilestatGet = stubFunction(i32, i32) // stubbed for GrainLang per #271.
// fdFilestatSetSize is the WASI function named functionFdFilestatSetSize which
// adjusts the size of an open file.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---errno
var fdFilestatSetSize = stubFunction(i32, i64) // stubbed for GrainLang per #271.
// fdFilestatSetTimes is the WASI function named functionFdFilestatSetTimes
// which adjusts the times of an open file.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_timesfd-fd-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno
var fdFilestatSetTimes = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271.
// fdPread is the WASI function named functionFdPread which reads from a file
// descriptor, without using and updating the file descriptor's offset.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size
var fdPread = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
// fdPrestatGet is the WASI function named functionFdPrestatGet which returns
// the prestat data of a file descriptor.
//
// Parameters
//
// * fd: file descriptor to get the prestat
// * resultPrestat: offset to write the result prestat data
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid or the `fd` is not a pre-opened directory
// * ErrnoFault: `resultPrestat` points to an offset out of memory
//
// prestat byte layout is 8 bytes, beginning with an 8-bit tag and 3 pad bytes.
// The only valid tag is `prestat_dir`, which is tag zero. This simplifies the
// byte layout to 4 empty bytes followed by the uint32le encoded path length.
//
// For example, the directory name corresponding with `fd` was "/tmp" and
// parameter resultPrestat=1, this function writes the below to api.Memory:
//
// padding uint32le
// uint8 --+ +-----+ +--------+
// | | | | |
// []byte{?, 0, 0, 0, 0, 4, 0, 0, 0, ?}
// resultPrestat --^ ^
// tag --+ |
// +-- size in bytes of the string "/tmp"
//
// See fdPrestatDirName and
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat
func fdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
entry, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
if !ok {
return ErrnoBadf
}
// Zero-value 8-bit tag, and 3-byte zero-value paddings, which is uint32le(0) in short.
if !mod.Memory().WriteUint32Le(ctx, resultPrestat, uint32(0)) {
return ErrnoFault
}
// Write the length of the directory name at offset 4.
if !mod.Memory().WriteUint32Le(ctx, resultPrestat+4, uint32(len(entry.Path))) {
return ErrnoFault
}
return ErrnoSuccess
}
// fdPrestatDirName is the WASI function named functionFdPrestatDirName which
// returns the path of the pre-opened directory of a file descriptor.
//
// Parameters
//
// * fd: file descriptor to get the path of the pre-opened directory
// * path: offset in api.Memory to write the result path
// * pathLen: count of bytes to write to `path`
// * This should match the uint32le fdPrestatGet writes to offset
// `resultPrestat`+4
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `path` points to an offset out of memory
// * ErrnoNametoolong: `pathLen` is longer than the actual length of the result
//
// For example, the directory name corresponding with `fd` was "/tmp" and
// parameters path=1 pathLen=4 (correct), this function will write the below to
// api.Memory:
//
// pathLen
// +--------------+
// | |
// []byte{?, '/', 't', 'm', 'p', ?}
// path --^
//
// See fdPrestatGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
func fdPrestatDirName(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
if !ok {
return ErrnoBadf
}
// Some runtimes may have another semantics. See /RATIONALE.md
if uint32(len(f.Path)) < pathLen {
return ErrnoNametoolong
}
// TODO: fdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
if !mod.Memory().Write(ctx, pathPtr, []byte(f.Path)[:pathLen]) {
return ErrnoFault
}
return ErrnoSuccess
}
// fdPwrite is the WASI function named functionFdPwrite which writes to a file
// descriptor, without using and updating the file descriptor's offset.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size
var fdPwrite = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
// fdRead is the WASI function named functionFdRead which reads from a file
// descriptor.
//
// Parameters
//
// * fd: an opened file descriptor to read data from
// * iovs: offset in api.Memory to read offset, size pairs representing where
// to write file data
// * Both offset and length are encoded as uint32le
// * iovsCount: count of memory offset, size pairs to read sequentially
// starting at iovs
// * resultSize: offset in api.Memory to write the number of bytes read
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `iovs` or `resultSize` point to an offset out of memory
// * ErrnoIo: a file system error
//
// For example, this function needs to first read `iovs` to determine where
// to write contents. If parameters iovs=1 iovsCount=2, this function reads two
// offset/length pairs from api.Memory:
//
// iovs[0] iovs[1]
// +---------------------+ +--------------------+
// | uint32le uint32le| |uint32le uint32le|
// +---------+ +--------+ +--------+ +--------+
// | | | | | | | |
// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... }
// iovs --^ ^ ^ ^
// | | | |
// offset --+ length --+ offset --+ length --+
//
// If the contents of the `fd` parameter was "wazero" (6 bytes) and parameter
// resultSize=26, this function writes the below to api.Memory:
//
// iovs[0].length iovs[1].length
// +--------------+ +----+ uint32le
// | | | | +--------+
// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ?, 6, 0, 0, 0 }
// iovs[0].offset --^ ^ ^
// iovs[1].offset --+ |
// resultSize --+
//
// Note: This is similar to `readv` in POSIX. https://linux.die.net/man/3/readv
//
// See fdWrite
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read
func fdRead(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
reader := internalsys.FdReader(ctx, sysCtx, fd)
if reader == nil {
return ErrnoBadf
}
var nread uint32
for i := uint32(0); i < iovsCount; i++ {
iovPtr := iovs + i*8
offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
if !ok {
return ErrnoFault
}
l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
if !ok {
return ErrnoFault
}
b, ok := mod.Memory().Read(ctx, offset, l)
if !ok {
return ErrnoFault
}
n, err := reader.Read(b) // Note: n <= l
nread += uint32(n)
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return ErrnoIo
}
}
if !mod.Memory().WriteUint32Le(ctx, resultSize, nread) {
return ErrnoFault
}
return ErrnoSuccess
}
// fdReaddir is the WASI function named functionFdReaddir which reads directory
// entries from a directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_readdirfd-fd-buf-pointeru8-buf_len-size-cookie-dircookie---errno-size
var fdReaddir = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
// fdRenumber is the WASI function named functionFdRenumber which atomically
// replaces a file descriptor by renumbering another file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
var fdRenumber = stubFunction(i32, i32) // stubbed for GrainLang per #271.
// fdSeek is the WASI function named functionFdSeek which moves the offset of a
// file descriptor.
//
// Parameters
//
// * fd: file descriptor to move the offset of
// * offset: signed int64, which is encoded as uint64, input argument to
// `whence`, which results in a new offset
// * whence: operator that creates the new offset, given `offset` bytes
// * If io.SeekStart, new offset == `offset`.
// * If io.SeekCurrent, new offset == existing offset + `offset`.
// * If io.SeekEnd, new offset == file size of `fd` + `offset`.
// * resultNewoffset: offset in api.Memory to write the new offset to,
// relative to start of the file
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `resultNewoffset` points to an offset out of memory
// * ErrnoInval: `whence` is an invalid value
// * ErrnoIo: a file system error
//
// For example, if fd 3 is a file with offset 0, and parameters fd=3, offset=4,
// whence=0 (=io.SeekStart), resultNewOffset=1, this function writes the below
// to api.Memory:
//
// uint64le
// +--------------------+
// | |
// []byte{?, 4, 0, 0, 0, 0, 0, 0, 0, ? }
// resultNewoffset --^
//
// Note: This is similar to `lseek` in POSIX. https://linux.die.net/man/3/lseek
//
// See io.Seeker
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
func fdSeek(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
var seeker io.Seeker
// Check to see if the file descriptor is available
if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
return ErrnoBadf
// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
} else if seeker, ok = f.File.(io.Seeker); !ok {
return ErrnoBadf
}
if whence > io.SeekEnd /* exceeds the largest valid whence */ {
return ErrnoInval
}
newOffset, err := seeker.Seek(int64(offset), int(whence))
if err != nil {
return ErrnoIo
}
if !mod.Memory().WriteUint32Le(ctx, resultNewoffset, uint32(newOffset)) {
return ErrnoFault
}
return ErrnoSuccess
}
// fdSync is the WASI function named functionFdSync which synchronizes the data
// and metadata of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno
var fdSync = stubFunction(i32) // stubbed for GrainLang per #271.
// fdTell is the WASI function named functionFdTell which returns the current
// offset of a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize
var fdTell = stubFunction(i32, i32) // stubbed for GrainLang per #271.
// fdWrite is the WASI function named functionFdWrite which writes to a file
// descriptor.
//
// Parameters
//
// * fd: an opened file descriptor to write data to
// * iovs: offset in api.Memory to read offset, size pairs representing the
// data to write to `fd`
// * Both offset and length are encoded as uint32le.
// * iovsCount: count of memory offset, size pairs to read sequentially
// starting at iovs
// * resultSize: offset in api.Memory to write the number of bytes written
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `iovs` or `resultSize` point to an offset out of memory
// * ErrnoIo: a file system error
//
// For example, this function needs to first read `iovs` to determine what to
// write to `fd`. If parameters iovs=1 iovsCount=2, this function reads two
// offset/length pairs from api.Memory:
//
// iovs[0] iovs[1]
// +---------------------+ +--------------------+
// | uint32le uint32le| |uint32le uint32le|
// +---------+ +--------+ +--------+ +--------+
// | | | | | | | |
// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... }
// iovs --^ ^ ^ ^
// | | | |
// offset --+ length --+ offset --+ length --+
//
// This function reads those chunks api.Memory into the `fd` sequentially.
//
// iovs[0].length iovs[1].length
// +--------------+ +----+
// | | | |
// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ? }
// iovs[0].offset --^ ^
// iovs[1].offset --+
//
// Since "wazero" was written, if parameter resultSize=26, this function writes
// the below to api.Memory:
//
// uint32le
// +--------+
// | |
// []byte{ 0..24, ?, 6, 0, 0, 0', ? }
// resultSize --^
//
// Note: This is similar to `writev` in POSIX. https://linux.die.net/man/3/writev
//
// See fdRead
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#ciovec
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
func fdWrite(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
writer := internalsys.FdWriter(ctx, sysCtx, fd)
if writer == nil {
return ErrnoBadf
}
var nwritten uint32
for i := uint32(0); i < iovsCount; i++ {
iovPtr := iovs + i*8
offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
if !ok {
return ErrnoFault
}
// Note: emscripten has been known to write zero length iovec. However,
// it is not common in other compilers, so we don't optimize for it.
l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
if !ok {
return ErrnoFault
}
b, ok := mod.Memory().Read(ctx, offset, l)
if !ok {
return ErrnoFault
}
n, err := writer.Write(b)
if err != nil {
return ErrnoIo
}
nwritten += uint32(n)
}
if !mod.Memory().WriteUint32Le(ctx, resultSize, nwritten) {
return ErrnoFault
}
return ErrnoSuccess
}
// pathCreateDirectory is the WASI function named functionPathCreateDirectory
// which creates a directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_create_directoryfd-fd-path-string---errno
var pathCreateDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.
// pathFilestatGet is the WASI function named functionPathFilestatGet which
// returns the attributes of a file or directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_getfd-fd-flags-lookupflags-path-string---errno-filestat
var pathFilestatGet = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// pathFilestatSetTimes is the WASI function named functionPathFilestatSetTimes
// which adjusts the timestamps of a file or directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_set_timesfd-fd-flags-lookupflags-path-string-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno
var pathFilestatSetTimes = stubFunction(i32, i32, i32, i32, i64, i64, i32) // stubbed for GrainLang per #271.
// pathLink is the WASI function named functionPathLink which adjusts the
// timestamps of a file or directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_link
var pathLink = stubFunction(i32, i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// pathOpen is the WASI function named functionPathOpen which opens a file or
// directory. This returns ErrnoBadf if the fd is invalid.
//
// Parameters
//
// * fd: file descriptor of a directory that `path` is relative to
// * dirflags: flags to indicate how to resolve `path`
// * path: offset in api.Memory to read the path string from
// * pathLen: length of `path`
// * oFlags: open flags to indicate the method by which to open the file
// * fsRightsBase: rights of the newly created file descriptor for `path`
// * fsRightsInheriting: rights of the file descriptors derived from the newly
// created file descriptor for `path`
// * fdFlags: file descriptor flags
// * resultOpenedFd: offset in api.Memory to write the newly created file
// descriptor to.
// * The result FD value is guaranteed to be less than 2**31
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoBadf: `fd` is invalid
// * ErrnoFault: `resultOpenedFd` points to an offset out of memory
// * ErrnoNoent: `path` does not exist.
// * ErrnoExist: `path` exists, while `oFlags` requires that it must not.
// * ErrnoNotdir: `path` is not a directory, while `oFlags` requires it.
// * ErrnoIo: a file system error
//
// For example, this function needs to first read `path` to determine the file
// to open. If parameters `path` = 1, `pathLen` = 6, and the path is "wazero",
// pathOpen reads the path from api.Memory:
//
// pathLen
// +------------------------+
// | |
// []byte{ ?, 'w', 'a', 'z', 'e', 'r', 'o', ?... }
// path --^
//
// Then, if parameters resultOpenedFd = 8, and this function opened a new file
// descriptor 5 with the given flags, this function writes the below to
// api.Memory:
//
// uint32le
// +--------+
// | |
// []byte{ 0..6, ?, 5, 0, 0, 0, ?}
// resultOpenedFd --^
//
// Notes
// * This is similar to `openat` in POSIX. https://linux.die.net/man/3/openat
// * The returned file descriptor is not guaranteed to be the lowest-number
// * Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
//
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open
func pathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) {
sysCtx := mod.(*wasm.CallContext).Sys
fsc := sysCtx.FS(ctx)
if _, ok := fsc.OpenedFile(ctx, fd); !ok {
return ErrnoBadf
}
b, ok := mod.Memory().Read(ctx, pathPtr, pathLen)
if !ok {
return ErrnoFault
}
if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil {
switch {
case errors.Is(err, fs.ErrNotExist):
return ErrnoNoent
case errors.Is(err, fs.ErrExist):
return ErrnoExist
default:
return ErrnoIo
}
} else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) {
_ = fsc.CloseFile(ctx, newFD)
return ErrnoFault
}
return ErrnoSuccess
}
// pathReadlink is the WASI function named functionPathReadlink that reads the
// contents of a symbolic link.
//
// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_readlinkfd-fd-path-string-buf-pointeru8-buf_len-size---errno-size
var pathReadlink = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// pathRemoveDirectory is the WASI function named functionPathRemoveDirectory
// which removes a directory.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_remove_directoryfd-fd-path-string---errno
var pathRemoveDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.
// pathRename is the WASI function named functionPathRename which renames a
// file or directory.
var pathRename = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// pathSymlink is the WASI function named functionPathSymlink which creates a
// symbolic link.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_symlink
var pathSymlink = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// pathUnlinkFile is the WASI function named functionPathUnlinkFile which
// unlinks a file.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_unlink_filefd-fd-path-string---errno
var pathUnlinkFile = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,11 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
const functionPollOneoff = "poll_oneoff"
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-eventtype-enumu8
const (
// eventTypeClock is the timeout event named "clock".
// eventTypeClock is the timeout event named "name".
eventTypeClock = iota
// eventTypeFdRead is the data available event named "fd_read".
eventTypeFdRead
@@ -19,22 +21,23 @@ const (
eventTypeFdWrite
)
// PollOneoff is the WASI function named functionPollOneoff that concurrently
// pollOneoff is the WASI function named functionPollOneoff that concurrently
// polls for the occurrence of a set of events.
//
// Parameters
//
// * in - pointer to the subscriptions (48 bytes each)
// * out - pointer to the resulting events (32 bytes each)
// * nsubscriptions - count of subscriptions, zero returns ErrnoInval.
// * resultNevents - count of events.
// * in: pointer to the subscriptions (48 bytes each)
// * out: pointer to the resulting events (32 bytes each)
// * nsubscriptions: count of subscriptions, zero returns ErrnoInval.
// * resultNevents: count of events.
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoInval - If the parameters are invalid
// * ErrnoNotsup - If a parameters is valid, but not yet supported.
// * ErrnoFault - if there is not enough memory to read the subscriptions or write results.
// * ErrnoInval: the parameters are invalid
// * ErrnoNotsup: a parameters is valid, but not yet supported.
// * ErrnoFault: there is not enough memory to read the subscriptions or
// write results.
//
// Notes
//
@@ -43,7 +46,7 @@ const (
// * This is similar to `poll` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#poll_oneoff
// See https://linux.die.net/man/3/poll
func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno {
func pollOneoff(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno {
if nsubscriptions == 0 {
return ErrnoInval
}
@@ -75,7 +78,7 @@ func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscri
eventType := inBuf[inOffset+8] // +8 past userdata
switch eventType {
case eventTypeClock: // handle later
// +8 past userdata +8 clock alignment
// +8 past userdata +8 name alignment
errno = processClockEvent(ctx, mod, inBuf[inOffset+8+8:])
case eventTypeFdRead, eventTypeFdWrite:
// +8 past userdata +4 FD alignment
@@ -95,7 +98,7 @@ func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscri
return ErrnoSuccess
}
// processClockEvent supports only relative clock events, as that's what's used
// processClockEvent supports only relative name events, as that's what's used
// to implement sleep in various compilers including Rust, Zig and TinyGo.
func processClockEvent(ctx context.Context, mod api.Module, inBuf []byte) Errno {
_ /* ID */ = binary.LittleEndian.Uint32(inBuf[0:8]) // See below
@@ -114,7 +117,7 @@ func processClockEvent(ctx context.Context, mod api.Module, inBuf []byte) Errno
// https://linux.die.net/man/3/clock_settime says relative timers are
// unaffected. Since this function only supports relative timeout, we can
// skip clock ID validation and use a single sleep function.
// skip name ID validation and use a single sleep function.
sysCtx := mod.(*wasm.CallContext).Sys
sysCtx.Nanosleep(ctx, int64(timeout))
@@ -127,7 +130,8 @@ func processFDEvent(ctx context.Context, mod api.Module, eventType byte, inBuf [
fd := binary.LittleEndian.Uint32(inBuf)
sysCtx := mod.(*wasm.CallContext).Sys
// Choose the best error, which falls back to unsupported, until we support files.
// Choose the best error, which falls back to unsupported, until we support
// files.
errno := ErrnoNotsup
if eventType == eventTypeFdRead && internalsys.FdReader(ctx, sysCtx, fd) == nil {
errno = ErrnoBadf

View File

@@ -3,23 +3,17 @@ package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
func Test_PollOneoff(t *testing.T) {
mod, fn := instantiateModule(testCtx, t, functionPollOneoff, importPollOneoff, nil)
defer mod.Close(testCtx)
func Test_pollOneoff(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
tests := []struct {
name string
mem []byte
expectedMem []byte // at offset out
}{
{
name: "monotonic relative",
mem: []byte{
mem := []byte{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
eventTypeClock, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // event type and padding
clockIDMonotonic, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // clockID
@@ -27,14 +21,13 @@ func Test_PollOneoff(t *testing.T) {
0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // precision (ns)
0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // flags (relative)
'?', // stopped after encoding
},
expectedMem: []byte{
}
expectedMem := []byte{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
byte(ErrnoSuccess), 0x0, // errno is 16 bit
eventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
'?', // stopped after encoding
},
},
}
in := uint32(0) // past in
@@ -42,7 +35,16 @@ func Test_PollOneoff(t *testing.T) {
nsubscriptions := uint32(1)
resultNevents := uint32(512) // past out
requireExpectedMem := func(expectedMem []byte) {
maskMemory(t, testCtx, mod, 1024)
mod.Memory().Write(testCtx, in, mem)
requireErrno(t, ErrnoSuccess, mod, functionPollOneoff, uint64(in), uint64(out), uint64(nsubscriptions),
uint64(resultNevents))
require.Equal(t, `
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1,result.nevents=512)
<== ESUCCESS
`, "\n"+log.String())
outMem, ok := mod.Memory().Read(testCtx, out, uint32(len(expectedMem)))
require.True(t, ok)
require.Equal(t, expectedMem, outMem)
@@ -50,37 +52,11 @@ func Test_PollOneoff(t *testing.T) {
nevents, ok := mod.Memory().ReadUint32Le(testCtx, resultNevents)
require.True(t, ok)
require.Equal(t, nsubscriptions, nevents)
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
t.Run("wasi.PollOneoff", func(t *testing.T) {
maskMemory(t, testCtx, mod, 1024)
mod.Memory().Write(testCtx, in, tc.mem)
errno := a.PollOneoff(testCtx, mod, in, out, nsubscriptions, resultNevents)
require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno))
requireExpectedMem(tc.expectedMem)
})
t.Run(functionPollOneoff, func(t *testing.T) {
maskMemory(t, testCtx, mod, 1024)
mod.Memory().Write(testCtx, in, tc.mem)
results, err := fn.Call(testCtx, uint64(in), uint64(out), uint64(nsubscriptions), uint64(resultNevents))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno))
requireExpectedMem(tc.expectedMem)
})
})
}
}
func Test_PollOneoff_Errors(t *testing.T) {
mod, _ := instantiateModule(testCtx, t, functionPollOneoff, importPollOneoff, nil)
defer mod.Close(testCtx)
func Test_pollOneoff_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
tests := []struct {
name string
@@ -88,6 +64,7 @@ func Test_PollOneoff_Errors(t *testing.T) {
mem []byte // at offset in
expectedErrno Errno
expectedMem []byte // at offset out
expectedLog string
}{
{
name: "in out of range",
@@ -96,6 +73,10 @@ func Test_PollOneoff_Errors(t *testing.T) {
out: 128, // past in
resultNevents: 512, //past out
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.poll_oneoff(in=65536,out=128,nsubscriptions=1,result.nevents=512)
<== EFAULT
`,
},
{
name: "out out of range",
@@ -103,18 +84,30 @@ func Test_PollOneoff_Errors(t *testing.T) {
resultNevents: 512, //past out
nsubscriptions: 1,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=65536,nsubscriptions=1,result.nevents=512)
<== EFAULT
`,
},
{
name: "resultNevents out of range",
resultNevents: wasm.MemoryPageSize,
nsubscriptions: 1,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=0,nsubscriptions=1,result.nevents=65536)
<== EFAULT
`,
},
{
name: "nsubscriptions zero",
out: 128, // past in
resultNevents: 512, //past out
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=0,result.nevents=512)
<== EINVAL
`,
},
{
name: "unsupported eventTypeFdRead",
@@ -134,20 +127,27 @@ func Test_PollOneoff_Errors(t *testing.T) {
eventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
'?', // stopped after encoding
},
expectedLog: `
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1,result.nevents=512)
<== ESUCCESS
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
maskMemory(t, testCtx, mod, 1024)
if tc.mem != nil {
mod.Memory().Write(testCtx, tc.in, tc.mem)
}
errno := a.PollOneoff(testCtx, mod, tc.in, tc.out, tc.nsubscriptions, tc.resultNevents)
require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
requireErrno(t, tc.expectedErrno, mod, functionPollOneoff, uint64(tc.in), uint64(tc.out),
uint64(tc.nsubscriptions), uint64(tc.resultNevents))
require.Equal(t, tc.expectedLog, "\n"+log.String())
out, ok := mod.Memory().Read(testCtx, tc.out, uint32(len(tc.expectedMem)))
require.True(t, ok)

View File

@@ -8,33 +8,20 @@ import (
)
const (
// functionProcExit terminates the execution of the module with an exit code.
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit
functionProcExit = "proc_exit"
// importProcExit is the WebAssembly 1.0 Text format import of functionProcExit.
importProcExit = `(import "wasi_snapshot_preview1" "proc_exit"
(func $wasi.proc_exit (param $rval i32)))`
// functionProcRaise sends a signal to the process of the calling thread.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-proc_raisesig-signal---errno
functionProcRaise = "proc_raise"
// importProcRaise is the WebAssembly 1.0 Text format import of functionProcRaise.
importProcRaise = `(import "wasi_snapshot_preview1" "proc_raise"
(func $wasi.proc_raise (param $sig i32) (result (;errno;) i32)))`
)
// ProcExit is the WASI function that terminates the execution of the module with an exit code.
// An exit code of 0 indicates successful termination. The meanings of other values are not defined by WASI.
// procExit is the WASI function named functionProcExit that terminates the
// execution of the module with an exit code. The only successful exit code is
// zero.
//
// * rval - The exit code.
// Parameters
//
// In wazero, this calls api.Module CloseWithExitCode.
// * exitCode: exit code.
//
// Note: importProcExit shows this signature in the WebAssembly 1.0 Text Format.
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit
func (a *wasi) ProcExit(ctx context.Context, mod api.Module, exitCode uint32) {
func procExit(ctx context.Context, mod api.Module, exitCode uint32) {
// Ensure other callers see the exit code.
_ = mod.CloseWithExitCode(ctx, exitCode)
@@ -44,7 +31,8 @@ func (a *wasi) ProcExit(ctx context.Context, mod api.Module, exitCode uint32) {
panic(sys.NewExitError(mod.Name(), exitCode))
}
// ProcRaise is the WASI function named functionProcRaise
func (a *wasi) ProcRaise(ctx context.Context, mod api.Module, sig uint32) Errno {
return ErrnoNosys // stubbed for GrainLang per #271
}
// procRaise is the WASI function named functionProcRaise which sends a signal
// to the process of the calling thread.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-proc_raisesig-signal---errno
var procRaise = stubFunction(i32) // stubbed for GrainLang per #271.

View File

@@ -5,14 +5,17 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/watzero"
"github.com/tetratelabs/wazero/sys"
)
func Test_ProcExit(t *testing.T) {
func Test_procExit(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig())
defer r.Close(testCtx)
tests := []struct {
name string
exitCode uint32
expectedLog string
}{
{
name: "success (exitcode 0)",
@@ -28,61 +31,21 @@ func Test_ProcExit(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
// Note: Unlike most tests, this uses fn, not the 'a' result
// parameter. This is because currently, this function body
// panics, and we expect Call to unwrap the panic.
mod, fn := instantiateModule(testCtx, t, functionProcExit, importProcExit, nil)
defer mod.Close(testCtx)
defer log.Reset()
// When ProcExit is called, CallEngine.Call returns immediately,
// returning the exit code as the error.
_, err := fn.Call(testCtx, uint64(tc.exitCode))
// Since procExit panics, any opcodes afterwards cannot be reached.
err := require.CapturePanic(func() { procExit(testCtx, mod, tc.exitCode) })
require.Equal(t, tc.exitCode, err.(*sys.ExitError).ExitCode())
require.Equal(t, tc.expectedLog, log.String())
})
}
}
var unreachableAfterExit = `(module
(import "wasi_snapshot_preview1" "proc_exit"
(func $wasi.proc_exit (param $rval i32)))
(func $main
i32.const 0
call $wasi.proc_exit
unreachable ;; If abort doesn't panic, this code is reached.
)
(start $main)
)`
// Test_ProcExit_StopsExecution ensures code that follows a proc_exit isn't invoked.
func Test_ProcExit_StopsExecution(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := NewBuilder(r).Instantiate(testCtx, r)
require.NoError(t, err)
exitWasm, err := watzero.Wat2Wasm(unreachableAfterExit)
require.NoError(t, err)
_, err = r.InstantiateModuleFromBinary(testCtx, exitWasm)
require.Error(t, err)
require.Equal(t, uint32(0), err.(*sys.ExitError).ExitCode())
}
// Test_ProcRaise only tests it is stubbed for GrainLang per #271
func Test_ProcRaise(t *testing.T) {
mod, fn := instantiateModule(testCtx, t, functionProcRaise, importProcRaise, nil)
defer mod.Close(testCtx)
t.Run("wasi.ProcRaise", func(t *testing.T) {
errno := a.ProcRaise(testCtx, mod, 0)
require.Equal(t, ErrnoNosys, errno, ErrnoName(errno))
})
t.Run(functionProcRaise, func(t *testing.T) {
results, err := fn.Call(testCtx, 0)
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, ErrnoNosys, errno, ErrnoName(errno))
})
// Test_procRaise only tests it is stubbed for GrainLang per #271
func Test_procRaise(t *testing.T) {
log := requireErrnoNosys(t, functionProcRaise, 0)
require.Equal(t, `
--> wasi_snapshot_preview1.proc_raise(sig=0)
<-- ENOSYS
`, log)
}

View File

@@ -0,0 +1,52 @@
package wasi_snapshot_preview1
import (
"context"
"io"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
const functionRandomGet = "random_get"
// randomGet is the WASI function named functionRandomGet which writes random
// data to a buffer.
//
// Parameters
//
// * buf: api.Memory offset to write random values
// * bufLen: size of random data in bytes
//
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// * ErrnoFault: `buf` or `bufLen` point to an offset out of memory
// * ErrnoIo: a file system error
//
// For example, if underlying random source was seeded like
// `rand.NewSource(42)`, we expect api.Memory to contain:
//
// bufLen (5)
// +--------------------------+
// | |
// []byte{?, 0x53, 0x8c, 0x7f, 0x96, 0xb1, ?}
// buf --^
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
func randomGet(ctx context.Context, mod api.Module, buf uint32, bufLen uint32) (errno Errno) {
sysCtx := mod.(*wasm.CallContext).Sys
randSource := sysCtx.RandSource()
randomBytes, ok := mod.Memory().Read(ctx, buf, bufLen)
if !ok { // out-of-range
return ErrnoFault
}
// We can ignore the returned n as it only != byteCount on error
if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil {
return ErrnoIo
}
return ErrnoSuccess
}

View File

@@ -0,0 +1,114 @@
package wasi_snapshot_preview1
import (
"bytes"
"errors"
"io"
"testing"
"testing/iotest"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func Test_randomGet(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithRandSource(deterministicRandomSource()))
defer r.Close(testCtx)
expectedMemory := []byte{
'?', // `offset` is after this
0x53, 0x8c, 0x7f, 0x96, 0xb1, // random data from seed value of 42
'?', // stopped after encoding
}
length := uint32(5) // arbitrary length,
offset := uint32(1) // offset,
maskMemory(t, testCtx, mod, len(expectedMemory))
// Invoke randomGet and check the memory side effects!
requireErrno(t, ErrnoSuccess, mod, functionRandomGet, uint64(offset), uint64(length))
require.Equal(t, `
==> wasi_snapshot_preview1.random_get(buf=1,buf_len=5)
<== ESUCCESS
`, "\n"+log.String())
actual, ok := mod.Memory().Read(testCtx, 0, offset+length+1)
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
}
func Test_randomGet_Errors(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithRandSource(deterministicRandomSource()))
defer r.Close(testCtx)
memorySize := mod.Memory().Size(testCtx)
tests := []struct {
name string
offset, length uint32
expectedLog string
}{
{
name: "out-of-memory",
offset: memorySize,
length: 1,
expectedLog: `
==> wasi_snapshot_preview1.random_get(buf=65536,buf_len=1)
<== EFAULT
`,
},
{
name: "random length exceeds maximum valid address by 1",
offset: 0, // arbitrary valid offset
length: memorySize + 1,
expectedLog: `
==> wasi_snapshot_preview1.random_get(buf=0,buf_len=65537)
<== EFAULT
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
requireErrno(t, ErrnoFault, mod, functionRandomGet, uint64(tc.offset), uint64(tc.length))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func Test_randomGet_SourceError(t *testing.T) {
tests := []struct {
name string
randSource io.Reader
expectedLog string
}{
{
name: "error",
randSource: iotest.ErrReader(errors.New("RandSource error")),
},
{
name: "incomplete",
randSource: bytes.NewReader([]byte{1, 2}),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithRandSource(tc.randSource))
defer r.Close(testCtx)
errno := randomGet(testCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
require.Equal(t, ErrnoIo, errno, ErrnoName(errno))
require.Equal(t, tc.expectedLog, log.String())
})
}
}

View File

@@ -0,0 +1,9 @@
package wasi_snapshot_preview1
const functionSchedYield = "sched_yield"
// schedYield is the WASI function named functionSchedYield which temporarily
// yields execution of the calling thread.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno
var schedYield = stubFunction() // stubbed for GrainLang per #271.

View File

@@ -0,0 +1,16 @@
package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
// Test_schedYield only tests it is stubbed for GrainLang per #271
func Test_schedYield(t *testing.T) {
log := requireErrnoNosys(t, functionSchedYield)
require.Equal(t, `
--> wasi_snapshot_preview1.sched_yield()
<-- ENOSYS
`, log)
}

View File

@@ -0,0 +1,25 @@
package wasi_snapshot_preview1
const (
functionSockRecv = "sock_recv"
functionSockSend = "sock_send"
functionSockShutdown = "sock_shutdown"
)
// sockRecv is the WASI function named functionSockRecv which receives a
// message from a socket.
//
// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_recvfd-fd-ri_data-iovec_array-ri_flags-riflags---errno-size-roflags
var sockRecv = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// sockSend is the WASI function named functionSockSend which sends a message
// on a socket.
//
// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_sendfd-fd-si_data-ciovec_array-si_flags-siflags---errno-size
var sockSend = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
// sockShutdown is the WASI function named functionSockShutdown which shuts
// down socket send and receive channels.
//
// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_shutdownfd-fd-how-sdflags---errno
var sockShutdown = stubFunction(i32, i32) // stubbed for GrainLang per #271.

View File

@@ -0,0 +1,34 @@
package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
// Test_sockRecv only tests it is stubbed for GrainLang per #271
func Test_sockRecv(t *testing.T) {
log := requireErrnoNosys(t, functionSockRecv, 0, 0, 0, 0, 0, 0)
require.Equal(t, `
--> wasi_snapshot_preview1.sock_recv(fd=0,ri_data=0,ri_data_count=0,ri_flags=0,result.ro_datalen=0,result.ro_flags=0)
<-- ENOSYS
`, log)
}
// Test_sockSend only tests it is stubbed for GrainLang per #271
func Test_sockSend(t *testing.T) {
log := requireErrnoNosys(t, functionSockSend, 0, 0, 0, 0, 0)
require.Equal(t, `
--> wasi_snapshot_preview1.sock_send(fd=0,si_data=0,si_data_count=0,si_flags=0,result.so_datalen=0)
<-- ENOSYS
`, log)
}
// Test_sockShutdown only tests it is stubbed for GrainLang per #271
func Test_sockShutdown(t *testing.T) {
log := requireErrnoNosys(t, functionSockShutdown, 0, 0)
require.Equal(t, `
--> wasi_snapshot_preview1.sock_shutdown(fd=0,how=0)
<-- ENOSYS
`, log)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,11 @@ package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
var testMem = &wasm.MemoryInstance{
Min: 1,
Buffer: []byte{
var testMem = []byte{
0, // environBuf is after this
'a', '=', 'b', 0, // null terminated "a=b",
'b', '=', 'c', 'd', 0, // null terminated "b=cd"
@@ -18,38 +15,43 @@ var testMem = &wasm.MemoryInstance{
1, 0, 0, 0, // little endian-encoded offset of "a=b"
5, 0, 0, 0, // little endian-encoded offset of "b=cd"
0,
},
}
func Test_Benchmark_EnvironGet(t *testing.T) {
sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil)
require.NoError(t, err)
mod, r, log := requireModule(t, wazero.NewModuleConfig().
WithEnv("a", "b").WithEnv("b", "cd"))
defer r.Close(testCtx)
mod := newModule(make([]byte, 20), sysCtx)
environGet := (&wasi{}).EnvironGet
// Invoke environGet and check the memory side effects.
requireErrno(t, ErrnoSuccess, mod, functionEnvironGet, uint64(11), uint64(1))
require.Equal(t, `
==> wasi_snapshot_preview1.environ_get(environ=11,environ_buf=1)
<== ESUCCESS
`, "\n"+log.String())
require.Equal(t, ErrnoSuccess, environGet(testCtx, mod, 11, 1))
require.Equal(t, mod.Memory(), testMem)
mem, ok := mod.Memory().Read(testCtx, 0, uint32(len(testMem)))
require.True(t, ok)
require.Equal(t, testMem, mem)
}
func Benchmark_EnvironGet(b *testing.B) {
sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil)
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
compiled, err := r.NewModuleBuilder(b.Name()).
ExportMemoryWithMax("memory", 1, 1).
Compile(testCtx, wazero.NewCompileConfig())
if err != nil {
b.Fatal(err)
}
mod := newModule([]byte{
0, // environBuf is after this
'a', '=', 'b', 0, // null terminated "a=b",
'b', '=', 'c', 'd', 0, // null terminated "b=cd"
0, // environ is after this
1, 0, 0, 0, // little endian-encoded offset of "a=b"
5, 0, 0, 0, // little endian-encoded offset of "b=cd"
0,
}, sysCtx)
mod, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig().
WithEnv("a", "bc").WithEnv("b", "cd"))
if err != nil {
b.Fatal(err)
}
defer r.Close(testCtx)
environGet := (&wasi{}).EnvironGet
b.Run("EnvironGet", func(b *testing.B) {
b.Run("environGet", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if environGet(testCtx, mod, 0, 4) != ErrnoSuccess {
b.Fatal()
@@ -57,9 +59,3 @@ func Benchmark_EnvironGet(b *testing.B) {
}
})
}
func newModule(buf []byte, sys *sys.Context) *wasm.CallContext {
return wasm.NewCallContext(nil, &wasm.ModuleInstance{
Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf},
}, sys)
}

File diff suppressed because it is too large Load Diff