module_instance.go

  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}