wazevo: supports for LookupFunction API (#1704)

Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
This commit is contained in:
Takeshi Yoneda
2023-09-13 12:36:56 +09:00
committed by GitHub
parent 9c4291a581
commit c9019e6406
9 changed files with 177 additions and 131 deletions

View File

@@ -53,11 +53,6 @@ func TestCompiler_MemoryGrowInRecursiveCall(t *testing.T) {
enginetest.RunTestEngineMemoryGrowInRecursiveCall(t, et)
}
func TestCompiler_ModuleEngine_LookupFunction(t *testing.T) {
defer functionLog.Reset()
enginetest.RunTestModuleEngineLookupFunction(t, et)
}
func TestCompiler_ModuleEngine_Call(t *testing.T) {
defer functionLog.Reset()
requireSupportedOSArch(t)

View File

@@ -92,10 +92,6 @@ func TestInterpreter_Engine_NewModuleEngine(t *testing.T) {
enginetest.RunTestEngineNewModuleEngine(t, et)
}
func TestInterpreter_ModuleEngine_LookupFunction(t *testing.T) {
enginetest.RunTestModuleEngineLookupFunction(t, et)
}
func TestInterpreter_ModuleEngine_Call(t *testing.T) {
defer functionLog.Reset()
enginetest.RunTestModuleEngineCall(t, et)

View File

@@ -141,6 +141,10 @@ func (c *callEngine) CallWithStack(ctx context.Context, paramResultStack []uint6
err = builder.FromRecovered(r)
// TODO: Abort listener.
} else {
if err != wasmruntime.ErrRuntimeStackOverflow { // Stackoverflow case shouldn't be panic (to avoid extreme stack unwinding).
err = c.parent.module.FailIfClosed()
}
}
}()
@@ -212,7 +216,7 @@ func (c *callEngine) CallWithStack(ctx context.Context, paramResultStack []uint6
}
func (c *callEngine) callerModuleInstance() *wasm.ModuleInstance {
return *(**wasm.ModuleInstance)(unsafe.Pointer(c.execCtx.callerModuleContextPtr))
return moduleInstanceFromOpaquePtr(c.execCtx.callerModuleContextPtr)
}
func opaqueViewFromPtr(ptr uintptr) []byte {

View File

@@ -27,12 +27,12 @@ const (
f64 = wasm.ValueTypeF64
)
// TODO: migrate to integration_test/spectest/v1/spec_test.go by the time when closing https://github.com/tetratelabs/wazero/issues/1496
func TestSpectestV1(t *testing.T) {
config := wazero.NewRuntimeConfigCompiler().WithCoreFeatures(api.CoreFeaturesV1)
// Configure the new optimizing backend!
wazevo.ConfigureWazevo(config)
// TODO: migrate to integration_test/spectest/v1/spec_test.go by the time when closing https://github.com/tetratelabs/wazero/issues/1496
for _, tc := range []struct {
name string
}{
@@ -120,6 +120,7 @@ func TestSpectestV1(t *testing.T) {
}
}
// TODO: migrate to integration_test/spectest/v2/spec_test.go by the time when closing https://github.com/tetratelabs/wazero/issues/1496
func TestSpectestV2(t *testing.T) {
config := wazero.NewRuntimeConfigCompiler().WithCoreFeatures(api.CoreFeaturesV2)
// Configure the new optimizing backend!

View File

@@ -3,6 +3,7 @@ package wazevo
import (
"context"
"encoding/hex"
"errors"
"fmt"
"runtime"
"sort"
@@ -464,7 +465,7 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.
compiled, ok := e.compiledModules[m.ID]
if !ok {
return nil, fmt.Errorf("binary of module %q is not compiled", mi.ModuleName)
return nil, errors.New("source module must be compiled before instantiation")
}
me.parent = compiled
me.module = mi

View File

@@ -7,6 +7,7 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmruntime"
)
type (
@@ -25,6 +26,7 @@ type (
executable *byte
moduleContextOpaquePtr *byte
typeID wasm.FunctionTypeID
indexInModule wasm.Index
}
importedFunction struct {
@@ -217,15 +219,16 @@ func (m *moduleEngine) FunctionInstanceReference(funcIndex wasm.Index) wasm.Refe
begin, _, _ := m.parent.offsets.ImportedFunctionOffset(funcIndex)
return uintptr(unsafe.Pointer(&m.opaque[begin]))
}
funcIndex -= m.module.Source.ImportFunctionCount
localIndex := funcIndex - m.module.Source.ImportFunctionCount
p := m.parent
executable := &p.executable[p.functionOffsets[funcIndex].nativeBegin()]
typeID := m.module.TypeIDs[m.module.Source.FunctionSection[funcIndex]]
executable := &p.executable[p.functionOffsets[localIndex].nativeBegin()]
typeID := m.module.TypeIDs[m.module.Source.FunctionSection[localIndex]]
lf := &functionInstance{
executable: executable,
moduleContextOpaquePtr: m.opaquePtr,
typeID: typeID,
indexInModule: funcIndex,
}
m.localFunctionInstances = append(m.localFunctionInstances, lf)
return uintptr(unsafe.Pointer(lf))
@@ -233,5 +236,33 @@ func (m *moduleEngine) FunctionInstanceReference(funcIndex wasm.Index) wasm.Refe
// LookupFunction implements wasm.ModuleEngine.
func (m *moduleEngine) LookupFunction(t *wasm.TableInstance, typeId wasm.FunctionTypeID, tableOffset wasm.Index) (*wasm.ModuleInstance, wasm.Index) {
panic("TODO")
if tableOffset >= uint32(len(t.References)) || t.Type != wasm.RefTypeFuncref {
panic(wasmruntime.ErrRuntimeInvalidTableAccess)
}
rawPtr := t.References[tableOffset]
if rawPtr == 0 {
panic(wasmruntime.ErrRuntimeInvalidTableAccess)
}
tf := functionFromUintptr(rawPtr)
if tf.typeID != typeId {
panic(wasmruntime.ErrRuntimeIndirectCallTypeMismatch)
}
return moduleInstanceFromOpaquePtr(tf.moduleContextOpaquePtr), tf.indexInModule
}
// functionFromUintptr resurrects the original *function from the given uintptr
// which comes from either funcref table or OpcodeRefFunc instruction.
func functionFromUintptr(ptr uintptr) *functionInstance {
// Wraps ptrs as the double pointer in order to avoid the unsafe access as detected by race detector.
//
// For example, if we have (*function)(unsafe.Pointer(ptr)) instead, then the race detector's "checkptr"
// subroutine wanrs as "checkptr: pointer arithmetic result points to invalid allocation"
// https://github.com/golang/go/blob/1ce7fcf139417d618c2730010ede2afb41664211/src/runtime/checkptr.go#L69
var wrapped *uintptr = &ptr
return *(**functionInstance)(unsafe.Pointer(wrapped))
}
func moduleInstanceFromOpaquePtr(ptr *byte) *wasm.ModuleInstance {
return *(**wasm.ModuleInstance)(unsafe.Pointer(ptr))
}

View File

@@ -4,6 +4,7 @@ import (
"context"
_ "embed"
"math"
"runtime"
"strconv"
"testing"
"time"
@@ -11,11 +12,14 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/table"
"github.com/tetratelabs/wazero/internal/engine/wazevo"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
"github.com/tetratelabs/wazero/internal/testing/proxy"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmruntime"
"github.com/tetratelabs/wazero/sys"
)
@@ -30,43 +34,62 @@ var memoryCapacityPages = uint32(2)
var moduleConfig = wazero.NewModuleConfig()
var tests = map[string]func(t *testing.T, r wazero.Runtime){
"huge stack": testHugeStack,
"unreachable": testUnreachable,
"recursive entry": testRecursiveEntry,
"host func memory": testHostFuncMemory,
"host function with context parameter": testHostFunctionContextParameter,
"host function with nested context": testNestedGoContext,
"host function with numeric parameter": testHostFunctionNumericParameter,
"close module with in-flight calls": testCloseInFlight,
"multiple instantiation from same source": testMultipleInstantiation,
"exported function that grows memory": testMemOps,
"import functions with reference type in signature": testReftypeImports,
"overflow integer addition": testOverflow,
"un-signed extend global": testGlobalExtend,
"user-defined primitive in host func": testUserDefinedPrimitiveHostFunc,
"ensures invocations terminate on module close": testEnsureTerminationOnClose,
"call host function indirectly": callHostFunctionIndirect,
type testCase struct {
f func(t *testing.T, r wazero.Runtime)
wazevoSkip bool
}
var tests = map[string]testCase{
"huge stack": {f: testHugeStack, wazevoSkip: true},
"unreachable": {f: testUnreachable},
"recursive entry": {f: testRecursiveEntry},
"host func memory": {f: testHostFuncMemory},
"host function with context parameter": {f: testHostFunctionContextParameter},
"host function with nested context": {f: testNestedGoContext},
"host function with numeric parameter": {f: testHostFunctionNumericParameter},
"close module with in-flight calls": {f: testCloseInFlight},
"multiple instantiation from same source": {f: testMultipleInstantiation},
"exported function that grows memory": {f: testMemOps, wazevoSkip: true},
"import functions with reference type in signature": {f: testReftypeImports, wazevoSkip: true},
"overflow integer addition": {f: testOverflow},
"un-signed extend global": {f: testGlobalExtend},
"user-defined primitive in host func": {f: testUserDefinedPrimitiveHostFunc},
"ensures invocations terminate on module close": {f: testEnsureTerminationOnClose, wazevoSkip: true},
"call host function indirectly": {f: callHostFunctionIndirect, wazevoSkip: true},
"lookup function": {f: testLookupFunction},
}
func TestEngineCompiler(t *testing.T) {
if !platform.CompilerSupported() {
t.Skip()
}
runAllTests(t, tests, wazero.NewRuntimeConfigCompiler().WithCloseOnContextDone(true))
runAllTests(t, tests, wazero.NewRuntimeConfigCompiler().WithCloseOnContextDone(true), false)
}
func TestEngineInterpreter(t *testing.T) {
runAllTests(t, tests, wazero.NewRuntimeConfigInterpreter().WithCloseOnContextDone(true))
runAllTests(t, tests, wazero.NewRuntimeConfigInterpreter().WithCloseOnContextDone(true), false)
}
func runAllTests(t *testing.T, tests map[string]func(t *testing.T, r wazero.Runtime), config wazero.RuntimeConfig) {
for name, testf := range tests {
name := name // pin
testf := testf // pin
func TestEngineWazevo(t *testing.T) {
if runtime.GOARCH != "arm64" {
t.Skip()
}
config := wazero.NewRuntimeConfigInterpreter()
wazevo.ConfigureWazevo(config)
runAllTests(t, tests, config.WithCloseOnContextDone(true), true)
}
func runAllTests(t *testing.T, tests map[string]testCase, config wazero.RuntimeConfig, isWazevo bool) {
for name, tc := range tests {
name := name
tc := tc
if isWazevo && tc.wazevoSkip {
t.Logf("skipping %s because it is not supported by wazevo", name)
continue
}
t.Run(name, func(t *testing.T) {
t.Parallel()
testf(t, wazero.NewRuntimeWithConfig(testCtx, config))
tc.f(t, wazero.NewRuntimeWithConfig(testCtx, config))
})
}
}
@@ -798,3 +821,61 @@ func testMultipleInstantiation(t *testing.T, r wazero.Runtime) {
require.Equal(t, uint64(1000), after)
}
}
func testLookupFunction(t *testing.T, r wazero.Runtime) {
bin := binaryencoding.EncodeModule(&wasm.Module{
TypeSection: []wasm.FunctionType{{Results: []wasm.ValueType{i32}}},
FunctionSection: []wasm.Index{0, 0, 0},
CodeSection: []wasm.Code{
{Body: []byte{wasm.OpcodeI32Const, 1, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeI32Const, 2, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeI32Const, 3, wasm.OpcodeEnd}},
},
TableSection: []wasm.Table{{Min: 10, Type: wasm.RefTypeFuncref}},
ElementSection: []wasm.ElementSegment{
{
OffsetExpr: wasm.ConstantExpression{
Opcode: wasm.OpcodeI32Const,
Data: []byte{0},
},
TableIndex: 0,
Init: []wasm.Index{2, 0},
},
},
})
inst, err := r.Instantiate(testCtx, bin)
require.NoError(t, err)
t.Run("null reference", func(t *testing.T) {
err = require.CapturePanic(func() {
table.LookupFunction(inst, 0, 3, nil, []wasm.ValueType{i32})
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
})
t.Run("out of range", func(t *testing.T) {
err = require.CapturePanic(func() {
table.LookupFunction(inst, 0, 1000, nil, []wasm.ValueType{i32})
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
})
t.Run("type mismatch", func(t *testing.T) {
err = require.CapturePanic(func() {
table.LookupFunction(inst, 0, 0, []wasm.ValueType{i32}, nil)
})
require.Equal(t, wasmruntime.ErrRuntimeIndirectCallTypeMismatch, err)
})
t.Run("ok", func(t *testing.T) {
f2 := table.LookupFunction(inst, 0, 0, nil, []wasm.ValueType{i32})
res, err := f2.Call(testCtx)
require.NoError(t, err)
require.Equal(t, uint64(3), res[0])
f0 := table.LookupFunction(inst, 0, 1, nil, []wasm.ValueType{i32})
res, err = f0.Call(testCtx)
require.NoError(t, err)
require.Equal(t, uint64(1), res[0])
})
}

View File

@@ -2,32 +2,43 @@ package adhoc
import (
"context"
"runtime"
"sync"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/wazevo"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/hammer"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
var hammers = map[string]func(t *testing.T, r wazero.Runtime){
var hammers = map[string]testCase{
// Tests here are similar to what's described in /RATIONALE.md, but deviate as they involve blocking functions.
"close importing module while in use": closeImportingModuleWhileInUse,
"close imported module while in use": closeImportedModuleWhileInUse,
"close importing module while in use": {f: closeImportingModuleWhileInUse, wazevoSkip: true},
"close imported module while in use": {f: closeImportedModuleWhileInUse, wazevoSkip: true},
}
func TestEngineCompiler_hammer(t *testing.T) {
if !platform.CompilerSupported() {
t.Skip()
}
runAllTests(t, hammers, wazero.NewRuntimeConfigCompiler())
runAllTests(t, hammers, wazero.NewRuntimeConfigCompiler(), false)
}
func TestEngineInterpreter_hammer(t *testing.T) {
runAllTests(t, hammers, wazero.NewRuntimeConfigInterpreter())
runAllTests(t, hammers, wazero.NewRuntimeConfigInterpreter(), false)
}
func TestEngineWazevo_hammer(t *testing.T) {
if runtime.GOARCH != "arm64" {
t.Skip()
}
c := wazero.NewRuntimeConfigInterpreter()
wazevo.ConfigureWazevo(c)
runAllTests(t, hammers, c, true)
}
func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) {

View File

@@ -16,6 +16,9 @@
//
// Note: These tests intentionally avoid using wasm.Store as it is important to know both the dependencies and
// the capabilities at the wasm.Engine abstraction.
//
// TODO: the purpose of enginetest overlaps with the purpose of internal/integration_test/engine. We should
// migrate there since the tests here are costly maintenance-wise.
package enginetest
import (
@@ -33,7 +36,6 @@ import (
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmdebug"
"github.com/tetratelabs/wazero/internal/wasmruntime"
)
const (
@@ -235,89 +237,6 @@ func RunTestModuleEngineCallWithStack(t *testing.T, et EngineTester) {
})
}
func RunTestModuleEngineLookupFunction(t *testing.T, et EngineTester) {
e := et.NewEngine(api.CoreFeaturesV1)
mod := &wasm.Module{
TypeSection: []wasm.FunctionType{{}, {Params: []wasm.ValueType{wasm.ValueTypeV128}}},
FunctionSection: []wasm.Index{0, 0, 0},
CodeSection: []wasm.Code{
{
Body: []byte{wasm.OpcodeEnd},
}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}},
},
}
err := e.CompileModule(testCtx, mod, nil, false)
require.NoError(t, err)
m := &wasm.ModuleInstance{
TypeIDs: []wasm.FunctionTypeID{0, 1},
Source: mod,
}
m.Tables = []*wasm.TableInstance{
{Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeFuncref},
{Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeExternref},
{Min: 10, References: make([]wasm.Reference, 10), Type: wasm.RefTypeFuncref},
}
me, err := e.NewModuleEngine(mod, m)
require.NoError(t, err)
linkModuleToEngine(m, me)
t.Run("null reference", func(t *testing.T) {
err = require.CapturePanic(func() {
m.LookupFunction(m.Tables[0], m.TypeIDs[0], 0) // offset 0 is not initialized yet.
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
err = require.CapturePanic(func() {
m.LookupFunction(m.Tables[0], m.TypeIDs[0], 1) // offset 1 is not initialized yet.
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
})
m.Tables[0].References[0] = me.FunctionInstanceReference(2)
m.Tables[0].References[1] = me.FunctionInstanceReference(0)
t.Run("initialized", func(t *testing.T) {
f1 := m.LookupFunction(m.Tables[0], m.TypeIDs[0], 0) // offset 0 is now initialized.
require.Equal(t, wasm.Index(2), f1.Definition().Index())
f2 := m.LookupFunction(m.Tables[0], m.TypeIDs[0], 1) // offset 1 is now initialized.
require.Equal(t, wasm.Index(0), f2.Definition().Index())
})
t.Run("out of range", func(t *testing.T) {
err = require.CapturePanic(func() {
me.LookupFunction(m.Tables[0], m.TypeIDs[0], 100 /* out of range */)
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
})
t.Run("access to externref table", func(t *testing.T) {
err := require.CapturePanic(func() {
m.LookupFunction(m.Tables[1], /* table[1] has externref type. */
m.TypeIDs[0], 0)
})
require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err)
})
t.Run("access to externref table", func(t *testing.T) {
err = require.CapturePanic(func() {
me.LookupFunction(m.Tables[0], /* type mismatch */
m.TypeIDs[1], 0)
})
require.Equal(t, wasmruntime.ErrRuntimeIndirectCallTypeMismatch, err)
})
m.Tables[2].References[0] = me.FunctionInstanceReference(1)
m.Tables[2].References[5] = me.FunctionInstanceReference(2)
t.Run("initialized - tables[2]", func(t *testing.T) {
f1 := m.LookupFunction(m.Tables[2], m.TypeIDs[0], 0)
require.Equal(t, wasm.Index(1), f1.Definition().Index())
f2 := m.LookupFunction(m.Tables[2], m.TypeIDs[0], 5)
require.Equal(t, wasm.Index(2), f2.Definition().Index())
})
}
func runTestModuleEngineCallHostFnMem(t *testing.T, et EngineTester, readMem *wasm.Code) {
e := et.NewEngine(api.CoreFeaturesV1)
defer e.Close()
@@ -1188,7 +1107,10 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
lns := buildFunctionListeners(fnlf, hostModule)
err := e.CompileModule(testCtx, hostModule, lns, false)
require.NoError(t, err)
host := &wasm.ModuleInstance{ModuleName: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}}
host := &wasm.ModuleInstance{
ModuleName: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
Source: hostModule,
}
host.Exports = exportMap(hostModule)
hostME, err := e.NewModuleEngine(hostModule, host)
@@ -1223,6 +1145,7 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
require.NoError(t, err)
imported := &wasm.ModuleInstance{
Source: importedModule,
ModuleName: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
}
imported.Exports = exportMap(importedModule)
@@ -1256,7 +1179,10 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
require.NoError(t, err)
// Add the exported function.
importing := &wasm.ModuleInstance{ModuleName: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}}
importing := &wasm.ModuleInstance{
ModuleName: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
Source: importingModule,
}
importing.Exports = exportMap(importingModule)
// Compile the importing module