github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/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/wasilibs/wazerox/api" 14 "github.com/wasilibs/wazerox/internal/wasmruntime" 15 "github.com/wasilibs/wazerox/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 frames []string 113 } 114 115 // GoRuntimeErrorTracePrefix is the prefix coming before the Go runtime stack trace included in the face of runtime.Error. 116 // This is exported for testing purpose. 117 const GoRuntimeErrorTracePrefix = "Go runtime stack trace:" 118 119 func (s *stackTrace) FromRecovered(recovered interface{}) error { 120 if false { 121 debug.PrintStack() 122 } 123 124 if exitErr, ok := recovered.(*sys.ExitError); ok { // Don't wrap an exit error! 125 return exitErr 126 } 127 128 stack := strings.Join(s.frames, "\n\t") 129 130 // If the error was internal, don't mention it was recovered. 131 if wasmErr, ok := recovered.(*wasmruntime.Error); ok { 132 return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", wasmErr, stack) 133 } 134 135 // If we have a runtime.Error, something severe happened which should include the stack trace. This could be 136 // a nil pointer from wazero or a user-defined function from HostModuleBuilder. 137 if runtimeErr, ok := recovered.(runtime.Error); ok { 138 return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s\n\n%s\n%s", 139 runtimeErr, stack, GoRuntimeErrorTracePrefix, debug.Stack()) 140 } 141 142 // At this point we expect the error was from a function defined by HostModuleBuilder that intentionally called panic. 143 if runtimeErr, ok := recovered.(error); ok { // e.g. panic(errors.New("whoops")) 144 return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack) 145 } else { // e.g. panic("whoops") 146 return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", recovered, stack) 147 } 148 } 149 150 // AddFrame implements ErrorBuilder.AddFrame 151 func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sources []string) { 152 sig := signature(funcName, paramTypes, resultTypes) 153 s.frames = append(s.frames, sig) 154 for _, source := range sources { 155 s.frames = append(s.frames, "\t"+source) 156 } 157 }