debug.go

  1// Package wasmdebug contains utilities used to give consistent search keys between stack traces and error messages.
  2// Note: This is named wasmdebug to avoid conflicts with the normal go module.
  3// Note: This only imports "api" as importing "wasm" would create a cyclic dependency.
  4package wasmdebug
  5
  6import (
  7	"fmt"
  8	"runtime"
  9	"runtime/debug"
 10	"strconv"
 11	"strings"
 12
 13	"github.com/tetratelabs/wazero/api"
 14	"github.com/tetratelabs/wazero/internal/wasmruntime"
 15	"github.com/tetratelabs/wazero/sys"
 16)
 17
 18// FuncName returns the naming convention of "moduleName.funcName".
 19//
 20//   - moduleName is the possibly empty name the module was instantiated with.
 21//   - funcName is the name in the Custom Name section.
 22//   - funcIdx is the position in the function index, prefixed with
 23//     imported functions.
 24//
 25// Note: "moduleName.$funcIdx" is used when the funcName is empty, as commonly
 26// the case in TinyGo.
 27func FuncName(moduleName, funcName string, funcIdx uint32) string {
 28	var ret strings.Builder
 29
 30	// Start module.function
 31	ret.WriteString(moduleName)
 32	ret.WriteByte('.')
 33	if funcName == "" {
 34		ret.WriteByte('$')
 35		ret.WriteString(strconv.Itoa(int(funcIdx)))
 36	} else {
 37		ret.WriteString(funcName)
 38	}
 39
 40	return ret.String()
 41}
 42
 43// signature returns a formatted signature similar to how it is defined in Go.
 44//
 45// * paramTypes should be from wasm.FunctionType
 46// * resultTypes should be from wasm.FunctionType
 47// TODO: add paramNames
 48func signature(funcName string, paramTypes []api.ValueType, resultTypes []api.ValueType) string {
 49	var ret strings.Builder
 50	ret.WriteString(funcName)
 51
 52	// Start params
 53	ret.WriteByte('(')
 54	paramCount := len(paramTypes)
 55	switch paramCount {
 56	case 0:
 57	case 1:
 58		ret.WriteString(api.ValueTypeName(paramTypes[0]))
 59	default:
 60		ret.WriteString(api.ValueTypeName(paramTypes[0]))
 61		for _, vt := range paramTypes[1:] {
 62			ret.WriteByte(',')
 63			ret.WriteString(api.ValueTypeName(vt))
 64		}
 65	}
 66	ret.WriteByte(')')
 67
 68	// Start results
 69	resultCount := len(resultTypes)
 70	switch resultCount {
 71	case 0:
 72	case 1:
 73		ret.WriteByte(' ')
 74		ret.WriteString(api.ValueTypeName(resultTypes[0]))
 75	default: // As this is used for errors, don't panic if there are multiple returns, even if that's invalid!
 76		ret.WriteByte(' ')
 77		ret.WriteByte('(')
 78		ret.WriteString(api.ValueTypeName(resultTypes[0]))
 79		for _, vt := range resultTypes[1:] {
 80			ret.WriteByte(',')
 81			ret.WriteString(api.ValueTypeName(vt))
 82		}
 83		ret.WriteByte(')')
 84	}
 85
 86	return ret.String()
 87}
 88
 89// ErrorBuilder helps build consistent errors, particularly adding a WASM stack trace.
 90//
 91// AddFrame should be called beginning at the frame that panicked until no more frames exist. Once done, call Format.
 92type ErrorBuilder interface {
 93	// AddFrame adds the next frame.
 94	//
 95	// * funcName should be from FuncName
 96	// * paramTypes should be from wasm.FunctionType
 97	// * resultTypes should be from wasm.FunctionType
 98	// * sources is the source code information for this frame and can be empty.
 99	//
100	// Note: paramTypes and resultTypes are present because signature misunderstanding, mismatch or overflow are common.
101	AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sources []string)
102
103	// FromRecovered returns an error with the wasm stack trace appended to it.
104	FromRecovered(recovered interface{}) error
105}
106
107func NewErrorBuilder() ErrorBuilder {
108	return &stackTrace{}
109}
110
111type stackTrace struct {
112	// frameCount is the number of stack frame currently pushed into lines.
113	frameCount int
114	// lines contains the stack trace and possibly the inlined source code information.
115	lines []string
116}
117
118// GoRuntimeErrorTracePrefix is the prefix coming before the Go runtime stack trace included in the face of runtime.Error.
119// This is exported for testing purpose.
120const GoRuntimeErrorTracePrefix = "Go runtime stack trace:"
121
122func (s *stackTrace) FromRecovered(recovered interface{}) error {
123	if false {
124		debug.PrintStack()
125	}
126
127	if exitErr, ok := recovered.(*sys.ExitError); ok { // Don't wrap an exit error!
128		return exitErr
129	}
130
131	stack := strings.Join(s.lines, "\n\t")
132
133	// If the error was internal, don't mention it was recovered.
134	if wasmErr, ok := recovered.(*wasmruntime.Error); ok {
135		return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", wasmErr, stack)
136	}
137
138	// If we have a runtime.Error, something severe happened which should include the stack trace. This could be
139	// a nil pointer from wazero or a user-defined function from HostModuleBuilder.
140	if runtimeErr, ok := recovered.(runtime.Error); ok {
141		return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s\n\n%s\n%s",
142			runtimeErr, stack, GoRuntimeErrorTracePrefix, debug.Stack())
143	}
144
145	// At this point we expect the error was from a function defined by HostModuleBuilder that intentionally called panic.
146	if runtimeErr, ok := recovered.(error); ok { // e.g. panic(errors.New("whoops"))
147		return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack)
148	} else { // e.g. panic("whoops")
149		return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", recovered, stack)
150	}
151}
152
153// MaxFrames is the maximum number of frames to include in the stack trace.
154const MaxFrames = 30
155
156// AddFrame implements ErrorBuilder.AddFrame
157func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sources []string) {
158	if s.frameCount == MaxFrames {
159		return
160	}
161	s.frameCount++
162	sig := signature(funcName, paramTypes, resultTypes)
163	s.lines = append(s.lines, sig)
164	for _, source := range sources {
165		s.lines = append(s.lines, "\t"+source)
166	}
167	if s.frameCount == MaxFrames {
168		s.lines = append(s.lines, "... maybe followed by omitted frames")
169	}
170}