Repository: tetratelabs/wazero
Pull request #963 (2022-12-27 02:17:51)

gojs: extracts parameter names into a pseudo name section
`GOARCH=wasm GOOS=js` defines parameter names in go source, and they are indirectly related to the wasm parameter "sp". This creates a pseudo name section so that we can access the parameter names. The alternative would be adding a hack to normal FunctionDefinition, only used for gojs.



Patch:
diff --git a/internal/gojs/argsenv.go b/internal/gojs/argsenv.go
index 6ab58bb3..4be4bf7c 100644
--- a/internal/gojs/argsenv.go
+++ b/internal/gojs/argsenv.go
@@ -59,7 +59,10 @@ func WriteArgsAndEnviron(mod api.Module) (argc, argv uint32, err error) {
 		err = errors.New("total length of command line and environment variables exceeds limit")
 	}
 
-	buf := util.MustRead(mem, "argvPtrs", argv, stop)
+	buf, ok := mem.Read(argv, stop)
+	if !ok {
+		panic("out of memory reading argvPtrs")
+	}
 	pos := uint32(0)
 	for _, ptr := range argvPtrs {
 		le.PutUint64(buf[pos:], uint64(ptr))
diff --git a/internal/gojs/custom/names.go b/internal/gojs/custom/names.go
new file mode 100644
index 00000000..e61e13af
--- /dev/null
+++ b/internal/gojs/custom/names.go
@@ -0,0 +1,186 @@
+// Package custom is similar to the WebAssembly Custom Sections. These are
+// needed because `GOARCH=wasm GOOS=js` functions aren't defined naturally
+// in WebAssembly. For example, every function has a single parameter "sp",
+// which implicitly maps to stack parameters in this package.
+package custom
+
+const (
+	// NamePadding is a marker for a parameter which has no purpose, except
+	// padding. It should not be logged.
+	NamePadding = "padding"
+)
+
+type Names struct {
+	// Name is the WebAssembly function name.
+	Name string
+
+	// ParamNames are the parameters read in 8-byte strides from the stack
+	// pointer (SP). This may be nil or include NamePadding.
+	ParamNames []string
+
+	// ResultNames are the results written in 8-byte strides from the stack
+	// pointer (SP), after ParamNames.
+	ResultNames []string
+}
+
+const NameDebug = "debug"
+
+const (
+	NameRuntimeWasmExit             = "runtime.wasmExit"
+	NameRuntimeWasmWrite            = "runtime.wasmWrite"
+	NameRuntimeResetMemoryDataView  = "runtime.resetMemoryDataView"
+	NameRuntimeNanotime1            = "runtime.nanotime1"
+	NameRuntimeWalltime             = "runtime.walltime"
+	NameRuntimeScheduleTimeoutEvent = "runtime.scheduleTimeoutEvent" // TODO: trigger usage
+	NameRuntimeClearTimeoutEvent    = "runtime.clearTimeoutEvent"    // TODO: trigger usage
+	NameRuntimeGetRandomData        = "runtime.getRandomData"
+)
+
+const (
+	NameSyscallFinalizeRef        = "syscall/js.finalizeRef"
+	NameSyscallStringVal          = "syscall/js.stringVal"
+	NameSyscallValueGet           = "syscall/js.valueGet"
+	NameSyscallValueSet           = "syscall/js.valueSet"
+	NameSyscallValueDelete        = "syscall/js.valueDelete" // stubbed
+	NameSyscallValueIndex         = "syscall/js.valueIndex"
+	NameSyscallValueSetIndex      = "syscall/js.valueSetIndex" // stubbed
+	NameSyscallValueCall          = "syscall/js.valueCall"
+	NameSyscallValueInvoke        = "syscall/js.valueInvoke" // stubbed
+	NameSyscallValueNew           = "syscall/js.valueNew"
+	NameSyscallValueLength        = "syscall/js.valueLength"
+	NameSyscallValuePrepareString = "syscall/js.valuePrepareString"
+	NameSyscallValueLoadString    = "syscall/js.valueLoadString"
+	NameSyscallValueInstanceOf    = "syscall/js.valueInstanceOf" // stubbed
+	NameSyscallCopyBytesToGo      = "syscall/js.copyBytesToGo"
+	NameSyscallCopyBytesToJS      = "syscall/js.copyBytesToJS"
+)
+
+var NameSection = map[string]*Names{
+	NameDebug: {
+		Name:        NameDebug,
+		ParamNames:  []string{},
+		ResultNames: []string{},
+	},
+
+	NameRuntimeWasmExit: {
+		Name:        NameRuntimeWasmExit,
+		ParamNames:  []string{"code"},
+		ResultNames: []string{},
+	},
+	NameRuntimeWasmWrite: {
+		Name:        NameRuntimeWasmWrite,
+		ParamNames:  []string{"fd", "p", "p_len"},
+		ResultNames: []string{},
+	},
+	NameRuntimeResetMemoryDataView: {
+		Name:        NameRuntimeResetMemoryDataView,
+		ParamNames:  []string{},
+		ResultNames: []string{},
+	},
+	NameRuntimeNanotime1: {
+		Name:        NameRuntimeNanotime1,
+		ParamNames:  []string{},
+		ResultNames: []string{"nsec"},
+	},
+	NameRuntimeWalltime: {
+		Name:        NameRuntimeWalltime,
+		ParamNames:  []string{},
+		ResultNames: []string{"sec", "nsec"},
+	},
+	NameRuntimeScheduleTimeoutEvent: {
+		Name:        NameRuntimeScheduleTimeoutEvent,
+		ParamNames:  []string{"ms"},
+		ResultNames: []string{"id"},
+	},
+	NameRuntimeClearTimeoutEvent: {
+		Name:        NameRuntimeClearTimeoutEvent,
+		ParamNames:  []string{"id"},
+		ResultNames: []string{},
+	},
+	NameRuntimeGetRandomData: {
+		Name:        NameRuntimeGetRandomData,
+		ParamNames:  []string{"r", "r_len"},
+		ResultNames: []string{},
+	},
+
+	NameSyscallFinalizeRef: {
+		Name:        NameSyscallFinalizeRef,
+		ParamNames:  []string{"r"},
+		ResultNames: []string{},
+	},
+	NameSyscallStringVal: {
+		Name:        NameSyscallStringVal,
+		ParamNames:  []string{"x", "x_len"},
+		ResultNames: []string{"r"},
+	},
+	NameSyscallValueGet: {
+		Name:        NameSyscallValueGet,
+		ParamNames:  []string{"v", "p", "p_len"},
+		ResultNames: []string{"r"},
+	},
+	NameSyscallValueSet: {
+		Name:        NameSyscallValueSet,
+		ParamNames:  []string{"v", "p", "p_len", "x"},
+		ResultNames: []string{},
+	},
+	NameSyscallValueDelete: {
+		Name:        NameSyscallValueDelete,
+		ParamNames:  []string{"v", "p", "p_len"},
+		ResultNames: []string{},
+	},
+	NameSyscallValueIndex: {
+		Name:        NameSyscallValueIndex,
+		ParamNames:  []string{"v", "i"},
+		ResultNames: []string{"r"},
+	},
+	NameSyscallValueSetIndex: {
+		Name:        NameSyscallValueSetIndex,
+		ParamNames:  []string{"v", "i", "x"},
+		ResultNames: []string{},
+	},
+	NameSyscallValueCall: {
+		Name:        NameSyscallValueCall,
+		ParamNames:  []string{"v", "m", "m_len", "args", "args_len", NamePadding},
+		ResultNames: []string{"res", "ok"},
+	},
+	NameSyscallValueInvoke: {
+		Name:        NameSyscallValueInvoke,
+		ParamNames:  []string{"v", "args", "args_len", NamePadding},
+		ResultNames: []string{"res", "ok"},
+	},
+	NameSyscallValueNew: {
+		Name:        NameSyscallValueNew,
+		ParamNames:  []string{"v", "args", "args_len", NamePadding},
+		ResultNames: []string{"res", "ok"},
+	},
+	NameSyscallValueLength: {
+		Name:        NameSyscallValueLength,
+		ParamNames:  []string{"v"},
+		ResultNames: []string{"len"},
+	},
+	NameSyscallValuePrepareString: {
+		Name:        NameSyscallValuePrepareString,
+		ParamNames:  []string{"v"},
+		ResultNames: []string{"str", "length"},
+	},
+	NameSyscallValueLoadString: {
+		Name:        NameSyscallValueLoadString,
+		ParamNames:  []string{"v", "b", "b_len"},
+		ResultNames: []string{},
+	},
+	NameSyscallValueInstanceOf: {
+		Name:        NameSyscallValueInstanceOf,
+		ParamNames:  []string{"v", "t"},
+		ResultNames: []string{"ok"},
+	},
+	NameSyscallCopyBytesToGo: {
+		Name:        NameSyscallCopyBytesToGo,
+		ParamNames:  []string{"dst", "dst_len", NamePadding, "src"},
+		ResultNames: []string{"n", "ok"},
+	},
+	NameSyscallCopyBytesToJS: {
+		Name:        NameSyscallCopyBytesToJS,
+		ParamNames:  []string{"dst", "src", "src_len", NamePadding},
+		ResultNames: []string{"n", "ok"},
+	},
+}
diff --git a/internal/gojs/goarch/wasm.go b/internal/gojs/goarch/wasm.go
index a79ace52..9b023237 100644
--- a/internal/gojs/goarch/wasm.go
+++ b/internal/gojs/goarch/wasm.go
@@ -5,9 +5,9 @@ package goarch
 import (
 	"context"
 	"encoding/binary"
-	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/gojs/util"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -37,8 +37,10 @@ func NoopFunction(name string) *wasm.HostFunc {
 var le = binary.LittleEndian
 
 type Stack interface {
+	// Name is the function name being invoked.
+	Name() string
+
 	Param(i int) uint64
-	ParamName(i int) string
 
 	// ParamBytes reads a byte slice, given its memory offset and length (stack
 	// positions i, i+1)
@@ -56,8 +58,6 @@ type Stack interface {
 	// can trigger a Go event handler.
 	Refresh(api.Module)
 
-	ResultName(i int) string
-
 	SetResult(i int, v uint64)
 
 	SetResultBool(i int, v bool)
@@ -69,17 +69,24 @@ type Stack interface {
 	SetResultUint32(i int, v uint32)
 }
 
-func NewStack(mem api.Memory, sp uint32, paramNames, resultNames []string) Stack {
-	s := &stack{paramNames: paramNames, resultNames: resultNames}
+func NewStack(name string, mem api.Memory, sp uint32) Stack {
+	names := custom.NameSection[name]
+	s := &stack{name: name, paramCount: len(names.ParamNames), resultCount: len(names.ResultNames)}
 	s.refresh(mem, sp)
 	return s
 }
 
 type stack struct {
-	paramNames, resultNames []string
+	name                    string
+	paramCount, resultCount int
 	buf                     []byte
 }
 
+// Name implements Stack.Name
+func (s *stack) Name() string {
+	return s.name
+}
+
 // Param implements Stack.Param
 func (s *stack) Param(i int) (res uint64) {
 	pos := i << 3
@@ -87,16 +94,11 @@ func (s *stack) Param(i int) (res uint64) {
 	return
 }
 
-// ParamName implements Stack.ParamName
-func (s *stack) ParamName(i int) string {
-	return s.paramNames[i]
-}
-
 // ParamBytes implements Stack.ParamBytes
 func (s *stack) ParamBytes(mem api.Memory, i int) (res []byte) {
 	offset := s.ParamUint32(i)
 	byteCount := s.ParamUint32(i + 1)
-	return mustRead(mem, s.paramNames[i], offset, byteCount)
+	return util.MustRead(mem, s.name, i, offset, byteCount)
 }
 
 // ParamString implements Stack.ParamString
@@ -115,21 +117,20 @@ func (s *stack) Refresh(mod api.Module) {
 }
 
 func (s *stack) refresh(mem api.Memory, sp uint32) {
-	count := uint32(len(s.paramNames) + len(s.resultNames))
-	s.buf = mustRead(mem, "sp", sp+8, count<<3)
+	count := uint32(s.paramCount + s.resultCount)
+	buf, ok := mem.Read(sp+8, count<<3)
+	if !ok {
+		panic("out of memory reading stack")
+	}
+	s.buf = buf
 }
 
 // SetResult implements Stack.SetResult
 func (s *stack) SetResult(i int, v uint64) {
-	pos := (len(s.paramNames) + i) << 3
+	pos := (s.paramCount + i) << 3
 	le.PutUint64(s.buf[pos:], v)
 }
 
-// ResultName implements Stack.ResultName
-func (s *stack) ResultName(i int) string {
-	return s.resultNames[i]
-}
-
 // SetResultBool implements Stack.SetResultBool
 func (s *stack) SetResultBool(i int, v bool) {
 	if v {
@@ -164,29 +165,19 @@ func getSP(mod api.Module) uint32 {
 	return uint32(mod.(*wasm.CallContext).GlobalVal(0))
 }
 
-// mustRead is like api.Memory except that it panics if the offset and
-// byteCount are out of range.
-func mustRead(mem api.Memory, fieldName string, offset, byteCount uint32) []byte {
-	buf, ok := mem.Read(offset, byteCount)
-	if !ok {
-		panic(fmt.Errorf("out of memory reading %s", fieldName))
-	}
-	return buf
-}
-
-func NewFunc(name string, goFunc Func, paramNames, resultNames []string) *wasm.HostFunc {
-	return util.NewFunc(name, (&stackFunc{f: goFunc, paramNames: paramNames, resultNames: resultNames}).Call)
+func NewFunc(name string, goFunc Func) *wasm.HostFunc {
+	return util.NewFunc(name, (&stackFunc{name: name, f: goFunc}).Call)
 }
 
 type Func func(context.Context, api.Module, Stack)
 
 type stackFunc struct {
-	f                       Func
-	paramNames, resultNames []string
+	name string
+	f    Func
 }
 
 // Call implements the same method as defined on api.GoModuleFunction.
 func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
-	s := NewStack(mod.Memory(), uint32(wasmStack[0]), f.paramNames, f.resultNames)
+	s := NewStack(f.name, mod.Memory(), uint32(wasmStack[0]))
 	f.f(ctx, mod, s)
 }
diff --git a/internal/gojs/goos/js.go b/internal/gojs/goos/js.go
index 62e98e7b..893c0ec5 100644
--- a/internal/gojs/goos/js.go
+++ b/internal/gojs/goos/js.go
@@ -95,16 +95,16 @@ type stack struct {
 	s goarch.Stack
 }
 
+// Name implements the same method as documented on goarch.Stack
+func (s *stack) Name() string {
+	return s.s.Name()
+}
+
 // Param implements the same method as documented on goarch.Stack
 func (s *stack) Param(i int) uint64 {
 	return s.s.Param(i)
 }
 
-// ParamName implements the same method as documented on goarch.Stack
-func (s *stack) ParamName(i int) string {
-	return s.s.ParamName(i)
-}
-
 // ParamBytes implements the same method as documented on goarch.Stack
 func (s *stack) ParamBytes(mem api.Memory, i int) []byte {
 	return s.s.ParamBytes(mem, i)
@@ -123,7 +123,7 @@ func (s *stack) ParamRefs(mem api.Memory, i int) []Ref {
 
 	result := make([]Ref, 0, size)
 
-	buf := util.MustRead(mem, s.s.ParamName(i), offset, byteCount)
+	buf := util.MustRead(mem, s.Name(), i, offset, byteCount)
 	for pos := uint32(0); pos < byteCount; pos += 8 {
 		ref := Ref(le.Uint64(buf[pos:]))
 		result = append(result, ref)
@@ -155,7 +155,7 @@ func (s *stack) ParamVals(ctx context.Context, mem api.Memory, i int, loader Val
 
 	result := make([]interface{}, 0, size)
 
-	buf := util.MustRead(mem, s.s.ParamName(i), offset, byteCount)
+	buf := util.MustRead(mem, s.Name(), i, offset, byteCount)
 	for pos := uint32(0); pos < byteCount; pos += 8 {
 		ref := Ref(le.Uint64(buf[pos:]))
 		result = append(result, loader(ctx, ref))
@@ -168,11 +168,6 @@ func (s *stack) Refresh(mod api.Module) {
 	s.s.Refresh(mod)
 }
 
-// ResultName implements the same method as documented on goarch.Stack
-func (s *stack) ResultName(i int) string {
-	return s.s.ResultName(i)
-}
-
 // SetResult implements the same method as documented on goarch.Stack
 func (s *stack) SetResult(i int, v uint64) {
 	s.s.SetResult(i, v)
@@ -203,19 +198,19 @@ func (s *stack) SetResultUint32(i int, v uint32) {
 	s.s.SetResultUint32(i, v)
 }
 
-func NewFunc(name string, goFunc Func, paramNames, resultNames []string) *wasm.HostFunc {
-	return util.NewFunc(name, (&stackFunc{f: goFunc, paramNames: paramNames, resultNames: resultNames}).Call)
+func NewFunc(name string, goFunc Func) *wasm.HostFunc {
+	return util.NewFunc(name, (&stackFunc{name: name, f: goFunc}).Call)
 }
 
 type Func func(context.Context, api.Module, Stack)
 
 type stackFunc struct {
-	f                       Func
-	paramNames, resultNames []string
+	name string
+	f    Func
 }
 
 // Call implements the same method as defined on api.GoModuleFunction.
 func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
-	s := &stack{goarch.NewStack(mod.Memory(), uint32(wasmStack[0]), f.paramNames, f.resultNames)}
+	s := &stack{goarch.NewStack(f.name, mod.Memory(), uint32(wasmStack[0]))}
 	f.f(ctx, mod, s)
 }
diff --git a/internal/gojs/runtime.go b/internal/gojs/runtime.go
index 87e762ee..da432641 100644
--- a/internal/gojs/runtime.go
+++ b/internal/gojs/runtime.go
@@ -5,33 +5,20 @@ import (
 	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/gojs/goarch"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-const (
-	wasmExitName             = "runtime.wasmExit"
-	wasmWriteName            = "runtime.wasmWrite"
-	resetMemoryDataViewName  = "runtime.resetMemoryDataView"
-	nanotime1Name            = "runtime.nanotime1"
-	walltimeName             = "runtime.walltime"
-	scheduleTimeoutEventName = "runtime.scheduleTimeoutEvent" // TODO: trigger usage
-	clearTimeoutEventName    = "runtime.clearTimeoutEvent"    // TODO: trigger usage
-	getRandomDataName        = "runtime.getRandomData"
-)
-
 // Debug has unknown use, so stubbed.
 //
 // See https://github.com/golang/go/blob/go1.19/src/cmd/link/internal/wasm/asm.go#L133-L138
-var Debug = goarch.StubFunction("debug")
+var Debug = goarch.StubFunction(custom.NameDebug)
 
 // WasmExit implements runtime.wasmExit which supports runtime.exit.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.go#L28
-var WasmExit = goarch.NewFunc(wasmExitName, wasmExit,
-	[]string{"code"},
-	[]string{},
-)
+var WasmExit = goarch.NewFunc(custom.NameRuntimeWasmExit, wasmExit)
 
 func wasmExit(ctx context.Context, mod api.Module, stack goarch.Stack) {
 	code := stack.ParamUint32(0)
@@ -44,10 +31,7 @@ func wasmExit(ctx context.Context, mod api.Module, stack goarch.Stack) {
 // runtime.writeErr. This implements `println`.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/os_js.go#L29
-var WasmWrite = goarch.NewFunc(wasmWriteName, wasmWrite,
-	[]string{"fd", "p", "p_len"},
-	[]string{},
-)
+var WasmWrite = goarch.NewFunc(custom.NameRuntimeWasmWrite, wasmWrite)
 
 func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
 	fd := stack.ParamUint32(0)
@@ -70,15 +54,12 @@ func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
 // See https://github.com/golang/go/blob/go1.19/src/runtime/mem_js.go#L82
 //
 // TODO: Compiler-based memory.grow callbacks are ignored until we have a generic solution #601
-var ResetMemoryDataView = goarch.NoopFunction(resetMemoryDataViewName)
+var ResetMemoryDataView = goarch.NoopFunction(custom.NameRuntimeResetMemoryDataView)
 
 // Nanotime1 implements runtime.nanotime which supports time.Since.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L184
-var Nanotime1 = goarch.NewFunc(nanotime1Name, nanotime1,
-	[]string{},
-	[]string{"nsec"},
-)
+var Nanotime1 = goarch.NewFunc(custom.NameRuntimeNanotime1, nanotime1)
 
 func nanotime1(_ context.Context, mod api.Module, stack goarch.Stack) {
 	nsec := mod.(*wasm.CallContext).Sys.Nanotime()
@@ -89,10 +70,7 @@ func nanotime1(_ context.Context, mod api.Module, stack goarch.Stack) {
 // Walltime implements runtime.walltime which supports time.Now.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L188
-var Walltime = goarch.NewFunc(walltimeName, walltime,
-	[]string{},
-	[]string{"sec", "nsec"},
-)
+var Walltime = goarch.NewFunc(custom.NameRuntimeWalltime, walltime)
 
 func walltime(_ context.Context, mod api.Module, stack goarch.Stack) {
 	sec, nsec := mod.(*wasm.CallContext).Sys.Walltime()
@@ -108,7 +86,7 @@ func walltime(_ context.Context, mod api.Module, stack goarch.Stack) {
 // goroutine and invokes code compiled into wasm "resume".
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L192
-var ScheduleTimeoutEvent = goarch.StubFunction(scheduleTimeoutEventName)
+var ScheduleTimeoutEvent = goarch.StubFunction(custom.NameRuntimeScheduleTimeoutEvent)
 
 // ^^ stubbed because signal handling is not implemented in GOOS=js
 
@@ -116,7 +94,7 @@ var ScheduleTimeoutEvent = goarch.StubFunction(scheduleTimeoutEventName)
 // runtime.notetsleepg used by runtime.signal_recv.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L196
-var ClearTimeoutEvent = goarch.StubFunction(clearTimeoutEventName)
+var ClearTimeoutEvent = goarch.StubFunction(custom.NameRuntimeClearTimeoutEvent)
 
 // ^^ stubbed because signal handling is not implemented in GOOS=js
 
@@ -124,10 +102,7 @@ var ClearTimeoutEvent = goarch.StubFunction(clearTimeoutEventName)
 // for runtime.fastrand.
 //
 // See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L200
-var GetRandomData = goarch.NewFunc(getRandomDataName, getRandomData,
-	[]string{"r", "r_len"},
-	[]string{},
-)
+var GetRandomData = goarch.NewFunc(custom.NameRuntimeGetRandomData, getRandomData)
 
 func getRandomData(_ context.Context, mod api.Module, stack goarch.Stack) {
 	r := stack.ParamBytes(mod.Memory(), 0 /*, 1 */)
diff --git a/internal/gojs/syscall.go b/internal/gojs/syscall.go
index 8e9d6d1c..4b590183 100644
--- a/internal/gojs/syscall.go
+++ b/internal/gojs/syscall.go
@@ -9,36 +9,18 @@ import (
 	"syscall"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/gojs/goarch"
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/sys"
 )
 
-const (
-	finalizeRefName        = "syscall/js.finalizeRef"
-	stringValName          = "syscall/js.stringVal"
-	valueGetName           = "syscall/js.valueGet"
-	valueSetName           = "syscall/js.valueSet"
-	valueDeleteName        = "syscall/js.valueDelete" // stubbed
-	valueIndexName         = "syscall/js.valueIndex"
-	valueSetIndexName      = "syscall/js.valueSetIndex" // stubbed
-	valueCallName          = "syscall/js.valueCall"
-	valueInvokeName        = "syscall/js.valueInvoke" // stubbed
-	valueNewName           = "syscall/js.valueNew"
-	valueLengthName        = "syscall/js.valueLength"
-	valuePrepareStringName = "syscall/js.valuePrepareString"
-	valueLoadStringName    = "syscall/js.valueLoadString"
-	valueInstanceOfName    = "syscall/js.valueInstanceOf" // stubbed
-	copyBytesToGoName      = "syscall/js.copyBytesToGo"
-	copyBytesToJSName      = "syscall/js.copyBytesToJS"
-)
-
 // FinalizeRef implements js.finalizeRef, which is used as a
 // runtime.SetFinalizer on the given reference.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L61
-var FinalizeRef = goos.NewFunc(finalizeRefName, finalizeRef, []string{"r"}, nil)
+var FinalizeRef = goos.NewFunc(custom.NameSyscallFinalizeRef, finalizeRef)
 
 func finalizeRef(ctx context.Context, _ api.Module, stack goos.Stack) {
 	r := stack.ParamRef(0)
@@ -53,7 +35,7 @@ func finalizeRef(ctx context.Context, _ api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L212
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L305-L308
-var StringVal = goos.NewFunc(stringValName, stringVal, []string{"x", "x_len"}, []string{"r"})
+var StringVal = goos.NewFunc(custom.NameSyscallStringVal, stringVal)
 
 func stringVal(ctx context.Context, mod api.Module, stack goos.Stack) {
 	x := stack.ParamString(mod.Memory(), 0)
@@ -69,10 +51,7 @@ func stringVal(ctx context.Context, mod api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L295
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L311-L316
-var ValueGet = goos.NewFunc(valueGetName, valueGet,
-	[]string{"v", "p", "p_len"},
-	[]string{"r"},
-)
+var ValueGet = goos.NewFunc(custom.NameSyscallValueGet, valueGet)
 
 func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -104,10 +83,7 @@ func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L309
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L318-L322
-var ValueSet = goos.NewFunc(valueSetName, valueSet,
-	[]string{"v", "p", "p_len", "x"},
-	[]string{},
-)
+var ValueSet = goos.NewFunc(custom.NameSyscallValueSet, valueSet)
 
 func valueSet(ctx context.Context, mod api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -138,7 +114,7 @@ func valueSet(ctx context.Context, mod api.Module, stack goos.Stack) {
 // ValueDelete is stubbed as it isn't used in Go's main source tree.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L321
-var ValueDelete = goarch.StubFunction(valueDeleteName)
+var ValueDelete = goarch.StubFunction(custom.NameSyscallValueDelete)
 
 // ValueIndex implements js.valueIndex, which is used to load a js.Value property
 // by index, e.g. `v.Index(0)`. Notably, this is used by js.handleEvent to read
@@ -146,10 +122,7 @@ var ValueDelete = goarch.StubFunction(valueDeleteName)
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L334
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L331-L334
-var ValueIndex = goos.NewFunc(valueIndexName, valueIndex,
-	[]string{"v", "i"},
-	[]string{"r"},
-)
+var ValueIndex = goos.NewFunc(custom.NameSyscallValueIndex, valueIndex)
 
 func valueIndex(ctx context.Context, _ api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -165,17 +138,14 @@ func valueIndex(ctx context.Context, _ api.Module, stack goos.Stack) {
 // []interface{}, which doesn't appear to occur in Go's source tree.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L348
-var ValueSetIndex = goarch.StubFunction(valueSetIndexName)
+var ValueSetIndex = goarch.StubFunction(custom.NameSyscallValueSetIndex)
 
 // ValueCall implements js.valueCall, which is used to call a js.Value function
 // by name, e.g. `document.Call("createElement", "div")`.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L394
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L343-L358
-var ValueCall = goos.NewFunc(valueCallName, valueCall,
-	[]string{"v", "m", "m_len", "args", "args_len", "padding"},
-	[]string{"res", "ok"},
-)
+var ValueCall = goos.NewFunc(custom.NameSyscallValueCall, valueCall)
 
 func valueCall(ctx context.Context, mod api.Module, stack goos.Stack) {
 	mem := mod.Memory()
@@ -207,17 +177,14 @@ func valueCall(ctx context.Context, mod api.Module, stack goos.Stack) {
 // ValueInvoke is stubbed as it isn't used in Go's main source tree.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L413
-var ValueInvoke = goarch.StubFunction(valueInvokeName)
+var ValueInvoke = goarch.StubFunction(custom.NameSyscallValueInvoke)
 
 // ValueNew implements js.valueNew, which is used to call a js.Value, e.g.
 // `array.New(2)`.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L432
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L380-L391
-var ValueNew = goos.NewFunc(valueNewName, valueNew,
-	[]string{"v", "args", "args_len", "padding"},
-	[]string{"res", "ok"},
-)
+var ValueNew = goos.NewFunc(custom.NameSyscallValueNew, valueNew)
 
 func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
 	mem := mod.Memory()
@@ -271,10 +238,7 @@ func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L372
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L396-L397
-var ValueLength = goos.NewFunc(valueLengthName, valueLength,
-	[]string{"v"},
-	[]string{"len"},
-)
+var ValueLength = goos.NewFunc(custom.NameSyscallValueLength, valueLength)
 
 func valueLength(ctx context.Context, _ api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -291,10 +255,7 @@ func valueLength(ctx context.Context, _ api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L531
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L402-L405
-var ValuePrepareString = goos.NewFunc(valuePrepareStringName, valuePrepareString,
-	[]string{"v"},
-	[]string{"str", "length"},
-)
+var ValuePrepareString = goos.NewFunc(custom.NameSyscallValuePrepareString, valuePrepareString)
 
 func valuePrepareString(ctx context.Context, _ api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -314,10 +275,7 @@ func valuePrepareString(ctx context.Context, _ api.Module, stack goos.Stack) {
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L533
 //
 //	https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L410-L412
-var ValueLoadString = goos.NewFunc(valueLoadStringName, valueLoadString,
-	[]string{"v", "b", "b_len"},
-	[]string{},
-)
+var ValueLoadString = goos.NewFunc(custom.NameSyscallValueLoadString, valueLoadString)
 
 func valueLoadString(ctx context.Context, mod api.Module, stack goos.Stack) {
 	v := stack.ParamVal(ctx, 0, loadValue)
@@ -330,7 +288,7 @@ func valueLoadString(ctx context.Context, mod api.Module, stack goos.Stack) {
 // ValueInstanceOf is stubbed as it isn't used in Go's main source tree.
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L543
-var ValueInstanceOf = goarch.StubFunction(valueInstanceOfName)
+var ValueInstanceOf = goarch.StubFunction(custom.NameSyscallValueInstanceOf)
 
 // CopyBytesToGo copies a JavaScript managed byte array to linear memory.
 // For example, this is used to read an HTTP response body.
@@ -342,10 +300,7 @@ var ValueInstanceOf = goarch.StubFunction(valueInstanceOfName)
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L569
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L424-L433
-var CopyBytesToGo = goos.NewFunc(copyBytesToGoName, copyBytesToGo,
-	[]string{"dst", "dst_len", "padding", "src"},
-	[]string{"n", "ok"},
-)
+var CopyBytesToGo = goos.NewFunc(custom.NameSyscallCopyBytesToGo, copyBytesToGo)
 
 func copyBytesToGo(ctx context.Context, mod api.Module, stack goos.Stack) {
 	dst := stack.ParamBytes(mod.Memory(), 0 /*, 1 */)
@@ -373,10 +328,7 @@ func copyBytesToGo(ctx context.Context, mod api.Module, stack goos.Stack) {
 //
 // See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L583
 // and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L438-L448
-var CopyBytesToJS = goos.NewFunc(copyBytesToJSName, copyBytesToJS,
-	[]string{"dst", "src", "src_len", "padding"},
-	[]string{"n", "ok"},
-)
+var CopyBytesToJS = goos.NewFunc(custom.NameSyscallCopyBytesToJS, copyBytesToJS)
 
 func copyBytesToJS(ctx context.Context, mod api.Module, stack goos.Stack) {
 	dst := stack.ParamVal(ctx, 0, loadValue)
diff --git a/internal/gojs/util/util.go b/internal/gojs/util/util.go
index e20de22a..d1a2f08a 100644
--- a/internal/gojs/util/util.go
+++ b/internal/gojs/util/util.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -17,12 +18,21 @@ func MustWrite(mem api.Memory, fieldName string, offset uint32, val []byte) {
 
 // MustRead is like api.Memory except that it panics if the offset and
 // byteCount are out of range.
-func MustRead(mem api.Memory, fieldName string, offset, byteCount uint32) []byte {
+func MustRead(mem api.Memory, funcName string, paramIdx int, offset, byteCount uint32) []byte {
 	buf, ok := mem.Read(offset, byteCount)
-	if !ok {
-		panic(fmt.Errorf("out of memory reading %s", fieldName))
+	if ok {
+		return buf
 	}
-	return buf
+	var paramName string
+	if names, ok := custom.NameSection[funcName]; ok {
+		if paramIdx < len(names.ParamNames) {
+			paramName = names.ParamNames[paramIdx]
+		}
+	}
+	if paramName == "" {
+		paramName = fmt.Sprintf("%s param[%d]", funcName, paramIdx)
+	}
+	panic(fmt.Errorf("out of memory reading %s", paramName))
 }
 
 func NewFunc(name string, goFunc api.GoModuleFunc) *wasm.HostFunc {


Test patch:
diff --git a/internal/gojs/util/util_test.go b/internal/gojs/util/util_test.go
new file mode 100644
index 00000000..e3027872
--- /dev/null
+++ b/internal/gojs/util/util_test.go
@@ -0,0 +1,75 @@
+package util
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestMustRead(t *testing.T) {
+	mem := &wasm.MemoryInstance{Buffer: []byte{1, 2, 3, 4, 5, 6, 7, 8}, Min: 1}
+
+	tests := []struct {
+		name              string
+		funcName          string
+		paramIdx          int
+		offset, byteCount uint32
+		expected          []byte
+		expectedPanic     string
+	}{
+		{
+			name: "read nothing",
+		},
+		{
+			name:      "read all",
+			offset:    0,
+			byteCount: 8,
+			expected:  []byte{1, 2, 3, 4, 5, 6, 7, 8},
+		},
+		{
+			name:      "read some",
+			offset:    4,
+			byteCount: 2,
+			expected:  []byte{5, 6},
+		},
+		{
+			name:          "read too many",
+			funcName:      custom.NameSyscallCopyBytesToGo,
+			offset:        4,
+			byteCount:     5,
+			expectedPanic: "out of memory reading dst",
+		},
+		{
+			name:          "read too many - function not in names",
+			funcName:      "not_in_names",
+			offset:        4,
+			byteCount:     5,
+			expectedPanic: "out of memory reading not_in_names param[0]",
+		},
+		{
+			name:          "read too many - in names, but no params",
+			funcName:      custom.NameDebug,
+			offset:        4,
+			byteCount:     5,
+			expectedPanic: "out of memory reading debug param[0]",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.expectedPanic != "" {
+				err := require.CapturePanic(func() {
+					MustRead(mem, tc.funcName, tc.paramIdx, tc.offset, tc.byteCount)
+				})
+				require.EqualError(t, err, tc.expectedPanic)
+			} else {
+				buf := MustRead(mem, tc.funcName, tc.paramIdx, tc.offset, tc.byteCount)
+				require.Equal(t, tc.expected, buf)
+			}
+		})
+	}
+}