1package wasm
2
3import (
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.
13func (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.
32func (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.
44func (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.
73func (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
85func (m *ModuleInstance) Name() string {
86 return m.ModuleName
87}
88
89// String implements the same method as documented on api.Module
90func (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.
95func (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.
100func (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.
109func (m *ModuleInstance) IsClosed() bool {
110 return m.Closed.Load() != 0
111}
112
113func (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.
122func (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
129type exitCodeFlag = uint64
130
131const exitCodeFlagMask = 0xff
132
133const (
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
140func (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.
147func (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.
175func (m *ModuleInstance) Memory() api.Memory {
176 return m.MemoryInstance
177}
178
179// ExportedMemory implements the same method as documented on api.Module.
180func (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.
191func (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.
205func (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.
215func (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.
226func (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.
231func (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.
244func (m *ModuleInstance) NumGlobal() int {
245 return len(m.Globals)
246}
247
248// Global implements experimental.InternalModule.
249func (m *ModuleInstance) Global(idx int) api.Global {
250 return constantGlobal{g: m.Globals[idx]}
251}