// Package wasmdebug contains utilities used to give consistent search keys between stack traces and error messages. // Note: This is named wasmdebug to avoid conflicts with the normal go module. // Note: This only imports "api" as importing "wasm" would create a cyclic dependency.
package wasmdebug import ( ) // FuncName returns the naming convention of "moduleName.funcName". // // - moduleName is the possibly empty name the module was instantiated with. // - funcName is the name in the Custom Name section. // - funcIdx is the position in the function index, prefixed with // imported functions. // // Note: "moduleName.$funcIdx" is used when the funcName is empty, as commonly // the case in TinyGo. func (, string, uint32) string { var strings.Builder // Start module.function .WriteString() .WriteByte('.') if == "" { .WriteByte('$') .WriteString(strconv.Itoa(int())) } else { .WriteString() } return .String() } // signature returns a formatted signature similar to how it is defined in Go. // // * paramTypes should be from wasm.FunctionType // * resultTypes should be from wasm.FunctionType // TODO: add paramNames func signature( string, []api.ValueType, []api.ValueType) string { var strings.Builder .WriteString() // Start params .WriteByte('(') := len() switch { case 0: case 1: .WriteString(api.ValueTypeName([0])) default: .WriteString(api.ValueTypeName([0])) for , := range [1:] { .WriteByte(',') .WriteString(api.ValueTypeName()) } } .WriteByte(')') // Start results := len() switch { case 0: case 1: .WriteByte(' ') .WriteString(api.ValueTypeName([0])) default: // As this is used for errors, don't panic if there are multiple returns, even if that's invalid! .WriteByte(' ') .WriteByte('(') .WriteString(api.ValueTypeName([0])) for , := range [1:] { .WriteByte(',') .WriteString(api.ValueTypeName()) } .WriteByte(')') } return .String() } // ErrorBuilder helps build consistent errors, particularly adding a WASM stack trace. // // AddFrame should be called beginning at the frame that panicked until no more frames exist. Once done, call Format. type ErrorBuilder interface { // AddFrame adds the next frame. // // * funcName should be from FuncName // * paramTypes should be from wasm.FunctionType // * resultTypes should be from wasm.FunctionType // * sources is the source code information for this frame and can be empty. // // Note: paramTypes and resultTypes are present because signature misunderstanding, mismatch or overflow are common. AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sources []string) // FromRecovered returns an error with the wasm stack trace appended to it. FromRecovered(recovered interface{}) error } func () ErrorBuilder { return &stackTrace{} } type stackTrace struct { // frameCount is the number of stack frame currently pushed into lines. frameCount int // lines contains the stack trace and possibly the inlined source code information. lines []string } // GoRuntimeErrorTracePrefix is the prefix coming before the Go runtime stack trace included in the face of runtime.Error. // This is exported for testing purpose. const GoRuntimeErrorTracePrefix = "Go runtime stack trace:" func ( *stackTrace) ( interface{}) error { if false { debug.PrintStack() } if , := .(*sys.ExitError); { // Don't wrap an exit error! return } := strings.Join(.lines, "\n\t") // If the error was internal, don't mention it was recovered. if , := .(*wasmruntime.Error); { return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", , ) } // If we have a runtime.Error, something severe happened which should include the stack trace. This could be // a nil pointer from wazero or a user-defined function from HostModuleBuilder. if , := .(runtime.Error); { return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s\n\n%s\n%s", , , GoRuntimeErrorTracePrefix, debug.Stack()) } // At this point we expect the error was from a function defined by HostModuleBuilder that intentionally called panic. if , := .(error); { // e.g. panic(errors.New("whoops")) return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", , ) } else { // e.g. panic("whoops") return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", , ) } } // MaxFrames is the maximum number of frames to include in the stack trace. const MaxFrames = 30 // AddFrame implements ErrorBuilder.AddFrame func ( *stackTrace) ( string, , []api.ValueType, []string) { if .frameCount == MaxFrames { return } .frameCount++ := signature(, , ) .lines = append(.lines, ) for , := range { .lines = append(.lines, "\t"+) } if .frameCount == MaxFrames { .lines = append(.lines, "... maybe followed by omitted frames") } }