experimental: add wazerotest package (#1437)
This commit is contained in:
@@ -12,11 +12,11 @@ import (
|
||||
type InternalModule interface {
|
||||
api.Module
|
||||
|
||||
// NumGlobal returns the count of all globals in the
|
||||
// module.
|
||||
// NumGlobal returns the count of all globals in the module.
|
||||
NumGlobal() int
|
||||
|
||||
// Global provides a read-only view for a given global
|
||||
// index. Panics if idx > GlobalsCount().
|
||||
Global(idx int) api.Global
|
||||
// Global provides a read-only view for a given global index.
|
||||
//
|
||||
// The methods panics if i is out of bounds.
|
||||
Global(i int) api.Global
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental/logging"
|
||||
"github.com/tetratelabs/wazero/experimental/wazerotest"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
wasi "github.com/tetratelabs/wazero/internal/wasip1"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
@@ -347,3 +348,25 @@ func Test_loggingListener_indentation(t *testing.T) {
|
||||
<--
|
||||
`, out.String())
|
||||
}
|
||||
|
||||
func BenchmarkLoggingListener(b *testing.B) {
|
||||
module := wazerotest.NewModule(
|
||||
wazerotest.NewMemory(0),
|
||||
wazerotest.NewFunction(func(ctx context.Context, mod api.Module) {}),
|
||||
)
|
||||
|
||||
function := module.Function(0)
|
||||
factory := logging.NewLoggingListenerFactory(discard{})
|
||||
listener := factory.NewListener(function.Definition())
|
||||
|
||||
stack := []wazerotest.StackFrame{
|
||||
{Function: function},
|
||||
}
|
||||
|
||||
wazerotest.BenchmarkFunctionListener(b, module, stack, listener)
|
||||
}
|
||||
|
||||
type discard struct{}
|
||||
|
||||
func (discard) Write(b []byte) (int, error) { return len(b), nil }
|
||||
func (discard) WriteString(s string) (int, error) { return len(s), nil }
|
||||
|
||||
727
experimental/wazerotest/wazerotest.go
Normal file
727
experimental/wazerotest/wazerotest.go
Normal file
@@ -0,0 +1,727 @@
|
||||
package wazerotest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"github.com/tetratelabs/wazero/internal/internalapi"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
const (
|
||||
exitStatusMarker = 1 << 63
|
||||
)
|
||||
|
||||
// Module is an implementation of the api.Module interface, it represents a
|
||||
// WebAssembly module.
|
||||
type Module struct {
|
||||
internalapi.WazeroOnlyType
|
||||
exitStatus uint64
|
||||
|
||||
// The module name that will be returned by calling the Name method.
|
||||
ModuleName string
|
||||
|
||||
// The list of functions of the module. Functions with a non-empty export
|
||||
// names will be exported by the module.
|
||||
Functions []*Function
|
||||
|
||||
// The list of globals of the module. Global
|
||||
Globals []*Global
|
||||
|
||||
// The program memory. If non-nil, the memory is automatically exported as
|
||||
// "memory".
|
||||
ExportMemory *Memory
|
||||
|
||||
once sync.Once
|
||||
exportedFunctions map[string]api.Function
|
||||
exportedFunctionDefinitions map[string]api.FunctionDefinition
|
||||
exportedGlobals map[string]api.Global
|
||||
exportedMemoryDefinitions map[string]api.MemoryDefinition
|
||||
}
|
||||
|
||||
// NewModule constructs a Module object with the given memory and function list.
|
||||
func NewModule(memory *Memory, functions ...*Function) *Module {
|
||||
return &Module{Functions: functions, ExportMemory: memory}
|
||||
}
|
||||
|
||||
func (m *Module) String() string {
|
||||
return "module[" + m.ModuleName + "]"
|
||||
}
|
||||
|
||||
func (m *Module) Name() string {
|
||||
return m.ModuleName
|
||||
}
|
||||
|
||||
func (m *Module) Memory() api.Memory {
|
||||
if m.ExportMemory != nil {
|
||||
m.once.Do(m.initialize)
|
||||
return m.ExportMemory
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) ExportedFunction(name string) api.Function {
|
||||
m.once.Do(m.initialize)
|
||||
return m.exportedFunctions[name]
|
||||
}
|
||||
|
||||
func (m *Module) ExportedFunctionDefinitions() map[string]api.FunctionDefinition {
|
||||
m.once.Do(m.initialize)
|
||||
return m.exportedFunctionDefinitions
|
||||
}
|
||||
|
||||
func (m *Module) ExportedMemory(name string) api.Memory {
|
||||
if m.ExportMemory != nil && name == "memory" {
|
||||
m.once.Do(m.initialize)
|
||||
return m.ExportMemory
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) ExportedMemoryDefinitions() map[string]api.MemoryDefinition {
|
||||
m.once.Do(m.initialize)
|
||||
return m.exportedMemoryDefinitions
|
||||
}
|
||||
|
||||
func (m *Module) ExportedGlobal(name string) api.Global {
|
||||
m.once.Do(m.initialize)
|
||||
return m.exportedGlobals[name]
|
||||
}
|
||||
|
||||
func (m *Module) NumFunction() int {
|
||||
return len(m.Functions)
|
||||
}
|
||||
|
||||
func (m *Module) Function(i int) api.Function {
|
||||
m.once.Do(m.initialize)
|
||||
return m.Functions[i]
|
||||
}
|
||||
|
||||
func (m *Module) NumGlobal() int {
|
||||
return len(m.Globals)
|
||||
}
|
||||
|
||||
func (m *Module) Global(i int) api.Global {
|
||||
m.once.Do(m.initialize)
|
||||
return m.Globals[i]
|
||||
}
|
||||
|
||||
func (m *Module) Close(ctx context.Context) error {
|
||||
return m.CloseWithExitCode(ctx, 0)
|
||||
}
|
||||
|
||||
func (m *Module) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
|
||||
atomic.CompareAndSwapUint64(&m.exitStatus, 0, exitStatusMarker|uint64(exitCode))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) ExitStatus() (exitCode uint32, exited bool) {
|
||||
exitStatus := atomic.LoadUint64(&m.exitStatus)
|
||||
return uint32(exitStatus), exitStatus != 0
|
||||
}
|
||||
|
||||
func (m *Module) initialize() {
|
||||
m.exportedFunctions = make(map[string]api.Function)
|
||||
m.exportedFunctionDefinitions = make(map[string]api.FunctionDefinition)
|
||||
m.exportedGlobals = make(map[string]api.Global)
|
||||
m.exportedMemoryDefinitions = make(map[string]api.MemoryDefinition)
|
||||
|
||||
for index, function := range m.Functions {
|
||||
for _, exportName := range function.ExportNames {
|
||||
m.exportedFunctions[exportName] = function
|
||||
m.exportedFunctionDefinitions[exportName] = function.Definition()
|
||||
}
|
||||
function.module = m
|
||||
function.index = index
|
||||
}
|
||||
|
||||
for _, global := range m.Globals {
|
||||
for _, exportName := range global.ExportNames {
|
||||
m.exportedGlobals[exportName] = global
|
||||
}
|
||||
}
|
||||
|
||||
if m.ExportMemory != nil {
|
||||
m.ExportMemory.module = m
|
||||
m.exportedMemoryDefinitions["memory"] = m.ExportMemory.Definition()
|
||||
}
|
||||
}
|
||||
|
||||
// Global is an implementation of the api.Global interface, it represents a
|
||||
// global in a WebAssembly module.
|
||||
type Global struct {
|
||||
internalapi.WazeroOnlyType
|
||||
|
||||
// Type of the global value, used to interpret bits of the Value field.
|
||||
ValueType api.ValueType
|
||||
|
||||
// Value of the global packed in a 64 bits field.
|
||||
Value uint64
|
||||
|
||||
// List of names that the globla is exported as.
|
||||
ExportNames []string
|
||||
}
|
||||
|
||||
func (g *Global) String() string {
|
||||
switch g.ValueType {
|
||||
case api.ValueTypeI32:
|
||||
return strconv.FormatInt(int64(api.DecodeI32(g.Value)), 10)
|
||||
case api.ValueTypeI64:
|
||||
return strconv.FormatInt(int64(g.Value), 10)
|
||||
case api.ValueTypeF32:
|
||||
return strconv.FormatFloat(float64(api.DecodeF32(g.Value)), 'g', -1, 32)
|
||||
case api.ValueTypeF64:
|
||||
return strconv.FormatFloat(api.DecodeF64(g.Value), 'g', -1, 64)
|
||||
default:
|
||||
return "0x" + strconv.FormatUint(g.Value, 16)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Global) Type() api.ValueType {
|
||||
return g.ValueType
|
||||
}
|
||||
|
||||
func (g *Global) Get() uint64 {
|
||||
return g.Value
|
||||
}
|
||||
|
||||
func GlobalI32(value int32, export ...string) *Global {
|
||||
return &Global{ValueType: api.ValueTypeI32, Value: api.EncodeI32(value), ExportNames: export}
|
||||
}
|
||||
|
||||
func GlobalI64(value int64, export ...string) *Global {
|
||||
return &Global{ValueType: api.ValueTypeI64, Value: api.EncodeI64(value), ExportNames: export}
|
||||
}
|
||||
|
||||
func GlobalF32(value float32, export ...string) *Global {
|
||||
return &Global{ValueType: api.ValueTypeF32, Value: api.EncodeF32(value), ExportNames: export}
|
||||
}
|
||||
|
||||
func GlobalF64(value float64, export ...string) *Global {
|
||||
return &Global{ValueType: api.ValueTypeF64, Value: api.EncodeF64(value), ExportNames: export}
|
||||
}
|
||||
|
||||
// Function is an implementation of the api.Function interface, it represents
|
||||
// a function in a WebAssembly module.
|
||||
//
|
||||
// Until accessed through a Module's method, the function definition's
|
||||
// ModuleName method returns an empty string and its Index method returns 0.
|
||||
type Function struct {
|
||||
internalapi.WazeroOnlyType
|
||||
|
||||
// GoModuleFunction may be set to a non-nil value to allow calling of the
|
||||
// function via Call or CallWithStack.
|
||||
//
|
||||
// It is the user's responsibility to ensure that the signature of this
|
||||
// implementation matches the ParamTypes and ResultTypes fields.
|
||||
GoModuleFunction api.GoModuleFunction
|
||||
|
||||
// Type lists representing the function signature. Those fields should be
|
||||
// set for the function to be properly constructed. The Function's Call
|
||||
// and CallWithStack methods will error if those fields are nil.
|
||||
ParamTypes []api.ValueType
|
||||
ResultTypes []api.ValueType
|
||||
|
||||
// Sets of names associated with the function. It is valid to leave those
|
||||
// names empty, they are only used for debugging purposes.
|
||||
FunctionName string
|
||||
DebugName string
|
||||
ParamNames []string
|
||||
ResultNames []string
|
||||
ExportNames []string
|
||||
|
||||
// Lazily initialized when accessed through the module.
|
||||
module *Module
|
||||
index int
|
||||
}
|
||||
|
||||
// NewFunction constructs a Function object from a Go function.
|
||||
//
|
||||
// The function fn must accept at least two arguments of type context.Context
|
||||
// and api.Module. Any other arguments and return values must be of type uint32,
|
||||
// uint64, int32, int64, float32, or float64. The call panics if fn is not a Go
|
||||
// functionn or has an unsupported signature.
|
||||
func NewFunction(fn any) *Function {
|
||||
functionType := reflect.TypeOf(fn)
|
||||
functionValue := reflect.ValueOf(fn)
|
||||
|
||||
paramTypes := make([]api.ValueType, functionType.NumIn()-2)
|
||||
paramFuncs := make([]func(uint64) reflect.Value, len(paramTypes))
|
||||
|
||||
resultTypes := make([]api.ValueType, functionType.NumOut())
|
||||
resultFuncs := make([]func(reflect.Value) uint64, len(resultTypes))
|
||||
|
||||
for i := range paramTypes {
|
||||
var paramType api.ValueType
|
||||
var paramFunc func(uint64) reflect.Value
|
||||
|
||||
switch functionType.In(i + 2).Kind() {
|
||||
case reflect.Uint32:
|
||||
paramType = api.ValueTypeI32
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeU32(v)) }
|
||||
case reflect.Uint64:
|
||||
paramType = api.ValueTypeI64
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(v) }
|
||||
case reflect.Int32:
|
||||
paramType = api.ValueTypeI32
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeI32(v)) }
|
||||
case reflect.Int64:
|
||||
paramType = api.ValueTypeI64
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(int64(v)) }
|
||||
case reflect.Float32:
|
||||
paramType = api.ValueTypeF32
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeF32(v)) }
|
||||
case reflect.Float64:
|
||||
paramType = api.ValueTypeF64
|
||||
paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeF64(v)) }
|
||||
default:
|
||||
panic("cannot construct wasm function from go function of type " + functionType.String())
|
||||
}
|
||||
|
||||
paramTypes[i] = paramType
|
||||
paramFuncs[i] = paramFunc
|
||||
}
|
||||
|
||||
for i := range resultTypes {
|
||||
var resultType api.ValueType
|
||||
var resultFunc func(reflect.Value) uint64
|
||||
|
||||
switch functionType.Out(i).Kind() {
|
||||
case reflect.Uint32:
|
||||
resultType = api.ValueTypeI32
|
||||
resultFunc = func(v reflect.Value) uint64 { return v.Uint() }
|
||||
case reflect.Uint64:
|
||||
resultType = api.ValueTypeI64
|
||||
resultFunc = func(v reflect.Value) uint64 { return v.Uint() }
|
||||
case reflect.Int32:
|
||||
resultType = api.ValueTypeI32
|
||||
resultFunc = func(v reflect.Value) uint64 { return api.EncodeI32(int32(v.Int())) }
|
||||
case reflect.Int64:
|
||||
resultType = api.ValueTypeI64
|
||||
resultFunc = func(v reflect.Value) uint64 { return api.EncodeI64(v.Int()) }
|
||||
case reflect.Float32:
|
||||
resultType = api.ValueTypeF32
|
||||
resultFunc = func(v reflect.Value) uint64 { return api.EncodeF32(float32(v.Float())) }
|
||||
case reflect.Float64:
|
||||
resultType = api.ValueTypeF64
|
||||
resultFunc = func(v reflect.Value) uint64 { return api.EncodeF64(v.Float()) }
|
||||
default:
|
||||
panic("cannot construct wasm function from go function of type " + functionType.String())
|
||||
}
|
||||
|
||||
resultTypes[i] = resultType
|
||||
resultFuncs[i] = resultFunc
|
||||
}
|
||||
|
||||
return &Function{
|
||||
GoModuleFunction: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
in := make([]reflect.Value, 2+len(paramFuncs))
|
||||
in[0] = reflect.ValueOf(ctx)
|
||||
in[1] = reflect.ValueOf(mod)
|
||||
for i, param := range paramFuncs {
|
||||
in[i+2] = param(stack[i])
|
||||
}
|
||||
out := functionValue.Call(in)
|
||||
for i, result := range resultFuncs {
|
||||
stack[i] = result(out[i])
|
||||
}
|
||||
}),
|
||||
ParamTypes: paramTypes,
|
||||
ResultTypes: resultTypes,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errMissingFunctionSignature = errors.New("missing function signature")
|
||||
errMissingFunctionModule = errors.New("missing function module")
|
||||
errMissingFunctionImplementation = errors.New("missing function implementation")
|
||||
)
|
||||
|
||||
func (f *Function) Definition() api.FunctionDefinition {
|
||||
return functionDefinition{function: f}
|
||||
}
|
||||
|
||||
func (f *Function) Call(ctx context.Context, params ...uint64) ([]uint64, error) {
|
||||
stackLen := len(f.ParamTypes)
|
||||
if stackLen < len(f.ResultTypes) {
|
||||
stackLen = len(f.ResultTypes)
|
||||
}
|
||||
stack := make([]uint64, stackLen)
|
||||
copy(stack, params)
|
||||
err := f.CallWithStack(ctx, stack)
|
||||
if err != nil {
|
||||
for i := range stack {
|
||||
stack[i] = 0
|
||||
}
|
||||
}
|
||||
return stack[:len(f.ResultTypes)], err
|
||||
}
|
||||
|
||||
func (f *Function) CallWithStack(ctx context.Context, stack []uint64) error {
|
||||
if f.ParamTypes == nil || f.ResultTypes == nil {
|
||||
return errMissingFunctionSignature
|
||||
}
|
||||
if f.GoModuleFunction == nil {
|
||||
return errMissingFunctionImplementation
|
||||
}
|
||||
if f.module == nil {
|
||||
return errMissingFunctionModule
|
||||
}
|
||||
if exitCode, exited := f.module.ExitStatus(); exited {
|
||||
return sys.NewExitError(exitCode)
|
||||
}
|
||||
f.GoModuleFunction.Call(ctx, f.module, stack)
|
||||
return nil
|
||||
}
|
||||
|
||||
type functionDefinition struct {
|
||||
internalapi.WazeroOnlyType
|
||||
function *Function
|
||||
}
|
||||
|
||||
func (def functionDefinition) Name() string {
|
||||
return def.function.FunctionName
|
||||
}
|
||||
|
||||
func (def functionDefinition) DebugName() string {
|
||||
if def.function.DebugName != "" {
|
||||
return def.function.DebugName
|
||||
}
|
||||
return fmt.Sprintf("%s.$%d", def.ModuleName(), def.Index())
|
||||
}
|
||||
|
||||
func (def functionDefinition) GoFunction() any {
|
||||
return def.function.GoModuleFunction
|
||||
}
|
||||
|
||||
func (def functionDefinition) ParamTypes() []api.ValueType {
|
||||
return def.function.ParamTypes
|
||||
}
|
||||
|
||||
func (def functionDefinition) ParamNames() []string {
|
||||
return def.function.ParamNames
|
||||
}
|
||||
|
||||
func (def functionDefinition) ResultTypes() []api.ValueType {
|
||||
return def.function.ResultTypes
|
||||
}
|
||||
|
||||
func (def functionDefinition) ResultNames() []string {
|
||||
return def.function.ResultNames
|
||||
}
|
||||
|
||||
func (def functionDefinition) ModuleName() string {
|
||||
if def.function.module != nil {
|
||||
return def.function.module.ModuleName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (def functionDefinition) Index() uint32 {
|
||||
return uint32(def.function.index)
|
||||
}
|
||||
|
||||
func (def functionDefinition) Import() (moduleName, name string, isImport bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (def functionDefinition) ExportNames() []string {
|
||||
return def.function.ExportNames
|
||||
}
|
||||
|
||||
// Memory is an implementation of the api.Memory interface, representing the
|
||||
// memory of a WebAssembly module.
|
||||
type Memory struct {
|
||||
internalapi.WazeroOnlyType
|
||||
|
||||
// Byte slices holding the memory pages.
|
||||
//
|
||||
// It is the user's repsonsibility to ensure that the length of this byte
|
||||
// slice is a multiple of the page size.
|
||||
Bytes []byte
|
||||
|
||||
// Min and max number of memory pages which may be held in this memory.
|
||||
//
|
||||
// Leaving Max to zero means no upper bound.
|
||||
Min uint32
|
||||
Max uint32
|
||||
|
||||
// Lazily initialized when accessed through the module.
|
||||
module *Module
|
||||
}
|
||||
|
||||
// NewMemory constructs a Memory object with a buffer of the given size, aligned
|
||||
// to the closest multiple of the page size.
|
||||
func NewMemory(size int) *Memory {
|
||||
numPages := (size + (PageSize - 1)) / PageSize
|
||||
return &Memory{
|
||||
Bytes: make([]byte, numPages*PageSize),
|
||||
Min: uint32(numPages),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFixedMemory constructs a Memory object of the given size. The returned
|
||||
// memory is configured with a max limit to prevent growing beyond its initial
|
||||
// size.
|
||||
func NewFixedMemory(size int) *Memory {
|
||||
memory := NewMemory(size)
|
||||
memory.Max = memory.Min
|
||||
return memory
|
||||
}
|
||||
|
||||
// The PageSize constant defines the size of WebAssembly memory pages in bytes.
|
||||
//
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#page-size
|
||||
const PageSize = 65536
|
||||
|
||||
func (m *Memory) Definition() api.MemoryDefinition {
|
||||
return memoryDefinition{memory: m}
|
||||
}
|
||||
|
||||
func (m *Memory) Size() uint32 {
|
||||
return uint32(len(m.Bytes))
|
||||
}
|
||||
|
||||
func (m *Memory) Grow(deltaPages uint32) (previousPages uint32, ok bool) {
|
||||
previousPages = uint32(len(m.Bytes) / PageSize)
|
||||
numPages := previousPages + deltaPages
|
||||
if m.Max != 0 && numPages > m.Max {
|
||||
return previousPages, false
|
||||
}
|
||||
bytes := make([]byte, PageSize*numPages)
|
||||
copy(bytes, m.Bytes)
|
||||
m.Bytes = bytes
|
||||
return previousPages, true
|
||||
}
|
||||
|
||||
func (m *Memory) ReadByte(offset uint32) (byte, bool) {
|
||||
if m.isOutOfRange(offset, 1) {
|
||||
return 0, false
|
||||
}
|
||||
return m.Bytes[offset], true
|
||||
}
|
||||
|
||||
func (m *Memory) ReadUint16Le(offset uint32) (uint16, bool) {
|
||||
if m.isOutOfRange(offset, 2) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint16(m.Bytes[offset:]), true
|
||||
}
|
||||
|
||||
func (m *Memory) ReadUint32Le(offset uint32) (uint32, bool) {
|
||||
if m.isOutOfRange(offset, 4) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint32(m.Bytes[offset:]), true
|
||||
}
|
||||
|
||||
func (m *Memory) ReadUint64Le(offset uint32) (uint64, bool) {
|
||||
if m.isOutOfRange(offset, 8) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint64(m.Bytes[offset:]), true
|
||||
}
|
||||
|
||||
func (m *Memory) ReadFloat32Le(offset uint32) (float32, bool) {
|
||||
v, ok := m.ReadUint32Le(offset)
|
||||
return math.Float32frombits(v), ok
|
||||
}
|
||||
|
||||
func (m *Memory) ReadFloat64Le(offset uint32) (float64, bool) {
|
||||
v, ok := m.ReadUint64Le(offset)
|
||||
return math.Float64frombits(v), ok
|
||||
}
|
||||
|
||||
func (m *Memory) Read(offset, length uint32) ([]byte, bool) {
|
||||
if m.isOutOfRange(offset, length) {
|
||||
return nil, false
|
||||
}
|
||||
return m.Bytes[offset : offset+length : offset+length], true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteByte(offset uint32, value byte) bool {
|
||||
if m.isOutOfRange(offset, 1) {
|
||||
return false
|
||||
}
|
||||
m.Bytes[offset] = value
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteUint16Le(offset uint32, value uint16) bool {
|
||||
if m.isOutOfRange(offset, 2) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint16(m.Bytes[offset:], value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteUint32Le(offset uint32, value uint32) bool {
|
||||
if m.isOutOfRange(offset, 4) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint32(m.Bytes[offset:], value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteUint64Le(offset uint32, value uint64) bool {
|
||||
if m.isOutOfRange(offset, 4) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint64(m.Bytes[offset:], value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteFloat32Le(offset uint32, value float32) bool {
|
||||
return m.WriteUint32Le(offset, math.Float32bits(value))
|
||||
}
|
||||
|
||||
func (m *Memory) WriteFloat64Le(offset uint32, value float64) bool {
|
||||
return m.WriteUint64Le(offset, math.Float64bits(value))
|
||||
}
|
||||
|
||||
func (m *Memory) Write(offset uint32, value []byte) bool {
|
||||
if m.isOutOfRange(offset, uint32(len(value))) {
|
||||
return false
|
||||
}
|
||||
copy(m.Bytes[offset:], value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) WriteString(offset uint32, value string) bool {
|
||||
if m.isOutOfRange(offset, uint32(len(value))) {
|
||||
return false
|
||||
}
|
||||
copy(m.Bytes[offset:], value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Memory) isOutOfRange(offset, length uint32) bool {
|
||||
size := m.Size()
|
||||
return offset >= size || length > size || offset > (size-length)
|
||||
}
|
||||
|
||||
type memoryDefinition struct {
|
||||
internalapi.WazeroOnlyType
|
||||
memory *Memory
|
||||
}
|
||||
|
||||
func (def memoryDefinition) ModuleName() string {
|
||||
if def.memory.module != nil {
|
||||
return def.memory.module.ModuleName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (def memoryDefinition) Index() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (def memoryDefinition) Import() (moduleName, name string, isImport bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (def memoryDefinition) ExportNames() []string {
|
||||
if def.memory.module != nil {
|
||||
return []string{"memory"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (def memoryDefinition) Min() uint32 {
|
||||
return def.memory.Min
|
||||
}
|
||||
|
||||
func (def memoryDefinition) Max() (uint32, bool) {
|
||||
return def.memory.Max, def.memory.Max != 0
|
||||
}
|
||||
|
||||
// StackFrame represents a frame on the call stack.
|
||||
type StackFrame struct {
|
||||
Function api.Function
|
||||
Params []uint64
|
||||
Results []uint64
|
||||
}
|
||||
|
||||
type stackIterator struct {
|
||||
stack []StackFrame
|
||||
fndef []api.FunctionDefinition
|
||||
index int
|
||||
}
|
||||
|
||||
func (si *stackIterator) Next() bool {
|
||||
si.index++
|
||||
return si.index < len(si.stack)
|
||||
}
|
||||
|
||||
func (si *stackIterator) FunctionDefinition() api.FunctionDefinition {
|
||||
return si.fndef[si.index]
|
||||
}
|
||||
|
||||
func (si *stackIterator) Parameters() []uint64 {
|
||||
return si.stack[si.index].Params
|
||||
}
|
||||
|
||||
func (si *stackIterator) reset() {
|
||||
si.index = 0
|
||||
}
|
||||
|
||||
func newStackIterator(stack []StackFrame) *stackIterator {
|
||||
si := &stackIterator{
|
||||
stack: stack,
|
||||
fndef: make([]api.FunctionDefinition, len(stack)),
|
||||
}
|
||||
// The size of functionDefinition is only one pointer which should allow
|
||||
// the compiler to optimize the conversion to api.FunctionDefinition; but
|
||||
// the presence of internal.WazeroOnlyType, despite being defined as an
|
||||
// empty struct, forces a heap allocation that we amortize by caching the
|
||||
// result.
|
||||
for i, frame := range stack {
|
||||
si.fndef[i] = frame.Function.Definition()
|
||||
}
|
||||
return si
|
||||
}
|
||||
|
||||
var (
|
||||
_ experimental.InternalModule = (*Module)(nil)
|
||||
|
||||
_ api.Module = (*Module)(nil)
|
||||
_ api.Function = (*Function)(nil)
|
||||
_ api.Global = (*Global)(nil)
|
||||
)
|
||||
|
||||
// BenchmarkFunctionListener implements a benchmark for function listeners.
|
||||
//
|
||||
// The benchmark calls Before and After methods repeatedly using the provided
|
||||
// module an stack frames to invoke the methods.
|
||||
//
|
||||
// The stack frame is a representation of the call stack that the Before method
|
||||
// will be invoked with. The top of the stack is stored at index zero. The stack
|
||||
// must contain at least one frame or the benchmark will fail.
|
||||
func BenchmarkFunctionListener(b *testing.B, module *Module, stack []StackFrame, listener experimental.FunctionListener) {
|
||||
if len(stack) == 0 {
|
||||
b.Error("cannot benchmark function listener with an empty stack")
|
||||
return
|
||||
}
|
||||
|
||||
functionDefinition := stack[0].Function.Definition()
|
||||
functionParams := stack[0].Params
|
||||
functionResults := stack[0].Results
|
||||
stackIterator := newStackIterator(stack)
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
stackIterator.reset()
|
||||
callContext := listener.Before(ctx, module, functionDefinition, functionParams, stackIterator)
|
||||
listener.After(callContext, module, functionDefinition, nil, functionResults)
|
||||
}
|
||||
}
|
||||
167
experimental/wazerotest/wazerotest_test.go
Normal file
167
experimental/wazerotest/wazerotest_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package wazerotest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
func TestNewFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
in any
|
||||
out *Function
|
||||
}{
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v uint32) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeI32},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v uint64) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeI64},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v int32) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeI32},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v int64) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeI64},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v float32) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeF32},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, v float64) {},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{api.ValueTypeF64},
|
||||
ResultTypes: []api.ValueType{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) uint32 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeI32},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) uint64 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeI64},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) int32 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeI32},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) int64 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeI64},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) float32 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeF32},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module) float64 { return 0 },
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{},
|
||||
ResultTypes: []api.ValueType{api.ValueTypeF64},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
in: func(ctx context.Context, mod api.Module, _ uint32, _ uint64, _ int32, _ int64, _ float32, _ float64) (_ uint32, _ uint64, _ int32, _ int64, _ float32, _ float64) {
|
||||
return
|
||||
},
|
||||
out: &Function{
|
||||
ParamTypes: []api.ValueType{
|
||||
api.ValueTypeI32, api.ValueTypeI64,
|
||||
api.ValueTypeI32, api.ValueTypeI64,
|
||||
api.ValueTypeF32, api.ValueTypeF64,
|
||||
},
|
||||
ResultTypes: []api.ValueType{
|
||||
api.ValueTypeI32, api.ValueTypeI64,
|
||||
api.ValueTypeI32, api.ValueTypeI64,
|
||||
api.ValueTypeF32, api.ValueTypeF64,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(reflect.TypeOf(test.in).String(), func(t *testing.T) {
|
||||
f := NewFunction(test.in)
|
||||
if !bytes.Equal(test.out.ParamTypes, f.ParamTypes) {
|
||||
t.Errorf("invalid parameter types: want=%v got=%v", test.out.ParamTypes, f.ParamTypes)
|
||||
}
|
||||
if !bytes.Equal(test.out.ResultTypes, f.ResultTypes) {
|
||||
t.Errorf("invalid result types: want=%v got=%v", test.out.ResultTypes, f.ResultTypes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMemory(t *testing.T) {
|
||||
if memory := NewMemory(1); len(memory.Bytes) != PageSize {
|
||||
t.Error("memory buffer not aligned on page size")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFixedMemory(t *testing.T) {
|
||||
if memory := NewFixedMemory(1); len(memory.Bytes) != PageSize {
|
||||
t.Error("memory buffer not aligned on page size")
|
||||
} else if memory.Min != 1 {
|
||||
t.Error("invalid min memory size:", memory.Min)
|
||||
} else if memory.Max != 1 {
|
||||
t.Error("invalid max memory size:", memory.Max)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user