You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
5.5 KiB
Go
171 lines
5.5 KiB
Go
// 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 (
|
|
"fmt"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/wasmruntime"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
// 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 FuncName(moduleName, funcName string, funcIdx uint32) string {
|
|
var ret strings.Builder
|
|
|
|
// Start module.function
|
|
ret.WriteString(moduleName)
|
|
ret.WriteByte('.')
|
|
if funcName == "" {
|
|
ret.WriteByte('$')
|
|
ret.WriteString(strconv.Itoa(int(funcIdx)))
|
|
} else {
|
|
ret.WriteString(funcName)
|
|
}
|
|
|
|
return ret.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(funcName string, paramTypes []api.ValueType, resultTypes []api.ValueType) string {
|
|
var ret strings.Builder
|
|
ret.WriteString(funcName)
|
|
|
|
// Start params
|
|
ret.WriteByte('(')
|
|
paramCount := len(paramTypes)
|
|
switch paramCount {
|
|
case 0:
|
|
case 1:
|
|
ret.WriteString(api.ValueTypeName(paramTypes[0]))
|
|
default:
|
|
ret.WriteString(api.ValueTypeName(paramTypes[0]))
|
|
for _, vt := range paramTypes[1:] {
|
|
ret.WriteByte(',')
|
|
ret.WriteString(api.ValueTypeName(vt))
|
|
}
|
|
}
|
|
ret.WriteByte(')')
|
|
|
|
// Start results
|
|
resultCount := len(resultTypes)
|
|
switch resultCount {
|
|
case 0:
|
|
case 1:
|
|
ret.WriteByte(' ')
|
|
ret.WriteString(api.ValueTypeName(resultTypes[0]))
|
|
default: // As this is used for errors, don't panic if there are multiple returns, even if that's invalid!
|
|
ret.WriteByte(' ')
|
|
ret.WriteByte('(')
|
|
ret.WriteString(api.ValueTypeName(resultTypes[0]))
|
|
for _, vt := range resultTypes[1:] {
|
|
ret.WriteByte(',')
|
|
ret.WriteString(api.ValueTypeName(vt))
|
|
}
|
|
ret.WriteByte(')')
|
|
}
|
|
|
|
return ret.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 NewErrorBuilder() 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 (s *stackTrace) FromRecovered(recovered interface{}) error {
|
|
if false {
|
|
debug.PrintStack()
|
|
}
|
|
|
|
if exitErr, ok := recovered.(*sys.ExitError); ok { // Don't wrap an exit error!
|
|
return exitErr
|
|
}
|
|
|
|
stack := strings.Join(s.lines, "\n\t")
|
|
|
|
// If the error was internal, don't mention it was recovered.
|
|
if wasmErr, ok := recovered.(*wasmruntime.Error); ok {
|
|
return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", wasmErr, stack)
|
|
}
|
|
|
|
// 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 runtimeErr, ok := recovered.(runtime.Error); ok {
|
|
return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s\n\n%s\n%s",
|
|
runtimeErr, stack, GoRuntimeErrorTracePrefix, debug.Stack())
|
|
}
|
|
|
|
// At this point we expect the error was from a function defined by HostModuleBuilder that intentionally called panic.
|
|
if runtimeErr, ok := recovered.(error); ok { // e.g. panic(errors.New("whoops"))
|
|
return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack)
|
|
} else { // e.g. panic("whoops")
|
|
return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", recovered, stack)
|
|
}
|
|
}
|
|
|
|
// MaxFrames is the maximum number of frames to include in the stack trace.
|
|
const MaxFrames = 30
|
|
|
|
// AddFrame implements ErrorBuilder.AddFrame
|
|
func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sources []string) {
|
|
if s.frameCount == MaxFrames {
|
|
return
|
|
}
|
|
s.frameCount++
|
|
sig := signature(funcName, paramTypes, resultTypes)
|
|
s.lines = append(s.lines, sig)
|
|
for _, source := range sources {
|
|
s.lines = append(s.lines, "\t"+source)
|
|
}
|
|
if s.frameCount == MaxFrames {
|
|
s.lines = append(s.lines, "... maybe followed by omitted frames")
|
|
}
|
|
}
|