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  }