emscripten: adds experimental InstantiateForModule for invoke exports (#1372)

This adds emscripten.InstantiateForModule into the experimental package.
This builds dynamic invoke exports in the same order and only matching
those needed by the importing modules.

Finally, this removes special casing of function type IDs by deferring
resolution of them only in Emscripten.

Fixes #1364

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-04-18 09:13:50 +02:00
committed by GitHub
parent 2a2e07a91f
commit df0faa5d16
9 changed files with 795 additions and 321 deletions

View File

@@ -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)
}
}

View File

@@ -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)
})
}
}

Binary file not shown.

View File

@@ -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)))
)

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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]

View File

@@ -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{}{}
}
}

View File

@@ -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.