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  }