Repository: tetratelabs/wazero
Pull request #1444 (2023-05-09 01:01:32)

experimental: add FunctionListener and FunctionListenerFactory implementations
This PR adds implementations of the `experimental.FunctionListenerFactory` which have been useful to implement listeners in external projects depending on wazero. Bringing those building blocks to wazero will likely benefit other users.

Please take a look and let me know if you would like to see anything changed!


Patch:
diff --git a/builder.go b/builder.go
index 0aade139..f64afabd 100644
--- a/builder.go
+++ b/builder.go
@@ -322,7 +322,7 @@ func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error)
 	}
 
 	c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
-	listeners, err := buildListeners(ctx, module)
+	listeners, err := buildFunctionListeners(ctx, module)
 	if err != nil {
 		return nil, err
 	}
diff --git a/experimental/listener.go b/experimental/listener.go
index 7455ec3f..2dd8f940 100644
--- a/experimental/listener.go
+++ b/experimental/listener.go
@@ -40,9 +40,9 @@ type FunctionListenerFactoryKey struct{}
 // FunctionListenerFactory returns FunctionListeners to be notified when a
 // function is called.
 type FunctionListenerFactory interface {
-	// NewListener returns a FunctionListener for a defined function. If nil is
-	// returned, no listener will be notified.
-	NewListener(api.FunctionDefinition) FunctionListener
+	// NewFunctionListener returns a FunctionListener for a defined function.
+	// If nil is returned, no listener will be notified.
+	NewFunctionListener(api.FunctionDefinition) FunctionListener
 	// ^^ A single instance can be returned to avoid instantiating a listener
 	// per function, especially as they may be thousands of functions. Shared
 	// listeners use their FunctionDefinition parameter to clarify.
@@ -83,6 +83,236 @@ type FunctionListener interface {
 	After(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64)
 }
 
+// FunctionListenerFunc is a function type implementing the FunctionListener
+// interface, making it possible to use regular functions and methods as
+// listeners of function invocation.
+//
+// The FunctionListener interface declares two methods (Before and After),
+// but this type invokes its value only when Before is called. It is best
+// suites for cases where the host does not need to perform correlation
+// between the start and end of the function call.
+type FunctionListenerFunc func(context.Context, api.Module, api.FunctionDefinition, []uint64, StackIterator)
+
+// Before satisfies the FunctionListener interface, calls f.
+func (f FunctionListenerFunc) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator StackIterator) context.Context {
+	f(ctx, mod, def, paramValues, stackIterator)
+	return ctx
+}
+
+// After is declared to satisfy the FunctionListener interface, but it does
+// nothing.
+func (f FunctionListenerFunc) After(context.Context, api.Module, api.FunctionDefinition, error, []uint64) {
+}
+
+// FunctionListenerFactoryFunc is a function type implementing the
+// FunctionListenerFactory interface, making it possible to use regular
+// functions and methods as factory of function listeners.
+type FunctionListenerFactoryFunc func(api.FunctionDefinition) FunctionListener
+
+// NewFunctionListener satisfies the FunctionListenerFactory interface, calls f.
+func (f FunctionListenerFactoryFunc) NewFunctionListener(def api.FunctionDefinition) FunctionListener {
+	return f(def)
+}
+
+// MultiFunctionListenerFactory constructs a FunctionListenerFactory which
+// combines the listeners created by each of the factories passed as arguments.
+//
+// This function is useful when multiple listeners need to be hooked to a module
+// because the propagation mechanism based on installing a listener factory in
+// the context.Context used when instantiating modules allows for a single
+// listener to be installed.
+//
+// The stack iterator passed to the Before method is reset so that each listener
+// can iterate the call stack independently without impacting the ability of
+// other listeners to do so.
+func MultiFunctionListenerFactory(factories ...FunctionListenerFactory) FunctionListenerFactory {
+	multi := make(multiFunctionListenerFactory, len(factories))
+	copy(multi, factories)
+	return multi
+}
+
+type multiFunctionListenerFactory []FunctionListenerFactory
+
+func (multi multiFunctionListenerFactory) NewFunctionListener(def api.FunctionDefinition) FunctionListener {
+	var lstns []FunctionListener
+	for _, factory := range multi {
+		if lstn := factory.NewFunctionListener(def); lstn != nil {
+			lstns = append(lstns, lstn)
+		}
+	}
+	switch len(lstns) {
+	case 0:
+		return nil
+	case 1:
+		return lstns[0]
+	default:
+		return &multiFunctionListener{lstns: lstns}
+	}
+}
+
+type multiFunctionListener struct {
+	lstns []FunctionListener
+	stack stackIterator
+}
+
+func (multi *multiFunctionListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, si StackIterator) context.Context {
+	multi.stack.base = si
+	for _, lstn := range multi.lstns {
+		multi.stack.index = -1
+		ctx = lstn.Before(ctx, mod, def, params, &multi.stack)
+	}
+	return ctx
+}
+
+func (multi *multiFunctionListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, results []uint64) {
+	for _, lstn := range multi.lstns {
+		lstn.After(ctx, mod, def, err, results)
+	}
+}
+
+type parameters struct {
+	values []uint64
+	limits []uint8
+}
+
+func (ps *parameters) append(values []uint64) {
+	ps.values = append(ps.values, values...)
+	ps.limits = append(ps.limits, uint8(len(ps.values)))
+}
+
+func (ps *parameters) clear() {
+	ps.values = ps.values[:0]
+	ps.limits = ps.limits[:0]
+}
+
+func (ps *parameters) index(i int) []uint64 {
+	j := uint8(0)
+	k := ps.limits[i]
+	if i > 0 {
+		j = ps.limits[i-1]
+	}
+	return ps.values[j:k:k]
+}
+
+type stackIterator struct {
+	base   StackIterator
+	index  int
+	pcs    []uint64
+	fns    []api.FunctionDefinition
+	params parameters
+}
+
+func (si *stackIterator) Next() bool {
+	if si.base != nil {
+		si.pcs = si.pcs[:0]
+		si.fns = si.fns[:0]
+		si.params.clear()
+
+		for si.base.Next() {
+			si.pcs = append(si.pcs, si.base.SourceOffset())
+			si.fns = append(si.fns, si.base.FunctionDefinition())
+			si.params.append(si.base.Parameters())
+		}
+
+		si.base = nil
+	}
+	si.index++
+	return si.index < len(si.pcs)
+}
+
+func (si *stackIterator) SourceOffset() uint64 {
+	return si.pcs[si.index]
+}
+
+func (si *stackIterator) FunctionDefinition() api.FunctionDefinition {
+	return si.fns[si.index]
+}
+
+func (si *stackIterator) Parameters() []uint64 {
+	return si.params.index(si.index)
+}
+
+// StackFrame represents a frame on the call stack.
+type StackFrame struct {
+	Function     api.Function
+	Params       []uint64
+	Results      []uint64
+	SourceOffset uint64
+}
+
+// stackFrameIterator is an implementation of the experimental.stackFrameIterator
+// interface.
+type stackFrameIterator struct {
+	index int
+	stack []StackFrame
+	fndef []api.FunctionDefinition
+}
+
+func (si *stackFrameIterator) Next() bool {
+	si.index++
+	return si.index < len(si.stack)
+}
+
+func (si *stackFrameIterator) FunctionDefinition() api.FunctionDefinition {
+	return si.fndef[si.index]
+}
+
+func (si *stackFrameIterator) SourceOffset() uint64 {
+	return si.stack[si.index].SourceOffset
+}
+
+func (si *stackFrameIterator) Parameters() []uint64 {
+	return si.stack[si.index].Params
+}
+
+// NewStackIterator constructs a stack iterator from a list of stack frames.
+// The top most frame is the last one.
+func NewStackIterator(stack ...StackFrame) StackIterator {
+	si := &stackFrameIterator{
+		index: -1,
+		stack: make([]StackFrame, len(stack)),
+		fndef: make([]api.FunctionDefinition, len(stack)),
+	}
+	for i := range stack {
+		si.stack[i] = stack[len(stack)-(i+1)]
+	}
+	// The size of function definition 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
+}
+
+// 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(n int, module api.Module, stack []StackFrame, listener FunctionListener) {
+	if len(stack) == 0 {
+		panic("cannot benchmark function listener with an empty stack")
+	}
+
+	functionDefinition := stack[0].Function.Definition()
+	functionParams := stack[0].Params
+	functionResults := stack[0].Results
+	stackIterator := &stackIterator{base: NewStackIterator(stack...)}
+	ctx := context.Background()
+
+	for i := 0; i < n; i++ {
+		stackIterator.index = -1
+		callContext := listener.Before(ctx, module, functionDefinition, functionParams, stackIterator)
+		listener.After(callContext, module, functionDefinition, nil, functionResults)
+	}
+}
+
 // TODO: We need to add tests to enginetest to ensure contexts nest. A good test can use a combination of call and call
 // indirect in terms of depth and breadth. The test could show a tree 3 calls deep where the there are a couple calls at
 // each depth under the root. The main thing this can help prevent is accidentally swapping the context internally.
diff --git a/experimental/logging/log_listener.go b/experimental/logging/log_listener.go
index b12a3abc..51452f86 100644
--- a/experimental/logging/log_listener.go
+++ b/experimental/logging/log_listener.go
@@ -93,9 +93,9 @@ type flusher interface {
 	Flush() error
 }
 
-// NewListener implements the same method as documented on
+// NewFunctionListener implements the same method as documented on
 // experimental.FunctionListener.
-func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experimental.FunctionListener {
+func (f *loggingListenerFactory) NewFunctionListener(fnd api.FunctionDefinition) experimental.FunctionListener {
 	exported := len(fnd.ExportNames()) > 0
 	if f.hostOnly && // choose functions defined or callable by the host
 		fnd.GoFunction() == nil && // not defined by the host
diff --git a/runtime.go b/runtime.go
index 9047ba29..b651861c 100644
--- a/runtime.go
+++ b/runtime.go
@@ -228,7 +228,7 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod
 	}
 	c.typeIDs = typeIDs
 
-	listeners, err := buildListeners(ctx, internal)
+	listeners, err := buildFunctionListeners(ctx, internal)
 	if err != nil {
 		return nil, err
 	}
@@ -239,7 +239,7 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod
 	return c, nil
 }
 
-func buildListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
+func buildFunctionListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
 	// Test to see if internal code are using an experimental feature.
 	fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{})
 	if fnlf == nil {
@@ -249,7 +249,7 @@ func buildListeners(ctx context.Context, internal *wasm.Module) ([]experimentala
 	importCount := internal.ImportFunctionCount
 	listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection))
 	for i := 0; i < len(listeners); i++ {
-		listeners[i] = factory.NewListener(&internal.FunctionDefinitionSection[uint32(i)+importCount])
+		listeners[i] = factory.NewFunctionListener(&internal.FunctionDefinitionSection[uint32(i)+importCount])
 	}
 	return listeners, nil
 }


Test patch:
diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go
index a468497b..5e443896 100644
--- a/experimental/listener_example_test.go
+++ b/experimental/listener_example_test.go
@@ -35,8 +35,8 @@ func (u uniqGoFuncs) callees() []string {
 	return ret
 }
 
-// NewListener implements FunctionListenerFactory.NewListener
-func (u uniqGoFuncs) NewListener(def api.FunctionDefinition) experimental.FunctionListener {
+// NewFunctionListener implements FunctionListenerFactory.NewFunctionListener
+func (u uniqGoFuncs) NewFunctionListener(def api.FunctionDefinition) experimental.FunctionListener {
 	if def.GoFunction() == nil {
 		return nil // only track go funcs
 	}
diff --git a/experimental/listener_test.go b/experimental/listener_test.go
index aaa3b6e6..8555f6f1 100644
--- a/experimental/listener_test.go
+++ b/experimental/listener_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/experimental/wazerotest"
 	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -30,7 +31,7 @@ func (r *recorder) After(_ context.Context, _ api.Module, def api.FunctionDefini
 	r.afterNames = append(r.afterNames, def.DebugName())
 }
 
-func (r *recorder) NewListener(definition api.FunctionDefinition) experimental.FunctionListener {
+func (r *recorder) NewFunctionListener(definition api.FunctionDefinition) experimental.FunctionListener {
 	r.m[definition.Name()] = struct{}{}
 	return r
 }
@@ -100,3 +101,110 @@ func TestFunctionListenerFactory(t *testing.T) {
 	require.Equal(t, []string{"test.fn1", "test.fn2", "test.fn2"}, factory.beforeNames)
 	require.Equal(t, []string{"test.fn2", "test.fn2", "test.fn1"}, factory.afterNames) // after is in the reverse order.
 }
+
+func TestMultiFunctionListenerFactory(t *testing.T) {
+	module := wazerotest.NewModule(nil,
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+	)
+
+	stack := []experimental.StackFrame{
+		{Function: module.Function(0), Params: []uint64{1}},
+		{Function: module.Function(1), Params: []uint64{2}},
+		{Function: module.Function(2), Params: []uint64{3}},
+	}
+
+	n := 0
+	f := func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator experimental.StackIterator) {
+		n++
+		i := 0
+		for stackIterator.Next() {
+			var param uint64
+			switch i {
+			case 0:
+				param = 3
+			case 1:
+				param = 2
+			case 2:
+				param = 1
+			default:
+				t.Errorf("too many frames seen by stack iterator: %d", i)
+			}
+			if params := stackIterator.Parameters(); len(params) != 1 {
+				t.Errorf("wrong number of parameters in call frame %d: want=1 got=%d", i, len(params))
+			} else if params[0] != param {
+				t.Errorf("wrong parameter in call frame %d: want=%d got=%d", i, param, params[0])
+			}
+			i++
+		}
+		if i != 3 {
+			t.Errorf("wrong number of call frames: want=3 got=%d", i)
+		}
+	}
+
+	factory := experimental.MultiFunctionListenerFactory(
+		experimental.FunctionListenerFactoryFunc(func(def api.FunctionDefinition) experimental.FunctionListener {
+			return experimental.FunctionListenerFunc(f)
+		}),
+		experimental.FunctionListenerFactoryFunc(func(def api.FunctionDefinition) experimental.FunctionListener {
+			return experimental.FunctionListenerFunc(f)
+		}),
+	)
+
+	function := module.Function(0).Definition()
+	listener := factory.NewFunctionListener(function)
+	listener.Before(context.Background(), module, function, stack[2].Params, experimental.NewStackIterator(stack...))
+
+	if n != 2 {
+		t.Errorf("wrong number of function calls: want=2 got=%d", n)
+	}
+}
+
+func BenchmarkMultiFunctionListener(b *testing.B) {
+	module := wazerotest.NewModule(nil,
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+		wazerotest.NewFunction(func(ctx context.Context, mod api.Module, value int32) {}),
+	)
+
+	stack := []experimental.StackFrame{
+		{Function: module.Function(0), Params: []uint64{1}},
+		{Function: module.Function(1), Params: []uint64{2}},
+		{Function: module.Function(2), Params: []uint64{3}},
+	}
+
+	tests := []struct {
+		scenario string
+		function func(context.Context, api.Module, api.FunctionDefinition, []uint64, experimental.StackIterator)
+	}{
+		{
+			scenario: "simple function listener",
+			function: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator experimental.StackIterator) {
+			},
+		},
+		{
+			scenario: "stack iterator",
+			function: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator experimental.StackIterator) {
+				for stackIterator.Next() {
+				}
+			},
+		},
+	}
+
+	for _, test := range tests {
+		b.Run(test.scenario, func(b *testing.B) {
+			factory := experimental.MultiFunctionListenerFactory(
+				experimental.FunctionListenerFactoryFunc(func(def api.FunctionDefinition) experimental.FunctionListener {
+					return experimental.FunctionListenerFunc(test.function)
+				}),
+				experimental.FunctionListenerFactoryFunc(func(def api.FunctionDefinition) experimental.FunctionListener {
+					return experimental.FunctionListenerFunc(test.function)
+				}),
+			)
+			function := module.Function(0).Definition()
+			listener := factory.NewFunctionListener(function)
+			experimental.BenchmarkFunctionListener(b.N, module, stack, listener)
+		})
+	}
+}
diff --git a/experimental/logging/log_listener_test.go b/experimental/logging/log_listener_test.go
index 7aa684c8..8417f56f 100644
--- a/experimental/logging/log_listener_test.go
+++ b/experimental/logging/log_listener_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/experimental/wazerotest"
 	"github.com/tetratelabs/wazero/internal/testing/require"
@@ -299,7 +300,7 @@ func Test_loggingListener(t *testing.T) {
 			}
 			m.BuildFunctionDefinitions()
 			def := &m.FunctionDefinitionSection[0]
-			l := lf.NewListener(def)
+			l := lf.NewFunctionListener(def)
 
 			out.Reset()
 			ctx := l.Before(testCtx, nil, def, tc.params, nil)
@@ -334,9 +335,9 @@ func Test_loggingListener_indentation(t *testing.T) {
 	}
 	m.BuildFunctionDefinitions()
 	def1 := &m.FunctionDefinitionSection[0]
-	l1 := lf.NewListener(def1)
+	l1 := lf.NewFunctionListener(def1)
 	def2 := &m.FunctionDefinitionSection[1]
-	l2 := lf.NewListener(def2)
+	l2 := lf.NewFunctionListener(def2)
 
 	ctx := l1.Before(testCtx, nil, def1, []uint64{}, nil)
 	ctx1 := l2.Before(ctx, nil, def2, []uint64{}, nil)
@@ -357,13 +358,13 @@ func BenchmarkLoggingListener(b *testing.B) {
 
 	function := module.Function(0)
 	factory := logging.NewLoggingListenerFactory(discard{})
-	listener := factory.NewListener(function.Definition())
+	listener := factory.NewFunctionListener(function.Definition())
 
-	stack := []wazerotest.StackFrame{
+	stack := []experimental.StackFrame{
 		{Function: function},
 	}
 
-	wazerotest.BenchmarkFunctionListener(b, module, stack, listener)
+	experimental.BenchmarkFunctionListener(b.N, module, stack, listener)
 }
 
 type discard struct{}
diff --git a/experimental/wazerotest/wazerotest.go b/experimental/wazerotest/wazerotest.go
index dba53302..722a9fd5 100644
--- a/experimental/wazerotest/wazerotest.go
+++ b/experimental/wazerotest/wazerotest.go
@@ -10,10 +10,8 @@ import (
 	"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"
 )
@@ -645,88 +643,8 @@ 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
-	SourceOffset 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) SourceOffset() uint64 {
-	return si.stack[si.index].SourceOffset
-}
-
-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)
-	}
-}
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index 6729d208..d74dae6a 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -156,7 +156,7 @@ func RunTestModuleEngineCall(t *testing.T, et EngineTester) {
 	}
 
 	m.BuildFunctionDefinitions()
-	listeners := buildListeners(et.ListenerFactory(), m)
+	listeners := buildFunctionListeners(et.ListenerFactory(), m)
 	err := e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
 
@@ -213,7 +213,7 @@ func RunTestModuleEngineCallWithStack(t *testing.T, et EngineTester) {
 	}
 
 	m.BuildFunctionDefinitions()
-	listeners := buildListeners(et.ListenerFactory(), m)
+	listeners := buildFunctionListeners(et.ListenerFactory(), m)
 	err := e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
 
@@ -656,7 +656,7 @@ func RunTestModuleEngineBeforeListenerStackIterator(t *testing.T, et EngineTeste
 	}
 	m.BuildFunctionDefinitions()
 
-	listeners := buildListeners(fnListener, m)
+	listeners := buildFunctionListeners(fnListener, m)
 	err := e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
 
@@ -777,7 +777,7 @@ func RunTestModuleEngineBeforeListenerGlobals(t *testing.T, et EngineTester) {
 	}
 	m.BuildFunctionDefinitions()
 
-	listeners := buildListeners(fnListener, m)
+	listeners := buildFunctionListeners(fnListener, m)
 	err := e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
 
@@ -807,7 +807,7 @@ type fnListener struct {
 	afterFn  func(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64)
 }
 
-func (f *fnListener) NewListener(api.FunctionDefinition) experimental.FunctionListener {
+func (f *fnListener) NewFunctionListener(api.FunctionDefinition) experimental.FunctionListener {
 	return f
 }
 
@@ -950,7 +950,7 @@ func RunTestModuleEngineStackIteratorOffset(t *testing.T, et EngineTester) {
 
 	m.BuildFunctionDefinitions()
 
-	listeners := buildListeners(fnListener, m)
+	listeners := buildFunctionListeners(fnListener, m)
 	err = e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
 
@@ -1060,7 +1060,7 @@ func RunTestModuleEngineMemory(t *testing.T, et EngineTester) {
 		},
 	}
 	m.BuildFunctionDefinitions()
-	listeners := buildListeners(et.ListenerFactory(), m)
+	listeners := buildFunctionListeners(et.ListenerFactory(), m)
 
 	err := e.CompileModule(testCtx, m, listeners, false)
 	require.NoError(t, err)
@@ -1192,7 +1192,7 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 		ID: wasm.ModuleID{0},
 	}
 	hostModule.BuildFunctionDefinitions()
-	lns := buildListeners(fnlf, hostModule)
+	lns := buildFunctionListeners(fnlf, hostModule)
 	err := e.CompileModule(testCtx, hostModule, lns, false)
 	require.NoError(t, err)
 	host := &wasm.ModuleInstance{
@@ -1229,7 +1229,7 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 		ID: wasm.ModuleID{1},
 	}
 	importedModule.BuildFunctionDefinitions()
-	lns = buildListeners(fnlf, importedModule)
+	lns = buildFunctionListeners(fnlf, importedModule)
 	err = e.CompileModule(testCtx, importedModule, lns, false)
 	require.NoError(t, err)
 
@@ -1264,7 +1264,7 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 		ID: wasm.ModuleID{2},
 	}
 	importingModule.BuildFunctionDefinitions()
-	lns = buildListeners(fnlf, importingModule)
+	lns = buildFunctionListeners(fnlf, importingModule)
 	err = e.CompileModule(testCtx, importingModule, lns, false)
 	require.NoError(t, err)
 
@@ -1367,14 +1367,14 @@ func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
 	module.Engine = me // for Compiler, links the module to the module-engine compiled from it (moduleInstanceEngineOffset).
 }
 
-func buildListeners(factory experimental.FunctionListenerFactory, m *wasm.Module) []experimental.FunctionListener {
+func buildFunctionListeners(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.ImportFunctionCount
 	for i := 0; i < len(listeners); i++ {
-		listeners[i] = factory.NewListener(&m.FunctionDefinitionSection[uint32(i)+importCount])
+		listeners[i] = factory.NewFunctionListener(&m.FunctionDefinitionSection[uint32(i)+importCount])
 	}
 	return listeners
 }
diff --git a/internal/testing/proxy/proxy.go b/internal/testing/proxy/proxy.go
index 793f7b95..4e9edb96 100644
--- a/internal/testing/proxy/proxy.go
+++ b/internal/testing/proxy/proxy.go
@@ -22,13 +22,13 @@ type loggingListenerFactory struct {
 	delegate experimental.FunctionListenerFactory
 }
 
-// NewListener implements the same method as documented on
+// NewFunctionListener implements the same method as documented on
 // experimental.FunctionListener.
-func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experimental.FunctionListener {
+func (f *loggingListenerFactory) NewFunctionListener(fnd api.FunctionDefinition) experimental.FunctionListener {
 	if fnd.ModuleName() == proxyModuleName {
 		return nil // don't log proxy stuff
 	}
-	return f.delegate.NewListener(fnd)
+	return f.delegate.NewFunctionListener(fnd)
 }
 
 // NewModuleBinary creates the proxy module to proxy a function call against