github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/experimental/listener.go (about)

     1  package experimental
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/bananabytelabs/wazero/api"
     7  )
     8  
     9  // StackIterator allows iterating on each function of the call stack, starting
    10  // from the top. At least one call to Next() is required to start the iteration.
    11  //
    12  // Note: The iterator provides a view of the call stack at the time of
    13  // iteration. As a result, parameter values may be different than the ones their
    14  // function was called with.
    15  type StackIterator interface {
    16  	// Next moves the iterator to the next function in the stack. Returns
    17  	// false if it reached the bottom of the stack.
    18  	Next() bool
    19  	// Function describes the function called by the current frame.
    20  	Function() InternalFunction
    21  	// ProgramCounter returns the program counter associated with the
    22  	// function call.
    23  	ProgramCounter() ProgramCounter
    24  }
    25  
    26  // FunctionListenerFactoryKey is a context.Context Value key. Its associated value should be a FunctionListenerFactory.
    27  //
    28  // See https://github.com/bananabytelabs/wazero/issues/451
    29  type FunctionListenerFactoryKey struct{}
    30  
    31  // FunctionListenerFactory returns FunctionListeners to be notified when a
    32  // function is called.
    33  type FunctionListenerFactory interface {
    34  	// NewFunctionListener returns a FunctionListener for a defined function.
    35  	// If nil is returned, no listener will be notified.
    36  	NewFunctionListener(api.FunctionDefinition) FunctionListener
    37  	// ^^ A single instance can be returned to avoid instantiating a listener
    38  	// per function, especially as they may be thousands of functions. Shared
    39  	// listeners use their FunctionDefinition parameter to clarify.
    40  }
    41  
    42  // FunctionListener can be registered for any function via
    43  // FunctionListenerFactory to be notified when the function is called.
    44  type FunctionListener interface {
    45  	// Before is invoked before a function is called.
    46  	//
    47  	// There is always one corresponding call to After or Abort for each call to
    48  	// Before. This guarantee allows the listener to maintain an internal stack
    49  	// to perform correlations between the entry and exit of functions.
    50  	//
    51  	// # Params
    52  	//
    53  	//   - ctx: the context of the caller function which must be the same
    54  	//	   instance or parent of the result.
    55  	//   - mod: the calling module.
    56  	//   - def: the function definition.
    57  	//   - params:  api.ValueType encoded parameters.
    58  	//   - stackIterator: iterator on the call stack. At least one entry is
    59  	//     guaranteed (the called function), whose Args() will be equal to
    60  	//     params. The iterator will be reused between calls to Before.
    61  	//
    62  	// Note: api.Memory is meant for inspection, not modification.
    63  	// mod can be cast to InternalModule to read non-exported globals.
    64  	Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, stackIterator StackIterator)
    65  
    66  	// After is invoked after a function is called.
    67  	//
    68  	// # Params
    69  	//
    70  	//   - ctx: the context of the caller function.
    71  	//   - mod: the calling module.
    72  	//   - def: the function definition.
    73  	//   - results: api.ValueType encoded results.
    74  	//
    75  	// # Notes
    76  	//
    77  	//   - api.Memory is meant for inspection, not modification.
    78  	//   - This is not called when a host function panics, or a guest function traps.
    79  	//      See Abort for more details.
    80  	After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64)
    81  
    82  	// Abort is invoked when a function does not return due to a trap or panic.
    83  	//
    84  	// # Params
    85  	//
    86  	//   - ctx: the context of the caller function.
    87  	//   - mod: the calling module.
    88  	//   - def: the function definition.
    89  	//   - err: the error value representing the reason why the function aborted.
    90  	//
    91  	// # Notes
    92  	//
    93  	//   - api.Memory is meant for inspection, not modification.
    94  	Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error)
    95  }
    96  
    97  // FunctionListenerFunc is a function type implementing the FunctionListener
    98  // interface, making it possible to use regular functions and methods as
    99  // listeners of function invocation.
   100  //
   101  // The FunctionListener interface declares two methods (Before and After),
   102  // but this type invokes its value only when Before is called. It is best
   103  // suites for cases where the host does not need to perform correlation
   104  // between the start and end of the function call.
   105  type FunctionListenerFunc func(context.Context, api.Module, api.FunctionDefinition, []uint64, StackIterator)
   106  
   107  // Before satisfies the FunctionListener interface, calls f.
   108  func (f FunctionListenerFunc) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, stackIterator StackIterator) {
   109  	f(ctx, mod, def, params, stackIterator)
   110  }
   111  
   112  // After is declared to satisfy the FunctionListener interface, but it does
   113  // nothing.
   114  func (f FunctionListenerFunc) After(context.Context, api.Module, api.FunctionDefinition, []uint64) {
   115  }
   116  
   117  // Abort is declared to satisfy the FunctionListener interface, but it does
   118  // nothing.
   119  func (f FunctionListenerFunc) Abort(context.Context, api.Module, api.FunctionDefinition, error) {
   120  }
   121  
   122  // FunctionListenerFactoryFunc is a function type implementing the
   123  // FunctionListenerFactory interface, making it possible to use regular
   124  // functions and methods as factory of function listeners.
   125  type FunctionListenerFactoryFunc func(api.FunctionDefinition) FunctionListener
   126  
   127  // NewFunctionListener satisfies the FunctionListenerFactory interface, calls f.
   128  func (f FunctionListenerFactoryFunc) NewFunctionListener(def api.FunctionDefinition) FunctionListener {
   129  	return f(def)
   130  }
   131  
   132  // MultiFunctionListenerFactory constructs a FunctionListenerFactory which
   133  // combines the listeners created by each of the factories passed as arguments.
   134  //
   135  // This function is useful when multiple listeners need to be hooked to a module
   136  // because the propagation mechanism based on installing a listener factory in
   137  // the context.Context used when instantiating modules allows for a single
   138  // listener to be installed.
   139  //
   140  // The stack iterator passed to the Before method is reset so that each listener
   141  // can iterate the call stack independently without impacting the ability of
   142  // other listeners to do so.
   143  func MultiFunctionListenerFactory(factories ...FunctionListenerFactory) FunctionListenerFactory {
   144  	multi := make(multiFunctionListenerFactory, len(factories))
   145  	copy(multi, factories)
   146  	return multi
   147  }
   148  
   149  type multiFunctionListenerFactory []FunctionListenerFactory
   150  
   151  func (multi multiFunctionListenerFactory) NewFunctionListener(def api.FunctionDefinition) FunctionListener {
   152  	var lstns []FunctionListener
   153  	for _, factory := range multi {
   154  		if lstn := factory.NewFunctionListener(def); lstn != nil {
   155  			lstns = append(lstns, lstn)
   156  		}
   157  	}
   158  	switch len(lstns) {
   159  	case 0:
   160  		return nil
   161  	case 1:
   162  		return lstns[0]
   163  	default:
   164  		return &multiFunctionListener{lstns: lstns}
   165  	}
   166  }
   167  
   168  type multiFunctionListener struct {
   169  	lstns []FunctionListener
   170  	stack stackIterator
   171  }
   172  
   173  func (multi *multiFunctionListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, si StackIterator) {
   174  	multi.stack.base = si
   175  	for _, lstn := range multi.lstns {
   176  		multi.stack.index = -1
   177  		lstn.Before(ctx, mod, def, params, &multi.stack)
   178  	}
   179  }
   180  
   181  func (multi *multiFunctionListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) {
   182  	for _, lstn := range multi.lstns {
   183  		lstn.After(ctx, mod, def, results)
   184  	}
   185  }
   186  
   187  func (multi *multiFunctionListener) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error) {
   188  	for _, lstn := range multi.lstns {
   189  		lstn.Abort(ctx, mod, def, err)
   190  	}
   191  }
   192  
   193  type stackIterator struct {
   194  	base  StackIterator
   195  	index int
   196  	pcs   []uint64
   197  	fns   []InternalFunction
   198  }
   199  
   200  func (si *stackIterator) Next() bool {
   201  	if si.base != nil {
   202  		si.pcs = si.pcs[:0]
   203  		si.fns = si.fns[:0]
   204  
   205  		for si.base.Next() {
   206  			si.pcs = append(si.pcs, uint64(si.base.ProgramCounter()))
   207  			si.fns = append(si.fns, si.base.Function())
   208  		}
   209  
   210  		si.base = nil
   211  	}
   212  	si.index++
   213  	return si.index < len(si.pcs)
   214  }
   215  
   216  func (si *stackIterator) ProgramCounter() ProgramCounter {
   217  	return ProgramCounter(si.pcs[si.index])
   218  }
   219  
   220  func (si *stackIterator) Function() InternalFunction {
   221  	return si.fns[si.index]
   222  }
   223  
   224  // StackFrame represents a frame on the call stack.
   225  type StackFrame struct {
   226  	Function     api.Function
   227  	Params       []uint64
   228  	Results      []uint64
   229  	PC           uint64
   230  	SourceOffset uint64
   231  }
   232  
   233  type internalFunction struct {
   234  	definition   api.FunctionDefinition
   235  	sourceOffset uint64
   236  }
   237  
   238  func (f internalFunction) Definition() api.FunctionDefinition {
   239  	return f.definition
   240  }
   241  
   242  func (f internalFunction) SourceOffsetForPC(pc ProgramCounter) uint64 {
   243  	return f.sourceOffset
   244  }
   245  
   246  // stackFrameIterator is an implementation of the experimental.stackFrameIterator
   247  // interface.
   248  type stackFrameIterator struct {
   249  	index int
   250  	stack []StackFrame
   251  	fndef []api.FunctionDefinition
   252  }
   253  
   254  func (si *stackFrameIterator) Next() bool {
   255  	si.index++
   256  	return si.index < len(si.stack)
   257  }
   258  
   259  func (si *stackFrameIterator) Function() InternalFunction {
   260  	return internalFunction{
   261  		definition:   si.fndef[si.index],
   262  		sourceOffset: si.stack[si.index].SourceOffset,
   263  	}
   264  }
   265  
   266  func (si *stackFrameIterator) ProgramCounter() ProgramCounter {
   267  	return ProgramCounter(si.stack[si.index].PC)
   268  }
   269  
   270  // NewStackIterator constructs a stack iterator from a list of stack frames.
   271  // The top most frame is the last one.
   272  func NewStackIterator(stack ...StackFrame) StackIterator {
   273  	si := &stackFrameIterator{
   274  		index: -1,
   275  		stack: make([]StackFrame, len(stack)),
   276  		fndef: make([]api.FunctionDefinition, len(stack)),
   277  	}
   278  	for i := range stack {
   279  		si.stack[i] = stack[len(stack)-(i+1)]
   280  	}
   281  	// The size of function definition is only one pointer which should allow
   282  	// the compiler to optimize the conversion to api.FunctionDefinition; but
   283  	// the presence of internal.WazeroOnlyType, despite being defined as an
   284  	// empty struct, forces a heap allocation that we amortize by caching the
   285  	// result.
   286  	for i, frame := range stack {
   287  		si.fndef[i] = frame.Function.Definition()
   288  	}
   289  	return si
   290  }
   291  
   292  // BenchmarkFunctionListener implements a benchmark for function listeners.
   293  //
   294  // The benchmark calls Before and After methods repeatedly using the provided
   295  // module an stack frames to invoke the methods.
   296  //
   297  // The stack frame is a representation of the call stack that the Before method
   298  // will be invoked with. The top of the stack is stored at index zero. The stack
   299  // must contain at least one frame or the benchmark will fail.
   300  func BenchmarkFunctionListener(n int, module api.Module, stack []StackFrame, listener FunctionListener) {
   301  	if len(stack) == 0 {
   302  		panic("cannot benchmark function listener with an empty stack")
   303  	}
   304  
   305  	ctx := context.Background()
   306  	def := stack[0].Function.Definition()
   307  	params := stack[0].Params
   308  	results := stack[0].Results
   309  	stackIterator := &stackIterator{base: NewStackIterator(stack...)}
   310  
   311  	for i := 0; i < n; i++ {
   312  		stackIterator.index = -1
   313  		listener.Before(ctx, module, def, params, stackIterator)
   314  		listener.After(ctx, module, def, results)
   315  	}
   316  }
   317  
   318  // TODO: the calls to Abort are not yet tested in internal/testing/enginetest,
   319  // but they are validated indirectly in tests which exercise host logging,
   320  // like Test_procExit in imports/wasi_snapshot_preview1. Eventually we should
   321  // add dedicated tests to validate the behavior of the interpreter and compiler
   322  // engines independently.