github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/emscripten/emscripten.go (about) 1 package emscripten 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 9 "github.com/bananabytelabs/wazero/api" 10 "github.com/bananabytelabs/wazero/internal/wasm" 11 "github.com/bananabytelabs/wazero/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/bananabytelabs/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 }