github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/emscripten/emscripten.go (about)

     1  package emscripten
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  
     9  	"github.com/wasilibs/wazerox/api"
    10  	"github.com/wasilibs/wazerox/internal/wasm"
    11  	"github.com/wasilibs/wazerox/sys"
    12  )
    13  
    14  const FunctionNotifyMemoryGrowth = "emscripten_notify_memory_growth"
    15  
    16  var NotifyMemoryGrowth = &wasm.HostFunc{
    17  	ExportName: FunctionNotifyMemoryGrowth,
    18  	Name:       FunctionNotifyMemoryGrowth,
    19  	ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
    20  	ParamNames: []string{"memory_index"},
    21  	Code:       wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {})},
    22  }
    23  
    24  // Emscripten uses this host method to throw an error that can then be caught
    25  // in the dynamic invoke functions. Emscripten uses this to allow for
    26  // setjmp/longjmp support. When this error is seen in the invoke handler,
    27  // it ignores the error and does not act on it.
    28  const FunctionThrowLongjmp = "_emscripten_throw_longjmp"
    29  
    30  var (
    31  	ThrowLongjmpError = errors.New("_emscripten_throw_longjmp")
    32  	ThrowLongjmp      = &wasm.HostFunc{
    33  		ExportName: FunctionThrowLongjmp,
    34  		Name:       FunctionThrowLongjmp,
    35  		ParamTypes: []wasm.ValueType{},
    36  		ParamNames: []string{},
    37  		Code: wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {
    38  			panic(ThrowLongjmpError)
    39  		})},
    40  	}
    41  )
    42  
    43  // InvokePrefix is the naming convention of Emscripten dynamic functions.
    44  //
    45  // All `invoke_` functions have an initial "index" parameter of
    46  // api.ValueTypeI32. This is the index of the desired funcref in the only table
    47  // in the module. The type of the funcref is via naming convention. The first
    48  // character after `invoke_` decides the result type: 'v' for no result or 'i'
    49  // for api.ValueTypeI32. Any count of 'i' following that are api.ValueTypeI32
    50  // parameters.
    51  //
    52  // For example, the function `invoke_iiiii` signature has five parameters, but
    53  // also five i's. The five 'i's mean there are four parameters
    54  //
    55  //	(import "env" "invoke_iiiii" (func $invoke_iiiii
    56  //		(param i32 i32 i32 i32 i32) (result i32))))
    57  //
    58  // So, the above function if invoked `invoke_iiiii(1234, 1, 2, 3, 4)` would
    59  // look up the funcref at table index 1234, which has a type i32i32i3232_i32
    60  // and invoke it with the remaining parameters.
    61  const InvokePrefix = "invoke_"
    62  
    63  func NewInvokeFunc(importName string, params, results []api.ValueType) *wasm.HostFunc {
    64  	// The type we invoke is the same type as the import except without the
    65  	// index parameter.
    66  	fn := &InvokeFunc{&wasm.FunctionType{Results: results}}
    67  	if len(params) > 1 {
    68  		fn.FunctionType.Params = params[1:]
    69  	}
    70  
    71  	// Now, make friendly parameter names.
    72  	paramNames := make([]string, len(params))
    73  	paramNames[0] = "index"
    74  	for i := 1; i < len(paramNames); i++ {
    75  		paramNames[i] = "a" + strconv.Itoa(i)
    76  	}
    77  	return &wasm.HostFunc{
    78  		ExportName:  importName,
    79  		ParamTypes:  params,
    80  		ParamNames:  paramNames,
    81  		ResultTypes: results,
    82  		Code:        wasm.Code{GoFunc: fn},
    83  	}
    84  }
    85  
    86  type InvokeFunc struct {
    87  	*wasm.FunctionType
    88  }
    89  
    90  // Call implements api.GoModuleFunction by special casing dynamic calls needed
    91  // for emscripten `invoke_` functions such as `invoke_ii` or `invoke_v`.
    92  func (v *InvokeFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
    93  	m := mod.(*wasm.ModuleInstance)
    94  
    95  	// Lookup the type of the function we are calling indirectly.
    96  	typeID := m.GetFunctionTypeID(v.FunctionType)
    97  
    98  	// This needs copy (not reslice) because the stack is reused for results.
    99  	// Consider invoke_i (zero arguments, one result): index zero (tableOffset)
   100  	// is needed to store the result.
   101  	tableOffset := wasm.Index(stack[0]) // position in the module's only table.
   102  	copy(stack, stack[1:])              // pop the tableOffset.
   103  
   104  	// Lookup the table index we will call.
   105  	t := m.Tables[0] // Note: Emscripten doesn't use multiple tables
   106  	f := m.LookupFunction(t, typeID, tableOffset)
   107  
   108  	// The Go implementation below mimics the Emscripten JS behaviour to support
   109  	// longjmps from indirect function calls. The implementation of these
   110  	// indirection function calls in Emscripten JS is like this:
   111  	//
   112  	// function invoke_iii(index,a1,a2) {
   113  	//  var sp = stackSave();
   114  	//  try {
   115  	//    return getWasmTableEntry(index)(a1,a2);
   116  	//  } catch(e) {
   117  	//    stackRestore(sp);
   118  	//    if (e !== e+0) throw e;
   119  	//    _setThrew(1, 0);
   120  	//  }
   121  	//}
   122  
   123  	// This is the equivalent of "var sp = stackSave();".
   124  	// We reuse savedStack to save allocations. We allocate with a size of 2
   125  	// here to accommodate for the input and output of setThrew.
   126  	var savedStack [2]uint64
   127  	callOrPanic(ctx, mod, "stackSave", savedStack[:])
   128  
   129  	err := f.CallWithStack(ctx, stack)
   130  	if err != nil {
   131  		// Module closed: any calls will just fail with the same error.
   132  		if _, ok := err.(*sys.ExitError); ok {
   133  			panic(err)
   134  		}
   135  
   136  		// This is the equivalent of "stackRestore(sp);".
   137  		// Do not overwrite err here to preserve the original error.
   138  		callOrPanic(ctx, mod, "stackRestore", savedStack[:])
   139  
   140  		// If we encounter ThrowLongjmpError, this means that the C code did a
   141  		// longjmp, which in turn called _emscripten_throw_longjmp and that is
   142  		// a host function that panics with ThrowLongjmpError. In that case we
   143  		// ignore the error because we have restored the stack to what it was
   144  		// before the indirect function call, so the program can continue.
   145  		// This is the equivalent of the "if (e !== e+0) throw e;" line in the
   146  		// JS implementation, which checks if the error is not a number, which
   147  		// is what the JS implementation throws (Infinity for
   148  		// _emscripten_throw_longjmp, a memory address for C++ exceptions).
   149  		if !errors.Is(err, ThrowLongjmpError) {
   150  			panic(err)
   151  		}
   152  
   153  		// This is the equivalent of "_setThrew(1, 0);".
   154  		savedStack[0] = 1
   155  		savedStack[1] = 0
   156  		callOrPanic(ctx, mod, "setThrew", savedStack[:])
   157  	}
   158  }
   159  
   160  // maybeCallOrPanic calls a given function if it is exported, otherwise panics.
   161  //
   162  // This ensures if the given name is exported before calling it. In other words, this explicitly checks if an api.Function
   163  // returned by api.Module.ExportedFunction is not nil. This is necessary because directly calling a method which is
   164  // potentially nil interface can be fatal on some platforms due to a bug? in Go/QEMU.
   165  // See https://github.com/tetratelabs/wazero/issues/1621
   166  func callOrPanic(ctx context.Context, m api.Module, name string, stack []uint64) {
   167  	if f := m.ExportedFunction(name); f != nil {
   168  		err := f.CallWithStack(ctx, stack)
   169  		if err != nil {
   170  			panic(err)
   171  		}
   172  	} else {
   173  		panic(fmt.Sprintf("%s not exported", name))
   174  	}
   175  }