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.