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>
223 lines
7.3 KiB
Go
223 lines
7.3 KiB
Go
package wasm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
// compile time check to ensure CallContext implements api.Module
|
|
var _ api.Module = &CallContext{}
|
|
|
|
func NewCallContext(ns *Namespace, instance *ModuleInstance, sys *internalsys.Context) *CallContext {
|
|
zero := uint64(0)
|
|
return &CallContext{memory: instance.Memory, module: instance, ns: ns, Sys: sys, closed: &zero}
|
|
}
|
|
|
|
// CallContext is a function call context bound to a module. This is important as one module's functions can call
|
|
// imported functions, but all need to effect the same memory.
|
|
//
|
|
// Note: This does not include the context.Context because doing so risks caching the wrong context which can break
|
|
// functionality like trace propagation.
|
|
// Note: this also implements api.Module in order to simplify usage as a host function parameter.
|
|
type CallContext struct {
|
|
// TODO: We've never found a great name for this. It is only used for function calls, hence CallContext, but it
|
|
// moves on a different axis than, for example, the context.Context. context.Context is the same root for the whole
|
|
// call stack, where the CallContext can change depending on where memory is defined and who defines the calling
|
|
// function. When we rename this again, we should try to capture as many key points possible on the docs.
|
|
|
|
module *ModuleInstance
|
|
// memory is returned by Memory and overridden WithMemory
|
|
memory api.Memory
|
|
ns *Namespace
|
|
|
|
// Sys is exposed for use in special imports such as WASI, assemblyscript
|
|
// and gojs.
|
|
//
|
|
// # Notes
|
|
//
|
|
// - This is a part of CallContext so that scope and Close is coherent.
|
|
// - This is not exposed outside this repository (as a host function
|
|
// parameter) because we haven't thought through capabilities based
|
|
// security implications.
|
|
Sys *internalsys.Context
|
|
|
|
// closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code.
|
|
//
|
|
// The update value is 1 + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed.
|
|
//
|
|
// Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations.
|
|
// See /RATIONALE.md
|
|
closed *uint64
|
|
|
|
// CodeCloser is non-nil when the code should be closed after this module.
|
|
CodeCloser api.Closer
|
|
}
|
|
|
|
// FailIfClosed returns a sys.ExitError if CloseWithExitCode was called.
|
|
func (m *CallContext) FailIfClosed() error {
|
|
if closed := atomic.LoadUint64(m.closed); closed != 0 {
|
|
return sys.NewExitError(m.module.Name, uint32(closed>>32)) // Unpack the high order bits as the exit code.
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Name implements the same method as documented on api.Module
|
|
func (m *CallContext) Name() string {
|
|
return m.module.Name
|
|
}
|
|
|
|
// WithMemory allows overriding memory without re-allocation when the result would be the same.
|
|
func (m *CallContext) WithMemory(memory *MemoryInstance) *CallContext {
|
|
if memory != nil && memory != m.memory { // only re-allocate if it will change the effective memory
|
|
return &CallContext{module: m.module, memory: memory, Sys: m.Sys, closed: m.closed}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// String implements the same method as documented on api.Module
|
|
func (m *CallContext) String() string {
|
|
return fmt.Sprintf("Module[%s]", m.Name())
|
|
}
|
|
|
|
// Close implements the same method as documented on api.Module.
|
|
func (m *CallContext) Close(ctx context.Context) (err error) {
|
|
return m.CloseWithExitCode(ctx, 0)
|
|
}
|
|
|
|
// CloseWithExitCode implements the same method as documented on api.Module.
|
|
func (m *CallContext) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
|
|
closed, err := m.close(ctx, exitCode)
|
|
if !closed {
|
|
return nil
|
|
}
|
|
m.ns.deleteModule(m.Name())
|
|
if m.CodeCloser == nil {
|
|
return err
|
|
}
|
|
if e := m.CodeCloser.Close(ctx); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
return err
|
|
}
|
|
|
|
// close marks this CallContext as closed and releases underlying system resources.
|
|
//
|
|
// Note: The caller is responsible for removing the module from the Namespace.
|
|
func (m *CallContext) close(ctx context.Context, exitCode uint32) (c bool, err error) {
|
|
closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits.
|
|
if !atomic.CompareAndSwapUint64(m.closed, 0, closed) {
|
|
return false, nil
|
|
}
|
|
c = true
|
|
if sysCtx := m.Sys; sysCtx != nil { // nil if from HostModuleBuilder
|
|
err = sysCtx.FS(ctx).Close(ctx)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Memory implements the same method as documented on api.Module.
|
|
func (m *CallContext) Memory() api.Memory {
|
|
return m.module.Memory
|
|
}
|
|
|
|
// ExportedMemory implements the same method as documented on api.Module.
|
|
func (m *CallContext) ExportedMemory(name string) api.Memory {
|
|
exp, err := m.module.getExport(name, ExternTypeMemory)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return exp.Memory
|
|
}
|
|
|
|
// ExportedFunction implements the same method as documented on api.Module.
|
|
func (m *CallContext) ExportedFunction(name string) api.Function {
|
|
exp, err := m.module.getExport(name, ExternTypeFunc)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
fi := exp.Function
|
|
ce, err := exp.Function.Module.Engine.NewCallEngine(m, fi)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if exp.Function.Module == m.module {
|
|
return &function{fi: fi, ce: ce}
|
|
} else {
|
|
return &importedFn{importingModule: m, importedFn: fi, ce: ce}
|
|
}
|
|
}
|
|
|
|
// function implements api.Function. This couples FunctionInstance with CallEngine so that
|
|
// it can be used to make function calls originating from the FunctionInstance.
|
|
type function struct {
|
|
fi *FunctionInstance
|
|
ce CallEngine
|
|
}
|
|
|
|
// Definition implements the same method as documented on api.FunctionDefinition.
|
|
func (f *function) Definition() api.FunctionDefinition {
|
|
return f.fi.Definition
|
|
}
|
|
|
|
// Call implements the same method as documented on api.Function.
|
|
func (f *function) Call(ctx context.Context, params ...uint64) (ret []uint64, err error) {
|
|
return f.ce.Call(ctx, f.fi.Module.CallCtx, params)
|
|
}
|
|
|
|
// importedFn implements api.Function and ensures the call context of an imported function is the importing module.
|
|
type importedFn struct {
|
|
ce CallEngine
|
|
importingModule *CallContext
|
|
importedFn *FunctionInstance
|
|
}
|
|
|
|
// Definition implements the same method as documented on api.Function.
|
|
func (f *importedFn) Definition() api.FunctionDefinition {
|
|
return f.importedFn.Definition
|
|
}
|
|
|
|
// Call implements the same method as documented on api.Function.
|
|
func (f *importedFn) Call(ctx context.Context, params ...uint64) (ret []uint64, err error) {
|
|
if f.importedFn.IsHostFunction {
|
|
return nil, fmt.Errorf("directly calling host function is not supported")
|
|
}
|
|
mod := f.importingModule
|
|
return f.ce.Call(ctx, mod, params)
|
|
}
|
|
|
|
// GlobalVal is an internal hack to get the lower 64 bits of a global.
|
|
func (m *CallContext) GlobalVal(idx Index) uint64 {
|
|
return m.module.Globals[idx].Val
|
|
}
|
|
|
|
// ExportedGlobal implements the same method as documented on api.Module.
|
|
func (m *CallContext) ExportedGlobal(name string) api.Global {
|
|
exp, err := m.module.getExport(name, ExternTypeGlobal)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if exp.Global.Type.Mutable {
|
|
return &mutableGlobal{exp.Global}
|
|
}
|
|
valType := exp.Global.Type.ValType
|
|
switch valType {
|
|
case ValueTypeI32:
|
|
return globalI32(exp.Global.Val)
|
|
case ValueTypeI64:
|
|
return globalI64(exp.Global.Val)
|
|
case ValueTypeF32:
|
|
return globalF32(exp.Global.Val)
|
|
case ValueTypeF64:
|
|
return globalF64(exp.Global.Val)
|
|
default:
|
|
panic(fmt.Errorf("BUG: unknown value type %X", valType))
|
|
}
|
|
}
|