wa-lang.org/wazero@v1.0.2/internal/wasm/call_context.go (about)

     1  package wasm
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync/atomic"
     7  
     8  	"wa-lang.org/wazero/api"
     9  	internalsys "wa-lang.org/wazero/internal/sys"
    10  	"wa-lang.org/wazero/sys"
    11  )
    12  
    13  // compile time check to ensure CallContext implements api.Module
    14  var _ api.Module = &CallContext{}
    15  
    16  func NewCallContext(ns *Namespace, instance *ModuleInstance, sys *internalsys.Context) *CallContext {
    17  	zero := uint64(0)
    18  	return &CallContext{memory: instance.Memory, module: instance, ns: ns, Sys: sys, closed: &zero}
    19  }
    20  
    21  // CallContext is a function call context bound to a module. This is important as one module's functions can call
    22  // imported functions, but all need to effect the same memory.
    23  //
    24  // Note: This does not include the context.Context because doing so risks caching the wrong context which can break
    25  // functionality like trace propagation.
    26  // Note: this also implements api.Module in order to simplify usage as a host function parameter.
    27  type CallContext struct {
    28  	// TODO: We've never found a great name for this. It is only used for function calls, hence CallContext, but it
    29  	// moves on a different axis than, for example, the context.Context. context.Context is the same root for the whole
    30  	// call stack, where the CallContext can change depending on where memory is defined and who defines the calling
    31  	// function. When we rename this again, we should try to capture as many key points possible on the docs.
    32  
    33  	module *ModuleInstance
    34  	// memory is returned by Memory and overridden WithMemory
    35  	memory api.Memory
    36  	ns     *Namespace
    37  
    38  	// Sys is exposed for use in special imports such as WASI, assemblyscript
    39  	// and gojs.
    40  	//
    41  	// # Notes
    42  	//
    43  	//   - This is a part of CallContext so that scope and Close is coherent.
    44  	//   - This is not exposed outside this repository (as a host function
    45  	//	  parameter) because we haven't thought through capabilities based
    46  	//	  security implications.
    47  	Sys *internalsys.Context
    48  
    49  	// closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code.
    50  	//
    51  	// The update value is 1 + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed.
    52  	//
    53  	// Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations.
    54  	// See /RATIONALE.md
    55  	closed *uint64
    56  
    57  	// CodeCloser is non-nil when the code should be closed after this module.
    58  	CodeCloser api.Closer
    59  }
    60  
    61  // FailIfClosed returns a sys.ExitError if CloseWithExitCode was called.
    62  func (m *CallContext) FailIfClosed() error {
    63  	if closed := atomic.LoadUint64(m.closed); closed != 0 {
    64  		return sys.NewExitError(m.module.Name, uint32(closed>>32)) // Unpack the high order bits as the exit code.
    65  	}
    66  	return nil
    67  }
    68  
    69  // Name implements the same method as documented on api.Module
    70  func (m *CallContext) Name() string {
    71  	return m.module.Name
    72  }
    73  
    74  // WithMemory allows overriding memory without re-allocation when the result would be the same.
    75  func (m *CallContext) WithMemory(memory *MemoryInstance) *CallContext {
    76  	if memory != nil && memory != m.memory { // only re-allocate if it will change the effective memory
    77  		return &CallContext{module: m.module, memory: memory, Sys: m.Sys, closed: m.closed}
    78  	}
    79  	return m
    80  }
    81  
    82  // String implements the same method as documented on api.Module
    83  func (m *CallContext) String() string {
    84  	return fmt.Sprintf("Module[%s]", m.Name())
    85  }
    86  
    87  // Close implements the same method as documented on api.Module.
    88  func (m *CallContext) Close(ctx context.Context) (err error) {
    89  	return m.CloseWithExitCode(ctx, 0)
    90  }
    91  
    92  // CloseWithExitCode implements the same method as documented on api.Module.
    93  func (m *CallContext) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
    94  	closed, err := m.close(ctx, exitCode)
    95  	if !closed {
    96  		return nil
    97  	}
    98  	m.ns.deleteModule(m.Name())
    99  	if m.CodeCloser == nil {
   100  		return err
   101  	}
   102  	if e := m.CodeCloser.Close(ctx); e != nil && err == nil {
   103  		err = e
   104  	}
   105  	return err
   106  }
   107  
   108  // close marks this CallContext as closed and releases underlying system resources.
   109  //
   110  // Note: The caller is responsible for removing the module from the Namespace.
   111  func (m *CallContext) close(ctx context.Context, exitCode uint32) (c bool, err error) {
   112  	closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits.
   113  	if !atomic.CompareAndSwapUint64(m.closed, 0, closed) {
   114  		return false, nil
   115  	}
   116  	c = true
   117  	if sysCtx := m.Sys; sysCtx != nil { // nil if from HostModuleBuilder
   118  		err = sysCtx.FS(ctx).Close(ctx)
   119  	}
   120  	return
   121  }
   122  
   123  // Memory implements the same method as documented on api.Module.
   124  func (m *CallContext) Memory() api.Memory {
   125  	return m.module.Memory
   126  }
   127  
   128  // ExportedMemory implements the same method as documented on api.Module.
   129  func (m *CallContext) ExportedMemory(name string) api.Memory {
   130  	exp, err := m.module.getExport(name, ExternTypeMemory)
   131  	if err != nil {
   132  		return nil
   133  	}
   134  	return exp.Memory
   135  }
   136  
   137  // ExportedFunction implements the same method as documented on api.Module.
   138  func (m *CallContext) ExportedFunction(name string) api.Function {
   139  	exp, err := m.module.getExport(name, ExternTypeFunc)
   140  	if err != nil {
   141  		return nil
   142  	}
   143  
   144  	return m.function(exp.Function)
   145  }
   146  
   147  // Module is exposed for emscripten.
   148  func (m *CallContext) Module() *ModuleInstance {
   149  	return m.module
   150  }
   151  
   152  func (m *CallContext) Function(funcIdx Index) api.Function {
   153  	if uint32(len(m.module.Functions)) < funcIdx {
   154  		return nil
   155  	}
   156  	return m.function(m.module.Functions[funcIdx])
   157  }
   158  
   159  func (m *CallContext) function(f *FunctionInstance) api.Function {
   160  	ce, err := f.Module.Engine.NewCallEngine(m, f)
   161  	if err != nil {
   162  		return nil
   163  	}
   164  
   165  	if f.Module == m.module {
   166  		return &function{fi: f, ce: ce}
   167  	} else {
   168  		return &importedFn{importingModule: m, importedFn: f, ce: ce}
   169  	}
   170  }
   171  
   172  // function implements api.Function. This couples FunctionInstance with CallEngine so that
   173  // it can be used to make function calls originating from the FunctionInstance.
   174  type function struct {
   175  	fi *FunctionInstance
   176  	ce CallEngine
   177  }
   178  
   179  // Definition implements the same method as documented on api.FunctionDefinition.
   180  func (f *function) Definition() api.FunctionDefinition {
   181  	return f.fi.Definition
   182  }
   183  
   184  // Call implements the same method as documented on api.Function.
   185  func (f *function) Call(ctx context.Context, params ...uint64) (ret []uint64, err error) {
   186  	return f.ce.Call(ctx, f.fi.Module.CallCtx, params)
   187  }
   188  
   189  // importedFn implements api.Function and ensures the call context of an imported function is the importing module.
   190  type importedFn struct {
   191  	ce              CallEngine
   192  	importingModule *CallContext
   193  	importedFn      *FunctionInstance
   194  }
   195  
   196  // Definition implements the same method as documented on api.Function.
   197  func (f *importedFn) Definition() api.FunctionDefinition {
   198  	return f.importedFn.Definition
   199  }
   200  
   201  // Call implements the same method as documented on api.Function.
   202  func (f *importedFn) Call(ctx context.Context, params ...uint64) (ret []uint64, err error) {
   203  	if f.importedFn.IsHostFunction {
   204  		return nil, fmt.Errorf("directly calling host function is not supported")
   205  	}
   206  	mod := f.importingModule
   207  	return f.ce.Call(ctx, mod, params)
   208  }
   209  
   210  // GlobalVal is an internal hack to get the lower 64 bits of a global.
   211  func (m *CallContext) GlobalVal(idx Index) uint64 {
   212  	return m.module.Globals[idx].Val
   213  }
   214  
   215  // ExportedGlobal implements the same method as documented on api.Module.
   216  func (m *CallContext) ExportedGlobal(name string) api.Global {
   217  	exp, err := m.module.getExport(name, ExternTypeGlobal)
   218  	if err != nil {
   219  		return nil
   220  	}
   221  	if exp.Global.Type.Mutable {
   222  		return &mutableGlobal{exp.Global}
   223  	}
   224  	valType := exp.Global.Type.ValType
   225  	switch valType {
   226  	case ValueTypeI32:
   227  		return globalI32(exp.Global.Val)
   228  	case ValueTypeI64:
   229  		return globalI64(exp.Global.Val)
   230  	case ValueTypeF32:
   231  		return globalF32(exp.Global.Val)
   232  	case ValueTypeF64:
   233  		return globalF64(exp.Global.Val)
   234  	default:
   235  		panic(fmt.Errorf("BUG: unknown value type %X", valType))
   236  	}
   237  }