wa-lang.org/wazero@v1.0.2/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 "wa-lang.org/wazero/api" 14 "wa-lang.org/wazero/internal/wasmruntime" 15 "wa-lang.org/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 namespace, 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 // 99 // Note: paramTypes and resultTypes are present because signature misunderstanding, mismatch or overflow are common. 100 AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) 101 102 // FromRecovered returns an error with the wasm stack trace appended to it. 103 FromRecovered(recovered interface{}) error 104 } 105 106 func NewErrorBuilder() ErrorBuilder { 107 return &stackTrace{} 108 } 109 110 type stackTrace struct { 111 frames []string 112 } 113 114 func (s *stackTrace) FromRecovered(recovered interface{}) error { 115 if false { 116 debug.PrintStack() 117 } 118 119 if exitErr, ok := recovered.(*sys.ExitError); ok { // Don't wrap an exit error! 120 return exitErr 121 } 122 123 stack := strings.Join(s.frames, "\n\t") 124 125 // If the error was internal, don't mention it was recovered. 126 if wasmErr, ok := recovered.(*wasmruntime.Error); ok { 127 return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", wasmErr, stack) 128 } 129 130 // If we have a runtime.Error, something severe happened which should include the stack trace. This could be 131 // a nil pointer from wazero or a user-defined function from HostModuleBuilder. 132 if runtimeErr, ok := recovered.(runtime.Error); ok { 133 // TODO: consider adding debug.Stack(), but last time we attempted, some tests became unstable. 134 return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack) 135 } 136 137 // At this point we expect the error was from a function defined by HostModuleBuilder that intentionally called panic. 138 if runtimeErr, ok := recovered.(error); ok { // e.g. panic(errors.New("whoops")) 139 return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack) 140 } else { // e.g. panic("whoops") 141 return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", recovered, stack) 142 } 143 } 144 145 // AddFrame implements ErrorBuilder.Format 146 func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) { 147 // Format as best as we can, considering we don't yet have source and line numbers, 148 // TODO: include DWARF symbols. See #58 149 s.frames = append(s.frames, signature(funcName, paramTypes, resultTypes)) 150 }