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}