834 lines
24 KiB
Go
834 lines
24 KiB
Go
package wazero
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/experimental"
|
|
"github.com/tetratelabs/wazero/internal/filecache"
|
|
"github.com/tetratelabs/wazero/internal/leb128"
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
var (
|
|
binaryNamedZero = binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}})
|
|
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
|
testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
|
)
|
|
|
|
var _ context.Context = &HostContext{}
|
|
|
|
// HostContext contain the content will be used in host function call
|
|
type HostContext struct {
|
|
Content string
|
|
}
|
|
|
|
func (h *HostContext) Deadline() (deadline time.Time, ok bool) { return }
|
|
|
|
func (h *HostContext) Done() <-chan struct{} { return nil }
|
|
|
|
func (h *HostContext) Err() error { return nil }
|
|
|
|
func (h *HostContext) Value(key interface{}) interface{} { return nil }
|
|
|
|
func TestRuntime_CompileModule(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runtime Runtime
|
|
wasm *wasm.Module
|
|
moduleBuilder HostModuleBuilder
|
|
expected func(CompiledModule)
|
|
}{
|
|
{
|
|
name: "no name section",
|
|
wasm: &wasm.Module{},
|
|
},
|
|
{
|
|
name: "empty NameSection.ModuleName",
|
|
wasm: &wasm.Module{NameSection: &wasm.NameSection{}},
|
|
},
|
|
{
|
|
name: "NameSection.ModuleName",
|
|
wasm: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}},
|
|
expected: func(compiled CompiledModule) {
|
|
require.Equal(t, "test", compiled.Name())
|
|
},
|
|
},
|
|
{
|
|
name: "FunctionSection, but not exported",
|
|
wasm: &wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}},
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
|
|
},
|
|
expected: func(compiled CompiledModule) {
|
|
require.Nil(t, compiled.ImportedFunctions())
|
|
require.Zero(t, len(compiled.ExportedFunctions()))
|
|
},
|
|
},
|
|
{
|
|
name: "FunctionSection exported",
|
|
wasm: &wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}},
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
|
|
ExportSection: []wasm.Export{{
|
|
Type: wasm.ExternTypeFunc,
|
|
Name: "function",
|
|
Index: 0,
|
|
}},
|
|
},
|
|
expected: func(compiled CompiledModule) {
|
|
require.Nil(t, compiled.ImportedFunctions())
|
|
f := compiled.ExportedFunctions()["function"]
|
|
require.Equal(t, []api.ValueType{api.ValueTypeI32}, f.ParamTypes())
|
|
},
|
|
},
|
|
{
|
|
name: "MemorySection, but not exported",
|
|
wasm: &wasm.Module{
|
|
MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
|
|
},
|
|
expected: func(compiled CompiledModule) {
|
|
require.Nil(t, compiled.ImportedMemories())
|
|
require.Zero(t, len(compiled.ExportedMemories()))
|
|
},
|
|
},
|
|
{
|
|
name: "MemorySection exported",
|
|
wasm: &wasm.Module{
|
|
MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
|
|
ExportSection: []wasm.Export{{
|
|
Type: wasm.ExternTypeMemory,
|
|
Name: "memory",
|
|
Index: 0,
|
|
}},
|
|
},
|
|
expected: func(compiled CompiledModule) {
|
|
require.Nil(t, compiled.ImportedMemories())
|
|
mem := compiled.ExportedMemories()["memory"]
|
|
require.Equal(t, uint32(2), mem.Min())
|
|
max, ok := mem.Max()
|
|
require.Equal(t, uint32(3), max)
|
|
require.True(t, ok)
|
|
},
|
|
},
|
|
}
|
|
|
|
_r := NewRuntime(testCtx)
|
|
defer _r.Close(testCtx)
|
|
|
|
r := _r.(*runtime)
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
bin := binaryencoding.EncodeModule(tc.wasm)
|
|
|
|
m, err := r.CompileModule(testCtx, bin)
|
|
require.NoError(t, err)
|
|
if tc.expected == nil {
|
|
tc.expected = func(CompiledModule) {}
|
|
}
|
|
tc.expected(m)
|
|
require.Equal(t, r.store.Engine, m.(*compiledModule).compiledEngine)
|
|
|
|
// TypeIDs must be assigned to compiledModule.
|
|
expTypeIDs, err := r.store.GetFunctionTypeIDs(tc.wasm.TypeSection)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expTypeIDs, m.(*compiledModule).typeIDs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_CompileModule_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wasm []byte
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "nil",
|
|
expectedErr: "invalid magic number",
|
|
},
|
|
{
|
|
name: "invalid binary",
|
|
wasm: append(binaryencoding.Magic, []byte("yolo")...),
|
|
expectedErr: "invalid version header",
|
|
},
|
|
{
|
|
name: "memory has too many pages",
|
|
wasm: binaryencoding.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
|
|
expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)",
|
|
},
|
|
}
|
|
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := r.CompileModule(testCtx, tc.wasm)
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
|
|
func TestModule_Memory(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wasm []byte
|
|
expected bool
|
|
expectedLen uint32
|
|
}{
|
|
{
|
|
name: "no memory",
|
|
wasm: binaryencoding.EncodeModule(&wasm.Module{}),
|
|
},
|
|
{
|
|
name: "memory exported, one page",
|
|
wasm: binaryencoding.EncodeModule(&wasm.Module{
|
|
MemorySection: &wasm.Memory{Min: 1},
|
|
ExportSection: []wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}},
|
|
}),
|
|
expected: true,
|
|
expectedLen: 65536,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
// Instantiate the module and get the export of the above memory
|
|
module, err := r.Instantiate(testCtx, tc.wasm)
|
|
require.NoError(t, err)
|
|
|
|
mem := module.ExportedMemory("memory")
|
|
if tc.expected {
|
|
require.Equal(t, tc.expectedLen, mem.Size())
|
|
defs := module.ExportedMemoryDefinitions()
|
|
require.Equal(t, 1, len(defs))
|
|
def := defs["memory"]
|
|
require.Equal(t, tc.expectedLen>>16, def.Min())
|
|
} else {
|
|
require.Nil(t, mem)
|
|
require.Zero(t, len(module.ExportedMemoryDefinitions()))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestModule_Global only covers a couple cases to avoid duplication of internal/wasm/global_test.go
|
|
func TestModule_Global(t *testing.T) {
|
|
globalVal := int64(100) // intentionally a value that differs in signed vs unsigned encoding
|
|
|
|
tests := []struct {
|
|
name string
|
|
module *wasm.Module // module as wat doesn't yet support globals
|
|
expected, expectedMutable bool
|
|
}{
|
|
{
|
|
name: "no global",
|
|
module: &wasm.Module{},
|
|
},
|
|
{
|
|
name: "global not exported",
|
|
module: &wasm.Module{
|
|
GlobalSection: []wasm.Global{
|
|
{
|
|
Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
|
|
Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "global exported",
|
|
module: &wasm.Module{
|
|
GlobalSection: []wasm.Global{
|
|
{
|
|
Type: wasm.GlobalType{ValType: wasm.ValueTypeI64},
|
|
Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)},
|
|
},
|
|
},
|
|
Exports: map[string]*wasm.Export{
|
|
"global": {Type: wasm.ExternTypeGlobal, Name: "global"},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "global exported and mutable",
|
|
module: &wasm.Module{
|
|
GlobalSection: []wasm.Global{
|
|
{
|
|
Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
|
|
Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)},
|
|
},
|
|
},
|
|
Exports: map[string]*wasm.Export{
|
|
"global": {Type: wasm.ExternTypeGlobal, Name: "global"},
|
|
},
|
|
},
|
|
expected: true,
|
|
expectedMutable: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewRuntime(testCtx).(*runtime)
|
|
defer r.Close(testCtx)
|
|
|
|
code := &compiledModule{module: tc.module}
|
|
|
|
err := r.store.Engine.CompileModule(testCtx, code.module, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
// Instantiate the module and get the export of the above global
|
|
module, err := r.InstantiateModule(testCtx, code, NewModuleConfig())
|
|
require.NoError(t, err)
|
|
|
|
global := module.ExportedGlobal("global")
|
|
if !tc.expected {
|
|
require.Nil(t, global)
|
|
return
|
|
}
|
|
require.Equal(t, uint64(globalVal), global.Get())
|
|
|
|
mutable, ok := global.(api.MutableGlobal)
|
|
require.Equal(t, tc.expectedMutable, ok)
|
|
if ok {
|
|
mutable.Set(2)
|
|
require.Equal(t, uint64(2), global.Get())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
// Define a function that will be set as the start function
|
|
var calledStart bool
|
|
start := func(ctx context.Context) {
|
|
calledStart = true
|
|
require.Equal(t, testCtx, ctx)
|
|
}
|
|
|
|
_, err := r.NewHostModuleBuilder("env").
|
|
NewFunctionBuilder().WithFunc(start).Export("start").
|
|
Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
one := uint32(1)
|
|
binary := binaryencoding.EncodeModule(&wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
ImportSection: []wasm.Import{{Module: "env", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}},
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{
|
|
{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start.
|
|
},
|
|
StartSection: &one,
|
|
})
|
|
|
|
code, err := r.CompileModule(testCtx, binary)
|
|
require.NoError(t, err)
|
|
|
|
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
|
|
mod, err := r.InstantiateModule(testCtx, code, NewModuleConfig())
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, calledStart)
|
|
|
|
// Closing the module shouldn't remove the compiler cache
|
|
require.NoError(t, mod.Close(testCtx))
|
|
require.Equal(t, uint32(2), r.(*runtime).store.Engine.CompiledModuleCount())
|
|
}
|
|
|
|
// TestRuntime_Instantiate_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't
|
|
// export "_start".
|
|
func TestRuntime_Instantiate_DoesntEnforce_Start(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
binary := binaryencoding.EncodeModule(&wasm.Module{
|
|
MemorySection: &wasm.Memory{Min: 1},
|
|
ExportSection: []wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}},
|
|
})
|
|
|
|
mod, err := r.Instantiate(testCtx, binary)
|
|
require.NoError(t, err)
|
|
require.NoError(t, mod.Close(testCtx))
|
|
}
|
|
|
|
func TestRuntime_Instantiate_ErrorOnStart(t *testing.T) {
|
|
tests := []struct {
|
|
name, wasm string
|
|
}{
|
|
{
|
|
name: "_start function",
|
|
wasm: `(module
|
|
(import "" "start" (func $start))
|
|
(export "_start" (func $start))
|
|
)`,
|
|
},
|
|
{
|
|
name: ".start function",
|
|
wasm: `(module
|
|
(import "" "start" (func $start))
|
|
(start $start)
|
|
)`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
start := func() {
|
|
panic(errors.New("ice cream"))
|
|
}
|
|
|
|
host, err := r.NewHostModuleBuilder("host").
|
|
NewFunctionBuilder().WithFunc(start).Export("start").
|
|
Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Start the module as a WASI command. We expect it to fail.
|
|
_, err = r.Instantiate(testCtx, []byte(tc.wasm))
|
|
require.Error(t, err)
|
|
|
|
// Close the imported module, which should remove its compiler cache.
|
|
require.NoError(t, host.Close(testCtx))
|
|
|
|
// The compiler cache of the importing module should be removed on error.
|
|
require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRuntime_InstantiateModule_WithName tests that we can pre-validate (cache) a module and instantiate it under
|
|
// different names. This pattern is used in wapc-go.
|
|
func TestRuntime_InstantiateModule_WithName(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
base, err := r.CompileModule(testCtx, binaryNamedZero)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName)
|
|
|
|
// Use the same runtime to instantiate multiple modules
|
|
internal := r.(*runtime)
|
|
m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "1", m1.Name())
|
|
|
|
require.Nil(t, internal.Module("0"))
|
|
require.Equal(t, internal.Module("1"), m1)
|
|
|
|
m2, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("2"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "2", m2.Name())
|
|
|
|
require.Nil(t, internal.Module("0"))
|
|
require.Equal(t, internal.Module("2"), m2)
|
|
|
|
// Empty name module shouldn't be returned via Module() for future optimization.
|
|
m3, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName(""))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", m3.Name())
|
|
|
|
ret := internal.Module("")
|
|
require.Nil(t, ret)
|
|
}
|
|
|
|
func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
defer r.Close(testCtx)
|
|
|
|
tests := []struct {
|
|
name string
|
|
exitCode uint32
|
|
export bool
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "start: exit code 0",
|
|
exitCode: 0,
|
|
expectedErr: sys.NewExitError(0),
|
|
},
|
|
{
|
|
name: "start: exit code 2",
|
|
exitCode: 2,
|
|
expectedErr: sys.NewExitError(2),
|
|
},
|
|
{
|
|
name: "_start: exit code 0",
|
|
exitCode: 0,
|
|
export: true,
|
|
},
|
|
{
|
|
name: "_start: exit code 2",
|
|
exitCode: 2,
|
|
export: true,
|
|
expectedErr: sys.NewExitError(2),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
start := func(ctx context.Context, m api.Module) {
|
|
require.NoError(t, m.CloseWithExitCode(ctx, tc.exitCode))
|
|
}
|
|
|
|
env, err := r.NewHostModuleBuilder("env").
|
|
NewFunctionBuilder().WithFunc(start).Export("exit").
|
|
Instantiate(testCtx)
|
|
require.NoError(t, err)
|
|
defer env.Close(testCtx)
|
|
|
|
mod := &wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
ImportSection: []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}},
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{
|
|
{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start.
|
|
},
|
|
}
|
|
if tc.export {
|
|
mod.ExportSection = []wasm.Export{
|
|
{Name: "_start", Type: wasm.ExternTypeFunc, Index: 1},
|
|
}
|
|
} else {
|
|
one := uint32(1)
|
|
mod.StartSection = &one
|
|
}
|
|
binary := binaryencoding.EncodeModule(mod)
|
|
|
|
// Instantiate the module, which calls the start function.
|
|
m, err := r.InstantiateWithConfig(testCtx, binary,
|
|
NewModuleConfig().WithName("call-exit"))
|
|
|
|
// Ensure the exit error propagated and didn't wrap.
|
|
require.Equal(t, tc.expectedErr, err)
|
|
|
|
// Ensure calling close again doesn't break
|
|
if err == nil {
|
|
require.NoError(t, m.Close(testCtx))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_CloseWithExitCode(t *testing.T) {
|
|
bin := binaryencoding.EncodeModule(&wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
|
|
ExportSection: []wasm.Export{{Type: wasm.ExternTypeFunc, Index: 0, Name: "func"}},
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
exitCode uint32
|
|
}{
|
|
{
|
|
name: "exit code 0",
|
|
exitCode: uint32(0),
|
|
},
|
|
{
|
|
name: "exit code 2",
|
|
exitCode: uint32(2),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewRuntime(testCtx)
|
|
|
|
code, err := r.CompileModule(testCtx, bin)
|
|
require.NoError(t, err)
|
|
|
|
// Instantiate two modules.
|
|
m1, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod1"))
|
|
require.NoError(t, err)
|
|
m2, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod2"))
|
|
require.NoError(t, err)
|
|
|
|
func1 := m1.ExportedFunction("func")
|
|
require.Equal(t, map[string]api.FunctionDefinition{"func": func1.Definition()},
|
|
m1.ExportedFunctionDefinitions())
|
|
func2 := m2.ExportedFunction("func")
|
|
require.Equal(t, map[string]api.FunctionDefinition{"func": func2.Definition()},
|
|
m2.ExportedFunctionDefinitions())
|
|
|
|
// Modules not closed so calls succeed
|
|
|
|
_, err = func1.Call(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
_, err = func2.Call(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
if tc.exitCode == 0 {
|
|
err = r.Close(testCtx)
|
|
} else {
|
|
err = r.CloseWithExitCode(testCtx, tc.exitCode)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// Modules closed so calls fail
|
|
_, err = func1.Call(testCtx)
|
|
require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))
|
|
|
|
_, err = func2.Call(testCtx)
|
|
require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHostFunctionWithCustomContext(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
config RuntimeConfig
|
|
}{
|
|
{name: "compiler", config: NewRuntimeConfigCompiler()},
|
|
{name: "interpreter", config: NewRuntimeConfigInterpreter()},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
const fistString = "hello"
|
|
const secondString = "hello call"
|
|
hostCtx := &HostContext{fistString}
|
|
r := NewRuntimeWithConfig(hostCtx, tc.config)
|
|
defer r.Close(hostCtx)
|
|
|
|
// Define a function that will be set as the start function
|
|
var calledStart bool
|
|
var calledCall bool
|
|
start := func(ctx context.Context, module api.Module) {
|
|
hts, ok := ctx.(*HostContext)
|
|
if !ok {
|
|
t.Fatal("decorate call context could effect host ctx cast failed, please consider it.")
|
|
}
|
|
calledStart = true
|
|
require.NotNil(t, hts)
|
|
require.Equal(t, fistString, hts.Content)
|
|
}
|
|
|
|
callFunc := func(ctx context.Context, module api.Module) {
|
|
hts, ok := ctx.(*HostContext)
|
|
if !ok {
|
|
t.Fatal("decorate call context could effect host ctx cast failed, please consider it.")
|
|
}
|
|
calledCall = true
|
|
require.NotNil(t, hts)
|
|
require.Equal(t, secondString, hts.Content)
|
|
}
|
|
|
|
_, err := r.NewHostModuleBuilder("env").
|
|
NewFunctionBuilder().WithFunc(start).Export("host").
|
|
NewFunctionBuilder().WithFunc(callFunc).Export("host2").
|
|
Instantiate(hostCtx)
|
|
require.NoError(t, err)
|
|
|
|
startFnIndex := uint32(2)
|
|
binary := binaryencoding.EncodeModule(&wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
ImportSection: []wasm.Import{
|
|
{Module: "env", Name: "host", Type: wasm.ExternTypeFunc, DescFunc: 0},
|
|
{Module: "env", Name: "host2", Type: wasm.ExternTypeFunc, DescFunc: 0},
|
|
},
|
|
FunctionSection: []wasm.Index{0, 0},
|
|
CodeSection: []wasm.Code{
|
|
{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.host.
|
|
{Body: []byte{wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Call the imported env.host.
|
|
},
|
|
ExportSection: []wasm.Export{
|
|
{Type: api.ExternTypeFunc, Name: "callHost", Index: uint32(3)},
|
|
},
|
|
StartSection: &startFnIndex,
|
|
})
|
|
|
|
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
|
|
ins, err := r.Instantiate(hostCtx, binary)
|
|
require.NoError(t, err)
|
|
require.True(t, calledStart)
|
|
|
|
// add the new context content for call with used in host function
|
|
hostCtx.Content = secondString
|
|
_, err = ins.ExportedFunction("callHost").Call(hostCtx)
|
|
require.NoError(t, err)
|
|
require.True(t, calledCall)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntime_Close_ClosesCompiledModules(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
withCompilationCache bool
|
|
}{
|
|
{name: "with cache", withCompilationCache: true},
|
|
{name: "without cache", withCompilationCache: false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
|
|
conf := *engineLessConfig
|
|
conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine }
|
|
if tc.withCompilationCache {
|
|
conf.cache = NewCompilationCache()
|
|
}
|
|
r := NewRuntimeWithConfig(testCtx, &conf)
|
|
defer r.Close(testCtx)
|
|
|
|
// Normally compiled modules are closed when instantiated but this is never instantiated.
|
|
_, err := r.CompileModule(testCtx, binaryNamedZero)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint32(1), engine.CompiledModuleCount())
|
|
|
|
err = r.Close(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Closing the runtime should remove the compiler cache if cache is not configured.
|
|
require.Equal(t, !tc.withCompilationCache, engine.closed)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRuntime_Closed ensures invocation of closed Runtime's methods is safe.
|
|
func TestRuntime_Closed(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
errFunc func(r Runtime, mod CompiledModule) error
|
|
}{
|
|
{
|
|
name: "InstantiateModule",
|
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
|
_, err := r.InstantiateModule(testCtx, mod, NewModuleConfig())
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "Instantiate",
|
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
|
_, err := r.Instantiate(testCtx, binaryNamedZero)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "CompileModule",
|
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
|
_, err := r.CompileModule(testCtx, binaryNamedZero)
|
|
return err
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
|
|
conf := *engineLessConfig
|
|
conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine }
|
|
r := NewRuntimeWithConfig(testCtx, &conf)
|
|
defer r.Close(testCtx)
|
|
|
|
// Normally compiled modules are closed when instantiated but this is never instantiated.
|
|
mod, err := r.CompileModule(testCtx, binaryNamedZero)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint32(1), engine.CompiledModuleCount())
|
|
|
|
err = r.Close(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Closing the runtime should remove the compiler cache if cache is not configured.
|
|
require.True(t, engine.closed)
|
|
|
|
require.EqualError(t, tc.errFunc(r, mod), "runtime closed with exit_code(0)")
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockEngine struct {
|
|
name string
|
|
cachedModules map[*wasm.Module]struct{}
|
|
closed bool
|
|
}
|
|
|
|
// CompileModule implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) CompileModule(_ context.Context, module *wasm.Module, _ []experimental.FunctionListener, _ bool) error {
|
|
e.cachedModules[module] = struct{}{}
|
|
return nil
|
|
}
|
|
|
|
// CompiledModuleCount implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) CompiledModuleCount() uint32 {
|
|
return uint32(len(e.cachedModules))
|
|
}
|
|
|
|
// DeleteCompiledModule implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) {
|
|
delete(e.cachedModules, module)
|
|
}
|
|
|
|
// NewModuleEngine implements the same method as documented on wasm.Engine.
|
|
func (e *mockEngine) NewModuleEngine(_ *wasm.Module, _ *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// NewModuleEngine implements the same method as documented on wasm.Close.
|
|
func (e *mockEngine) Close() (err error) {
|
|
e.closed = true
|
|
return
|
|
}
|
|
|
|
// TestNewRuntime_concurrent ensures that concurrent execution of NewRuntime is race-free.
|
|
// This depends on -race flag.
|
|
func TestNewRuntime_concurrent(t *testing.T) {
|
|
const num = 100
|
|
var wg sync.WaitGroup
|
|
c := NewCompilationCache()
|
|
// If available, uses two engine configurations for the single compilation cache.
|
|
configs := [2]RuntimeConfig{NewRuntimeConfigInterpreter().WithCompilationCache(c)}
|
|
if platform.CompilerSupported() {
|
|
configs[1] = NewRuntimeConfigCompiler().WithCompilationCache(c)
|
|
} else {
|
|
configs[1] = NewRuntimeConfigInterpreter().WithCompilationCache(c)
|
|
}
|
|
wg.Add(num)
|
|
for i := 0; i < num; i++ {
|
|
i := i
|
|
go func() {
|
|
defer wg.Done()
|
|
r := NewRuntimeWithConfig(testCtx, configs[i%2])
|
|
err := r.Close(testCtx)
|
|
require.NoError(t, err)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|