github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/wasm/module_instance.go (about) 1 package wasm 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 "github.com/tetratelabs/wazero/api" 9 "github.com/tetratelabs/wazero/sys" 10 ) 11 12 // FailIfClosed returns a sys.ExitError if CloseWithExitCode was called. 13 func (m *ModuleInstance) FailIfClosed() (err error) { 14 if closed := m.Closed.Load(); closed != 0 { 15 switch closed & exitCodeFlagMask { 16 case exitCodeFlagResourceClosed: 17 case exitCodeFlagResourceNotClosed: 18 // This happens when this module is closed asynchronously in CloseModuleOnCanceledOrTimeout, 19 // and the closure of resources have been deferred here. 20 _ = m.ensureResourcesClosed(context.Background()) 21 } 22 return sys.NewExitError(uint32(closed >> 32)) // Unpack the high order bits as the exit code. 23 } 24 return nil 25 } 26 27 // CloseModuleOnCanceledOrTimeout take a context `ctx`, which might be a Cancel or Timeout context, 28 // and spawns the Goroutine to check the context is canceled ot deadline exceeded. If it reaches 29 // one of the conditions, it sets the appropriate exit code. 30 // 31 // Callers of this function must invoke the returned context.CancelFunc to release the spawned Goroutine. 32 func (m *ModuleInstance) CloseModuleOnCanceledOrTimeout(ctx context.Context) context.CancelFunc { 33 // Creating an empty channel in this case is a bit more efficient than 34 // creating a context.Context and canceling it with the same effect. We 35 // really just need to be notified when to stop listening to the users 36 // context. Closing the channel will unblock the select in the goroutine 37 // causing it to return an stop listening to ctx.Done(). 38 cancelChan := make(chan struct{}) 39 go m.closeModuleOnCanceledOrTimeout(ctx, cancelChan) 40 return func() { close(cancelChan) } 41 } 42 43 // closeModuleOnCanceledOrTimeout is extracted from CloseModuleOnCanceledOrTimeout for testing. 44 func (m *ModuleInstance) closeModuleOnCanceledOrTimeout(ctx context.Context, cancelChan <-chan struct{}) { 45 select { 46 case <-ctx.Done(): 47 select { 48 case <-cancelChan: 49 // In some cases by the time this goroutine is scheduled, the caller 50 // has already closed both the context and the cancelChan. In this 51 // case go will randomize which branch of the outer select to enter 52 // and we don't want to close the module. 53 default: 54 // This is the same logic as CloseWithCtxErr except this calls closeWithExitCodeWithoutClosingResource 55 // so that we can defer the resource closure in FailIfClosed. 56 switch { 57 case errors.Is(ctx.Err(), context.Canceled): 58 // TODO: figure out how to report error here. 59 _ = m.closeWithExitCodeWithoutClosingResource(sys.ExitCodeContextCanceled) 60 case errors.Is(ctx.Err(), context.DeadlineExceeded): 61 // TODO: figure out how to report error here. 62 _ = m.closeWithExitCodeWithoutClosingResource(sys.ExitCodeDeadlineExceeded) 63 } 64 } 65 case <-cancelChan: 66 } 67 } 68 69 // CloseWithCtxErr closes the module with an exit code based on the type of 70 // error reported by the context. 71 // 72 // If the context's error is unknown or nil, the module does not close. 73 func (m *ModuleInstance) CloseWithCtxErr(ctx context.Context) { 74 switch { 75 case errors.Is(ctx.Err(), context.Canceled): 76 // TODO: figure out how to report error here. 77 _ = m.CloseWithExitCode(ctx, sys.ExitCodeContextCanceled) 78 case errors.Is(ctx.Err(), context.DeadlineExceeded): 79 // TODO: figure out how to report error here. 80 _ = m.CloseWithExitCode(ctx, sys.ExitCodeDeadlineExceeded) 81 } 82 } 83 84 // Name implements the same method as documented on api.Module 85 func (m *ModuleInstance) Name() string { 86 return m.ModuleName 87 } 88 89 // String implements the same method as documented on api.Module 90 func (m *ModuleInstance) String() string { 91 return fmt.Sprintf("Module[%s]", m.Name()) 92 } 93 94 // Close implements the same method as documented on api.Module. 95 func (m *ModuleInstance) Close(ctx context.Context) (err error) { 96 return m.CloseWithExitCode(ctx, 0) 97 } 98 99 // CloseWithExitCode implements the same method as documented on api.Module. 100 func (m *ModuleInstance) CloseWithExitCode(ctx context.Context, exitCode uint32) (err error) { 101 if !m.setExitCode(exitCode, exitCodeFlagResourceClosed) { 102 return nil // not an error to have already closed 103 } 104 _ = m.s.deleteModule(m) 105 return m.ensureResourcesClosed(ctx) 106 } 107 108 // IsClosed implements the same method as documented on api.Module. 109 func (m *ModuleInstance) IsClosed() bool { 110 return m.Closed.Load() != 0 111 } 112 113 func (m *ModuleInstance) closeWithExitCodeWithoutClosingResource(exitCode uint32) (err error) { 114 if !m.setExitCode(exitCode, exitCodeFlagResourceNotClosed) { 115 return nil // not an error to have already closed 116 } 117 _ = m.s.deleteModule(m) 118 return nil 119 } 120 121 // closeWithExitCode is the same as CloseWithExitCode besides this doesn't delete it from Store.moduleList. 122 func (m *ModuleInstance) closeWithExitCode(ctx context.Context, exitCode uint32) (err error) { 123 if !m.setExitCode(exitCode, exitCodeFlagResourceClosed) { 124 return nil // not an error to have already closed 125 } 126 return m.ensureResourcesClosed(ctx) 127 } 128 129 type exitCodeFlag = uint64 130 131 const exitCodeFlagMask = 0xff 132 133 const ( 134 // exitCodeFlagResourceClosed indicates that the module was closed and resources were already closed. 135 exitCodeFlagResourceClosed = 1 << iota 136 // exitCodeFlagResourceNotClosed indicates that the module was closed while resources are not closed yet. 137 exitCodeFlagResourceNotClosed 138 ) 139 140 func (m *ModuleInstance) setExitCode(exitCode uint32, flag exitCodeFlag) bool { 141 closed := flag | uint64(exitCode)<<32 // Store exitCode as high-order bits. 142 return m.Closed.CompareAndSwap(0, closed) 143 } 144 145 // ensureResourcesClosed ensures that resources assigned to ModuleInstance is released. 146 // Only one call will happen per module, due to external atomic guards on Closed. 147 func (m *ModuleInstance) ensureResourcesClosed(ctx context.Context) (err error) { 148 if closeNotifier := m.CloseNotifier; closeNotifier != nil { // experimental 149 closeNotifier.CloseNotify(ctx, uint32(m.Closed.Load()>>32)) 150 m.CloseNotifier = nil 151 } 152 153 if sysCtx := m.Sys; sysCtx != nil { // nil if from HostModuleBuilder 154 err = sysCtx.FS().Close() 155 m.Sys = nil 156 } 157 158 if mem := m.MemoryInstance; mem != nil { 159 if mem.expBuffer != nil { 160 mem.expBuffer.Free() 161 mem.expBuffer = nil 162 } 163 } 164 165 if m.CodeCloser != nil { 166 if e := m.CodeCloser.Close(ctx); err == nil { 167 err = e 168 } 169 m.CodeCloser = nil 170 } 171 return err 172 } 173 174 // Memory implements the same method as documented on api.Module. 175 func (m *ModuleInstance) Memory() api.Memory { 176 return m.MemoryInstance 177 } 178 179 // ExportedMemory implements the same method as documented on api.Module. 180 func (m *ModuleInstance) ExportedMemory(name string) api.Memory { 181 _, err := m.getExport(name, ExternTypeMemory) 182 if err != nil { 183 return nil 184 } 185 // We Assume that we have at most one memory. 186 return m.MemoryInstance 187 } 188 189 // ExportedMemoryDefinitions implements the same method as documented on 190 // api.Module. 191 func (m *ModuleInstance) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { 192 // Special case as we currently only support one memory. 193 if mem := m.MemoryInstance; mem != nil { 194 // Now, find out if it is exported 195 for name, exp := range m.Exports { 196 if exp.Type == ExternTypeMemory { 197 return map[string]api.MemoryDefinition{name: mem.definition} 198 } 199 } 200 } 201 return map[string]api.MemoryDefinition{} 202 } 203 204 // ExportedFunction implements the same method as documented on api.Module. 205 func (m *ModuleInstance) ExportedFunction(name string) api.Function { 206 exp, err := m.getExport(name, ExternTypeFunc) 207 if err != nil { 208 return nil 209 } 210 return m.Engine.NewFunction(exp.Index) 211 } 212 213 // ExportedFunctionDefinitions implements the same method as documented on 214 // api.Module. 215 func (m *ModuleInstance) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { 216 result := map[string]api.FunctionDefinition{} 217 for name, exp := range m.Exports { 218 if exp.Type == ExternTypeFunc { 219 result[name] = m.Source.FunctionDefinition(exp.Index) 220 } 221 } 222 return result 223 } 224 225 // GlobalVal is an internal hack to get the lower 64 bits of a global. 226 func (m *ModuleInstance) GlobalVal(idx Index) uint64 { 227 return m.Globals[idx].Val 228 } 229 230 // ExportedGlobal implements the same method as documented on api.Module. 231 func (m *ModuleInstance) ExportedGlobal(name string) api.Global { 232 exp, err := m.getExport(name, ExternTypeGlobal) 233 if err != nil { 234 return nil 235 } 236 g := m.Globals[exp.Index] 237 if g.Type.Mutable { 238 return mutableGlobal{g: g} 239 } 240 return constantGlobal{g: g} 241 } 242 243 // NumGlobal implements experimental.InternalModule. 244 func (m *ModuleInstance) NumGlobal() int { 245 return len(m.Globals) 246 } 247 248 // Global implements experimental.InternalModule. 249 func (m *ModuleInstance) Global(idx int) api.Global { 250 return constantGlobal{g: m.Globals[idx]} 251 }