github.com/tetratelabs/wazero@v1.7.1/experimental/listener.go (about)

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