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

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