diff --git a/experimental/emscripten/emscripten.go b/experimental/emscripten/emscripten.go new file mode 100644 index 00000000..f71b2db2 --- /dev/null +++ b/experimental/emscripten/emscripten.go @@ -0,0 +1,60 @@ +package emscripten + +import ( + "context" + "strings" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/emscripten" + internal "github.com/tetratelabs/wazero/internal/emscripten" + "github.com/tetratelabs/wazero/internal/wasm" +) + +type emscriptenFns []*wasm.HostFunc + +// InstantiateForModule instantiates a module named "env" populated with any +// known functions used in emscripten. +func InstantiateForModule(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) (api.Closer, error) { + // Create the exporter for the supplied wasm + exporter, err := NewFunctionExporterForModule(guest) + if err != nil { + return nil, err + } + + // Instantiate it! + env := r.NewHostModuleBuilder("env") + exporter.ExportFunctions(env) + return env.Instantiate(ctx) +} + +// NewFunctionExporterForModule returns a guest-specific FunctionExporter, +// populated with any known functions used in emscripten. +func NewFunctionExporterForModule(guest wazero.CompiledModule) (emscripten.FunctionExporter, error) { + ret := emscriptenFns{} + for _, fn := range guest.ImportedFunctions() { + importModule, importName, isImport := fn.Import() + if !isImport || importModule != "env" { + continue // not emscripten + } + if importName == internal.FunctionNotifyMemoryGrowth { + ret = append(ret, internal.NotifyMemoryGrowth) + continue + } + if !strings.HasPrefix(importName, internal.InvokePrefix) { + continue // not invoke, and maybe not emscripten + } + + hf := internal.NewInvokeFunc(importName, fn.ParamTypes(), fn.ResultTypes()) + ret = append(ret, hf) + } + return ret, nil +} + +// ExportFunctions implements FunctionExporter.ExportFunctions +func (i emscriptenFns) ExportFunctions(builder wazero.HostModuleBuilder) { + exporter := builder.(wasm.HostFuncExporter) + for _, fn := range i { + exporter.ExportHostFunc(fn) + } +} diff --git a/experimental/emscripten/emscripten_test.go b/experimental/emscripten/emscripten_test.go new file mode 100644 index 00000000..9f88db3f --- /dev/null +++ b/experimental/emscripten/emscripten_test.go @@ -0,0 +1,494 @@ +package emscripten + +import ( + "bytes" + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/experimental/logging" + internal "github.com/tetratelabs/wazero/internal/emscripten" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + i32 = wasm.ValueTypeI32 + i64 = wasm.ValueTypeI64 + f32 = wasm.ValueTypeF32 + f64 = wasm.ValueTypeF64 +) + +// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. +var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") + +func TestNewFunctionExporterForModule(t *testing.T) { + tests := []struct { + name string + input *wasm.Module + expected emscriptenFns + }{ + { + name: "empty", + input: &wasm.Module{}, + expected: emscriptenFns{}, + }, + { + name: internal.FunctionNotifyMemoryGrowth, + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, + }, + ImportSection: []wasm.Import{ + { + Module: "env", Name: internal.FunctionNotifyMemoryGrowth, + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{internal.NotifyMemoryGrowth}, + }, + { + name: "all result types", + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64}}, + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f32}}, + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f64}}, + }, + ImportSection: []wasm.Import{ + { + Module: "env", Name: "invoke_v", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "env", Name: "invoke_i", + Type: wasm.ExternTypeFunc, + DescFunc: 1, + }, + { + Module: "env", Name: "invoke_p", + Type: wasm.ExternTypeFunc, + DescFunc: 1, + }, + { + Module: "env", Name: "invoke_j", + Type: wasm.ExternTypeFunc, + DescFunc: 2, + }, + { + Module: "env", Name: "invoke_f", + Type: wasm.ExternTypeFunc, + DescFunc: 3, + }, + { + Module: "env", Name: "invoke_d", + Type: wasm.ExternTypeFunc, + DescFunc: 4, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_v", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, + }, + { + ExportName: "invoke_i", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + ResultTypes: []api.ValueType{i32}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}}, + }, + { + ExportName: "invoke_p", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + ResultTypes: []api.ValueType{i32}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}}, + }, + { + ExportName: "invoke_j", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + ResultTypes: []api.ValueType{i64}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i64}}}}, + }, + { + ExportName: "invoke_f", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + ResultTypes: []api.ValueType{f32}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f32}}}}, + }, + { + ExportName: "invoke_d", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + ResultTypes: []api.ValueType{f64}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f64}}}}, + }, + }, + }, + { + name: "ignores other imports", + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, + }, + ImportSection: []wasm.Import{ + { + Module: "anv", Name: "invoke_v", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "env", Name: "invoke_v", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "env", Name: "grow", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_v", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, + }, + }, + }, + { + name: "invoke_v and " + internal.FunctionNotifyMemoryGrowth, + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, + ImportSection: []wasm.Import{ + { + Module: "env", Name: "invoke_v", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "env", Name: internal.FunctionNotifyMemoryGrowth, + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_v", + ParamTypes: []api.ValueType{i32}, + ParamNames: []string{"index"}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, + }, + internal.NotifyMemoryGrowth, + }, + }, + { + name: "invoke_vi", + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32, i32}}, + }, + ImportSection: []wasm.Import{ + { + Module: "env", Name: "invoke_vi", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_vi", + ParamTypes: []api.ValueType{i32, i32}, + ParamNames: []string{"index", "a1"}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Params: []api.ValueType{i32}}}}, + }, + }, + }, + { + name: "invoke_iiiii", + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + { + Params: []wasm.ValueType{i32, i32, i32, i32, i32}, + Results: []wasm.ValueType{i32}, + }, + }, + ImportSection: []wasm.Import{ + { + Module: "env", Name: "invoke_iiiii", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_iiiii", + ParamTypes: []api.ValueType{i32, i32, i32, i32, i32}, + ParamNames: []string{"index", "a1", "a2", "a3", "a4"}, + ResultTypes: []wasm.ValueType{i32}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{ + Params: []api.ValueType{i32, i32, i32, i32}, + Results: []api.ValueType{i32}, + }}}, + }, + }, + }, + { + name: "invoke_viiiddiiiiii", + input: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + { + Params: []wasm.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, + }, + }, + ImportSection: []wasm.Import{ + { + Module: "env", Name: "invoke_viiiddiiiiii", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, + }, + expected: []*wasm.HostFunc{ + { + ExportName: "invoke_viiiddiiiiii", + ParamTypes: []api.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, + ParamNames: []string{"index", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11"}, + Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{ + Params: []api.ValueType{i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, + }}}, + }, + }, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + r := wazero.NewRuntime(testCtx) + defer r.Close(testCtx) + + guest, err := r.CompileModule(testCtx, binaryencoding.EncodeModule(tc.input)) + require.NoError(t, err) + + exporter, err := NewFunctionExporterForModule(guest) + require.NoError(t, err) + actual := exporter.(emscriptenFns) + + require.Equal(t, len(tc.expected), len(actual)) + for i, expected := range tc.expected { + require.Equal(t, expected, actual[i], actual[i].ExportName) + } + }) + } +} + +// invokeWasm was generated by the following: +// +// cd testdata; wat2wasm --debug-names invoke.wat +// +//go:embed testdata/invoke.wasm +var invokeWasm []byte + +func TestInstantiateForModule(t *testing.T) { + var log bytes.Buffer + + // Set context to one that has an experimental listener + ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&log)) + + r := wazero.NewRuntime(ctx) + defer r.Close(ctx) + + compiled, err := r.CompileModule(ctx, invokeWasm) + require.NoError(t, err) + + _, err = InstantiateForModule(ctx, r, compiled) + require.NoError(t, err) + + mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig()) + require.NoError(t, err) + + tests := []struct { + name, funcName string + tableOffset int + params, expectedResults []uint64 + expectedLog string + }{ + { + name: "invoke_i", + funcName: "call_v_i32", + expectedResults: []uint64{42}, + expectedLog: `--> .call_v_i32(0) + ==> env.invoke_i(index=0) + --> .v_i32() + <-- 42 + <== 42 +<-- 42 +`, + }, + { + name: "invoke_ii", + funcName: "call_i32_i32", + tableOffset: 2, + params: []uint64{42}, + expectedResults: []uint64{42}, + expectedLog: `--> .call_i32_i32(2,42) + ==> env.invoke_ii(index=2,a1=42) + --> .i32_i32(42) + <-- 42 + <== 42 +<-- 42 +`, + }, + { + name: "invoke_iii", + funcName: "call_i32i32_i32", + tableOffset: 4, + params: []uint64{1, 2}, + expectedResults: []uint64{3}, + expectedLog: `--> .call_i32i32_i32(4,1,2) + ==> env.invoke_iii(index=4,a1=1,a2=2) + --> .i32i32_i32(1,2) + <-- 3 + <== 3 +<-- 3 +`, + }, + { + name: "invoke_iiii", + funcName: "call_i32i32i32_i32", + tableOffset: 6, + params: []uint64{1, 2, 4}, + expectedResults: []uint64{7}, + expectedLog: `--> .call_i32i32i32_i32(6,1,2,4) + ==> env.invoke_iiii(index=6,a1=1,a2=2,a3=4) + --> .i32i32i32_i32(1,2,4) + <-- 7 + <== 7 +<-- 7 +`, + }, + { + name: "invoke_iiiii", + funcName: "calli32_i32i32i32i32_i32", + tableOffset: 8, + params: []uint64{1, 2, 4, 8}, + expectedResults: []uint64{15}, + expectedLog: `--> .calli32_i32i32i32i32_i32(8,1,2,4,8) + ==> env.invoke_iiiii(index=8,a1=1,a2=2,a3=4,a4=8) + --> .i32i32i32i32_i32(1,2,4,8) + <-- 15 + <== 15 +<-- 15 +`, + }, + { + name: "invoke_v", + funcName: "call_v_v", + tableOffset: 10, + expectedLog: `--> .call_v_v(10) + ==> env.invoke_v(index=10) + --> .v_v() + <-- + <== +<-- +`, + }, + { + name: "invoke_vi", + funcName: "call_i32_v", + tableOffset: 12, + params: []uint64{42}, + expectedLog: `--> .call_i32_v(12,42) + ==> env.invoke_vi(index=12,a1=42) + --> .i32_v(42) + <-- + <== +<-- +`, + }, + { + name: "invoke_vii", + funcName: "call_i32i32_v", + tableOffset: 14, + params: []uint64{1, 2}, + expectedLog: `--> .call_i32i32_v(14,1,2) + ==> env.invoke_vii(index=14,a1=1,a2=2) + --> .i32i32_v(1,2) + <-- + <== +<-- +`, + }, + { + name: "invoke_viii", + funcName: "call_i32i32i32_v", + tableOffset: 16, + params: []uint64{1, 2, 4}, + expectedLog: `--> .call_i32i32i32_v(16,1,2,4) + ==> env.invoke_viii(index=16,a1=1,a2=2,a3=4) + --> .i32i32i32_v(1,2,4) + <-- + <== +<-- +`, + }, + { + name: "invoke_viiii", + funcName: "calli32_i32i32i32i32_v", + tableOffset: 18, + params: []uint64{1, 2, 4, 8}, + expectedLog: `--> .calli32_i32i32i32i32_v(18,1,2,4,8) + ==> env.invoke_viiii(index=18,a1=1,a2=2,a3=4,a4=8) + --> .i32i32i32i32_v(1,2,4,8) + <-- + <== +<-- +`, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + params := tc.params + params = append([]uint64{uint64(tc.tableOffset)}, params...) + + results, err := mod.ExportedFunction(tc.funcName).Call(testCtx, params...) + require.NoError(t, err) + require.Equal(t, tc.expectedResults, results) + + // We expect to see the dynamic function call target + require.Equal(t, tc.expectedLog, log.String()) + + // We expect an unreachable function to err + params[0]++ + _, err = mod.ExportedFunction(tc.funcName).Call(testCtx, params...) + require.Error(t, err) + }) + } +} diff --git a/experimental/emscripten/testdata/invoke.wasm b/experimental/emscripten/testdata/invoke.wasm new file mode 100644 index 00000000..64e152a9 Binary files /dev/null and b/experimental/emscripten/testdata/invoke.wasm differ diff --git a/experimental/emscripten/testdata/invoke.wat b/experimental/emscripten/testdata/invoke.wat new file mode 100644 index 00000000..7cb0e90c --- /dev/null +++ b/experimental/emscripten/testdata/invoke.wat @@ -0,0 +1,115 @@ +(module + (type $0 (func (param i32))) + (import "env" "invoke_i" (func $invoke_i (param i32) (result i32))) + (import "env" "invoke_ii" (func $invoke_ii (param i32 i32) (result i32))) + (import "env" "invoke_iii" (func $invoke_iii (param i32 i32 i32) (result i32))) + (import "env" "invoke_iiii" (func $invoke_iiii (param i32 i32 i32 i32) (result i32))) + (import "env" "invoke_iiiii" (func $invoke_iiiii (param i32 i32 i32 i32 i32) (result i32))) + (import "env" "invoke_v" (func $invoke_v (param i32))) + (import "env" "invoke_vi" (func $invoke_vi (param i32 i32))) + (import "env" "invoke_vii" (func $invoke_vii (param i32 i32 i32))) + (import "env" "invoke_viii" (func $invoke_viii (param i32 i32 i32 i32))) + (import "env" "invoke_viiii" (func $invoke_viiii (param i32 i32 i32 i32 i32))) + + (table 20 20 funcref) + + (func $v_i32 (result i32) (i32.const 42)) + (func $v_i32_unreachable (result i32) unreachable) + + (elem (i32.const 0) $v_i32 $v_i32_unreachable) + + ;; call_v_i32 should be called with 0 or 1 and expect 42 or unreachable. + (func $call_v_i32 (export "call_v_i32") (param i32) (result i32) + (call $invoke_i (local.get 0))) + + (func $i32_i32 (param i32) (result i32) (local.get 0)) + (func $i32_i32_unreachable (param i32) (result i32) unreachable) + + (elem (i32.const 2) $i32_i32 $i32_i32_unreachable) + + ;; call_i32_i32 should be called with 2 or 3 followed by one number which is + ;; the result on $0 == 2 or unreachable on 3. + (func $call_i32_i32 (export "call_i32_i32") (param i32 i32) (result i32) + (call $invoke_ii (local.get 0) (local.get 1))) + + (func $i32i32_i32 (param i32 i32) (result i32) (i32.add (local.get 0) (local.get 1))) + (func $i32i32_i32_unreachable (param i32 i32) (result i32) unreachable) + + (elem (i32.const 4) $i32i32_i32 $i32i32_i32_unreachable) + + ;; call_i32i32_i32 should be called with 4 or 5 followed by two numbers + ;; whose sum is the result on $0 == 4 or unreachable on 5. + (func $call_i32i32_i32 (export "call_i32i32_i32") (param i32 i32 i32) (result i32) + (call $invoke_iii (local.get 0) (local.get 1) (local.get 2))) + + (func $i32i32i32_i32 (param i32 i32 i32) (result i32) + (i32.add (i32.add (local.get 0) (local.get 1)) (local.get 2))) + (func $i32i32i32_i32_unreachable (param i32 i32 i32) (result i32) unreachable) + + (elem (i32.const 6) $i32i32i32_i32 $i32i32i32_i32_unreachable) + + ;; call_i32i32i32_i32 should be called with 6 or 7 followed by three numbers + ;; whose sum is the result on $0 == 6 or unreachable on 7. + (func $call_i32i32i32_i32 (export "call_i32i32i32_i32") (param i32 i32 i32 i32) (result i32) + (call $invoke_iiii (local.get 0) (local.get 1) (local.get 2) (local.get 3))) + + (func $i32i32i32i32_i32 (param i32 i32 i32 i32) (result i32) + (i32.add (i32.add (i32.add (local.get 0) (local.get 1)) (local.get 2)) (local.get 3))) + (func $i32i32i32i32_i32_unreachable (param i32 i32 i32 i32) (result i32) unreachable) + + (elem (i32.const 8) $i32i32i32i32_i32 $i32i32i32i32_i32_unreachable) + + ;; calli32_i32i32i32i32_i32 should be called with 8 or 9 followed by four numbers + ;; whose sum is the result on $0 == 8 or unreachable on 9. + (func $calli32_i32i32i32i32_i32 (export "calli32_i32i32i32i32_i32") (param i32 i32 i32 i32 i32) (result i32) + (call $invoke_iiiii (local.get 0) (local.get 1) (local.get 2) (local.get 3) (local.get 4))) + + (func $v_v) + (func $v_v_unreachable unreachable) + + (elem (i32.const 10) $v_v $v_v_unreachable) + + ;; call_v_v should be called with 10 or 11 and expect unreachable on 11. + (func $call_v_v (export "call_v_v") (param i32) + (call $invoke_v (local.get 0))) + + (func $i32_v (param i32)) + (func $i32_v_unreachable (param i32) unreachable) + + (elem (i32.const 12) $i32_v $i32_v_unreachable) + + ;; call_i32_v should be called with 12 or 13 followed by one number and + ;; expect unreachable on 2. + (func $call_i32_v (export "call_i32_v") (param i32 i32) + (call $invoke_vi (local.get 0) (local.get 1))) + + (func $i32i32_v (param i32 i32)) + (func $i32i32_v_unreachable (param i32 i32) unreachable) + + (elem (i32.const 14) $i32i32_v $i32i32_v_unreachable) + + ;; call_i32i32_v should be called with 14 or 15 followed by two numbers + ;; and expect unreachable on 15. + (func $call_i32i32_v (export "call_i32i32_v") (param i32 i32 i32) + (call $invoke_vii (local.get 0) (local.get 1) (local.get 2))) + + (func $i32i32i32_v (param i32 i32 i32)) + (func $i32i32i32_v_unreachable (param i32 i32 i32) unreachable) + + (elem (i32.const 16) $i32i32i32_v $i32i32i32_v_unreachable) + + ;; call_i32i32i32_v should be called with 16 or 17 followed by three numbers + ;; and expect unreachable on 17. + (func $call_i32i32i32_v (export "call_i32i32i32_v") (param i32 i32 i32 i32) + (call $invoke_viii (local.get 0) (local.get 1) (local.get 2) (local.get 3))) + + (func $i32i32i32i32_v (param i32 i32 i32 i32)) + (func $i32i32i32i32_v_unreachable (param i32 i32 i32 i32) unreachable) + + (elem (i32.const 18) $i32i32i32i32_v $i32i32i32i32_v_unreachable) + + ;; calli32_i32i32i32i32_v should be called with 18 or 19 followed by four + ;; numbers and expect unreachable on 19. + (func $calli32_i32i32i32i32_v (export "calli32_i32i32i32i32_v") (param i32 i32 i32 i32 i32) + (call $invoke_viiii (local.get 0) (local.get 1) (local.get 2) (local.get 3) (local.get 4))) +) diff --git a/imports/emscripten/emscripten.go b/imports/emscripten/emscripten.go index 2f904ff8..05cc05af 100644 --- a/imports/emscripten/emscripten.go +++ b/imports/emscripten/emscripten.go @@ -17,9 +17,12 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" + internal "github.com/tetratelabs/wazero/internal/emscripten" "github.com/tetratelabs/wazero/internal/wasm" ) +const i32 = wasm.ValueTypeI32 + // MustInstantiate calls Instantiate or panics on error. // // This is a simpler function for those who know the module "env" is not @@ -62,257 +65,16 @@ type functionExporter struct{} // ExportFunctions implements FunctionExporter.ExportFunctions func (functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) { exporter := builder.(wasm.HostFuncExporter) - exporter.ExportHostFunc(notifyMemoryGrowth) - exporter.ExportHostFunc(invokeI) - exporter.ExportHostFunc(invokeIi) - exporter.ExportHostFunc(invokeIii) - exporter.ExportHostFunc(invokeIiii) - exporter.ExportHostFunc(invokeIiiii) - exporter.ExportHostFunc(invokeV) - exporter.ExportHostFunc(invokeVi) - exporter.ExportHostFunc(invokeVii) - exporter.ExportHostFunc(invokeViii) - exporter.ExportHostFunc(invokeViiii) -} - -// emscriptenNotifyMemoryGrowth is called when wasm is compiled with -// `-s ALLOW_MEMORY_GROWTH` and a "memory.grow" instruction succeeded. -// The memoryIndex parameter will be zero until "multi-memory" is implemented. -// -// Note: This implementation is a no-op and can be overridden by users manually -// by redefining the same function. wazero will likely implement a generic -// memory growth hook obviating this as well. -// -// Here's the import in a user's module that ends up using this, in WebAssembly -// 1.0 (MVP) Text Format: -// -// (import "env" "emscripten_notify_memory_growth" -// (func $emscripten_notify_memory_growth (param $memory_index i32))) -// -// See https://github.com/emscripten-core/emscripten/blob/3.1.16/system/lib/standalone/standalone.c#L118 -// and https://emscripten.org/docs/api_reference/emscripten.h.html#abi-functions -const functionNotifyMemoryGrowth = "emscripten_notify_memory_growth" - -var notifyMemoryGrowth = &wasm.HostFunc{ - ExportName: functionNotifyMemoryGrowth, - Name: functionNotifyMemoryGrowth, - ParamTypes: []wasm.ValueType{wasm.ValueTypeI32}, - ParamNames: []string{"memory_index"}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {})}, -} - -// All `invoke_` functions have an initial "index" parameter of -// api.ValueTypeI32. This is the index of the desired funcref in the only table -// in the module. The type of the funcref is via naming convention. The first -// character after `invoke_` decides the result type: 'v' for no result or 'i' -// for api.ValueTypeI32. Any count of 'i' following that are api.ValueTypeI32 -// parameters. -// -// For example, the function `invoke_iiiii` signature has five parameters, but -// also five i's. The five 'i's mean there are four parameters -// -// (import "env" "invoke_iiiii" (func $invoke_iiiii -// (param i32 i32 i32 i32 i32) (result i32)))) -// -// So, the above function if invoked `invoke_iiiii(1234, 1, 2, 3, 4)` would -// look up the funcref at table index 1234, which has a type i32i32i3232_i32 -// and invoke it with the remaining parameters, -const ( - i32 = wasm.ValueTypeI32 - - functionInvokeI = "invoke_i" - functionInvokeIi = "invoke_ii" - functionInvokeIii = "invoke_iii" - functionInvokeIiii = "invoke_iiii" - functionInvokeIiiii = "invoke_iiiii" - - functionInvokeV = "invoke_v" - functionInvokeVi = "invoke_vi" - functionInvokeVii = "invoke_vii" - functionInvokeViii = "invoke_viii" - functionInvokeViiii = "invoke_viiii" -) - -var invokeI = &wasm.HostFunc{ - ExportName: functionInvokeI, - Name: functionInvokeI, - ParamTypes: []api.ValueType{i32}, - ParamNames: []string{"index"}, - ResultTypes: []api.ValueType{i32}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeIFn)}, -} - -func invokeIFn(ctx context.Context, mod api.Module, stack []uint64) { - ret, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_v_i32, wasm.Index(stack[0]), nil) - if err != nil { - panic(err) - } - stack[0] = ret[0] -} - -var invokeIi = &wasm.HostFunc{ - ExportName: functionInvokeIi, - Name: functionInvokeIi, - ParamTypes: []api.ValueType{i32, i32}, - ParamNames: []string{"index", "a1"}, - ResultTypes: []api.ValueType{i32}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeIiFn)}, -} - -func invokeIiFn(ctx context.Context, mod api.Module, stack []uint64) { - ret, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32_i32, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } - stack[0] = ret[0] -} - -var invokeIii = &wasm.HostFunc{ - ExportName: functionInvokeIii, - Name: functionInvokeIii, - ParamTypes: []api.ValueType{i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2"}, - ResultTypes: []api.ValueType{i32}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeIiiFn)}, -} - -func invokeIiiFn(ctx context.Context, mod api.Module, stack []uint64) { - ret, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32_i32, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } - stack[0] = ret[0] -} - -var invokeIiii = &wasm.HostFunc{ - ExportName: functionInvokeIiii, - Name: functionInvokeIiii, - ParamTypes: []api.ValueType{i32, i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2", "a3"}, - ResultTypes: []api.ValueType{i32}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeIiiiFn)}, -} - -func invokeIiiiFn(ctx context.Context, mod api.Module, stack []uint64) { - ret, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32i32_i32, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } - stack[0] = ret[0] -} - -var invokeIiiii = &wasm.HostFunc{ - ExportName: functionInvokeIiiii, - Name: functionInvokeIiiii, - ParamTypes: []api.ValueType{i32, i32, i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2", "a3", "a4"}, - ResultTypes: []api.ValueType{i32}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeIiiiiFn)}, -} - -func invokeIiiiiFn(ctx context.Context, mod api.Module, stack []uint64) { - ret, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32i32i32_i32, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } - stack[0] = ret[0] -} - -var invokeV = &wasm.HostFunc{ - ExportName: functionInvokeV, - Name: functionInvokeV, - ParamTypes: []api.ValueType{i32}, - ParamNames: []string{"index"}, - ResultTypes: []api.ValueType{}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeVFn)}, -} - -func invokeVFn(ctx context.Context, mod api.Module, stack []uint64) { - _, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_v_v, wasm.Index(stack[0]), nil) - if err != nil { - panic(err) - } -} - -var invokeVi = &wasm.HostFunc{ - ExportName: functionInvokeVi, - Name: functionInvokeVi, - ParamTypes: []api.ValueType{i32, i32}, - ParamNames: []string{"index", "a1"}, - ResultTypes: []api.ValueType{}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeViFn)}, -} - -func invokeViFn(ctx context.Context, mod api.Module, stack []uint64) { - _, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32_v, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } -} - -var invokeVii = &wasm.HostFunc{ - ExportName: functionInvokeVii, - Name: functionInvokeVii, - ParamTypes: []api.ValueType{i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2"}, - ResultTypes: []api.ValueType{}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeViiFn)}, -} - -func invokeViiFn(ctx context.Context, mod api.Module, stack []uint64) { - _, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32_v, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } -} - -var invokeViii = &wasm.HostFunc{ - ExportName: functionInvokeViii, - Name: functionInvokeViii, - ParamTypes: []api.ValueType{i32, i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2", "a3"}, - ResultTypes: []api.ValueType{}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeViiiFn)}, -} - -func invokeViiiFn(ctx context.Context, mod api.Module, stack []uint64) { - _, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32i32_v, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } -} - -var invokeViiii = &wasm.HostFunc{ - ExportName: functionInvokeViiii, - Name: functionInvokeViiii, - ParamTypes: []api.ValueType{i32, i32, i32, i32, i32}, - ParamNames: []string{"index", "a1", "a2", "a3", "a4"}, - ResultTypes: []api.ValueType{}, - Code: wasm.Code{GoFunc: api.GoModuleFunc(invokeViiiiFn)}, -} - -func invokeViiiiFn(ctx context.Context, mod api.Module, stack []uint64) { - _, err := callDynamic(ctx, mod.(*wasm.ModuleInstance), wasm.PreAllocatedTypeID_i32i32i32i32_v, wasm.Index(stack[0]), stack[1:]) - if err != nil { - panic(err) - } -} - -// callDynamic special cases dynamic calls needed for emscripten `invoke_` -// functions such as `invoke_ii` or `invoke_v`. -// -// # Parameters -// -// - ctx: the propagated go context. -// - m: the incoming module instance of the `invoke_` function. -// - typeID: used to type check on indirect calls. -// - tableOffset: position in the module's only table -// - params: parameters to the funcref -func callDynamic(ctx context.Context, m *wasm.ModuleInstance, typeID wasm.FunctionTypeID, tableOffset wasm.Index, params []uint64) (results []uint64, err error) { - t := m.Tables[0] // Emscripten doesn't use multiple tables - idx, err := m.Engine.LookupFunction(t, typeID, tableOffset) - if err != nil { - return nil, err - } - return m.Engine.NewFunction(idx).Call(ctx, params...) + exporter.ExportHostFunc(internal.NotifyMemoryGrowth) + + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_i", []api.ValueType{i32}, []api.ValueType{i32})) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_ii", []api.ValueType{i32, i32}, []api.ValueType{i32})) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_iii", []api.ValueType{i32, i32, i32}, []api.ValueType{i32})) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_iiii", []api.ValueType{i32, i32, i32, i32}, []api.ValueType{i32})) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_iiiii", []api.ValueType{i32, i32, i32, i32, i32}, []api.ValueType{i32})) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_v", []api.ValueType{i32}, nil)) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_vi", []api.ValueType{i32, i32}, nil)) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_vii", []api.ValueType{i32, i32, i32}, nil)) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_viii", []api.ValueType{i32, i32, i32, i32}, nil)) + exporter.ExportHostFunc(internal.NewInvokeFunc("invoke_viiii", []api.ValueType{i32, i32, i32, i32, i32}, nil)) } diff --git a/internal/emscripten/emscripten.go b/internal/emscripten/emscripten.go new file mode 100644 index 00000000..b014d09c --- /dev/null +++ b/internal/emscripten/emscripten.go @@ -0,0 +1,95 @@ +package emscripten + +import ( + "context" + "strconv" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const FunctionNotifyMemoryGrowth = "emscripten_notify_memory_growth" + +var NotifyMemoryGrowth = &wasm.HostFunc{ + ExportName: FunctionNotifyMemoryGrowth, + Name: FunctionNotifyMemoryGrowth, + ParamTypes: []wasm.ValueType{wasm.ValueTypeI32}, + ParamNames: []string{"memory_index"}, + Code: wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {})}, +} + +// InvokePrefix is the naming convention of Emscripten dynamic functions. +// +// All `invoke_` functions have an initial "index" parameter of +// api.ValueTypeI32. This is the index of the desired funcref in the only table +// in the module. The type of the funcref is via naming convention. The first +// character after `invoke_` decides the result type: 'v' for no result or 'i' +// for api.ValueTypeI32. Any count of 'i' following that are api.ValueTypeI32 +// parameters. +// +// For example, the function `invoke_iiiii` signature has five parameters, but +// also five i's. The five 'i's mean there are four parameters +// +// (import "env" "invoke_iiiii" (func $invoke_iiiii +// (param i32 i32 i32 i32 i32) (result i32)))) +// +// So, the above function if invoked `invoke_iiiii(1234, 1, 2, 3, 4)` would +// look up the funcref at table index 1234, which has a type i32i32i3232_i32 +// and invoke it with the remaining parameters. +const InvokePrefix = "invoke_" + +func NewInvokeFunc(importName string, params, results []api.ValueType) *wasm.HostFunc { + // The type we invoke is the same type as the import except without the + // index parameter. + fn := &InvokeFunc{&wasm.FunctionType{Results: results}} + if len(params) > 1 { + fn.FunctionType.Params = params[1:] + } + + // Now, make friendly parameter names. + paramNames := make([]string, len(params)) + paramNames[0] = "index" + for i := 1; i < len(paramNames); i++ { + paramNames[i] = "a" + strconv.Itoa(i) + } + return &wasm.HostFunc{ + ExportName: importName, + ParamTypes: params, + ParamNames: paramNames, + ResultTypes: results, + Code: wasm.Code{GoFunc: fn}, + } +} + +type InvokeFunc struct { + *wasm.FunctionType +} + +// Call implements api.GoModuleFunction by special casing dynamic calls needed +// for emscripten `invoke_` functions such as `invoke_ii` or `invoke_v`. +func (v *InvokeFunc) Call(ctx context.Context, mod api.Module, stack []uint64) { + m := mod.(*wasm.ModuleInstance) + + // Lookup the type of the function we are calling indirectly. + typeID, err := m.GetFunctionTypeID(v.FunctionType) + if err != nil { + panic(err) + } + + tableOffset := wasm.Index(stack[0]) // position in the module's only table. + params := stack[1:] // parameters to the dynamic function being called + + // Lookup the table index we will call. + t := m.Tables[0] // Note: Emscripten doesn't use multiple tables + idx, err := m.Engine.LookupFunction(t, typeID, tableOffset) + if err != nil { + panic(err) + } + + ret, err := m.Engine.NewFunction(idx).Call(ctx, params...) + if err != nil { + panic(err) + } + // if there are any results, copy them back to the stack + copy(stack, ret) +} diff --git a/internal/wasm/store.go b/internal/wasm/store.go index bc6bc627..9aac46fa 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -153,6 +153,11 @@ type ( // The wazero specific limitations described at RATIONALE.md. const maximumFunctionTypes = 1 << 27 +// GetFunctionTypeID is used by emscripten. +func (m *ModuleInstance) GetFunctionTypeID(t *FunctionType) (FunctionTypeID, error) { + return m.s.GetFunctionTypeID(t) +} + func (m *ModuleInstance) buildElementInstances(elements []ElementSegment) { m.ElementInstances = make([]ElementInstance, len(elements)) for i, elm := range elements { @@ -267,16 +272,12 @@ func (m *ModuleInstance) getExport(name string, et ExternType) (*Export, error) } func NewStore(enabledFeatures api.CoreFeatures, engine Engine) *Store { - typeIDs := make(map[string]FunctionTypeID, len(preAllocatedTypeIDs)) - for k, v := range preAllocatedTypeIDs { - typeIDs[k] = v - } return &Store{ nameToModule: map[string]*ModuleInstance{}, nameToModuleCap: nameToModuleShrinkThreshold, EnabledFeatures: enabledFeatures, Engine: engine, - typeIDs: typeIDs, + typeIDs: map[string]FunctionTypeID{}, functionMaxTypes: maximumFunctionTypes, } } @@ -544,7 +545,7 @@ func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) ret := make([]FunctionTypeID, len(ts)) for i := range ts { t := &ts[i] - inst, err := s.getFunctionTypeID(t) + inst, err := s.GetFunctionTypeID(t) if err != nil { return nil, err } @@ -553,46 +554,7 @@ func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) return ret, nil } -// preAllocatedTypeIDs maps several "well-known" FunctionType strings to the pre allocated FunctionID. -// This is used by emscripten integration, but it is harmless to have this all the time as it's only -// used during Store creation. -var preAllocatedTypeIDs = map[string]FunctionTypeID{ - "i32i32i32i32_v": PreAllocatedTypeID_i32i32i32i32_v, - "i32i32i32_v": PreAllocatedTypeID_i32i32i32_v, - "i32i32_v": PreAllocatedTypeID_i32i32_v, - "i32_v": PreAllocatedTypeID_i32_v, - "v_v": PreAllocatedTypeID_v_v, - "i32i32i32i32_i32": PreAllocatedTypeID_i32i32i32i32_i32, - "i32i32i32_i32": PreAllocatedTypeID_i32i32i32_i32, - "i32i32_i32": PreAllocatedTypeID_i32i32_i32, - "i32_i32": PreAllocatedTypeID_i32_i32, - "v_i32": PreAllocatedTypeID_v_i32, -} - -const ( - // PreAllocatedTypeID_i32i32i32i32_v is FunctionTypeID for i32i32i32i32_v. - PreAllocatedTypeID_i32i32i32i32_v FunctionTypeID = iota - // PreAllocatedTypeID_i32i32i32_v is FunctionTypeID for i32i32i32_v - PreAllocatedTypeID_i32i32i32_v - // PreAllocatedTypeID_i32i32_v is FunctionTypeID for i32i32_v - PreAllocatedTypeID_i32i32_v - // PreAllocatedTypeID_i32_v is FunctionTypeID for i32_v - PreAllocatedTypeID_i32_v - // PreAllocatedTypeID_v_v is FunctionTypeID for v_v - PreAllocatedTypeID_v_v - // PreAllocatedTypeID_i32i32i32i32_i32 is FunctionTypeID for i32i32i32i32_i32 - PreAllocatedTypeID_i32i32i32i32_i32 - // PreAllocatedTypeID_i32i32i32_i32 is FunctionTypeID for i32i32i32_i32 - PreAllocatedTypeID_i32i32i32_i32 - // PreAllocatedTypeID_i32i32_i32 is FunctionTypeID for i32i32_i32 - PreAllocatedTypeID_i32i32_i32 - // PreAllocatedTypeID_i32_i32 is FunctionTypeID for i32_i32 - PreAllocatedTypeID_i32_i32 - // PreAllocatedTypeID_v_i32 is FunctionTypeID for v_i32 - PreAllocatedTypeID_v_i32 -) - -func (s *Store) getFunctionTypeID(t *FunctionType) (FunctionTypeID, error) { +func (s *Store) GetFunctionTypeID(t *FunctionType) (FunctionTypeID, error) { s.mux.RLock() key := t.key() id, ok := s.typeIDs[key] diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 2bb1af1c..0c9fe047 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -93,16 +93,6 @@ func TestModuleInstance_Memory(t *testing.T) { } } -func TestNewStore(t *testing.T) { - s := NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1}) - // Ensures that a newly created store has the pre allocated type IDs. - for k, v := range preAllocatedTypeIDs { - actual, ok := s.typeIDs[k] - require.True(t, ok) - require.Equal(t, v, actual) - } -} - func TestStore_Instantiate(t *testing.T) { s := newStore() m, err := NewHostModule( @@ -513,7 +503,7 @@ func TestStore_getFunctionTypeID(t *testing.T) { for i := 0; i < max; i++ { s.typeIDs[strconv.Itoa(i)] = 0 } - _, err := s.getFunctionTypeID(&FunctionType{}) + _, err := s.GetFunctionTypeID(&FunctionType{}) require.Error(t, err) }) t.Run("ok", func(t *testing.T) { @@ -528,7 +518,7 @@ func TestStore_getFunctionTypeID(t *testing.T) { tc := tt t.Run(tc.String(), func(t *testing.T) { s := newStore() - actual, err := s.getFunctionTypeID(&tc) + actual, err := s.GetFunctionTypeID(&tc) require.NoError(t, err) expectedTypeID, ok := s.typeIDs[tc.String()] @@ -986,13 +976,3 @@ func TestModuleInstance_applyElementsapplyElements(t *testing.T) { m.Tables[0].References) }) } - -// TestPreAllocatedTypeIDs ensures that PreAllocatedTypeIDs has no duplication on the values (FunctionTypeID). -func TestPreAllocatedTypeIDs(t *testing.T) { - exists := make(map[FunctionTypeID]struct{}, len(preAllocatedTypeIDs)) - for _, v := range preAllocatedTypeIDs { - _, ok := exists[v] - require.False(t, ok) - exists[v] = struct{}{} - } -} diff --git a/runtime.go b/runtime.go index 34bc51f7..475938f0 100644 --- a/runtime.go +++ b/runtime.go @@ -23,6 +23,12 @@ import ( // defer r.Close(ctx) // This closes everything this Runtime created. // // mod, _ := r.Instantiate(ctx, wasm) +// +// # Notes +// +// - Closing this closes any CompiledModule or Module it instantiated. +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. type Runtime interface { // Instantiate instantiates a module from the WebAssembly binary (%.wasm) // with default configuration.