This improves the experimental logging listener to show parameter name
and values like so:
```
--> ._start.command_export()
--> .__wasm_call_ctors()
--> .__wasilibc_initialize_environ()
==> wasi_snapshot_preview1.environ_sizes_get(result.environc=1048572,result.environBufSize=1048568)
<== ESUCCESS
<-- ()
==> wasi_snapshot_preview1.fd_prestat_get(fd=3,result.prestat=1048568)
<== ESUCCESS
--> .dlmalloc(2)
--> .sbrk(0)
<-- (1114112)
<-- (1060080)
--snip--
```
The convention `==>` implies it was a host function call
(def.IsHostFunction). This also improves the lifecycle by creating
listeners during compile. Finally, this backfills param names for
assemblyscript and wasi.
Signed-off-by: Adrian Cole <adrian@tetrate.io>
265 lines
8.3 KiB
Go
265 lines
8.3 KiB
Go
package wasm
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/leb128"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
)
|
|
|
|
// wasiAPI simulates the real WASI api
|
|
type wasiAPI struct {
|
|
}
|
|
|
|
func ArgsSizesGet(ctx api.Module, resultArgc, resultArgvBufSize uint32) uint32 {
|
|
return 0
|
|
}
|
|
|
|
func (a *wasiAPI) ArgsSizesGet(ctx api.Module, resultArgc, resultArgvBufSize uint32) uint32 {
|
|
return 0
|
|
}
|
|
|
|
func (a *wasiAPI) FdWrite(ctx api.Module, fd, iovs, iovsCount, resultSize uint32) uint32 {
|
|
return 0
|
|
}
|
|
|
|
func swap(x, y uint32) (uint32, uint32) {
|
|
return y, x
|
|
}
|
|
|
|
func TestNewHostModule(t *testing.T) {
|
|
i32 := ValueTypeI32
|
|
|
|
a := wasiAPI{}
|
|
functionArgsSizesGet := "args_sizes_get"
|
|
fnArgsSizesGet := reflect.ValueOf(a.ArgsSizesGet)
|
|
functionFdWrite := "fd_write"
|
|
fnFdWrite := reflect.ValueOf(a.FdWrite)
|
|
functionSwap := "swap"
|
|
fnSwap := reflect.ValueOf(swap)
|
|
|
|
tests := []struct {
|
|
name, moduleName string
|
|
nameToGoFunc map[string]interface{}
|
|
nameToMemory map[string]*Memory
|
|
nameToGlobal map[string]*Global
|
|
expected *Module
|
|
}{
|
|
{
|
|
name: "empty",
|
|
expected: &Module{},
|
|
},
|
|
{
|
|
name: "only name",
|
|
moduleName: "test",
|
|
expected: &Module{NameSection: &NameSection{ModuleName: "test"}},
|
|
},
|
|
{
|
|
name: "funcs",
|
|
moduleName: "wasi_snapshot_preview1",
|
|
nameToGoFunc: map[string]interface{}{
|
|
functionArgsSizesGet: a.ArgsSizesGet,
|
|
functionFdWrite: a.FdWrite,
|
|
},
|
|
expected: &Module{
|
|
TypeSection: []*FunctionType{
|
|
{Params: []ValueType{i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
|
|
{Params: []ValueType{i32, i32, i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
|
|
},
|
|
FunctionSection: []Index{0, 1},
|
|
HostFunctionSection: []*reflect.Value{&fnArgsSizesGet, &fnFdWrite},
|
|
ExportSection: []*Export{
|
|
{Name: "args_sizes_get", Type: ExternTypeFunc, Index: 0},
|
|
{Name: "fd_write", Type: ExternTypeFunc, Index: 1},
|
|
},
|
|
NameSection: &NameSection{
|
|
ModuleName: "wasi_snapshot_preview1",
|
|
FunctionNames: NameMap{
|
|
{Index: 0, Name: "args_sizes_get"},
|
|
{Index: 1, Name: "fd_write"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multi-value",
|
|
moduleName: "swapper",
|
|
nameToGoFunc: map[string]interface{}{
|
|
functionSwap: swap,
|
|
},
|
|
expected: &Module{
|
|
TypeSection: []*FunctionType{{Params: []ValueType{i32, i32}, Results: []ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2}},
|
|
FunctionSection: []Index{0},
|
|
HostFunctionSection: []*reflect.Value{&fnSwap},
|
|
ExportSection: []*Export{{Name: "swap", Type: ExternTypeFunc, Index: 0}},
|
|
NameSection: &NameSection{ModuleName: "swapper", FunctionNames: NameMap{{Index: 0, Name: "swap"}}},
|
|
},
|
|
},
|
|
{
|
|
name: "memory",
|
|
nameToMemory: map[string]*Memory{"memory": {Min: 1, Max: 2}},
|
|
expected: &Module{
|
|
MemorySection: &Memory{Min: 1, Max: 2},
|
|
ExportSection: []*Export{{Name: "memory", Type: ExternTypeMemory, Index: 0}},
|
|
},
|
|
},
|
|
{
|
|
name: "globals",
|
|
nameToGlobal: map[string]*Global{
|
|
"g2": {
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)},
|
|
},
|
|
"g1": {
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1},
|
|
},
|
|
},
|
|
expected: &Module{
|
|
GlobalSection: []*Global{
|
|
{
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1},
|
|
},
|
|
{
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)},
|
|
},
|
|
},
|
|
ExportSection: []*Export{
|
|
{Name: "g1", Type: ExternTypeGlobal, Index: 0},
|
|
{Name: "g2", Type: ExternTypeGlobal, Index: 1},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "one of each",
|
|
moduleName: "env",
|
|
nameToGoFunc: map[string]interface{}{
|
|
functionArgsSizesGet: a.ArgsSizesGet,
|
|
},
|
|
nameToMemory: map[string]*Memory{
|
|
"memory": {Min: 1, Max: 1},
|
|
},
|
|
nameToGlobal: map[string]*Global{
|
|
"g": {
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1},
|
|
},
|
|
},
|
|
expected: &Module{
|
|
TypeSection: []*FunctionType{
|
|
{Params: []ValueType{i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
|
|
},
|
|
FunctionSection: []Index{0},
|
|
HostFunctionSection: []*reflect.Value{&fnArgsSizesGet},
|
|
GlobalSection: []*Global{
|
|
{
|
|
Type: &GlobalType{ValType: i32},
|
|
Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1},
|
|
},
|
|
},
|
|
MemorySection: &Memory{Min: 1, Max: 1},
|
|
ExportSection: []*Export{
|
|
{Name: "args_sizes_get", Type: ExternTypeFunc, Index: 0},
|
|
{Name: "memory", Type: ExternTypeMemory, Index: 0},
|
|
{Name: "g", Type: ExternTypeGlobal, Index: 0},
|
|
},
|
|
NameSection: &NameSection{
|
|
ModuleName: "env",
|
|
FunctionNames: NameMap{
|
|
{Index: 0, Name: "args_sizes_get"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205|FeatureMultiValue)
|
|
require.NoError(t, e)
|
|
requireHostModuleEquals(t, tc.expected, m)
|
|
})
|
|
}
|
|
}
|
|
|
|
func requireHostModuleEquals(t *testing.T, expected, actual *Module) {
|
|
// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
|
|
require.Equal(t, expected.TypeSection, actual.TypeSection)
|
|
require.Equal(t, expected.ImportSection, actual.ImportSection)
|
|
require.Equal(t, expected.FunctionSection, actual.FunctionSection)
|
|
require.Equal(t, expected.TableSection, actual.TableSection)
|
|
require.Equal(t, expected.MemorySection, actual.MemorySection)
|
|
require.Equal(t, expected.GlobalSection, actual.GlobalSection)
|
|
require.Equal(t, expected.ExportSection, actual.ExportSection)
|
|
require.Equal(t, expected.StartSection, actual.StartSection)
|
|
require.Equal(t, expected.ElementSection, actual.ElementSection)
|
|
require.Nil(t, actual.CodeSection) // Host functions are implemented in Go, not Wasm!
|
|
require.Equal(t, expected.DataSection, actual.DataSection)
|
|
require.Equal(t, expected.NameSection, actual.NameSection)
|
|
|
|
// Special case because reflect.Value can't be compared with Equals
|
|
require.Equal(t, len(expected.HostFunctionSection), len(actual.HostFunctionSection))
|
|
for i := range expected.HostFunctionSection {
|
|
require.Equal(t, (*expected.HostFunctionSection[i]).Type(), (*actual.HostFunctionSection[i]).Type())
|
|
}
|
|
}
|
|
|
|
func TestNewHostModule_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name, moduleName string
|
|
nameToGoFunc map[string]interface{}
|
|
nameToMemory map[string]*Memory
|
|
nameToGlobal map[string]*Global
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "not a function",
|
|
nameToGoFunc: map[string]interface{}{"fn": t},
|
|
expectedErr: "func[.fn] kind != func: ptr",
|
|
},
|
|
{
|
|
name: "function has multiple results",
|
|
nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }},
|
|
nameToMemory: map[string]*Memory{"mem": {Min: 1, Max: 1}},
|
|
expectedErr: "func[.fn] multiple result types invalid as feature \"multi-value\" is disabled",
|
|
},
|
|
{
|
|
name: "func collides on memory name",
|
|
nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
|
|
nameToMemory: map[string]*Memory{"fn": {Min: 1, Max: 1}},
|
|
expectedErr: "func[.fn] exports the same name as a memory",
|
|
},
|
|
{
|
|
name: "multiple memories",
|
|
nameToMemory: map[string]*Memory{"memory": {Min: 1, Max: 1}, "mem": {Min: 2, Max: 2}},
|
|
expectedErr: "only one memory is allowed, but configured: mem, memory",
|
|
},
|
|
{
|
|
name: "memory max < min",
|
|
nameToMemory: map[string]*Memory{"memory": {Min: 1, Max: 0}},
|
|
expectedErr: "memory[memory] min 1 pages (64 Ki) > max 0 pages (0 Ki)",
|
|
},
|
|
{
|
|
name: "func collides on global name",
|
|
nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
|
|
nameToGlobal: map[string]*Global{"fn": {}},
|
|
expectedErr: "func[.fn] exports the same name as a global",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205)
|
|
require.EqualError(t, e, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|