github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/wasmdebug/debug.go (about) 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. 4 package wasmdebug 5 6 import ( 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. 27 func 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 48 func 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. 92 type 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 107 func NewErrorBuilder() ErrorBuilder { 108 return &stackTrace{} 109 } 110 111 type 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. 120 const GoRuntimeErrorTracePrefix = "Go runtime stack trace:" 121 122 func (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. 154 const MaxFrames = 30 155 156 // AddFrame implements ErrorBuilder.AddFrame 157 func (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 }