Adds HostFunctionBuilder to enable high performance host functions (#828)

This PR follows @hafeidejiangyou advice to not only enable end users to
avoid reflection when calling host functions, but also use that approach
ourselves internally. The performance results are staggering and will be
noticable in high performance applications.

Before
```
BenchmarkHostCall/Call
BenchmarkHostCall/Call-16            	 1000000	      1050 ns/op
Benchmark_EnvironGet/environGet
Benchmark_EnvironGet/environGet-16         	  525492	      2224 ns/op
```

Now
```
BenchmarkHostCall/Call
BenchmarkHostCall/Call-16            	14807203	        83.22 ns/op
Benchmark_EnvironGet/environGet
Benchmark_EnvironGet/environGet-16         	  951690	      1054 ns/op
```

To accomplish this, this PR consolidates code around host function
definition and enables a fast path for functions where the user takes
responsibility for defining its WebAssembly mappings. Existing users
will need to change their code a bit, as signatures have changed.

For example, we are now more strict that all host functions require a
context parameter zero. Also, we've replaced
`HostModuleBuilder.ExportFunction` and `ExportFunctions` with a new type
`HostFunctionBuilder` that consolidates the responsibility and the
documentation.

```diff
 ctx := context.Background()
-hello := func() {
+hello := func(context.Context) {
         fmt.Fprintln(stdout, "hello!")
 }
-_, err := r.NewHostModuleBuilder("env").ExportFunction("hello", hello).Instantiate(ctx, r)
+_, err := r.NewHostModuleBuilder("env").
+        NewFunctionBuilder().WithFunc(hello).Export("hello").
+        Instantiate(ctx, r)
```

Power users can now use `HostFunctionBuilder` to define functions that
won't use reflection. There are two choices of interfaces to use
depending on if that function needs access to the calling module or not:
`api.GoFunction` and `api.GoModuleFunction`. Here's an example defining
one.

```go
builder.WithGoFunction(api.GoFunc(func(ctx context.Context, params []uint64) []uint64 {
	x, y := uint32(params[0]), uint32(params[1])
	sum := x + y
	return []uint64{sum}
}, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
```
As you'll notice and as documented, this approach is more verbose and
not for everyone. If you aren't making a low-level library, you are
likely able to afford the 1us penalty for the convenience of reflection.
However, we are happy to enable this option for foundational libraries
and those with high performance requirements (like ourselves)!

Fixes #825

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-10-28 07:51:08 -07:00
committed by GitHub
parent 2173f30758
commit be33572289
62 changed files with 2130 additions and 1544 deletions

View File

@@ -1,6 +1,7 @@
package wazero
import (
"context"
"testing"
"github.com/tetratelabs/wazero/api"
@@ -12,13 +13,20 @@ import (
func TestNewHostModuleBuilder_Compile(t *testing.T) {
i32, i64 := api.ValueTypeI32, api.ValueTypeI64
uint32_uint32 := func(uint32) uint32 {
uint32_uint32 := func(context.Context, uint32) uint32 {
return 0
}
uint64_uint32 := func(uint64) uint32 {
uint64_uint32 := func(context.Context, uint64) uint32 {
return 0
}
gofunc1 := api.GoFunc(func(ctx context.Context, params []uint64) []uint64 {
return []uint64{0}
})
gofunc2 := api.GoFunc(func(ctx context.Context, params []uint64) []uint64 {
return []uint64{0}
})
tests := []struct {
name string
input func(Runtime) HostModuleBuilder
@@ -39,16 +47,17 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}},
},
{
name: "ExportFunction",
name: "WithFunc",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").ExportFunction("1", uint32_uint32)
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint32_uint32)},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
@@ -58,16 +67,19 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
},
},
{
name: "ExportFunction with names",
name: "WithFunc WithName WithParameterNames",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").ExportFunction("1", uint32_uint32, "get", "x")
return r.NewHostModuleBuilder("").NewFunctionBuilder().
WithFunc(uint32_uint32).
WithName("get").WithParameterNames("x").
Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint32_uint32)},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
@@ -78,16 +90,18 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
},
},
{
name: "ExportFunction overwrites existing",
name: "WithFunc overwrites existing",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").ExportFunction("1", uint32_uint32).ExportFunction("1", uint64_uint32)
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1").
NewFunctionBuilder().WithFunc(uint64_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint64_uint32)},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32)},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
@@ -97,10 +111,12 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
},
},
{
name: "ExportFunction twice",
name: "WithFunc twice",
input: func(r Runtime) HostModuleBuilder {
// Intentionally out of order
return r.NewHostModuleBuilder("").ExportFunction("2", uint64_uint32).ExportFunction("1", uint32_uint32)
return r.NewHostModuleBuilder("").
NewFunctionBuilder().WithFunc(uint64_uint32).Export("2").
NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
@@ -108,7 +124,7 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0, 1},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint32_uint32), wasm.MustParseGoFuncCode(uint64_uint32)},
CodeSection: []*wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32), wasm.MustParseGoReflectFuncCode(uint64_uint32)},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
@@ -119,37 +135,92 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
},
},
{
name: "ExportFunctions",
name: "WithGoFunction",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").ExportFunctions(map[string]interface{}{
"1": uint32_uint32,
"2": uint64_uint32,
})
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0, 1},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint32_uint32), wasm.MustParseGoFuncCode(uint64_uint32)},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{IsHostFunction: true, GoFunc: gofunc1},
},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "ExportFunctions overwrites",
name: "WithGoFunction WithName WithParameterNames",
input: func(r Runtime) HostModuleBuilder {
b := r.NewHostModuleBuilder("").ExportFunction("1", uint64_uint32)
return b.ExportFunctions(map[string]interface{}{
"1": uint32_uint32,
"2": uint64_uint32,
})
return r.NewHostModuleBuilder("").NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
WithName("get").WithParameterNames("x").
Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{IsHostFunction: true, GoFunc: gofunc1},
},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
LocalNames: []*wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
},
},
},
{
name: "WithGoFunction overwrites existing",
input: func(r Runtime) HostModuleBuilder {
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1").
NewFunctionBuilder().
WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{
{IsHostFunction: true, GoFunc: gofunc2},
},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
},
},
},
{
name: "WithGoFunction twice",
input: func(r Runtime) HostModuleBuilder {
// Intentionally out of order
return r.NewHostModuleBuilder("").
NewFunctionBuilder().
WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
Export("2").
NewFunctionBuilder().
WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
Export("1")
},
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
@@ -157,7 +228,10 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
},
FunctionSection: []wasm.Index{0, 1},
CodeSection: []*wasm.Code{wasm.MustParseGoFuncCode(uint32_uint32), wasm.MustParseGoFuncCode(uint64_uint32)},
CodeSection: []*wasm.Code{
{IsHostFunction: true, GoFunc: gofunc1},
{IsHostFunction: true, GoFunc: gofunc2},
},
ExportSection: []*wasm.Export{
{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
@@ -204,12 +278,12 @@ func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) {
{
name: "error compiling", // should fail due to missing result.
input: func(rt Runtime) HostModuleBuilder {
return rt.NewHostModuleBuilder("").
ExportFunction("fn", &wasm.HostFunc{
return rt.NewHostModuleBuilder("").NewFunctionBuilder().
WithFunc(&wasm.HostFunc{
ExportNames: []string{"fn"},
ResultTypes: []wasm.ValueType{wasm.ValueTypeI32},
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeEnd}},
})
}).Export("fn")
},
expectedErr: `invalid function[0] export["fn"]: not enough results
have ()
@@ -275,8 +349,7 @@ func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) {
for i, c := range expected.CodeSection {
actualCode := actual.CodeSection[i]
require.True(t, actualCode.IsHostFunction)
require.Equal(t, c.Kind, actualCode.Kind)
require.Equal(t, c.GoFunc.Type(), actualCode.GoFunc.Type())
require.Equal(t, c.GoFunc, actualCode.GoFunc)
// Not wasm
require.Nil(t, actualCode.Body)