github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/experimental/listener.go (about) 1 package experimental 2 3 import ( 4 "context" 5 6 "github.com/wasilibs/wazerox/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/tetratelabs/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.