Renames ModuleBuilder to HostModuleBuilder and drops memory and globals (#812)

We at one point considered making `ModuleBuilder` create complete
WebAssembly binaries. However, we recently spun out
[wabin](https://github.com/tetratelabs/wabin), which allows this.

Meanwhile, the features in `ModuleBuilder` were confusing and misused.
For example, the only two cases memory was exported on GitHub were done
by accident. This is because host functions act on the guest's memory,
not their own.

Hence, this removes memory and globals from host side definitions, and
renames the type to HostModuleBuilder to clarify this is not ever going
to be used to construct normal Wasm binaries.

Most importantly, this simplifies the API and reduces a lot of code. It
is important to make changes like this, particularly deleting any
experimental things that didn't end up useful.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Anuraag Agrawal <anuraaga@gmail.com>
This commit is contained in:
Crypt Keeper
2022-09-28 14:42:14 +08:00
committed by GitHub
parent 06e2cd3757
commit 429334cf98
38 changed files with 196 additions and 703 deletions

View File

@@ -2,15 +2,16 @@ package wazero
import (
"context"
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
)
// ModuleBuilder is a way to define a WebAssembly Module in Go.
// HostModuleBuilder is a way to define host functions (in Go), so that a
// WebAssembly binary (ex. %.wasm file) can import and use them.
//
// Specifically, this implements the host side of an Application Binary
// Interface (ABI) like WASI or AssemblyScript.
//
// Ex. Below defines and instantiates a module named "env" with one function:
//
@@ -21,30 +22,47 @@ import (
// hello := func() {
// fmt.Fprintln(stdout, "hello!")
// }
// env, _ := r.NewModuleBuilder("env").
// env, _ := r.NewHostModuleBuilder("env").
// ExportFunction("hello", hello).
// Instantiate(ctx, r)
//
// If the same module may be instantiated multiple times, it is more efficient
// to separate steps. Ex.
//
// compiled, _ := r.NewModuleBuilder("env").
// compiled, _ := r.NewHostModuleBuilder("env").
// ExportFunction("get_random_string", getRandomString).
// Compile(ctx, wazero.NewCompileConfig())
// Compile(ctx)
//
// env1, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.1"))
//
// env2, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.2"))
//
// # Memory
//
// All host functions act on the importing api.Module, including any memory
// exported in its binary (%.wasm file). If you are reading or writing memory,
// it is sand-boxed Wasm memory defined by the guest.
//
// Below, `m` is the importing module, defined in Wasm. `fn` is a host function
// added via ExportFunction. This means that `x` was read from memory defined
// in Wasm, not arbitrary memory in the process.
//
// fn := func(ctx context.Context, m api.Module, offset uint32) uint32 {
// x, _ := m.Memory().ReadUint32Le(ctx, offset)
// return x
// }
//
// See ExportFunction for valid host function signatures and other details.
//
// # Notes
//
// - ModuleBuilder is mutable: each method returns the same instance for
// - HostModuleBuilder is mutable: each method returns the same instance for
// chaining.
// - methods do not return errors, to allow chaining. Any validation errors
// are deferred until Compile.
// - Insertion order is not retained. Anything defined by this builder is
// sorted lexicographically on Compile.
type ModuleBuilder interface {
type HostModuleBuilder interface {
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
// ExportFunction adds a function written in Go, which a WebAssembly module can import.
@@ -66,7 +84,7 @@ type ModuleBuilder interface {
// builder.ExportFunction("abort", env.abort, "~lib/builtins/abort",
// "message", "fileName", "lineNumber", "columnNumber")
//
// Valid Signature
// # Valid Signature
//
// Noting a context exception described later, all parameters or result
// types must match WebAssembly 1.0 (20191205) value types. This means
@@ -108,90 +126,15 @@ type ModuleBuilder interface {
// --snip--
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2
ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder
ExportFunction(exportName string, goFunc interface{}, names ...string) HostModuleBuilder
// ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map.
ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder
// ExportMemory adds linear memory, which a WebAssembly module can import and become available via api.Memory.
// If a memory is already exported with the same name, this overwrites it.
//
// # Parameters
//
// - name - the name to export. Ex "memory" for wasi_snapshot_preview1.ModuleSnapshotPreview1
// - minPages - the possibly zero initial size in pages (65536 bytes per page).
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (memory (export "memory") 1)
// builder.ExportMemory(1)
//
// # Notes
//
// - This is allowed to grow to (4GiB) limited by api.MemorySizer. To bound it, use ExportMemoryWithMax.
// - Version 1.0 (20191205) of the WebAssembly spec allows at most one memory per module.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
ExportMemory(name string, minPages uint32) ModuleBuilder
// ExportMemoryWithMax is like ExportMemory, but can prevent overuse of memory.
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (memory (export "memory") 1 1)
// builder.ExportMemoryWithMax(1, 1)
//
// Note: api.MemorySizer determines the capacity.
ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder
// ExportGlobalI32 exports a global constant of type api.ValueTypeI32.
// If a global is already exported with the same name, this overwrites it.
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (global (export "canvas_width") i32 (i32.const 1024))
// builder.ExportGlobalI32("canvas_width", 1024)
//
// Note: The maximum value of v is math.MaxInt32 to match constraints of initialization in binary format.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0 and
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
ExportGlobalI32(name string, v int32) ModuleBuilder
// ExportGlobalI64 exports a global constant of type api.ValueTypeI64.
// If a global is already exported with the same name, this overwrites it.
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (global (export "start_epoch") i64 (i64.const 1620216263544))
// builder.ExportGlobalI64("start_epoch", 1620216263544)
//
// Note: The maximum value of v is math.MaxInt64 to match constraints of initialization in binary format.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0 and
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
ExportGlobalI64(name string, v int64) ModuleBuilder
// ExportGlobalF32 exports a global constant of type api.ValueTypeF32.
// If a global is already exported with the same name, this overwrites it.
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (global (export "math/pi") f32 (f32.const 3.1415926536))
// builder.ExportGlobalF32("math/pi", 3.1415926536)
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
ExportGlobalF32(name string, v float32) ModuleBuilder
// ExportGlobalF64 exports a global constant of type api.ValueTypeF64.
// If a global is already exported with the same name, this overwrites it.
//
// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
// // (global (export "math/pi") f64 (f64.const 3.14159265358979323846264338327950288419716939937510582097494459))
// builder.ExportGlobalF64("math/pi", 3.14159265358979323846264338327950288419716939937510582097494459)
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
ExportGlobalF64(name string, v float64) ModuleBuilder
ExportFunctions(nameToGoFunc map[string]interface{}) HostModuleBuilder
// Compile returns a CompiledModule that can instantiated in any namespace (Namespace).
//
// Note: Closing the Namespace has the same effect as closing the result.
Compile(context.Context, CompileConfig) (CompiledModule, error)
Compile(context.Context) (CompiledModule, error)
// Instantiate is a convenience that calls Compile, then Namespace.InstantiateModule.
//
@@ -204,7 +147,7 @@ type ModuleBuilder interface {
// hello := func() {
// fmt.Fprintln(stdout, "hello!")
// }
// env, _ := r.NewModuleBuilder("env").
// env, _ := r.NewHostModuleBuilder("env").
// ExportFunction("hello", hello).
// Instantiate(ctx, r)
//
@@ -216,30 +159,26 @@ type ModuleBuilder interface {
Instantiate(context.Context, Namespace) (api.Module, error)
}
// moduleBuilder implements ModuleBuilder
type moduleBuilder struct {
// hostModuleBuilder implements HostModuleBuilder
type hostModuleBuilder struct {
r *runtime
moduleName string
nameToGoFunc map[string]interface{}
funcToNames map[string][]string
nameToMemory map[string]*wasm.Memory
nameToGlobal map[string]*wasm.Global
}
// NewModuleBuilder implements Runtime.NewModuleBuilder
func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
return &moduleBuilder{
// NewHostModuleBuilder implements Runtime.NewHostModuleBuilder
func (r *runtime) NewHostModuleBuilder(moduleName string) HostModuleBuilder {
return &hostModuleBuilder{
r: r,
moduleName: moduleName,
nameToGoFunc: map[string]interface{}{},
funcToNames: map[string][]string{},
nameToMemory: map[string]*wasm.Memory{},
nameToGlobal: map[string]*wasm.Global{},
}
}
// ExportFunction implements ModuleBuilder.ExportFunction
func (b *moduleBuilder) ExportFunction(exportName string, goFunc interface{}, names ...string) ModuleBuilder {
// ExportFunction implements HostModuleBuilder.ExportFunction
func (b *hostModuleBuilder) ExportFunction(exportName string, goFunc interface{}, names ...string) HostModuleBuilder {
b.nameToGoFunc[exportName] = goFunc
if len(names) > 0 {
b.funcToNames[exportName] = names
@@ -247,81 +186,17 @@ func (b *moduleBuilder) ExportFunction(exportName string, goFunc interface{}, na
return b
}
// ExportFunctions implements ModuleBuilder.ExportFunctions
func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder {
// ExportFunctions implements HostModuleBuilder.ExportFunctions
func (b *hostModuleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) HostModuleBuilder {
for k, v := range nameToGoFunc {
b.ExportFunction(k, v)
}
return b
}
// ExportMemory implements ModuleBuilder.ExportMemory
func (b *moduleBuilder) ExportMemory(name string, minPages uint32) ModuleBuilder {
b.nameToMemory[name] = &wasm.Memory{Min: minPages}
return b
}
// ExportMemoryWithMax implements ModuleBuilder.ExportMemoryWithMax
func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder {
b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: maxPages, IsMaxEncoded: true}
return b
}
// ExportGlobalI32 implements ModuleBuilder.ExportGlobalI32
func (b *moduleBuilder) ExportGlobalI32(name string, v int32) ModuleBuilder {
b.nameToGlobal[name] = &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(v)},
}
return b
}
// ExportGlobalI64 implements ModuleBuilder.ExportGlobalI64
func (b *moduleBuilder) ExportGlobalI64(name string, v int64) ModuleBuilder {
b.nameToGlobal[name] = &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64},
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(v)},
}
return b
}
// ExportGlobalF32 implements ModuleBuilder.ExportGlobalF32
func (b *moduleBuilder) ExportGlobalF32(name string, v float32) ModuleBuilder {
b.nameToGlobal[name] = &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32},
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: u64.LeBytes(api.EncodeF32(v))},
}
return b
}
// ExportGlobalF64 implements ModuleBuilder.ExportGlobalF64
func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder {
b.nameToGlobal[name] = &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: u64.LeBytes(api.EncodeF64(v))},
}
return b
}
// Compile implements ModuleBuilder.Compile
func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (CompiledModule, error) {
config := cConfig.(*compileConfig)
// Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule
for name, mem := range b.nameToMemory {
var maxP *uint32
if mem.IsMaxEncoded {
maxP = &mem.Max
}
mem.Min, mem.Cap, mem.Max = config.memorySizer(mem.Min, maxP)
if err := mem.Validate(); err != nil {
return nil, fmt.Errorf("memory[%s] %v", name, err)
}
}
module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures)
// Compile implements HostModuleBuilder.Compile
func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error) {
module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.r.enabledFeatures)
if err != nil {
return nil, err
} else if err = module.Validate(b.r.enabledFeatures); err != nil {
@@ -340,9 +215,9 @@ func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (Com
return c, nil
}
// Instantiate implements ModuleBuilder.Instantiate
func (b *moduleBuilder) Instantiate(ctx context.Context, ns Namespace) (api.Module, error) {
if compiled, err := b.Compile(ctx, NewCompileConfig()); err != nil {
// Instantiate implements HostModuleBuilder.Instantiate
func (b *hostModuleBuilder) Instantiate(ctx context.Context, ns Namespace) (api.Module, error) {
if compiled, err := b.Compile(ctx); err != nil {
return nil, err
} else {
compiled.(*compiledModule).closeWithModule = true