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