github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/wasm/module_instance.go (about) 1 package wasm 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 "github.com/bananabytelabs/wazero/api" 9 "github.com/bananabytelabs/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 if err = sysCtx.FS().Close(); err != nil { 155 return err 156 } 157 m.Sys = nil 158 } 159 160 if m.CodeCloser == nil { 161 return 162 } 163 if e := m.CodeCloser.Close(ctx); e != nil && err == nil { 164 err = e 165 } 166 m.CodeCloser = nil 167 return 168 } 169 170 // Memory implements the same method as documented on api.Module. 171 func (m *ModuleInstance) Memory() api.Memory { 172 return m.MemoryInstance 173 } 174 175 // ExportedMemory implements the same method as documented on api.Module. 176 func (m *ModuleInstance) ExportedMemory(name string) api.Memory { 177 _, err := m.getExport(name, ExternTypeMemory) 178 if err != nil { 179 return nil 180 } 181 // We Assume that we have at most one memory. 182 return m.MemoryInstance 183 } 184 185 // ExportedMemoryDefinitions implements the same method as documented on 186 // api.Module. 187 func (m *ModuleInstance) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { 188 // Special case as we currently only support one memory. 189 if mem := m.MemoryInstance; mem != nil { 190 // Now, find out if it is exported 191 for name, exp := range m.Exports { 192 if exp.Type == ExternTypeMemory { 193 return map[string]api.MemoryDefinition{name: mem.definition} 194 } 195 } 196 } 197 return map[string]api.MemoryDefinition{} 198 } 199 200 // ExportedFunction implements the same method as documented on api.Module. 201 func (m *ModuleInstance) ExportedFunction(name string) api.Function { 202 exp, err := m.getExport(name, ExternTypeFunc) 203 if err != nil { 204 return nil 205 } 206 return m.Engine.NewFunction(exp.Index) 207 } 208 209 // ExportedFunctionDefinitions implements the same method as documented on 210 // api.Module. 211 func (m *ModuleInstance) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { 212 result := map[string]api.FunctionDefinition{} 213 for name, exp := range m.Exports { 214 if exp.Type == ExternTypeFunc { 215 result[name] = m.Source.FunctionDefinition(exp.Index) 216 } 217 } 218 return result 219 } 220 221 // GlobalVal is an internal hack to get the lower 64 bits of a global. 222 func (m *ModuleInstance) GlobalVal(idx Index) uint64 { 223 return m.Globals[idx].Val 224 } 225 226 // ExportedGlobal implements the same method as documented on api.Module. 227 func (m *ModuleInstance) ExportedGlobal(name string) api.Global { 228 exp, err := m.getExport(name, ExternTypeGlobal) 229 if err != nil { 230 return nil 231 } 232 g := m.Globals[exp.Index] 233 if g.Type.Mutable { 234 return mutableGlobal{g: g} 235 } 236 return constantGlobal{g: g} 237 } 238 239 // NumGlobal implements experimental.InternalModule. 240 func (m *ModuleInstance) NumGlobal() int { 241 return len(m.Globals) 242 } 243 244 // Global implements experimental.InternalModule. 245 func (m *ModuleInstance) Global(idx int) api.Global { 246 return constantGlobal{g: m.Globals[idx]} 247 }