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  }