Repository: tetratelabs/wazero
Pull request #697 (2022-07-14 06:26:54)

Adds function names to host functions and improves logging listener
This improves the experimental logging listener to show parameter name
and values.

Ex. If you configure a logging listener like so:
```go
	// Set context to one that has an experimental listener
 	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, experimental.NewLoggingListenerFactory(os.Stdout))

 	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
```

A rust WASI startup would look like this:
```
--> ._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.

See #451 


Patch:
diff --git a/api/wasm.go b/api/wasm.go
index e7ea620e..08c5a5a8 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -83,12 +83,17 @@ const (
 	ValueTypeF32 ValueType = 0x7d
 	// ValueTypeF64 is a 64-bit floating point number.
 	ValueTypeF64 ValueType = 0x7c
+
 	// ValueTypeExternref is a externref type.
 	//
-	// Note: in wazero, externref type value are opaque raw 64-bit pointers, and the ValueTypeExternref type
-	// in the signature will be translated as uintptr in wazero's API level.
-	// For example, the import function `(func (import "env" "f") (param externref) (result externref))` can be defined in Go as:
+	// Note: in wazero, externref type value are opaque raw 64-bit pointers,
+	// and the ValueTypeExternref type in the signature will be translated as
+	// uintptr in wazero's API level.
+	//
+	// For example, given the import function:
+	//	(func (import "env" "f") (param externref) (result externref))
 	//
+	// This can be defined in Go as:
 	//  r.NewModuleBuilder("env").ExportFunctions(map[string]interface{}{
 	//    "f": func(externref uintptr) (resultExternRef uintptr) { return },
 	//  })
@@ -222,6 +227,13 @@ type FunctionDefinition interface {
 	// is possible.
 	ExportNames() []string
 
+	// IsHostFunction returns true if the function was implemented by the
+	// embedder (ex via wazero.ModuleBuilder) instead of a wasm binary.
+	//
+	// Note: Host functions can be non-deterministic or cause side effects.
+	// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A0
+	IsHostFunction() bool
+
 	// ParamTypes are the possibly empty sequence of value types accepted by a
 	// function with this signature.
 	//
diff --git a/assemblyscript/assemblyscript.go b/assemblyscript/assemblyscript.go
index 50b5ce90..ae69027a 100644
--- a/assemblyscript/assemblyscript.go
+++ b/assemblyscript/assemblyscript.go
@@ -45,9 +45,9 @@ import (
 //	* To add more functions to the "env" module, use FunctionExporter.
 //	* To instantiate into another wazero.Namespace, use FunctionExporter.
 func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
-	return r.NewModuleBuilder("env").
-		ExportFunctions(NewFunctionExporter().ExportFunctions()).
-		Instantiate(ctx, r)
+	builder := r.NewModuleBuilder("env")
+	NewFunctionExporter().ExportFunctions(builder)
+	return builder.Instantiate(ctx, r)
 }
 
 // FunctionExporter configures the functions in the "env" module used by
@@ -70,7 +70,7 @@ type FunctionExporter interface {
 
 	// ExportFunctions builds functions to export with a wazero.ModuleBuilder
 	// named "env".
-	ExportFunctions() (nameToGoFunc map[string]interface{})
+	ExportFunctions(builder wazero.ModuleBuilder)
 }
 
 // NewFunctionExporter returns a FunctionExporter object with trace disabled.
@@ -113,13 +113,13 @@ func (e *functionExporter) WithTraceToStderr() FunctionExporter {
 }
 
 // ExportFunctions implements FunctionExporter.ExportFunctions
-func (e *functionExporter) ExportFunctions() (nameToGoFunc map[string]interface{}) {
+func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
 	env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode}
-	return map[string]interface{}{
-		"abort": env.abort,
-		"trace": env.trace,
-		"seed":  env.seed,
-	}
+	builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
+		"message", "fileName", "lineNumber", "columnNumber")
+	builder.ExportFunction("trace", env.trace, "~lib/builtins/trace",
+		"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4")
+	builder.ExportFunction("seed", env.seed, "~lib/builtins/seed")
 }
 
 // assemblyScript includes "Special imports" only used In AssemblyScript when a
diff --git a/builder.go b/builder.go
index 60bac687..a832922c 100644
--- a/builder.go
+++ b/builder.go
@@ -10,7 +10,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-// ModuleBuilder is a way to define a WebAssembly 1.0 (20191205) in Go.
+// ModuleBuilder is a way to define a WebAssembly Module in Go.
 //
 // Ex. Below defines and instantiates a module named "env" with one function:
 //
@@ -25,7 +25,8 @@ import (
 //		ExportFunction("hello", hello).
 //		Instantiate(ctx, r)
 //
-// If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex.
+// If the same module may be instantiated multiple times, it is more efficient
+// to separate steps. Ex.
 //
 //	compiled, _ := r.NewModuleBuilder("env").
 //		ExportFunction("get_random_string", getRandomString).
@@ -37,9 +38,12 @@ import (
 //
 // Notes
 //
-//	* ModuleBuilder is mutable. WithXXX functions return the same instance for chaining.
-//	* WithXXX methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
-//	* Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
+//	* ModuleBuilder is mutable: each method returns the same instance for
+//	  chaining.
+//	* methods do not return errors, to allow chaining. Any validation errors
+//	  are deferred until Compile.
+//	* Insertion order is not retained. Anything defined by this builder is
+//	  sorted lexicographically on Compile.
 type ModuleBuilder interface {
 	// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
 
@@ -48,29 +52,45 @@ type ModuleBuilder interface {
 	//
 	// Parameters
 	//
-	//	* name - the name to export. Ex "random_get"
-	//	* goFunc - the `func` to export.
+	//	* exportName - The name to export. Ex "random_get"
+	//	* goFunc - The `func` to export.
+	//	* names - If present, the first is the api.FunctionDefinition name.
+	//	  If any follow, they must match the count of goFunc's parameters.
 	//
-	// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (20191205) value
-	// types. This means uint32, uint64, float32 or float64. Up to one result can be returned.
+	// Ex.
+	//	// Just export the function, and use "abort" in stack traces.
+	// 	builder.ExportFunction("abort", env.abort)
+	//	// Ensure "~lib/builtins/abort" is used in stack traces.
+	//	builder.ExportFunction("abort", env.abort, "~lib/builtins/abort")
+	//	// Allow function listeners to know the param names for logging, etc.
+	//	builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
+	//		"message", "fileName", "lineNumber", "columnNumber")
+	//
+	// Valid Signature
+	//
+	// Noting a context exception described later, all parameters or result
+	// types must match WebAssembly 1.0 (20191205) value types. This means
+	// uint32, uint64, float32 or float64. Up to one result can be returned.
 	//
 	// Ex. This is a valid host function:
 	//
-	//	addInts := func(x uint32, uint32) uint32 {
+	//	addInts := func(x, y uint32) uint32 {
 	//		return x + y
 	//	}
 	//
-	// Host functions may also have an initial parameter (param[0]) of type context.Context or api.Module.
+	// Host functions may also have an initial parameter (param[0]) of type
+	// context.Context or api.Module.
 	//
 	// Ex. This uses a Go Context:
 	//
-	//	addInts := func(ctx context.Context, x uint32, uint32) uint32 {
+	//	addInts := func(ctx context.Context, x, y uint32) uint32 {
 	//		// add a little extra if we put some in the context!
 	//		return x + y + ctx.Value(extraKey).(uint32)
 	//	}
 	//
-	// Ex. This uses an api.Module to reads the parameters from memory. This is important because there are only numeric
-	// types in Wasm. The only way to share other data is via writing memory and sharing offsets.
+	// Ex. This uses an api.Module to reads the parameters from memory. This is
+	// important because there are only numeric types in Wasm. The only way to
+	// share other data is via writing memory and sharing offsets.
 	//
 	//	addInts := func(ctx context.Context, m api.Module, offset uint32) uint32 {
 	//		x, _ := m.Memory().ReadUint32Le(ctx, offset)
@@ -78,16 +98,17 @@ type ModuleBuilder interface {
 	//		return x + y
 	//	}
 	//
-	// If both parameters exist, they must be in order at positions zero and one.
+	// If both parameters exist, they must be in order at positions 0 and 1.
 	//
-	// Ex. This uses propagates context properly when calling other functions exported in the api.Module:
+	// Ex. This uses propagates context properly when calling other functions
+	// exported in the api.Module:
 	//	callRead := func(ctx context.Context, m api.Module, offset, byteCount uint32) uint32 {
 	//		fn = m.ExportedFunction("__read")
 	//		results, err := fn(ctx, offset, byteCount)
 	//	--snip--
 	//
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2
-	ExportFunction(name string, goFunc interface{}) ModuleBuilder
+	ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder
 
 	// ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map.
 	ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder
@@ -200,6 +221,7 @@ type moduleBuilder struct {
 	r            *runtime
 	moduleName   string
 	nameToGoFunc map[string]interface{}
+	funcToNames  map[string][]string
 	nameToMemory map[string]*wasm.Memory
 	nameToGlobal map[string]*wasm.Global
 }
@@ -210,14 +232,18 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
 		r:            r,
 		moduleName:   moduleName,
 		nameToGoFunc: map[string]interface{}{},
+		funcToNames:  map[string][]string{},
 		nameToMemory: map[string]*wasm.Memory{},
 		nameToGlobal: map[string]*wasm.Global{},
 	}
 }
 
 // ExportFunction implements ModuleBuilder.ExportFunction
-func (b *moduleBuilder) ExportFunction(name string, goFunc interface{}) ModuleBuilder {
-	b.nameToGoFunc[name] = goFunc
+func (b *moduleBuilder) ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder {
+	b.nameToGoFunc[exportName] = goFunc
+	if len(names) > 0 {
+		b.funcToNames[exportName] = names
+	}
 	return b
 }
 
@@ -295,16 +321,21 @@ func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (Com
 		}
 	}
 
-	module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures)
+	module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures)
 	if err != nil {
 		return nil, err
 	}
 
+	c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
+	if c.listeners, err = buildListeners(ctx, b.r, module); err != nil {
+		return nil, err
+	}
+
 	if err = b.r.store.Engine.CompileModule(ctx, module); err != nil {
 		return nil, err
 	}
 
-	return &compiledModule{module: module, compiledEngine: b.r.store.Engine}, nil
+	return c, nil
 }
 
 // Instantiate implements ModuleBuilder.Instantiate
diff --git a/config.go b/config.go
index 4d239a50..51b5b6ab 100644
--- a/config.go
+++ b/config.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/engine/compiler"
 	"github.com/tetratelabs/wazero/internal/engine/interpreter"
 	"github.com/tetratelabs/wazero/internal/platform"
@@ -148,6 +149,7 @@ func NewRuntimeConfig() RuntimeConfig {
 
 type runtimeConfig struct {
 	enabledFeatures wasm.Features
+	isInterpreter   bool
 	newEngine       func(wasm.Features) wasm.Engine
 }
 
@@ -179,6 +181,7 @@ func NewRuntimeConfigCompiler() RuntimeConfig {
 // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly.
 func NewRuntimeConfigInterpreter() RuntimeConfig {
 	ret := engineLessConfig.clone()
+	ret.isInterpreter = true
 	ret.newEngine = interpreter.NewEngine
 	return ret
 }
@@ -280,7 +283,8 @@ type compiledModule struct {
 	module *wasm.Module
 	// compiledEngine holds an engine on which `module` is compiled.
 	compiledEngine wasm.Engine
-
+	// listeners are present if the code was compiled with a listener
+	listeners []experimental.FunctionListener
 	// closeWithModule prevents leaking compiled code when a module is compiled implicitly.
 	closeWithModule bool
 }
diff --git a/experimental/listener.go b/experimental/listener.go
index b0aa6667..36dca873 100644
--- a/experimental/listener.go
+++ b/experimental/listener.go
@@ -18,13 +18,7 @@ type FunctionListenerFactoryKey struct{}
 type FunctionListenerFactory interface {
 	// NewListener returns a FunctionListener for a defined function. If nil is
 	// returned, no listener will be notified.
-	//
-	// Params
-	//
-	//	* moduleName - the name this listener was instantiated with, and might
-	//	  vary for the same binary.
-	//	* definition - metadata about this function parsed from its Wasm binary.
-	NewListener(moduleName string, definition api.FunctionDefinition) FunctionListener
+	NewListener(api.FunctionDefinition) FunctionListener
 }
 
 // FunctionListener can be registered for any function via
diff --git a/experimental/log_listener.go b/experimental/log_listener.go
new file mode 100644
index 00000000..202a6260
--- /dev/null
+++ b/experimental/log_listener.go
@@ -0,0 +1,155 @@
+package experimental
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
+)
+
+// NewLoggingListenerFactory implements FunctionListenerFactory to log all
+// function calls to the writer.
+func NewLoggingListenerFactory(writer io.Writer) FunctionListenerFactory {
+	return &loggingListenerFactory{writer}
+}
+
+type loggingListenerFactory struct{ writer io.Writer }
+
+// NewListener implements the same method as documented on
+// experimental.FunctionListener.
+func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) FunctionListener {
+	return &loggingListener{writer: f.writer, fnd: fnd, isWasi: fnd.ModuleName() == "wasi_snapshot_preview1"}
+}
+
+// nestLevelKey holds state between logger.Before and loggingListener.After to ensure
+// call depth is reflected.
+type nestLevelKey struct{}
+
+// loggingListener implements experimental.FunctionListener to log entrance and exit
+// of each function call.
+type loggingListener struct {
+	writer io.Writer
+	fnd    api.FunctionDefinition
+	isWasi bool
+}
+
+// Before logs to stdout the module and function name, prefixed with '-->' and
+// indented based on the call nesting level.
+func (l *loggingListener) Before(ctx context.Context, vals []uint64) context.Context {
+	nestLevel, _ := ctx.Value(nestLevelKey{}).(int)
+
+	l.writeIndented(true, nil, vals, nestLevel+1)
+
+	// Increase the next nesting level.
+	return context.WithValue(ctx, nestLevelKey{}, nestLevel+1)
+}
+
+// After logs to stdout the module and function name, prefixed with '<--' and
+// indented based on the call nesting level.
+func (l *loggingListener) After(ctx context.Context, err error, vals []uint64) {
+	// Note: We use the nest level directly even though it is the "next" nesting level.
+	// This works because our indent of zero nesting is one tab.
+	l.writeIndented(false, err, vals, ctx.Value(nestLevelKey{}).(int))
+}
+
+// writeIndented writes an indented message like this: "-->\t\t\t$indentLevel$funcName\n"
+func (l *loggingListener) writeIndented(before bool, err error, vals []uint64, indentLevel int) {
+	var message strings.Builder
+	for i := 1; i < indentLevel; i++ {
+		message.WriteByte('\t')
+	}
+	if before {
+		if l.fnd.IsHostFunction() {
+			message.WriteString("==> ")
+		} else {
+			message.WriteString("--> ")
+		}
+		l.writeFuncEnter(&message, vals)
+	} else { // after
+		if l.fnd.IsHostFunction() {
+			message.WriteString("<== ")
+		} else {
+			message.WriteString("<-- ")
+		}
+		l.writeFuncExit(&message, err, vals)
+	}
+	message.WriteByte('\n')
+
+	_, _ = l.writer.Write([]byte(message.String()))
+}
+
+func (l *loggingListener) writeFuncEnter(message *strings.Builder, vals []uint64) {
+	valLen := len(vals)
+	message.WriteString(l.fnd.DebugName())
+	message.WriteByte('(')
+	switch valLen {
+	case 0:
+	default:
+		i := l.writeParam(message, 0, vals)
+		for i < valLen {
+			message.WriteByte(',')
+			i = l.writeParam(message, i, vals)
+		}
+	}
+	message.WriteByte(')')
+}
+
+func (l *loggingListener) writeFuncExit(message *strings.Builder, err error, vals []uint64) {
+	if err != nil {
+		message.WriteString("error: ")
+		message.WriteString(err.Error())
+		return
+	} else if l.isWasi {
+		message.WriteString(wasi_snapshot_preview1.ErrnoName(uint32(vals[0])))
+		return
+	}
+	valLen := len(vals)
+	message.WriteByte('(')
+	switch valLen {
+	case 0:
+	default:
+		i := l.writeResult(message, 0, vals)
+		for i < valLen {
+			message.WriteByte(',')
+			i = l.writeResult(message, i, vals)
+		}
+	}
+	message.WriteByte(')')
+}
+
+func (l *loggingListener) writeResult(message *strings.Builder, i int, vals []uint64) int {
+	return l.writeVal(message, l.fnd.ResultTypes()[i], i, vals)
+}
+
+func (l *loggingListener) writeParam(message *strings.Builder, i int, vals []uint64) int {
+	if len(l.fnd.ParamNames()) > 0 {
+		message.WriteString(l.fnd.ParamNames()[i])
+		message.WriteByte('=')
+	}
+	return l.writeVal(message, l.fnd.ParamTypes()[i], i, vals)
+}
+
+func (l *loggingListener) writeVal(message *strings.Builder, t api.ValueType, i int, vals []uint64) int {
+	v := vals[i]
+	i++
+	switch t {
+	case api.ValueTypeI32:
+		message.WriteString(strconv.FormatUint(uint64(uint32(v)), 10))
+	case api.ValueTypeI64:
+		message.WriteString(strconv.FormatUint(v, 10))
+	case api.ValueTypeF32:
+		message.WriteString(strconv.FormatFloat(float64(api.DecodeF32(v)), 'g', -1, 32))
+	case api.ValueTypeF64:
+		message.WriteString(strconv.FormatFloat(api.DecodeF64(v), 'g', -1, 64))
+	case 0x7b: // wasm.ValueTypeV128
+		message.WriteString(fmt.Sprintf("%016x%016x", v, vals[i])) // fixed-width hex
+		i++
+	case api.ValueTypeExternref, 0x70: // wasm.ValueTypeFuncref
+		message.WriteString(fmt.Sprintf("%016x", v)) // fixed-width hex
+	}
+	return i
+}
diff --git a/internal/wasi_snapshot_preview1/errno.go b/internal/wasi_snapshot_preview1/errno.go
new file mode 100644
index 00000000..8dacc266
--- /dev/null
+++ b/internal/wasi_snapshot_preview1/errno.go
@@ -0,0 +1,95 @@
+// Package wasi_snapshot_preview1 is an internal helper to remove package
+// cycles re-using errno
+package wasi_snapshot_preview1
+
+import (
+	"fmt"
+)
+
+// ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG"
+func ErrnoName(errno uint32) string {
+	if int(errno) < len(errnoToString) {
+		return errnoToString[errno]
+	}
+	return fmt.Sprintf("errno(%d)", errno)
+}
+
+var errnoToString = [...]string{
+	"ESUCCESS",
+	"E2BIG",
+	"EACCES",
+	"EADDRINUSE",
+	"EADDRNOTAVAIL",
+	"EAFNOSUPPORT",
+	"EAGAIN",
+	"EALREADY",
+	"EBADF",
+	"EBADMSG",
+	"EBUSY",
+	"ECANCELED",
+	"ECHILD",
+	"ECONNABORTED",
+	"ECONNREFUSED",
+	"ECONNRESET",
+	"EDEADLK",
+	"EDESTADDRREQ",
+	"EDOM",
+	"EDQUOT",
+	"EEXIST",
+	"EFAULT",
+	"EFBIG",
+	"EHOSTUNREACH",
+	"EIDRM",
+	"EILSEQ",
+	"EINPROGRESS",
+	"EINTR",
+	"EINVAL",
+	"EIO",
+	"EISCONN",
+	"EISDIR",
+	"ELOOP",
+	"EMFILE",
+	"EMLINK",
+	"EMSGSIZE",
+	"EMULTIHOP",
+	"ENAMETOOLONG",
+	"ENETDOWN",
+	"ENETRESET",
+	"ENETUNREACH",
+	"ENFILE",
+	"ENOBUFS",
+	"ENODEV",
+	"ENOENT",
+	"ENOEXEC",
+	"ENOLCK",
+	"ENOLINK",
+	"ENOMEM",
+	"ENOMSG",
+	"ENOPROTOOPT",
+	"ENOSPC",
+	"ENOSYS",
+	"ENOTCONN",
+	"ENOTDIR",
+	"ENOTEMPTY",
+	"ENOTRECOVERABLE",
+	"ENOTSOCK",
+	"ENOTSUP",
+	"ENOTTY",
+	"ENXIO",
+	"EOVERFLOW",
+	"EOWNERDEAD",
+	"EPERM",
+	"EPIPE",
+	"EPROTO",
+	"EPROTONOSUPPORT",
+	"EPROTOTYPE",
+	"ERANGE",
+	"EROFS",
+	"ESPIPE",
+	"ESRCH",
+	"ESTALE",
+	"ETIMEDOUT",
+	"ETXTBSY",
+	"EXDEV",
+	"ENOTCAPABLE",
+}
diff --git a/internal/wasm/function_definition.go b/internal/wasm/function_definition.go
index 30e4c895..281fe790 100644
--- a/internal/wasm/function_definition.go
+++ b/internal/wasm/function_definition.go
@@ -61,10 +61,13 @@ func (m *Module) BuildFunctionDefinitions() {
 		importFuncIdx++
 	}
 
+	// At the moment, a module can either be solely wasm or host functions.
+	isHostFunction := m.HostFunctionSection != nil
 	for codeIndex, typeIndex := range m.FunctionSection {
 		m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{
-			index:    Index(codeIndex) + importCount,
-			funcType: m.TypeSection[typeIndex],
+			index:          Index(codeIndex) + importCount,
+			funcType:       m.TypeSection[typeIndex],
+			isHostFunction: isHostFunction,
 		})
 	}
 
@@ -99,14 +102,15 @@ func (m *Module) BuildFunctionDefinitions() {
 
 // FunctionDefinition implements api.FunctionDefinition
 type FunctionDefinition struct {
-	moduleName  string
-	index       Index
-	name        string
-	debugName   string
-	funcType    *FunctionType
-	importDesc  *[2]string
-	exportNames []string
-	paramNames  []string
+	moduleName     string
+	index          Index
+	name           string
+	debugName      string
+	isHostFunction bool
+	funcType       *FunctionType
+	importDesc     *[2]string
+	exportNames    []string
+	paramNames     []string
 }
 
 // ModuleName implements the same method as documented on api.FunctionDefinition.
@@ -142,6 +146,11 @@ func (f *FunctionDefinition) ExportNames() []string {
 	return f.exportNames
 }
 
+// IsHostFunction implements the same method as documented on api.FunctionDefinition.
+func (f *FunctionDefinition) IsHostFunction() bool {
+	return f.isHostFunction
+}
+
 // ParamNames implements the same method as documented on api.FunctionDefinition.
 func (f *FunctionDefinition) ParamNames() []string {
 	return f.paramNames
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index f88b406f..a9906839 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -13,6 +13,7 @@ import (
 func NewHostModule(
 	moduleName string,
 	nameToGoFunc map[string]interface{},
+	funcToNames map[string][]string,
 	nameToMemory map[string]*Memory,
 	nameToGlobal map[string]*Global,
 	enabledFeatures Features,
@@ -33,11 +34,12 @@ func NewHostModule(
 
 	// Check name collision as exports cannot collide on names, regardless of type.
 	for name := range nameToGoFunc {
+		// manually generate the error message as we don't have debug names yet.
 		if _, ok := nameToMemory[name]; ok {
-			return nil, fmt.Errorf("func[%s] exports the same name as a memory", name)
+			return nil, fmt.Errorf("func[%s.%s] exports the same name as a memory", moduleName, name)
 		}
 		if _, ok := nameToGlobal[name]; ok {
-			return nil, fmt.Errorf("func[%s] exports the same name as a global", name)
+			return nil, fmt.Errorf("func[%s.%s] exports the same name as a global", moduleName, name)
 		}
 	}
 	for name := range nameToMemory {
@@ -47,7 +49,7 @@ func NewHostModule(
 	}
 
 	if funcCount > 0 {
-		if err = addFuncs(m, nameToGoFunc, enabledFeatures); err != nil {
+		if err = addFuncs(m, nameToGoFunc, funcToNames, enabledFeatures); err != nil {
 			return
 		}
 	}
@@ -76,7 +78,12 @@ func (m *Module) IsHostModule() bool {
 	return len(m.HostFunctionSection) > 0
 }
 
-func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Features) error {
+func addFuncs(
+	m *Module,
+	nameToGoFunc map[string]interface{},
+	funcToNames map[string][]string,
+	enabledFeatures Features,
+) error {
 	funcCount := uint32(len(nameToGoFunc))
 	funcNames := make([]string, 0, funcCount)
 	if m.NameSection == nil {
@@ -95,26 +102,32 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Fe
 	sort.Strings(funcNames)
 
 	for idx := Index(0); idx < funcCount; idx++ {
-		name := funcNames[idx]
-		fn := reflect.ValueOf(nameToGoFunc[name])
+		exportName := funcNames[idx]
+		debugName := wasmdebug.FuncName(moduleName, exportName, idx)
+		fn := reflect.ValueOf(nameToGoFunc[exportName])
 		_, functionType, err := getFunctionType(&fn, enabledFeatures)
 		if err != nil {
-			return fmt.Errorf("func[%s] %w", name, err)
+			return fmt.Errorf("func[%s] %w", debugName, err)
+		}
+		names := funcToNames[exportName]
+		namesLen := len(names)
+		if namesLen > 1 && namesLen-1 != len(functionType.Params) {
+			return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(functionType.Params))
 		}
 
 		m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType))
 		m.HostFunctionSection = append(m.HostFunctionSection, &fn)
-		m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: name, Index: idx})
-		m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name})
-		m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{
-			moduleName:  moduleName,
-			index:       idx,
-			name:        name,
-			debugName:   wasmdebug.FuncName(moduleName, name, idx),
-			funcType:    functionType,
-			exportNames: []string{name},
-			paramNames:  nil, // TODO
-		})
+		m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: exportName, Index: idx})
+		if namesLen > 0 {
+			m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: names[0]})
+			localNames := &NameMapAssoc{Index: idx}
+			for i, n := range names[1:] {
+				localNames.NameMap = append(localNames.NameMap, &NameAssoc{Index: Index(i), Name: n})
+			}
+			m.NameSection.LocalNames = append(m.NameSection.LocalNames, localNames)
+		} else {
+			m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: exportName})
+		}
 	}
 	return nil
 }
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index e453328b..26d83859 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -586,7 +586,7 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo
 //	* This relies on data generated by Module.BuildFunctionDefinitions.
 //	* This is exported for tests that don't call Instantiate, notably only
 //	  enginetest.go.
-func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionListenerFactory) (fns []*FunctionInstance) {
+func (m *ModuleInstance) BuildFunctions(mod *Module, listeners []experimental.FunctionListener) (fns []*FunctionInstance) {
 	fns = make([]*FunctionInstance, 0, len(mod.FunctionDefinitionSection))
 	if mod.IsHostModule() {
 		for i := range mod.HostFunctionSection {
@@ -615,8 +615,8 @@ func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionL
 		f.Idx = d.index
 		f.Type = d.funcType
 		f.definition = d
-		if fnlf != nil {
-			f.FunctionListener = fnlf.NewListener(m.Name, d)
+		if listeners != nil {
+			f.FunctionListener = listeners[i]
 		}
 	}
 	return
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 8472fa9d..d0611265 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -305,7 +305,7 @@ func (s *Store) Instantiate(
 	module *Module,
 	name string,
 	sys *internalsys.Context,
-	functionListenerFactory experimentalapi.FunctionListenerFactory,
+	listeners []experimentalapi.FunctionListener,
 ) (*CallContext, error) {
 	if ctx == nil {
 		ctx = context.Background()
@@ -329,7 +329,7 @@ func (s *Store) Instantiate(
 	}
 
 	// Instantiate the module and add it to the namespace so that other modules can import it.
-	if callCtx, err := s.instantiate(ctx, ns, module, name, sys, functionListenerFactory, importedModules); err != nil {
+	if callCtx, err := s.instantiate(ctx, ns, module, name, sys, listeners, importedModules); err != nil {
 		ns.deleteModule(name)
 		return nil, err
 	} else {
@@ -346,7 +346,7 @@ func (s *Store) instantiate(
 	module *Module,
 	name string,
 	sysCtx *internalsys.Context,
-	functionListenerFactory experimentalapi.FunctionListenerFactory,
+	listeners []experimentalapi.FunctionListener,
 	modules map[string]*ModuleInstance,
 ) (*CallContext, error) {
 	typeIDs, err := s.getFunctionTypeIDs(module.TypeSection)
@@ -376,7 +376,7 @@ func (s *Store) instantiate(
 	}
 
 	m := &ModuleInstance{Name: name, TypeIDs: typeIDs}
-	functions := m.BuildFunctions(module, functionListenerFactory)
+	functions := m.BuildFunctions(module, listeners)
 
 	// Now we have all instances from imports and local ones, so ready to create a new ModuleInstance.
 	m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection)
diff --git a/namespace.go b/namespace.go
index 8844be60..dbf97e19 100644
--- a/namespace.go
+++ b/namespace.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
-	experimentalapi "github.com/tetratelabs/wazero/experimental"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/sys"
@@ -80,15 +79,8 @@ func (ns *namespace) InstantiateModule(
 		name = code.module.NameSection.ModuleName
 	}
 
-	var functionListenerFactory experimentalapi.FunctionListenerFactory
-	if ctx != nil { // Test to see if internal code are using an experimental feature.
-		if fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{}); fnlf != nil {
-			functionListenerFactory = fnlf.(experimentalapi.FunctionListenerFactory)
-		}
-	}
-
 	// Instantiate the module in the appropriate namespace.
-	mod, err = ns.store.Instantiate(ctx, ns.ns, code.module, name, sysCtx, functionListenerFactory)
+	mod, err = ns.store.Instantiate(ctx, ns.ns, code.module, name, sysCtx, code.listeners)
 	if err != nil {
 		// If there was an error, don't leak the compiled module.
 		if code.closeWithModule {
diff --git a/runtime.go b/runtime.go
index 1173ae1f..eda89aa4 100644
--- a/runtime.go
+++ b/runtime.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 
 	"github.com/tetratelabs/wazero/api"
+	experimentalapi "github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 )
@@ -130,6 +131,7 @@ func NewRuntimeWithConfig(rConfig RuntimeConfig) Runtime {
 		store:           store,
 		ns:              &namespace{store: store, ns: ns},
 		enabledFeatures: config.enabledFeatures,
+		isInterpreter:   config.isInterpreter,
 	}
 }
 
@@ -138,15 +140,13 @@ type runtime struct {
 	store           *wasm.Store
 	ns              *namespace
 	enabledFeatures wasm.Features
+	isInterpreter   bool
 	compiledModules []*compiledModule
 }
 
 // NewNamespace implements Runtime.NewNamespace.
 func (r *runtime) NewNamespace(ctx context.Context) Namespace {
-	return &namespace{
-		store: r.store,
-		ns:    r.store.NewNamespace(ctx),
-	}
+	return &namespace{store: r.store, ns: r.store.NewNamespace(ctx)}
 }
 
 // Module implements Namespace.Module embedded by Runtime.
@@ -179,15 +179,41 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte, cConfig Comp
 	// Now that the module is validated, cache the function definitions.
 	internal.BuildFunctionDefinitions()
 
+	c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
+
+	if c.listeners, err = buildListeners(ctx, r, internal); err != nil {
+		return nil, err
+	}
+
 	if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
 		return nil, err
 	}
 
-	c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
 	r.compiledModules = append(r.compiledModules, c)
 	return c, nil
 }
 
+func buildListeners(ctx context.Context, r *runtime, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
+	if ctx == nil {
+		return nil, nil
+	}
+	// Test to see if internal code are using an experimental feature.
+	fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{})
+	if fnlf == nil {
+		return nil, nil
+	}
+	if !r.isInterpreter {
+		return nil, errors.New("context includes a FunctionListenerFactoryKey, which is only supported in the interpreter")
+	}
+	factory := fnlf.(experimentalapi.FunctionListenerFactory)
+	importCount := internal.ImportFuncCount()
+	listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection))
+	for i := 0; i < len(listeners); i++ {
+		listeners[i] = factory.NewListener(internal.FunctionDefinitionSection[uint32(i)+importCount])
+	}
+	return listeners, nil
+}
+
 // InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
 func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
 	if compiled, err := r.CompileModule(ctx, binary, NewCompileConfig()); err != nil {
diff --git a/wasi_snapshot_preview1/errno.go b/wasi_snapshot_preview1/errno.go
index 83473324..5f430279 100644
--- a/wasi_snapshot_preview1/errno.go
+++ b/wasi_snapshot_preview1/errno.go
@@ -1,7 +1,7 @@
 package wasi_snapshot_preview1
 
 import (
-	"fmt"
+	internalwasi "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 )
 
 // Errno are the error codes returned by WASI functions.
@@ -17,10 +17,7 @@ type Errno = uint32 // neither uint16 nor an alias for parity with wasm.ValueTyp
 
 // ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG"
 func ErrnoName(errno Errno) string {
-	if int(errno) < len(errnoToString) {
-		return errnoToString[errno]
-	}
-	return fmt.Sprintf("errno(%d)", errno)
+	return internalwasi.ErrnoName(errno)
 }
 
 // Note: Below prefers POSIX symbol names over WASI ones, even if the docs are from WASI.
@@ -182,83 +179,3 @@ const (
 	// ErrnoNotcapable Extension: Capabilities insufficient.
 	ErrnoNotcapable
 )
-
-var errnoToString = [...]string{
-	ErrnoSuccess:        "ESUCCESS",
-	Errno2big:           "E2BIG",
-	ErrnoAcces:          "EACCES",
-	ErrnoAddrinuse:      "EADDRINUSE",
-	ErrnoAddrnotavail:   "EADDRNOTAVAIL",
-	ErrnoAfnosupport:    "EAFNOSUPPORT",
-	ErrnoAgain:          "EAGAIN",
-	ErrnoAlready:        "EALREADY",
-	ErrnoBadf:           "EBADF",
-	ErrnoBadmsg:         "EBADMSG",
-	ErrnoBusy:           "EBUSY",
-	ErrnoCanceled:       "ECANCELED",
-	ErrnoChild:          "ECHILD",
-	ErrnoConnaborted:    "ECONNABORTED",
-	ErrnoConnrefused:    "ECONNREFUSED",
-	ErrnoConnreset:      "ECONNRESET",
-	ErrnoDeadlk:         "EDEADLK",
-	ErrnoDestaddrreq:    "EDESTADDRREQ",
-	ErrnoDom:            "EDOM",
-	ErrnoDquot:          "EDQUOT",
-	ErrnoExist:          "EEXIST",
-	ErrnoFault:          "EFAULT",
-	ErrnoFbig:           "EFBIG",
-	ErrnoHostunreach:    "EHOSTUNREACH",
-	ErrnoIdrm:           "EIDRM",
-	ErrnoIlseq:          "EILSEQ",
-	ErrnoInprogress:     "EINPROGRESS",
-	ErrnoIntr:           "EINTR",
-	ErrnoInval:          "EINVAL",
-	ErrnoIo:             "EIO",
-	ErrnoIsconn:         "EISCONN",
-	ErrnoIsdir:          "EISDIR",
-	ErrnoLoop:           "ELOOP",
-	ErrnoMfile:          "EMFILE",
-	ErrnoMlink:          "EMLINK",
-	ErrnoMsgsize:        "EMSGSIZE",
-	ErrnoMultihop:       "EMULTIHOP",
-	ErrnoNametoolong:    "ENAMETOOLONG",
-	ErrnoNetdown:        "ENETDOWN",
-	ErrnoNetreset:       "ENETRESET",
-	ErrnoNetunreach:     "ENETUNREACH",
-	ErrnoNfile:          "ENFILE",
-	ErrnoNobufs:         "ENOBUFS",
-	ErrnoNodev:          "ENODEV",
-	ErrnoNoent:          "ENOENT",
-	ErrnoNoexec:         "ENOEXEC",
-	ErrnoNolck:          "ENOLCK",
-	ErrnoNolink:         "ENOLINK",
-	ErrnoNomem:          "ENOMEM",
-	ErrnoNomsg:          "ENOMSG",
-	ErrnoNoprotoopt:     "ENOPROTOOPT",
-	ErrnoNospc:          "ENOSPC",
-	ErrnoNosys:          "ENOSYS",
-	ErrnoNotconn:        "ENOTCONN",
-	ErrnoNotdir:         "ENOTDIR",
-	ErrnoNotempty:       "ENOTEMPTY",
-	ErrnoNotrecoverable: "ENOTRECOVERABLE",
-	ErrnoNotsock:        "ENOTSOCK",
-	ErrnoNotsup:         "ENOTSUP",
-	ErrnoNotty:          "ENOTTY",
-	ErrnoNxio:           "ENXIO",
-	ErrnoOverflow:       "EOVERFLOW",
-	ErrnoOwnerdead:      "EOWNERDEAD",
-	ErrnoPerm:           "EPERM",
-	ErrnoPipe:           "EPIPE",
-	ErrnoProto:          "EPROTO",
-	ErrnoProtonosupport: "EPROTONOSUPPORT",
-	ErrnoPrototype:      "EPROTOTYPE",
-	ErrnoRange:          "ERANGE",
-	ErrnoRofs:           "EROFS",
-	ErrnoSpipe:          "ESPIPE",
-	ErrnoSrch:           "ESRCH",
-	ErrnoStale:          "ESTALE",
-	ErrnoTimedout:       "ETIMEDOUT",
-	ErrnoTxtbsy:         "ETXTBSY",
-	ErrnoXdev:           "EXDEV",
-	ErrnoNotcapable:     "ENOTCAPABLE",
-}
diff --git a/wasi_snapshot_preview1/wasi.go b/wasi_snapshot_preview1/wasi.go
index bbe4b294..d8f57744 100644
--- a/wasi_snapshot_preview1/wasi.go
+++ b/wasi_snapshot_preview1/wasi.go
@@ -1,9 +1,10 @@
-// Package wasi_snapshot_preview1 contains Go-defined functions to access system calls, such as opening a file, similar
-// to Go's x/sys package. These are accessible from WebAssembly-defined functions via importing ModuleName.
-// All WASI functions return a single Errno result, which is ErrnoSuccess on success.
+// Package wasi_snapshot_preview1 contains Go-defined functions to access
+// system calls, such as opening a file, similar to Go's x/sys package. These
+// are accessible from WebAssembly-defined functions via importing ModuleName.
+// All WASI functions return a single Errno result: ErrnoSuccess on success.
 //
-// Ex. If your source (%.wasm binary) includes an import "wasi_snapshot_preview1", call Instantiate
-// prior to instantiating it. Otherwise, it will error due to missing imports.
+// Ex. Call Instantiate before instantiating any wasm binary that imports
+// "wasi_snapshot_preview1", Otherwise, it will error due to missing imports.
 //	ctx := context.Background()
 //	r := wazero.NewRuntime()
 //	defer r.Close(ctx) // This closes everything this Runtime created.
@@ -31,7 +32,8 @@ import (
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
 const ModuleName = "wasi_snapshot_preview1"
 
-// Instantiate instantiates the ModuleName module into the runtime default namespace.
+// Instantiate instantiates the ModuleName module into the runtime default
+// namespace.
 //
 // Notes
 //
@@ -44,12 +46,13 @@ func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
 // Builder configures the ModuleName module for later use via Compile or Instantiate.
 type Builder interface {
 
-	// Compile compiles the ModuleName module that can instantiated in any namespace (wazero.Namespace).
+	// Compile compiles the ModuleName module that can instantiated in any
+	// namespace (wazero.Namespace).
 	//
 	// Note: This has the same effect as the same function on wazero.ModuleBuilder.
 	Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error)
 
-	// Instantiate instantiates the ModuleName module into the provided namespace.
+	// Instantiate instantiates the ModuleName module into the given namespace.
 	//
 	// Note: This has the same effect as the same function on wazero.ModuleBuilder.
 	Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
@@ -64,7 +67,9 @@ type builder struct{ r wazero.Runtime }
 
 // moduleBuilder returns a new wazero.ModuleBuilder for ModuleName
 func (b *builder) moduleBuilder() wazero.ModuleBuilder {
-	return b.r.NewModuleBuilder(ModuleName).ExportFunctions(wasiFunctions())
+	builder := b.r.NewModuleBuilder(ModuleName)
+	exportFunctions(builder)
+	return builder
 }
 
 // Compile implements Builder.Compile
@@ -437,59 +442,102 @@ const (
 // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
 type wasi struct{}
 
-// wasiFunctions returns all go functions that implement wasi.
+// exportFunctions adds all go functions that implement wasi.
 // These should be exported in the module named ModuleName.
-func wasiFunctions() map[string]interface{} {
+func exportFunctions(builder wazero.ModuleBuilder) {
 	a := &wasi{}
 	// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
-	return map[string]interface{}{
-		functionArgsGet:              a.ArgsGet,
-		functionArgsSizesGet:         a.ArgsSizesGet,
-		functionEnvironGet:           a.EnvironGet,
-		functionEnvironSizesGet:      a.EnvironSizesGet,
-		functionClockResGet:          a.ClockResGet,
-		functionClockTimeGet:         a.ClockTimeGet,
-		functionFdAdvise:             a.FdAdvise,
-		functionFdAllocate:           a.FdAllocate,
-		functionFdClose:              a.FdClose,
-		functionFdDatasync:           a.FdDatasync,
-		functionFdFdstatGet:          a.FdFdstatGet,
-		functionFdFdstatSetFlags:     a.FdFdstatSetFlags,
-		functionFdFdstatSetRights:    a.FdFdstatSetRights,
-		functionFdFilestatGet:        a.FdFilestatGet,
-		functionFdFilestatSetSize:    a.FdFilestatSetSize,
-		functionFdFilestatSetTimes:   a.FdFilestatSetTimes,
-		functionFdPread:              a.FdPread,
-		functionFdPrestatGet:         a.FdPrestatGet,
-		functionFdPrestatDirName:     a.FdPrestatDirName,
-		functionFdPwrite:             a.FdPwrite,
-		functionFdRead:               a.FdRead,
-		functionFdReaddir:            a.FdReaddir,
-		functionFdRenumber:           a.FdRenumber,
-		functionFdSeek:               a.FdSeek,
-		functionFdSync:               a.FdSync,
-		functionFdTell:               a.FdTell,
-		functionFdWrite:              a.FdWrite,
-		functionPathCreateDirectory:  a.PathCreateDirectory,
-		functionPathFilestatGet:      a.PathFilestatGet,
-		functionPathFilestatSetTimes: a.PathFilestatSetTimes,
-		functionPathLink:             a.PathLink,
-		functionPathOpen:             a.PathOpen,
-		functionPathReadlink:         a.PathReadlink,
-		functionPathRemoveDirectory:  a.PathRemoveDirectory,
-		functionPathRename:           a.PathRename,
-		functionPathSymlink:          a.PathSymlink,
-		functionPathUnlinkFile:       a.PathUnlinkFile,
-		functionPollOneoff:           a.PollOneoff,
-		functionProcExit:             a.ProcExit,
-		functionProcRaise:            a.ProcRaise,
-		functionSchedYield:           a.SchedYield,
-		functionRandomGet:            a.RandomGet,
-		functionSockRecv:             a.SockRecv,
-		functionSockSend:             a.SockSend,
-		functionSockShutdown:         a.SockShutdown,
-	}
+	builder.ExportFunction(functionArgsGet, a.ArgsGet,
+		functionArgsGet, "argv", "argv_buf")
+	builder.ExportFunction(functionArgsSizesGet, a.ArgsSizesGet,
+		functionArgsSizesGet, "result.argc", "result.argv_buf_size")
+	builder.ExportFunction(functionEnvironGet, a.EnvironGet,
+		functionEnvironGet, "environ", "environ_buf")
+	builder.ExportFunction(functionEnvironSizesGet, a.EnvironSizesGet,
+		functionEnvironSizesGet, "result.environc", "result.environBufSize")
+	builder.ExportFunction(functionClockResGet, a.ClockResGet,
+		functionClockResGet, "id", "result.resolution")
+	builder.ExportFunction(functionClockTimeGet, a.ClockTimeGet,
+		functionClockTimeGet, "id", "precision", "result.timestamp")
+	builder.ExportFunction(functionFdAdvise, a.FdAdvise,
+		functionFdAdvise, "fd", "offset", "len", "result.advice")
+	builder.ExportFunction(functionFdAllocate, a.FdAllocate,
+		functionFdAllocate, "fd", "offset", "len")
+	builder.ExportFunction(functionFdClose, a.FdClose,
+		functionFdClose, "fd")
+	builder.ExportFunction(functionFdDatasync, a.FdDatasync,
+		functionFdDatasync, "fd")
+	builder.ExportFunction(functionFdFdstatGet, a.FdFdstatGet,
+		functionFdFdstatGet, "fd", "result.stat")
+	builder.ExportFunction(functionFdFdstatSetFlags, a.FdFdstatSetFlags,
+		functionFdFdstatSetFlags, "fd", "flags")
+	builder.ExportFunction(functionFdFdstatSetRights, a.FdFdstatSetRights,
+		functionFdFdstatSetRights, "fd", "fs_rights_base", "fs_rights_inheriting")
+	builder.ExportFunction(functionFdFilestatGet, a.FdFilestatGet,
+		functionFdFilestatGet, "fd", "result.buf")
+	builder.ExportFunction(functionFdFilestatSetSize, a.FdFilestatSetSize,
+		functionFdFilestatSetSize, "fd", "size")
+	builder.ExportFunction(functionFdFilestatSetTimes, a.FdFilestatSetTimes,
+		functionFdFilestatSetTimes, "fd", "atim", "mtim", "fst_flags")
+	builder.ExportFunction(functionFdPread, a.FdPread,
+		functionFdPread, "fd", "iovs", "iovs_len", "offset", "result.nread")
+	builder.ExportFunction(functionFdPrestatGet, a.FdPrestatGet,
+		functionFdPrestatGet, "fd", "result.prestat")
+	builder.ExportFunction(functionFdPrestatDirName, a.FdPrestatDirName,
+		functionFdPrestatDirName, "fd", "path", "path_len")
+	builder.ExportFunction(functionFdPwrite, a.FdPwrite,
+		functionFdPwrite, "fd", "iovs", "iovs_len", "offset", "result.nwritten")
+	builder.ExportFunction(functionFdRead, a.FdRead,
+		functionFdRead, "fd", "iovs", "iovs_len", "result.size")
+	builder.ExportFunction(functionFdReaddir, a.FdReaddir,
+		functionFdReaddir, "fd", "buf", "buf_len", "cookie", "result.bufused")
+	builder.ExportFunction(functionFdRenumber, a.FdRenumber,
+		functionFdRenumber, "fd", "to")
+	builder.ExportFunction(functionFdSeek, a.FdSeek,
+		functionFdSeek, "fd", "offset", "whence", "result.newoffset")
+	builder.ExportFunction(functionFdSync, a.FdSync,
+		functionFdSync, "fd")
+	builder.ExportFunction(functionFdTell, a.FdTell,
+		functionFdTell, "fd", "result.offset")
+	builder.ExportFunction(functionFdWrite, a.FdWrite,
+		functionFdWrite, "fd", "iovs", "iovs_len", "result.size")
+	builder.ExportFunction(functionPathCreateDirectory, a.PathCreateDirectory,
+		functionPathCreateDirectory, "fd", "path", "path_len")
+	builder.ExportFunction(functionPathFilestatGet, a.PathFilestatGet,
+		functionPathFilestatGet, "fd", "flags", "path", "path_len", "result.buf")
+	builder.ExportFunction(functionPathFilestatSetTimes, a.PathFilestatSetTimes,
+		functionPathFilestatSetTimes, "fd", "flags", "path", "path_len", "atim", "mtim", "fst_flags")
+	builder.ExportFunction(functionPathLink, a.PathLink,
+		functionPathLink, "old_fd", "old_flags", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
+	builder.ExportFunction(functionPathOpen, a.PathOpen,
+		functionPathOpen, "fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd")
+	builder.ExportFunction(functionPathReadlink, a.PathReadlink,
+		functionPathReadlink, "fd", "path", "path_len", "buf", "buf_len", "result.bufused")
+	builder.ExportFunction(functionPathRemoveDirectory, a.PathRemoveDirectory,
+		functionPathRemoveDirectory, "fd", "path", "path_len")
+	builder.ExportFunction(functionPathRename, a.PathRename,
+		functionPathRename, "fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
+	builder.ExportFunction(functionPathSymlink, a.PathSymlink,
+		functionPathSymlink, "old_path", "old_path_len", "fd", "new_path", "new_path_len")
+	builder.ExportFunction(functionPathUnlinkFile, a.PathUnlinkFile,
+		functionPathUnlinkFile, "fd", "path", "path_len")
+	builder.ExportFunction(functionPollOneoff, a.PollOneoff,
+		functionPollOneoff, "in", "out", "nsubscriptions", "result.nevents")
+	builder.ExportFunction(functionProcExit, a.ProcExit,
+		functionProcExit, "rval")
+	builder.ExportFunction(functionProcRaise, a.ProcRaise,
+		functionProcRaise, "sig")
+	builder.ExportFunction(functionSchedYield, a.SchedYield,
+		functionSchedYield)
+	builder.ExportFunction(functionRandomGet, a.RandomGet,
+		functionRandomGet, "buf", "buf_len")
+	builder.ExportFunction(functionSockRecv, a.SockRecv,
+		functionSockRecv, "fd", "ri_data", "ri_data_count", "ri_flags", "result.ro_datalen", "result.ro_flags")
+	builder.ExportFunction(functionSockSend, a.SockSend,
+		functionSockSend, "fd", "si_data", "si_data_count", "si_flags", "result.so_datalen")
+	builder.ExportFunction(functionSockShutdown, a.SockShutdown,
+		functionSockShutdown, "fd", "how")
 }
 
 // ArgsGet is the WASI function that reads command-line argument data (WithArgs).


Test patch:
diff --git a/assemblyscript/assemblyscript_example_test.go b/assemblyscript/assemblyscript_example_test.go
index 56d0d811..b4d2c276 100644
--- a/assemblyscript/assemblyscript_example_test.go
+++ b/assemblyscript/assemblyscript_example_test.go
@@ -38,8 +38,9 @@ func Example_functionExporter() {
 		ExportFunction("get_int", func() uint32 { return 1 })
 
 	// Now, add AssemblyScript special function imports into it.
-	envBuilder.ExportFunctions(assemblyscript.NewFunctionExporter().
-		WithAbortMessageDisabled().ExportFunctions())
+	assemblyscript.NewFunctionExporter().
+		WithAbortMessageDisabled().
+		ExportFunctions(envBuilder)
 
 	// Output:
 }
diff --git a/assemblyscript/assemblyscript_test.go b/assemblyscript/assemblyscript_test.go
index 92d9c59c..945a71be 100644
--- a/assemblyscript/assemblyscript_test.go
+++ b/assemblyscript/assemblyscript_test.go
@@ -6,12 +6,14 @@ import (
 	_ "embed"
 	"errors"
 	"io"
+	"strings"
 	"testing"
 	"testing/iotest"
 	"unicode/utf16"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
+	. "github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/watzero"
 	"github.com/tetratelabs/wazero/sys"
@@ -60,32 +62,37 @@ func TestAbort(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			r := wazero.NewRuntime()
-			defer r.Close(testCtx)
+			var out, log bytes.Buffer
 
-			out := &bytes.Buffer{}
+			// Set context to one that has an experimental listener
+			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
 
-			_, err := r.NewModuleBuilder("env").
-				ExportFunctions(tc.exporter.ExportFunctions()).
-				Instantiate(testCtx, r)
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+			defer r.Close(ctx)
+
+			envBuilder := r.NewModuleBuilder("env")
+			tc.exporter.ExportFunctions(envBuilder)
+			_, err := envBuilder.Instantiate(ctx, r)
 			require.NoError(t, err)
 
 			abortWasm, err := watzero.Wat2Wasm(abortWat)
 			require.NoError(t, err)
 
-			code, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
+			code, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig())
 			require.NoError(t, err)
 
-			mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStderr(out))
+			mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStderr(&out))
 			require.NoError(t, err)
 
-			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
+			messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
 
-			_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
+			_, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2)
 			require.Error(t, err)
 			require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
 
 			require.Equal(t, tc.expected, out.String())
+			require.Equal(t, `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
+`, log.String())
 		})
 	}
 }
@@ -95,16 +102,23 @@ func TestAbort_Error(t *testing.T) {
 		name          string
 		messageUTF16  []byte
 		fileNameUTF16 []byte
+		expectedLog   string
 	}{
 		{
 			name:          "bad message",
 			messageUTF16:  encodeUTF16("message")[:5],
 			fileNameUTF16: encodeUTF16("filename"),
+			expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
+<== ()
+`,
 		},
 		{
 			name:          "bad filename",
 			messageUTF16:  encodeUTF16("message"),
 			fileNameUTF16: encodeUTF16("filename")[:5],
+			expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
+<== ()
+`,
 		},
 	}
 
@@ -112,28 +126,33 @@ func TestAbort_Error(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			r := wazero.NewRuntime()
-			defer r.Close(testCtx)
+			var out, log bytes.Buffer
+
+			// Set context to one that has an experimental listener
+			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
 
-			_, err := Instantiate(testCtx, r)
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+			defer r.Close(ctx)
+
+			_, err := Instantiate(ctx, r)
 			require.NoError(t, err)
 
 			abortWasm, err := watzero.Wat2Wasm(abortWat)
 			require.NoError(t, err)
 
-			compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
+			compiled, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig())
 			require.NoError(t, err)
 
-			out := &bytes.Buffer{}
-			exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out)
-			mod, err := r.InstantiateModule(testCtx, compiled, exporter)
+			exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(&out)
+			mod, err := r.InstantiateModule(ctx, compiled, exporter)
 			require.NoError(t, err)
 
-			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
+			messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
 
-			_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
+			_, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2)
 			require.NoError(t, err)
 			require.Equal(t, "", out.String()) // nothing output if strings fail to read.
+			require.Equal(t, tc.expectedLog, log.String())
 		})
 	}
 }
@@ -153,48 +172,63 @@ var unreachableAfterAbort = `(module
 
 // TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked.
 func TestAbort_UnreachableAfter(t *testing.T) {
-	r := wazero.NewRuntime()
-	defer r.Close(testCtx)
+	var log bytes.Buffer
+
+	// Set context to one that has an experimental listener
+	ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
 
-	_, err := r.NewModuleBuilder("env").
-		// Disable the abort message as we are passing invalid memory offsets.
-		ExportFunctions(NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions()).
-		Instantiate(testCtx, r)
+	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+	defer r.Close(ctx)
+
+	envBuilder := r.NewModuleBuilder("env")
+	// Disable the abort message as we are passing invalid memory offsets.
+	NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions(envBuilder)
+	_, err := envBuilder.Instantiate(ctx, r)
 	require.NoError(t, err)
 
 	abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort)
 	require.NoError(t, err)
 
-	_, err = r.InstantiateModuleFromBinary(testCtx, abortWasm)
+	_, err = r.InstantiateModuleFromBinary(ctx, abortWasm)
 	require.Error(t, err)
 	require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
+	require.Equal(t, `--> .main()
+	==> env.~lib/builtins/abort(message=0,fileName=0,lineNumber=0,columnNumber=0)
+`, log.String())
 }
 
 func TestSeed(t *testing.T) {
-	r := wazero.NewRuntime()
-	defer r.Close(testCtx)
+	var log bytes.Buffer
+
+	// Set context to one that has an experimental listener
+	ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
+
+	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+	defer r.Close(ctx)
 
 	seed := []byte{0, 1, 2, 3, 4, 5, 6, 7}
 
-	_, err := Instantiate(testCtx, r)
+	_, err := Instantiate(ctx, r)
 	require.NoError(t, err)
 
 	seedWasm, err := watzero.Wat2Wasm(seedWat)
 	require.NoError(t, err)
 
-	code, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig())
 	require.NoError(t, err)
 
-	mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
+	mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
 	require.NoError(t, err)
 
 	seedFn := mod.ExportedFunction("seed")
 
-	res, err := seedFn.Call(testCtx)
+	_, err = seedFn.Call(ctx)
 	require.NoError(t, err)
 
 	// If this test doesn't break, the seed is deterministic.
-	require.Equal(t, uint64(506097522914230528), res[0])
+	require.Equal(t, `==> env.~lib/builtins/seed()
+<== (7.949928895127363e-275)
+`, log.String())
 }
 
 func TestSeed_error(t *testing.T) {
@@ -208,14 +242,14 @@ func TestSeed_error(t *testing.T) {
 			source: bytes.NewReader([]byte{0, 1}),
 			expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
 wasm stack trace:
-	env.seed() f64`,
+	env.~lib/builtins/seed() f64`,
 		},
 		{
 			name:   "error reading",
 			source: iotest.ErrReader(errors.New("ice cream")),
 			expectedErr: `error reading random seed: ice cream (recovered by wazero)
 wasm stack trace:
-	env.seed() f64`,
+	env.~lib/builtins/seed() f64`,
 		},
 	}
 
@@ -223,65 +257,84 @@ wasm stack trace:
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			r := wazero.NewRuntime()
-			defer r.Close(testCtx)
+			var log bytes.Buffer
+
+			// Set context to one that has an experimental listener
+			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
 
-			_, err := Instantiate(testCtx, r)
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+			defer r.Close(ctx)
+
+			_, err := Instantiate(ctx, r)
 			require.NoError(t, err)
 
 			seedWasm, err := watzero.Wat2Wasm(seedWat)
 			require.NoError(t, err)
 
-			compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
+			compiled, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig())
 			require.NoError(t, err)
 
 			exporter := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
-			mod, err := r.InstantiateModule(testCtx, compiled, exporter)
+			mod, err := r.InstantiateModule(ctx, compiled, exporter)
 			require.NoError(t, err)
 
-			_, err = mod.ExportedFunction("seed").Call(testCtx)
+			_, err = mod.ExportedFunction("seed").Call(ctx)
 			require.EqualError(t, err, tc.expectedErr)
+			require.Equal(t, `==> env.~lib/builtins/seed()
+`, log.String())
 		})
 	}
 }
 
 func TestTrace(t *testing.T) {
 	noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
+	noArgsLog := `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
+<== ()
+`
 	tests := []struct {
-		name     string
-		exporter FunctionExporter
-		params   []uint64
-		expected string
+		name                  string
+		exporter              FunctionExporter
+		params                []uint64
+		expected, expectedLog string
 	}{
 		{
-			name:     "disabled",
-			exporter: NewFunctionExporter(),
-			params:   noArgs,
-			expected: "",
+			name:        "disabled",
+			exporter:    NewFunctionExporter(),
+			params:      noArgs,
+			expected:    "",
+			expectedLog: noArgsLog,
 		},
 		{
-			name:     "ToStderr",
-			exporter: NewFunctionExporter().WithTraceToStderr(),
-			params:   noArgs,
-			expected: "trace: hello\n",
+			name:        "ToStderr",
+			exporter:    NewFunctionExporter().WithTraceToStderr(),
+			params:      noArgs,
+			expected:    "trace: hello\n",
+			expectedLog: noArgsLog,
 		},
 		{
-			name:     "ToStdout - no args",
-			exporter: NewFunctionExporter().WithTraceToStdout(),
-			params:   noArgs,
-			expected: "trace: hello\n",
+			name:        "ToStdout - no args",
+			exporter:    NewFunctionExporter().WithTraceToStdout(),
+			params:      noArgs,
+			expected:    "trace: hello\n",
+			expectedLog: noArgsLog,
 		},
 		{
 			name:     "ToStdout - one arg",
 			exporter: NewFunctionExporter().WithTraceToStdout(),
 			params:   []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
 			expected: "trace: hello 1\n",
+			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
+<== ()
+`,
 		},
 		{
 			name:     "ToStdout - two args",
 			exporter: NewFunctionExporter().WithTraceToStdout(),
 			params:   []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
 			expected: "trace: hello 1,2\n",
+			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
+<== ()
+`,
 		},
 		{
 			name:     "ToStdout - five args",
@@ -296,6 +349,9 @@ func TestTrace(t *testing.T) {
 				api.EncodeF64(5),
 			},
 			expected: "trace: hello 1,2,3.3,4.4,5\n",
+			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
+<== ()
+`,
 		},
 	}
 
@@ -303,40 +359,45 @@ func TestTrace(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			r := wazero.NewRuntime()
-			defer r.Close(testCtx)
+			var out, log bytes.Buffer
 
-			_, err := r.NewModuleBuilder("env").
-				ExportFunctions(tc.exporter.ExportFunctions()).
-				Instantiate(testCtx, r)
+			// Set context to one that has an experimental listener
+			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
+
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+			defer r.Close(ctx)
+
+			envBuilder := r.NewModuleBuilder("env")
+			tc.exporter.ExportFunctions(envBuilder)
+			_, err := envBuilder.Instantiate(ctx, r)
 			require.NoError(t, err)
 
 			traceWasm, err := watzero.Wat2Wasm(traceWat)
 			require.NoError(t, err)
 
-			code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
+			code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
 			require.NoError(t, err)
 
-			out := &bytes.Buffer{}
 			config := wazero.NewModuleConfig()
-			if tc.name == "ToStderr" {
-				config.WithStderr(out)
+			if strings.Contains("ToStderr", tc.name) {
+				config = config.WithStderr(&out)
 			} else {
-				config.WithStdout(out)
+				config = config.WithStdout(&out)
 			}
 
-			mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStdout(out).WithStderr(out))
+			mod, err := r.InstantiateModule(ctx, code, config)
 			require.NoError(t, err)
 
 			message := encodeUTF16("hello")
-			ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message)))
+			ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(message)))
 			require.True(t, ok)
-			ok = mod.Memory().Write(testCtx, uint32(4), message)
+			ok = mod.Memory().Write(ctx, uint32(4), message)
 			require.True(t, ok)
 
-			_, err = mod.ExportedFunction("trace").Call(testCtx, tc.params...)
+			_, err = mod.ExportedFunction("trace").Call(ctx, tc.params...)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, out.String())
+			require.Equal(t, tc.expectedLog, log.String())
 		})
 	}
 }
@@ -354,7 +415,7 @@ func TestTrace_error(t *testing.T) {
 			out:     &bytes.Buffer{},
 			expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
 wasm stack trace:
-	env.trace(i32,i32,f64,f64,f64,f64,f64)`,
+	env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
 		},
 		{
 			name:    "error writing",
@@ -362,7 +423,7 @@ wasm stack trace:
 			out:     &errWriter{err: errors.New("ice cream")},
 			expectedErr: `ice cream (recovered by wazero)
 wasm stack trace:
-	env.trace(i32,i32,f64,f64,f64,f64,f64)`,
+	env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`,
 		},
 	}
 
@@ -370,31 +431,38 @@ wasm stack trace:
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			r := wazero.NewRuntime()
-			defer r.Close(testCtx)
+			var log bytes.Buffer
 
-			_, err := r.NewModuleBuilder("env").
-				ExportFunctions(NewFunctionExporter().WithTraceToStdout().ExportFunctions()).
-				Instantiate(testCtx, r)
+			// Set context to one that has an experimental listener
+			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
+
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+			defer r.Close(ctx)
+
+			envBuilder := r.NewModuleBuilder("env")
+			NewFunctionExporter().WithTraceToStdout().ExportFunctions(envBuilder)
+			_, err := envBuilder.Instantiate(ctx, r)
 			require.NoError(t, err)
 
 			traceWasm, err := watzero.Wat2Wasm(traceWat)
 			require.NoError(t, err)
 
-			compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
+			compiled, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
 			require.NoError(t, err)
 
 			exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
-			mod, err := r.InstantiateModule(testCtx, compiled, exporter)
+			mod, err := r.InstantiateModule(ctx, compiled, exporter)
 			require.NoError(t, err)
 
-			ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
+			ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(tc.message)))
 			require.True(t, ok)
-			ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
+			ok = mod.Memory().Write(ctx, uint32(4), tc.message)
 			require.True(t, ok)
 
-			_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0)
+			_, err = mod.ExportedFunction("trace").Call(ctx, 4, 0, 0, 0, 0, 0, 0)
 			require.EqualError(t, err, tc.expectedErr)
+			require.Equal(t, `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
+`, log.String())
 		})
 	}
 }
@@ -402,45 +470,45 @@ wasm stack trace:
 func Test_readAssemblyScriptString(t *testing.T) {
 	tests := []struct {
 		name                  string
-		memory                func(api.Memory)
+		memory                func(context.Context, api.Memory)
 		offset                int
 		expected, expectedErr string
 	}{
 		{
 			name: "success",
-			memory: func(memory api.Memory) {
-				memory.WriteUint32Le(testCtx, 0, 10)
+			memory: func(ctx context.Context, memory api.Memory) {
+				memory.WriteUint32Le(ctx, 0, 10)
 				b := encodeUTF16("hello")
-				memory.Write(testCtx, 4, b)
+				memory.Write(ctx, 4, b)
 			},
 			offset:   4,
 			expected: "hello",
 		},
 		{
 			name: "can't read size",
-			memory: func(memory api.Memory) {
+			memory: func(ctx context.Context, memory api.Memory) {
 				b := encodeUTF16("hello")
-				memory.Write(testCtx, 0, b)
+				memory.Write(ctx, 0, b)
 			},
 			offset:      0, // will attempt to read size from offset -4
 			expectedErr: "Memory.ReadUint32Le(4294967292) out of range",
 		},
 		{
 			name: "odd size",
-			memory: func(memory api.Memory) {
-				memory.WriteUint32Le(testCtx, 0, 9)
+			memory: func(ctx context.Context, memory api.Memory) {
+				memory.WriteUint32Le(ctx, 0, 9)
 				b := encodeUTF16("hello")
-				memory.Write(testCtx, 4, b)
+				memory.Write(ctx, 4, b)
 			},
 			offset:      4,
 			expectedErr: "read an odd number of bytes for utf-16 string: 9",
 		},
 		{
 			name: "can't read string",
-			memory: func(memory api.Memory) {
-				memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value
+			memory: func(ctx context.Context, memory api.Memory) {
+				memory.WriteUint32Le(ctx, 0, 10_000_000) // set size to too large value
 				b := encodeUTF16("hello")
-				memory.Write(testCtx, 4, b)
+				memory.Write(ctx, 4, b)
 			},
 			offset:      4,
 			expectedErr: "Memory.Read(4, 10000000) out of range",
@@ -459,7 +527,7 @@ func Test_readAssemblyScriptString(t *testing.T) {
 				Instantiate(testCtx, r)
 			require.NoError(t, err)
 
-			tc.memory(mod.Memory())
+			tc.memory(testCtx, mod.Memory())
 
 			s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset))
 			if tc.expectedErr != "" {
@@ -472,20 +540,20 @@ func Test_readAssemblyScriptString(t *testing.T) {
 	}
 }
 
-func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
+func writeAbortMessageAndFileName(ctx context.Context, t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
 	off := 0
-	ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16)))
+	ok := mem.WriteUint32Le(ctx, uint32(off), uint32(len(messageUTF16)))
 	require.True(t, ok)
 	off += 4
 	messageOff := off
-	ok = mem.Write(testCtx, uint32(off), messageUTF16)
+	ok = mem.Write(ctx, uint32(off), messageUTF16)
 	require.True(t, ok)
 	off += len(messageUTF16)
-	ok = mem.WriteUint32Le(testCtx, uint32(off), uint32(len(fileNameUTF16)))
+	ok = mem.WriteUint32Le(ctx, uint32(off), uint32(len(fileNameUTF16)))
 	require.True(t, ok)
 	off += 4
 	filenameOff := off
-	ok = mem.Write(testCtx, uint32(off), fileNameUTF16)
+	ok = mem.Write(ctx, uint32(off), fileNameUTF16)
 	require.True(t, ok)
 	return messageOff, filenameOff
 }
diff --git a/builder_test.go b/builder_test.go
index a6b03151..3dbd8646 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -63,6 +63,26 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "ExportFunction with names",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportFunction("1", uint32_uint32, "get", "x")
+			},
+			expected: &wasm.Module{
+				TypeSection: []*wasm.FunctionType{
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+				},
+				FunctionSection:     []wasm.Index{0},
+				HostFunctionSection: []*reflect.Value{&fnUint32_uint32},
+				ExportSection: []*wasm.Export{
+					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
+				},
+				NameSection: &wasm.NameSection{
+					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
+					LocalNames:    []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
+				},
+			},
+		},
 		{
 			name: "ExportFunction overwrites existing",
 			input: func(r Runtime) ModuleBuilder {
diff --git a/experimental/experimental_test.go b/experimental/experimental_test.go
index 8081544d..2b89d07e 100644
--- a/experimental/experimental_test.go
+++ b/experimental/experimental_test.go
@@ -1,3 +1,8 @@
 // Package experimental_test includes examples for experimental features. When these complete, they'll end up as real
 // examples in the /examples directory.
 package experimental_test
+
+import "context"
+
+// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
+var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go
deleted file mode 100644
index d5af3cee..00000000
--- a/experimental/listener_example_test.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package experimental_test
-
-import (
-	"context"
-	_ "embed"
-	"fmt"
-	"io"
-	"log"
-	"os"
-
-	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/experimental"
-	"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
-)
-
-// listenerWasm was generated by the following:
-//	cd testdata; wat2wasm --debug-names listener.wat
-//go:embed testdata/listener.wasm
-var listenerWasm []byte
-
-// compile-time check to ensure loggerFactory implements experimental.FunctionListenerFactory.
-var _ experimental.FunctionListenerFactory = &loggerFactory{}
-
-// loggerFactory implements experimental.FunctionListenerFactory to log all function calls to the console.
-type loggerFactory struct{}
-
-// NewListener implements the same method as documented on experimental.FunctionListener.
-func (f *loggerFactory) NewListener(moduleName string, fnd api.FunctionDefinition) experimental.FunctionListener {
-	return &logger{funcName: []byte(moduleName + "." + funcName(fnd))}
-}
-
-// nestLevelKey holds state between logger.Before and logger.After to ensure call depth is reflected.
-type nestLevelKey struct{}
-
-// logger implements experimental.FunctionListener to log entrance and exit of each function call.
-type logger struct{ funcName []byte }
-
-// Before logs to stdout the module and function name, prefixed with '>>' and indented based on the call nesting level.
-func (l *logger) Before(ctx context.Context, _ []uint64) context.Context {
-	nestLevel, _ := ctx.Value(nestLevelKey{}).(int)
-
-	l.writeIndented(os.Stdout, true, nestLevel+1)
-
-	// Increase the next nesting level.
-	return context.WithValue(ctx, nestLevelKey{}, nestLevel+1)
-}
-
-// After logs to stdout the module and function name, prefixed with '<<' and indented based on the call nesting level.
-func (l *logger) After(ctx context.Context, _ error, _ []uint64) {
-	// Note: We use the nest level directly even though it is the "next" nesting level.
-	// This works because our indent of zero nesting is one tab.
-	l.writeIndented(os.Stdout, false, ctx.Value(nestLevelKey{}).(int))
-}
-
-// funcName returns the name in priority order: first export name, module-defined name, numeric index.
-func funcName(fnd api.FunctionDefinition) string {
-	if len(fnd.ExportNames()) > 0 {
-		return fnd.ExportNames()[0]
-	}
-	if fnd.Name() != "" {
-		return fnd.Name()
-	}
-	return fmt.Sprintf("[%d]", fnd.Index())
-}
-
-// This is a very basic integration of listener. The main goal is to show how it is configured.
-func Example_listener() {
-	// Set context to one that has an experimental listener
-	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, &loggerFactory{})
-
-	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
-	defer r.Close(ctx) // This closes everything this Runtime created.
-
-	if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
-		log.Panicln(err)
-	}
-
-	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
-	if err != nil {
-		log.Panicln(err)
-	}
-
-	mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout))
-	if err != nil {
-		log.Panicln(err)
-	}
-
-	_, err = mod.ExportedFunction("rand").Call(ctx, 4)
-	if err != nil {
-		log.Panicln(err)
-	}
-
-	// We should see the same function called twice: directly and indirectly.
-
-	// Output:
-	//>>	listener.rand
-	//>>		wasi_snapshot_preview1.random_get
-	//<<		wasi_snapshot_preview1.random_get
-	//>>		wasi_snapshot_preview1.random_get
-	//<<		wasi_snapshot_preview1.random_get
-	//<<	listener.rand
-}
-
-// writeIndented writes an indented message like this: ">>\t\t\t$indentLevel$funcName\n"
-func (l *logger) writeIndented(writer io.Writer, before bool, indentLevel int) {
-	var message = make([]byte, 0, 2+indentLevel+len(l.funcName)+1)
-	if before {
-		message = append(message, '>', '>')
-	} else { // after
-		message = append(message, '<', '<')
-	}
-
-	for i := 0; i < indentLevel; i++ {
-		message = append(message, '\t')
-	}
-	message = append(message, l.funcName...)
-	message = append(message, '\n')
-	_, _ = writer.Write(message)
-}
diff --git a/experimental/listener_test.go b/experimental/listener_test.go
new file mode 100644
index 00000000..ba85d745
--- /dev/null
+++ b/experimental/listener_test.go
@@ -0,0 +1,73 @@
+package experimental_test
+
+import (
+	"context"
+	_ "embed"
+	"testing"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/api"
+	. "github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+	"github.com/tetratelabs/wazero/internal/wasm/binary"
+)
+
+// compile-time check to ensure recorder implements FunctionListenerFactory
+var _ FunctionListenerFactory = &recorder{}
+
+type recorder struct {
+	m map[string]struct{}
+}
+
+func (r *recorder) NewListener(definition api.FunctionDefinition) FunctionListener {
+	r.m[definition.Name()] = struct{}{}
+	return nil
+}
+
+func TestFunctionListenerFactory(t *testing.T) {
+	// Set context to one that has an experimental listener
+	factory := &recorder{map[string]struct{}{}}
+	ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, factory)
+
+	// Define a module with two functions
+	bin := binary.EncodeModule(&wasm.Module{
+		TypeSection:     []*wasm.FunctionType{{}},
+		ImportSection:   []*wasm.Import{{}},
+		FunctionSection: []wasm.Index{0, 0},
+		CodeSection: []*wasm.Code{
+			{Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}},
+		},
+		NameSection: &wasm.NameSection{
+			ModuleName: "test",
+			FunctionNames: wasm.NameMap{
+				{Index: 0, Name: "import"}, // should skip
+				{Index: 1, Name: "fn1"},
+				{Index: 2, Name: "fn2"},
+			},
+		},
+	})
+
+	if platform.CompilerSupported() {
+		t.Run("fails on compile if compiler", func(t *testing.T) {
+			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigCompiler())
+			defer r.Close(testCtx) // This closes everything this Runtime created.
+			_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+			require.EqualError(t, err,
+				"context includes a FunctionListenerFactoryKey, which is only supported in the interpreter")
+		})
+	}
+
+	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+	defer r.Close(ctx) // This closes everything this Runtime created.
+
+	_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+	require.NoError(t, err)
+
+	// Ensure each function was converted to a listener eagerly
+	require.Equal(t, map[string]struct{}{
+		"fn1": {},
+		"fn2": {},
+	}, factory.m)
+}
diff --git a/experimental/log_listener_example_test.go b/experimental/log_listener_example_test.go
new file mode 100644
index 00000000..9a848418
--- /dev/null
+++ b/experimental/log_listener_example_test.go
@@ -0,0 +1,56 @@
+package experimental_test
+
+import (
+	"context"
+	_ "embed"
+	"log"
+	"os"
+
+	"github.com/tetratelabs/wazero"
+	. "github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
+)
+
+// listenerWasm was generated by the following:
+//	cd testdata; wat2wasm --debug-names listener.wat
+//go:embed testdata/listener.wasm
+var listenerWasm []byte
+
+// This is a very basic integration of listener. The main goal is to show how it is configured.
+func Example_newLoggingListenerFactory() {
+	// Set context to one that has an experimental listener
+	ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, NewLoggingListenerFactory(os.Stdout))
+
+	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
+	defer r.Close(ctx) // This closes everything this Runtime created.
+
+	if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
+		log.Panicln(err)
+	}
+
+	// Compile the WebAssembly module using the default configuration.
+	code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout))
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	_, err = mod.ExportedFunction("rand").Call(ctx, 4)
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	// We should see the same function called twice: directly and indirectly.
+
+	// Output:
+	//--> listener.rand(len=4)
+	//	==> wasi_snapshot_preview1.random_get(buf=4,buf_len=4)
+	//	<== ESUCCESS
+	//	==> wasi_snapshot_preview1.random_get(buf=8,buf_len=4)
+	//	<== ESUCCESS
+	//<-- ()
+}
diff --git a/experimental/log_listener_test.go b/experimental/log_listener_test.go
new file mode 100644
index 00000000..d1ef1244
--- /dev/null
+++ b/experimental/log_listener_test.go
@@ -0,0 +1,336 @@
+package experimental_test
+
+import (
+	"bytes"
+	"io"
+	"math"
+	"reflect"
+	"testing"
+
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+	"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
+)
+
+func Test_loggingListener(t *testing.T) {
+	wasiFuncName := "random_get"
+	wasiFuncType := &wasm.FunctionType{
+		Params:  []api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
+		Results: []api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
+	}
+	wasiParamNames := []string{"buf", "buf_len"}
+	wasiParams := []uint64{0, 8}
+
+	tests := []struct {
+		name                 string
+		moduleName, funcName string
+		functype             *wasm.FunctionType
+		isHostFunc           bool
+		paramNames           []string
+		params, results      []uint64
+		err                  error
+		expected             string
+	}{
+		{
+			name:     "v_v",
+			functype: &wasm.FunctionType{},
+			expected: `--> test.fn()
+<-- ()
+`,
+		},
+		{
+			name:     "error",
+			functype: &wasm.FunctionType{},
+			err:      io.EOF,
+			expected: `--> test.fn()
+<-- error: EOF
+`,
+		},
+		{
+			name:       "host",
+			functype:   &wasm.FunctionType{},
+			isHostFunc: true,
+			expected: `==> test.fn()
+<== ()
+`,
+		},
+		{
+			name:       "wasi",
+			functype:   wasiFuncType,
+			moduleName: wasi_snapshot_preview1.ModuleName,
+			funcName:   wasiFuncName,
+			paramNames: wasiParamNames,
+			isHostFunc: true,
+			params:     wasiParams,
+			results:    []uint64{uint64(wasi_snapshot_preview1.ErrnoSuccess)},
+			expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
+<== ESUCCESS
+`,
+		},
+		{
+			name:       "wasi errno",
+			functype:   wasiFuncType,
+			moduleName: wasi_snapshot_preview1.ModuleName,
+			funcName:   wasiFuncName,
+			paramNames: wasiParamNames,
+			isHostFunc: true,
+			params:     wasiParams,
+			results:    []uint64{uint64(wasi_snapshot_preview1.ErrnoFault)},
+			expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
+<== EFAULT
+`,
+		},
+		{
+			name:       "wasi error",
+			functype:   wasiFuncType,
+			moduleName: wasi_snapshot_preview1.ModuleName,
+			funcName:   wasiFuncName,
+			paramNames: wasiParamNames,
+			isHostFunc: true,
+			params:     wasiParams,
+			err:        io.EOF, // not possible as we coerce errors to numbers, but test anyway!
+			expected: `==> wasi_snapshot_preview1.random_get(buf=0,buf_len=8)
+<== error: EOF
+`,
+		},
+		{
+			name:     "i32",
+			functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI32}},
+			params:   []uint64{math.MaxUint32},
+			expected: `--> test.fn(4294967295)
+<-- ()
+`,
+		},
+		{
+			name:       "i32 named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI32}},
+			params:     []uint64{math.MaxUint32},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=4294967295)
+<-- ()
+`,
+		},
+		{
+			name:     "i64",
+			functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI64}},
+			params:   []uint64{math.MaxUint64},
+			expected: `--> test.fn(18446744073709551615)
+<-- ()
+`,
+		},
+		{
+			name:       "i64 named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeI64}},
+			params:     []uint64{math.MaxUint64},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=18446744073709551615)
+<-- ()
+`,
+		},
+		{
+			name:     "f32",
+			functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF32}},
+			params:   []uint64{api.EncodeF32(math.MaxFloat32)},
+			expected: `--> test.fn(3.4028235e+38)
+<-- ()
+`,
+		},
+		{
+			name:       "f32 named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF32}},
+			params:     []uint64{api.EncodeF32(math.MaxFloat32)},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=3.4028235e+38)
+<-- ()
+`,
+		},
+		{
+			name:     "f64",
+			functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF64}},
+			params:   []uint64{api.EncodeF64(math.MaxFloat64)},
+			expected: `--> test.fn(1.7976931348623157e+308)
+<-- ()
+`,
+		},
+		{
+			name:       "f64 named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeF64}},
+			params:     []uint64{api.EncodeF64(math.MaxFloat64)},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=1.7976931348623157e+308)
+<-- ()
+`,
+		},
+		{
+			name:     "externref",
+			functype: &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeExternref}},
+			params:   []uint64{0},
+			expected: `--> test.fn(0000000000000000)
+<-- ()
+`,
+		},
+		{
+			name:       "externref named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{api.ValueTypeExternref}},
+			params:     []uint64{0},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=0000000000000000)
+<-- ()
+`,
+		},
+		{
+			name:     "v128",
+			functype: &wasm.FunctionType{Params: []api.ValueType{0x7b}},
+			params:   []uint64{0, 1},
+			expected: `--> test.fn(00000000000000000000000000000001)
+<-- ()
+`,
+		},
+		{
+			name:       "v128 named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{0x7b}},
+			params:     []uint64{0, 1},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=00000000000000000000000000000001)
+<-- ()
+`,
+		},
+		{
+			name:     "funcref",
+			functype: &wasm.FunctionType{Params: []api.ValueType{0x70}},
+			params:   []uint64{0},
+			expected: `--> test.fn(0000000000000000)
+<-- ()
+`,
+		},
+		{
+			name:       "funcref named",
+			functype:   &wasm.FunctionType{Params: []api.ValueType{0x70}},
+			params:     []uint64{0},
+			paramNames: []string{"x"},
+			expected: `--> test.fn(x=0000000000000000)
+<-- ()
+`,
+		},
+		{
+			name:     "no params, one result",
+			functype: &wasm.FunctionType{Results: []api.ValueType{api.ValueTypeI32}},
+			results:  []uint64{math.MaxUint32},
+			expected: `--> test.fn()
+<-- (4294967295)
+`,
+		},
+		{
+			name: "one param, one result",
+			functype: &wasm.FunctionType{
+				Params:  []api.ValueType{api.ValueTypeI32},
+				Results: []api.ValueType{api.ValueTypeF32},
+			},
+			params:  []uint64{math.MaxUint32},
+			results: []uint64{api.EncodeF32(math.MaxFloat32)},
+			expected: `--> test.fn(4294967295)
+<-- (3.4028235e+38)
+`,
+		},
+		{
+			name: "two params, two results",
+			functype: &wasm.FunctionType{
+				Params:  []api.ValueType{api.ValueTypeI32, api.ValueTypeI64},
+				Results: []api.ValueType{api.ValueTypeF32, api.ValueTypeF64},
+			},
+			params:  []uint64{math.MaxUint32, math.MaxUint64},
+			results: []uint64{api.EncodeF32(math.MaxFloat32), api.EncodeF64(math.MaxFloat64)},
+			expected: `--> test.fn(4294967295,18446744073709551615)
+<-- (3.4028235e+38,1.7976931348623157e+308)
+`,
+		},
+		{
+			name: "two params, two results named",
+			functype: &wasm.FunctionType{
+				Params:  []api.ValueType{api.ValueTypeI32, api.ValueTypeI64},
+				Results: []api.ValueType{api.ValueTypeF32, api.ValueTypeF64},
+			},
+			params:     []uint64{math.MaxUint32, math.MaxUint64},
+			paramNames: []string{"x", "y"},
+			results:    []uint64{api.EncodeF32(math.MaxFloat32), api.EncodeF64(math.MaxFloat64)},
+			expected: `--> test.fn(x=4294967295,y=18446744073709551615)
+<-- (3.4028235e+38,1.7976931348623157e+308)
+`,
+		},
+	}
+
+	out := bytes.NewBuffer(nil)
+	lf := experimental.NewLoggingListenerFactory(out)
+	fnV := reflect.ValueOf(func() {})
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.moduleName == "" {
+				tc.moduleName = "test"
+			}
+			if tc.funcName == "" {
+				tc.funcName = "fn"
+			}
+			m := &wasm.Module{
+				TypeSection:     []*wasm.FunctionType{tc.functype},
+				FunctionSection: []wasm.Index{0},
+				NameSection: &wasm.NameSection{
+					ModuleName:    tc.moduleName,
+					FunctionNames: wasm.NameMap{{Name: tc.funcName}},
+					LocalNames:    wasm.IndirectNameMap{{NameMap: toNameMap(tc.paramNames)}},
+				},
+			}
+
+			if tc.isHostFunc {
+				m.HostFunctionSection = []*reflect.Value{&fnV}
+			}
+			m.BuildFunctionDefinitions()
+			l := lf.NewListener(m.FunctionDefinitionSection[0])
+
+			out.Reset()
+			ctx := l.Before(testCtx, tc.params)
+			l.After(ctx, tc.err, tc.results)
+			require.Equal(t, tc.expected, out.String())
+		})
+	}
+}
+
+func toNameMap(names []string) wasm.NameMap {
+	if len(names) == 0 {
+		return nil
+	}
+	var ret wasm.NameMap
+	for i, n := range names {
+		ret = append(ret, &wasm.NameAssoc{Index: wasm.Index(i), Name: n})
+	}
+	return ret
+}
+
+func Test_loggingListener_indentation(t *testing.T) {
+	out := bytes.NewBuffer(nil)
+	lf := experimental.NewLoggingListenerFactory(out)
+	m := &wasm.Module{
+		TypeSection:     []*wasm.FunctionType{{}},
+		FunctionSection: []wasm.Index{0, 0},
+		NameSection: &wasm.NameSection{
+			ModuleName:    "test",
+			FunctionNames: wasm.NameMap{{Index: 0, Name: "fn1"}, {Index: 1, Name: "fn2"}},
+		},
+	}
+	m.BuildFunctionDefinitions()
+	l1 := lf.NewListener(m.FunctionDefinitionSection[0])
+	l2 := lf.NewListener(m.FunctionDefinitionSection[1])
+
+	ctx := l1.Before(testCtx, []uint64{})
+	ctx1 := l2.Before(ctx, []uint64{})
+	l2.After(ctx1, nil, []uint64{})
+	l1.After(ctx, nil, []uint64{})
+	require.Equal(t, `--> test.fn1()
+	--> test.fn2()
+	<-- ()
+<-- ()
+`, out.String())
+
+}
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 6ebf1776..0a5ce5b2 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -197,7 +197,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 		// Trigger relocation of goroutine stack because at this point we have the majority of
 		// goroutine stack unused after recursive call.
 		runtime.GC()
-	}}, map[string]*wasm.Memory{}, map[string]*wasm.Global{}, enabledFeatures)
+	}}, nil, map[string]*wasm.Memory{}, map[string]*wasm.Global{}, enabledFeatures)
 	require.NoError(t, err)
 
 	err = s.Engine.CompileModule(testCtx, hm)
diff --git a/internal/wasm/function_definition_test.go b/internal/wasm/function_definition_test.go
index 5a97a71a..408893ff 100644
--- a/internal/wasm/function_definition_test.go
+++ b/internal/wasm/function_definition_test.go
@@ -1,6 +1,7 @@
 package wasm
 
 import (
+	"reflect"
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
@@ -9,6 +10,7 @@ import (
 
 func TestModule_BuildFunctionDefinitions(t *testing.T) {
 	nopCode := &Code{nil, []byte{OpcodeEnd}}
+	fnV := reflect.ValueOf(func() {})
 	tests := []struct {
 		name                             string
 		m                                *Module
@@ -26,6 +28,22 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 				GlobalSection: []*Global{{}},
 			},
 		},
+		{
+			name: "host func",
+			m: &Module{
+				TypeSection:         []*FunctionType{v_v},
+				FunctionSection:     []Index{0},
+				HostFunctionSection: []*reflect.Value{&fnV},
+			},
+			expected: []*FunctionDefinition{
+				{
+					index:          0,
+					debugName:      ".$0",
+					isHostFunction: true,
+					funcType:       v_v,
+				},
+			},
+		},
 		{
 			name: "without imports",
 			m: &Module{
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index 5ee672a3..7f3c5e34 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -181,13 +181,7 @@ func TestNewHostModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := NewHostModule(
-				tc.moduleName,
-				tc.nameToGoFunc,
-				tc.nameToMemory,
-				tc.nameToGlobal,
-				Features20191205|FeatureMultiValue,
-			)
+			m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205|FeatureMultiValue)
 			require.NoError(t, e)
 			requireHostModuleEquals(t, tc.expected, m)
 		})
@@ -227,19 +221,19 @@ func TestNewHostModule_Errors(t *testing.T) {
 		{
 			name:         "not a function",
 			nameToGoFunc: map[string]interface{}{"fn": t},
-			expectedErr:  "func[fn] kind != func: ptr",
+			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",
+			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",
+			expectedErr:  "func[.fn] exports the same name as a memory",
 		},
 		{
 			name:         "multiple memories",
@@ -255,7 +249,7 @@ func TestNewHostModule_Errors(t *testing.T) {
 			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",
+			expectedErr:  "func[.fn] exports the same name as a global",
 		},
 	}
 
@@ -263,7 +257,7 @@ func TestNewHostModule_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal, Features20191205)
+			_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, nil, tc.nameToMemory, tc.nameToGlobal, Features20191205)
 			require.EqualError(t, e, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 9cea4868..8d73f4ee 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -91,13 +91,7 @@ func TestModuleInstance_Memory(t *testing.T) {
 
 func TestStore_Instantiate(t *testing.T) {
 	s, ns := newStore()
-	m, err := NewHostModule(
-		"",
-		map[string]interface{}{"fn": func(api.Module) {}},
-		map[string]*Memory{},
-		map[string]*Global{},
-		Features20191205,
-	)
+	m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
 	require.NoError(t, err)
 
 	sysCtx := sys.DefaultContext(nil)
@@ -175,13 +169,7 @@ func TestStore_CloseWithExitCode(t *testing.T) {
 func TestStore_hammer(t *testing.T) {
 	const importedModuleName = "imported"
 
-	m, err := NewHostModule(
-		importedModuleName,
-		map[string]interface{}{"fn": func(api.Module) {}},
-		map[string]*Memory{},
-		map[string]*Global{},
-		Features20191205,
-	)
+	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
 	require.NoError(t, err)
 
 	s, ns := newStore()
@@ -235,13 +223,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 	const importedModuleName = "imported"
 	const importingModuleName = "test"
 
-	m, err := NewHostModule(
-		importedModuleName,
-		map[string]interface{}{"fn": func(api.Module) {}},
-		map[string]*Memory{},
-		map[string]*Global{},
-		Features20191205,
-	)
+	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
 	require.NoError(t, err)
 
 	t.Run("Fails if module name already in use", func(t *testing.T) {
@@ -332,13 +314,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 }
 
 func TestCallContext_ExportedFunction(t *testing.T) {
-	host, err := NewHostModule(
-		"host",
-		map[string]interface{}{"host_fn": func(api.Module) {}},
-		map[string]*Memory{},
-		map[string]*Global{},
-		Features20191205,
-	)
+	host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, nil, map[string]*Memory{}, map[string]*Global{}, Features20191205)
 	require.NoError(t, err)
 
 	s, ns := newStore()