From 8f8c9ee205cf1127342ecff819b862dcb9864dfa Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Mon, 9 May 2022 11:02:32 +0800 Subject: [PATCH] Extracts CompileConfig and consolidates code. (#533) This performs several changes to allow compilation config to be centralized and scoped properly. The immediate effects are that we can now process external types during `Runtime.CompileModule` instead of doing so later during `Runtime.InstantiateModule`. Another nice side effect is memory size problems can err at a source line instead of having to be handled in several places. There are some API effects to this, and to pay for them, some less used APIs were removed. The "easy APIs" are left alone. For example, the APIs to compile and instantiate a module from Go or Wasm in one step are left alone. Here are the changes, some of which are only for consistency. Rationale is summarized in each point. * ModuleBuilder.Build -> ModuleBuilder.Compile * The result of this is similar to `CompileModule`, and pairs better with `ModuleBuilder.Instantiate` which is like `InstantiateModule`. * CompiledCode -> CompiledModule * We punted on this name, the result is more than just code. This is better I think and more consistent as it introduces less terms. * Adds CompileConfig param to Runtime.CompileModule. * This holds existing features and will have future ones, such as mapping externtypes to uint64 for wasm that doesn't yet support it. * Merges Runtime.InstantiateModuleWithConfig with Runtime.InstantiateModule * This allows us to explain APIs in terms of implicit or explicit compilation and config, vs implicit, kindof implicit, and explicit. * Removes Runtime.InstantiateModuleFromCodeWithConfig * Similar to above, this API only saves the compilation step and also difficult to reason with from a name POV. * RuntimeConfig.WithMemory(CapacityPages|LimitPages) -> CompileConfig.WithMemorySizer * This allows all error handling to be attached to the source line * This also allows someone to reduce unbounded memory while knowing what its minimum is. * ModuleConfig.With(Import|ImportModule) -> CompileConfig.WithImportRenamer * This allows more types of import manipulation, also without conflating functions with globals. * Adds api.ExternType * Needed for ImportRenamer and will be needed later for ExportRenamer. Signed-off-by: Adrian Cole --- README.md | 4 +- api/wasm.go | 85 ++++ api/wasm_test.go | 22 + builder.go | 51 +- builder_test.go | 25 +- config.go | 248 +++------- config_test.go | 457 +++--------------- examples/replace-import/replace-import.go | 18 +- examples/wasi/cat.go | 11 +- experimental/listener_test.go | 12 +- experimental/sys_test.go | 11 +- internal/asm/impl.go | 2 +- .../asm/golang_asm/golang_asm.go | 2 +- internal/integration_test/bench/bench_test.go | 4 +- .../integration_test/engine/adhoc_test.go | 33 +- .../integration_test/spectest/encoder_test.go | 2 +- .../integration_test/spectest/spectest.go | 6 +- internal/integration_test/vs/codec.go | 4 +- internal/integration_test/vs/codec_test.go | 8 +- internal/integration_test/vs/runtime.go | 10 +- internal/modgen/modgen_test.go | 2 +- internal/wasm/binary/decoder.go | 10 +- internal/wasm/binary/decoder_test.go | 22 +- internal/wasm/binary/import.go | 9 +- internal/wasm/binary/memory.go | 17 +- internal/wasm/binary/memory_test.go | 25 +- internal/wasm/binary/section.go | 15 +- internal/wasm/binary/section_test.go | 17 +- internal/wasm/func_validation_test.go | 2 +- internal/wasm/jit/jit_controlflow_test.go | 2 +- internal/wasm/jit/jit_numeric_test.go | 2 +- internal/wasm/jit/jit_stack_test.go | 4 +- internal/wasm/memory.go | 9 + internal/wasm/memory_test.go | 15 + internal/wasm/module.go | 97 ++-- internal/wasm/module_test.go | 66 +-- internal/wasm/store.go | 2 +- internal/wasm/text/decoder.go | 20 +- internal/wasm/text/decoder_test.go | 27 +- internal/wasm/text/memory_parser.go | 58 ++- internal/wasm/text/memory_parser_test.go | 18 +- internal/wazeroir/compiler_test.go | 2 +- wasi/example_test.go | 19 +- wasi/usage_test.go | 6 +- wasi/wasi_test.go | 4 +- wasm.go | 188 ++++--- wasm_test.go | 215 ++++---- 47 files changed, 784 insertions(+), 1104 deletions(-) diff --git a/README.md b/README.md index bdfbf0fd..c2f25ce0 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ For example, here's how you can allow WebAssembly modules to read wm, err := wasi.InstantiateSnapshotPreview1(ctx, r) defer wm.Close(ctx) -config := wazero.ModuleConfig().WithFS(os.DirFS("/work/home")) -module, err := r.InstantiateModule(ctx, binary, config) +config := wazero.NewModuleConfig().WithFS(os.DirFS("/work/home")) +module, err := r.InstantiateModule(ctx, compiled, config) defer module.Close(ctx) ... ``` diff --git a/api/wasm.go b/api/wasm.go index a0ba7a66..2ed07497 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -7,6 +7,50 @@ import ( "math" ) +// ExternType classifies imports and exports with their respective types. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#external-types%E2%91%A0 +type ExternType = byte + +const ( + ExternTypeFunc ExternType = 0x00 + ExternTypeTable ExternType = 0x01 + ExternTypeMemory ExternType = 0x02 + ExternTypeGlobal ExternType = 0x03 +) + +// The below are exported to consolidate parsing behavior for external types. +const ( + // ExternTypeFuncName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeFunc. + ExternTypeFuncName = "func" + // ExternTypeTableName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeTable. + ExternTypeTableName = "table" + // ExternTypeMemoryName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeMemory. + ExternTypeMemoryName = "memory" + // ExternTypeGlobalName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeGlobal. + ExternTypeGlobalName = "global" +) + +// ExternTypeName returns the name of the WebAssembly 1.0 (20191205) Text Format field of the given type. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#imports⑤ +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A4 +func ExternTypeName(et ExternType) string { + switch et { + case ExternTypeFunc: + return ExternTypeFuncName + case ExternTypeTable: + return ExternTypeTableName + case ExternTypeMemory: + return ExternTypeMemoryName + case ExternTypeGlobal: + return ExternTypeGlobalName + } + return fmt.Sprintf("%#x", et) +} + // ValueType describes a numeric type used in Web Assembly 1.0 (20191205). For example, Function parameters and results are // only definable as a value type. // @@ -293,3 +337,44 @@ func EncodeF64(input float64) uint64 { func DecodeF64(input uint64) float64 { return math.Float64frombits(input) } + +// ImportRenamer applies during compilation after a module has been decoded from source, but before it is instantiated. +// +// For example, you may have a module like below, but the exported functions are in two different modules: +// (import "js" "increment" (func $increment (result i32))) +// (import "js" "decrement" (func $decrement (result i32))) +// (import "js" "wasm_increment" (func $wasm_increment (result i32))) +// (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) +// +// The below breaks up the imports: "increment" and "decrement" from the module "go" and other functions from "wasm": +// renamer := func(externType api.ExternType, oldModule, oldName string) (string, string) { +// if externType != api.ExternTypeFunc { +// return oldModule, oldName +// } +// switch oldName { +// case "increment", "decrement": return "go", oldName +// default: return "wasm", oldName +// } +// } +// +// The resulting CompiledModule imports will look identical to this: +// (import "go" "increment" (func $increment (result i32))) +// (import "go" "decrement" (func $decrement (result i32))) +// (import "wasm" "wasm_increment" (func $wasm_increment (result i32))) +// (import "wasm" "wasm_decrement" (func $wasm_decrement (result i32))) +// +type ImportRenamer func(externType ExternType, oldModule, oldName string) (newModule, newName string) + +// MemorySizer applies during compilation after a module has been decoded from source, but before it is instantiated. +// This determines the amount of memory pages (65536 bytes per page) to use when a memory is instantiated as a []byte. +// +// Ex. Here's how to set the capacity to max instead of min, when set: +// capIsMax := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { +// if maxPages != nil { +// return minPages, *maxPages, *maxPages +// } +// return minPages, minPages, 65536 +// } +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem +type MemorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) diff --git a/api/wasm_test.go b/api/wasm_test.go index 12943b5a..8f409566 100644 --- a/api/wasm_test.go +++ b/api/wasm_test.go @@ -8,6 +8,28 @@ import ( "github.com/tetratelabs/wazero/internal/testing/require" ) +func TestExternTypeName(t *testing.T) { + tests := []struct { + name string + input ExternType + expected string + }{ + {"func", ExternTypeFunc, "func"}, + {"table", ExternTypeTable, "table"}, + {"mem", ExternTypeMemory, "memory"}, + {"global", ExternTypeGlobal, "global"}, + {"unknown", 100, "0x64"}, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, ExternTypeName(tc.input)) + }) + } +} + func TestValueTypeName(t *testing.T) { tests := []struct { name string diff --git a/builder.go b/builder.go index 724fa638..2490455a 100644 --- a/builder.go +++ b/builder.go @@ -18,16 +18,20 @@ import ( // hello := func() { // fmt.Fprintln(stdout, "hello!") // } -// env, _ := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate(ctx) +// env, _ := r.NewModuleBuilder("env"). +// ExportFunction("hello", hello). +// Instantiate(ctx) // // If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex. // -// env, _ := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Build(ctx) +// compiled, _ := r.NewModuleBuilder("env"). +// ExportFunction("get_random_string", getRandomString). +// Compile(ctx, wazero.NewCompileConfig()) // -// env1, _ := r.InstantiateModuleWithConfig(ctx, env, NewModuleConfig().WithName("env.1")) +// env1, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.1")) // defer env1.Close(ctx) // -// env2, _ := r.InstantiateModuleWithConfig(ctx, env, NewModuleConfig().WithName("env.2")) +// env2, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.2")) // defer env2.Close(ctx) // // Notes: @@ -152,12 +156,13 @@ type ModuleBuilder interface { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype ExportGlobalF64(name string, v float64) ModuleBuilder - // Build returns a module to instantiate, or returns an error if any of the configuration is invalid. - Build(context.Context) (CompiledCode, error) + // Compile returns a module to instantiate, or an error if any of the configuration is invalid. + Compile(context.Context, CompileConfig) (CompiledModule, error) - // Instantiate is a convenience that calls Build, then Runtime.InstantiateModule + // Instantiate is a convenience that calls Build, then Runtime.InstantiateModule, using default configuration. // // Note: Fields in the builder are copied during instantiation: Later changes do not affect the instantiated result. + // Note: To avoid using configuration defaults, use Compile instead. Instantiate(context.Context) (api.Module, error) } @@ -197,17 +202,13 @@ func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) Mod // ExportMemory implements ModuleBuilder.ExportMemory func (b *moduleBuilder) ExportMemory(name string, minPages uint32) ModuleBuilder { - mem := &wasm.Memory{Min: minPages, Max: b.r.memoryLimitPages} - mem.Cap = b.r.memoryCapacityPages(mem.Min, nil) - b.nameToMemory[name] = mem + b.nameToMemory[name] = &wasm.Memory{Min: minPages} return b } // ExportMemoryWithMax implements ModuleBuilder.ExportMemoryWithMax func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder { - mem := &wasm.Memory{Min: minPages, Max: maxPages, IsMaxEncoded: true} - mem.Cap = b.r.memoryCapacityPages(mem.Min, &maxPages) - b.nameToMemory[name] = mem + b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: maxPages, IsMaxEncoded: true} return b } @@ -249,16 +250,22 @@ func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder { return b } -// Build implements ModuleBuilder.Build -func (b *moduleBuilder) Build(ctx context.Context) (CompiledCode, error) { +// Compile implements ModuleBuilder.Compile +func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (CompiledModule, error) { + config, ok := cConfig.(*compileConfig) + if !ok { + panic(fmt.Errorf("unsupported wazero.CompileConfig implementation: %#v", cConfig)) + } + // Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule - memoryLimitPages := b.r.memoryLimitPages for name, mem := range b.nameToMemory { - if err := mem.ValidateMinMax(memoryLimitPages); err != nil { - return nil, fmt.Errorf("memory[%s] %v", name, err) + var maxP *uint32 + if mem.IsMaxEncoded { + maxP = &mem.Max } - if err := b.r.setMemoryCapacity(name, mem); err != nil { - return nil, err + 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) } } @@ -276,7 +283,7 @@ func (b *moduleBuilder) Build(ctx context.Context) (CompiledCode, error) { // Instantiate implements ModuleBuilder.Instantiate func (b *moduleBuilder) Instantiate(ctx context.Context) (api.Module, error) { - if compiled, err := b.Build(ctx); err != nil { + if compiled, err := b.Compile(ctx, NewCompileConfig()); err != nil { return nil, err } else { if err = b.r.store.Engine.CompileModule(ctx, compiled.(*compiledCode).module); err != nil { @@ -284,6 +291,6 @@ func (b *moduleBuilder) Instantiate(ctx context.Context) (api.Module, error) { } // *wasm.ModuleInstance cannot be tracked, so we release the cache inside this function. defer compiled.Close(ctx) - return b.r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig().WithName(b.moduleName)) + return b.r.InstantiateModule(ctx, compiled, NewModuleConfig().WithName(b.moduleName)) } } diff --git a/builder_test.go b/builder_test.go index 5cc13a6a..57b123e0 100644 --- a/builder_test.go +++ b/builder_test.go @@ -344,7 +344,7 @@ func TestNewModuleBuilder_Build(t *testing.T) { t.Run(tc.name, func(t *testing.T) { b := tc.input(NewRuntime()).(*moduleBuilder) - compiled, err := b.Build(testCtx) + compiled, err := b.Compile(testCtx, NewCompileConfig()) require.NoError(t, err) m := compiled.(*compiledCode) @@ -353,7 +353,7 @@ func TestNewModuleBuilder_Build(t *testing.T) { require.Equal(t, b.r.store.Engine, m.compiledEngine) // Built module must be instantiable by Engine. - _, err = b.r.InstantiateModule(testCtx, m) + _, err = b.r.InstantiateModule(testCtx, m, NewModuleConfig()) require.NoError(t, err) }) } @@ -363,25 +363,26 @@ func TestNewModuleBuilder_Build(t *testing.T) { func TestNewModuleBuilder_Build_Errors(t *testing.T) { tests := []struct { name string - input func(RuntimeConfig) ModuleBuilder + input func(Runtime) ModuleBuilder + config CompileConfig expectedErr string }{ { name: "memory min > limit", // only one test to avoid duplicating tests in module_test.go - input: func(cfg RuntimeConfig) ModuleBuilder { - return NewRuntimeWithConfig(cfg).NewModuleBuilder(""). - ExportMemory("memory", math.MaxUint32) + input: func(rt Runtime) ModuleBuilder { + return rt.NewModuleBuilder("").ExportMemory("memory", math.MaxUint32) }, + config: NewCompileConfig(), expectedErr: "memory[memory] min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", }, { name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go - input: func(cfg RuntimeConfig) ModuleBuilder { - cfg = cfg.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { - return 1 - }) - return NewRuntimeWithConfig(cfg).NewModuleBuilder("").ExportMemory("memory", 2) + input: func(rt Runtime) ModuleBuilder { + return rt.NewModuleBuilder("").ExportMemory("memory", 2) }, + config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + return 2, 1, 2 + }), expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)", }, } @@ -390,7 +391,7 @@ func TestNewModuleBuilder_Build_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, e := tc.input(NewRuntimeConfig()).Build(testCtx) + _, e := tc.input(NewRuntime()).Compile(testCtx, tc.config) require.EqualError(t, e, tc.expectedErr) }) } diff --git a/config.go b/config.go index e951020e..4e6a706b 100644 --- a/config.go +++ b/config.go @@ -7,8 +7,8 @@ import ( "io" "io/fs" "math" - "strings" + "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/interpreter" "github.com/tetratelabs/wazero/internal/wasm/jit" @@ -105,33 +105,6 @@ type RuntimeConfig interface { // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md WithFeatureSignExtensionOps(bool) RuntimeConfig - // WithMemoryCapacityPages is a function that determines memory capacity in pages (65536 bytes per page). The input - // are the min and possibly nil max defined by the module, and the default is to return the min. - // - // Ex. To set capacity to max when exists: - // c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { - // if maxPages != nil { - // return *maxPages - // } - // return minPages - // }) - // - // This function is used at compile time (ModuleBuilder.Build or Runtime.CompileModule). Compile will err if the - // function returns a value lower than minPages or greater than WithMemoryLimitPages. - WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32) RuntimeConfig - - // WithMemoryLimitPages limits the maximum number of pages a module can define from 65536 pages (4GiB) to the input. - // - // Notes: - // * If a module defines no memory max value, Runtime.CompileModule sets max to the limit. - // * If a module defines a memory max larger than this limit, it will fail to compile (Runtime.CompileModule). - // * Any "memory.grow" instruction that results in a larger value than this results in an error at runtime. - // * Zero is a valid value and results in a crash if any module uses memory. - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-types%E2%91%A0 - WithMemoryLimitPages(uint32) RuntimeConfig - // WithWasmCore1 enables features included in the WebAssembly Core Specification 1.0. Selecting this // overwrites any currently accumulated features with only those included in this W3C recommendation. // @@ -157,17 +130,13 @@ type RuntimeConfig interface { } type runtimeConfig struct { - enabledFeatures wasm.Features - newEngine func(wasm.Features) wasm.Engine - memoryLimitPages uint32 - memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32 + enabledFeatures wasm.Features + newEngine func(wasm.Features) wasm.Engine } // engineLessConfig helps avoid copy/pasting the wrong defaults. var engineLessConfig = &runtimeConfig{ - enabledFeatures: wasm.Features20191205, - memoryLimitPages: wasm.MemoryLimitPages, - memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { return minPages }, + enabledFeatures: wasm.Features20191205, } // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance. @@ -233,23 +202,6 @@ func (c *runtimeConfig) WithFeatureSignExtensionOps(enabled bool) RuntimeConfig return &ret } -// WithMemoryCapacityPages implements RuntimeConfig.WithMemoryCapacityPages -func (c *runtimeConfig) WithMemoryCapacityPages(maxCapacityPages func(minPages uint32, maxPages *uint32) uint32) RuntimeConfig { - if maxCapacityPages == nil { - return c // Instead of erring. - } - ret := *c // copy - ret.memoryCapacityPages = maxCapacityPages - return &ret -} - -// WithMemoryLimitPages implements RuntimeConfig.WithMemoryLimitPages -func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig { - ret := *c // copy - ret.memoryLimitPages = memoryLimitPages - return &ret -} - // WithWasmCore1 implements RuntimeConfig.WithWasmCore1 func (c *runtimeConfig) WithWasmCore1() RuntimeConfig { ret := *c // copy @@ -264,16 +216,15 @@ func (c *runtimeConfig) WithWasmCore2() RuntimeConfig { return &ret } -// CompiledCode is a WebAssembly 1.0 module ready to be instantiated (Runtime.InstantiateModule) as an -// api.Module. +// CompiledModule is a WebAssembly 1.0 module ready to be instantiated (Runtime.InstantiateModule) as an api.Module. // // Note: In WebAssembly language, this is a decoded, validated, and possibly also compiled module. wazero avoids using // the name "Module" for both before and after instantiation as the name conflation has caused confusion. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#semantic-phases%E2%91%A0 -type CompiledCode interface { - // Close releases all the allocated resources for this CompiledCode. +type CompiledModule interface { + // Close releases all the allocated resources for this CompiledModule. // - // Note: It is safe to call Close while having outstanding calls from Modules instantiated from this CompiledCode. + // Note: It is safe to call Close while having outstanding calls from an api.Module instantiated from this. Close(context.Context) error } @@ -283,7 +234,7 @@ type compiledCode struct { compiledEngine wasm.Engine } -// Close implements CompiledCode.Close +// Close implements CompiledModule.Close func (c *compiledCode) Close(_ context.Context) error { // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! @@ -292,6 +243,59 @@ func (c *compiledCode) Close(_ context.Context) error { return nil } +// CompileConfig allows you to override what was decoded from source, prior to compilation (ModuleBuilder.Compile or +// Runtime.CompileModule). +// +// For example, WithImportRenamer allows you to override hard-coded names that don't match your requirements. +// +// Note: CompileConfig is immutable. Each WithXXX function returns a new instance including the corresponding change. +type CompileConfig interface { + + // WithImportRenamer can rename imports or break them into different modules. No default. + // + // Note: A nil function is invalid and ignored. + // Note: This is currently not relevant for ModuleBuilder as it has no means to define imports. + WithImportRenamer(api.ImportRenamer) CompileConfig + + // WithMemorySizer are the allocation parameters used for a Wasm memory. + // The default is to set cap=min and max=65536 if unset. + // + // Note: A nil function is invalid and ignored. + WithMemorySizer(api.MemorySizer) CompileConfig +} + +type compileConfig struct { + importRenamer api.ImportRenamer + memorySizer api.MemorySizer +} + +func NewCompileConfig() CompileConfig { + return &compileConfig{ + importRenamer: nil, + memorySizer: wasm.MemorySizer, + } +} + +// WithImportRenamer implements CompileConfig.WithImportRenamer +func (c *compileConfig) WithImportRenamer(importRenamer api.ImportRenamer) CompileConfig { + if importRenamer == nil { + return c + } + ret := *c // copy + ret.importRenamer = importRenamer + return &ret +} + +// WithMemorySizer implements CompileConfig.WithMemorySizer +func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConfig { + if memorySizer == nil { + return c + } + ret := *c // copy + ret.memorySizer = memorySizer + return &ret +} + // ModuleConfig configures resources needed by functions that have low-level interactions with the host operating // system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated // multiple times. @@ -351,54 +355,7 @@ type ModuleConfig interface { // Note: This sets WithWorkDirFS to the same file-system unless already set. WithFS(fs.FS) ModuleConfig - // WithImport replaces a specific import module and name with a new one. This allows you to break up a monolithic - // module imports, such as "env". This can also help reduce cyclic dependencies. - // - // For example, if a module was compiled with one module owning all imports: - // (import "js" "tbl" (table $tbl 4 funcref)) - // (import "js" "increment" (func $increment (result i32))) - // (import "js" "decrement" (func $decrement (result i32))) - // (import "js" "wasm_increment" (func $wasm_increment (result i32))) - // (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) - // - // Use this function to import "increment" and "decrement" from the module "go" and other imports from "wasm": - // config.WithImportModule("js", "wasm") - // config.WithImport("wasm", "increment", "go", "increment") - // config.WithImport("wasm", "decrement", "go", "decrement") - // - // Upon instantiation, imports resolve as if they were compiled like so: - // (import "wasm" "tbl" (table $tbl 4 funcref)) - // (import "go" "increment" (func $increment (result i32))) - // (import "go" "decrement" (func $decrement (result i32))) - // (import "wasm" "wasm_increment" (func $wasm_increment (result i32))) - // (import "wasm" "wasm_decrement" (func $wasm_decrement (result i32))) - // - // Note: Any WithImport instructions happen in order, after any WithImportModule instructions. - WithImport(oldModule, oldName, newModule, newName string) ModuleConfig - - // WithImportModule replaces every import with oldModule with newModule. This is helpful for modules who have - // transitioned to a stable status since the underlying wasm was compiled. - // - // For example, if a module was compiled like below, with an old module for WASI: - // (import "wasi_unstable" "args_get" (func (param i32, i32) (result i32))) - // - // Use this function to update it to the current version: - // config.WithImportModule("wasi_unstable", wasi.ModuleSnapshotPreview1) - // - // See WithImport for a comprehensive example. - // Note: Any WithImportModule instructions happen in order, before any WithImport instructions. - WithImportModule(oldModule, newModule string) ModuleConfig - - // WithName configures the module name. Defaults to what was decoded from the module source. - // - // If the source was in WebAssembly 1.0 Binary Format, this defaults to what was decoded from the custom name - // section. Otherwise, if it was decoded from Text Format, this defaults to the module ID stripped of leading '$'. - // - // For example, if the Module was decoded from the text format `(module $math)`, the default name is "math". - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%91%A2 + // WithName configures the module name. Defaults to what was decoded or overridden via CompileConfig.WithModuleName. WithName(string) ModuleConfig // WithStartFunctions configures the functions to call after the module is instantiated. Defaults to "_start". @@ -469,11 +426,6 @@ type moduleConfig struct { preopens map[uint32]*wasm.FileEntry // preopenPaths allow overwriting of existing paths. preopenPaths map[string]uint32 - // replacedImports holds the latest state of WithImport - // Note: Key is NUL delimited as import module and name can both include any UTF-8 characters. - replacedImports map[string][2]string - // replacedImportModules holds the latest state of WithImportModule - replacedImportModules map[string]string } func NewModuleConfig() ModuleConfig { @@ -513,30 +465,6 @@ func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig { return &ret } -// WithImport implements ModuleConfig.WithImport -func (c *moduleConfig) WithImport(oldModule, oldName, newModule, newName string) ModuleConfig { - ret := *c // copy - if ret.replacedImports == nil { - ret.replacedImports = map[string][2]string{} - } - var builder strings.Builder - builder.WriteString(oldModule) - builder.WriteByte(0) // delimit with NUL as module and name can be any UTF-8 characters. - builder.WriteString(oldName) - ret.replacedImports[builder.String()] = [2]string{newModule, newName} - return &ret -} - -// WithImportModule implements ModuleConfig.WithImportModule -func (c *moduleConfig) WithImportModule(oldModule, newModule string) ModuleConfig { - ret := *c // copy - if ret.replacedImportModules == nil { - ret.replacedImportModules = map[string]string{} - } - ret.replacedImportModules[oldModule] = newModule - return &ret -} - // WithName implements ModuleConfig.WithName func (c *moduleConfig) WithName(name string) ModuleConfig { ret := *c // copy @@ -633,53 +561,3 @@ func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) { return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, preopens) } - -func (c *moduleConfig) replaceImports(module *wasm.Module) *wasm.Module { - if (c.replacedImportModules == nil && c.replacedImports == nil) || module.ImportSection == nil { - return module - } - - changed := false - - ret := *module // shallow copy - replacedImports := make([]*wasm.Import, len(module.ImportSection)) - copy(replacedImports, module.ImportSection) - - // First, replace any import.Module - for oldModule, newModule := range c.replacedImportModules { - for i, imp := range replacedImports { - if imp.Module == oldModule { - changed = true - cp := *imp // shallow copy - cp.Module = newModule - replacedImports[i] = &cp - } else { - replacedImports[i] = imp - } - } - } - - // Now, replace any import.Module+import.Name - for oldImport, newImport := range c.replacedImports { - for i, imp := range replacedImports { - nulIdx := strings.IndexByte(oldImport, 0) - oldModule := oldImport[0:nulIdx] - oldName := oldImport[nulIdx+1:] - if imp.Module == oldModule && imp.Name == oldName { - changed = true - cp := *imp // shallow copy - cp.Module = newImport[0] - cp.Name = newImport[1] - replacedImports[i] = &cp - } else { - replacedImports[i] = imp - } - } - } - - if !changed { - return module - } - ret.ImportSection = replacedImports - return &ret -} diff --git a/config_test.go b/config_test.go index 1c0340a6..f50557d3 100644 --- a/config_test.go +++ b/config_test.go @@ -4,9 +4,11 @@ import ( "context" "io" "math" + "reflect" "testing" "testing/fstest" + "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -17,15 +19,6 @@ func TestRuntimeConfig(t *testing.T) { with func(RuntimeConfig) RuntimeConfig expected RuntimeConfig }{ - { - name: "WithMemoryLimitPages", - with: func(c RuntimeConfig) RuntimeConfig { - return c.WithMemoryLimitPages(1) - }, - expected: &runtimeConfig{ - memoryLimitPages: 1, - }, - }, { name: "bulk-memory-operations", with: func(c RuntimeConfig) RuntimeConfig { @@ -110,24 +103,6 @@ func TestRuntimeConfig(t *testing.T) { require.Equal(t, &runtimeConfig{}, input) }) } - - t.Run("WithMemoryCapacityPages", func(t *testing.T) { - c := NewRuntimeConfig().(*runtimeConfig) - - // Test default returns min - require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil)) - - // Nil ignored - c = c.WithMemoryCapacityPages(nil).(*runtimeConfig) - require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil)) - - // Assign a valid function - c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { - return 2 - }).(*runtimeConfig) - // Returns updated value - require.Equal(t, uint32(2), c.memoryCapacityPages(1, nil)) - }) } func TestRuntimeConfig_FeatureToggle(t *testing.T) { @@ -201,6 +176,67 @@ func TestRuntimeConfig_FeatureToggle(t *testing.T) { } } +func TestCompileConfig(t *testing.T) { + im := func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) { + return "a", oldName + } + im2 := func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) { + return "b", oldName + } + mp := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + return 0, 1, 1 + } + tests := []struct { + name string + with func(CompileConfig) CompileConfig + expected *compileConfig + }{ + { + name: "WithImportRenamer", + with: func(c CompileConfig) CompileConfig { + return c.WithImportRenamer(im) + }, + expected: &compileConfig{importRenamer: im}, + }, + { + name: "WithImportRenamer twice", + with: func(c CompileConfig) CompileConfig { + return c.WithImportRenamer(im).WithImportRenamer(im2) + }, + expected: &compileConfig{importRenamer: im2}, + }, + { + name: "WithMemorySizer", + with: func(c CompileConfig) CompileConfig { + return c.WithMemorySizer(mp) + }, + expected: &compileConfig{memorySizer: mp}, + }, + { + name: "WithMemorySizer twice", + with: func(c CompileConfig) CompileConfig { + return c.WithMemorySizer(wasm.MemorySizer).WithMemorySizer(mp) + }, + expected: &compileConfig{memorySizer: mp}, + }, + } + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + input := &compileConfig{} + rc := tc.with(input).(*compileConfig) + + // We cannot compare func, but we can compare reflect.Value + // See https://go.dev/ref/spec#Comparison_operators + require.Equal(t, reflect.ValueOf(tc.expected.importRenamer), reflect.ValueOf(rc.importRenamer)) + require.Equal(t, reflect.ValueOf(tc.expected.memorySizer), reflect.ValueOf(rc.memorySizer)) + // The source wasn't modified + require.Equal(t, &compileConfig{}, input) + }) + } +} + func TestModuleConfig(t *testing.T) { tests := []struct { name string @@ -217,128 +253,19 @@ func TestModuleConfig(t *testing.T) { }, }, { - name: "WithName - empty", + name: "WithName empty", with: func(c ModuleConfig) ModuleConfig { return c.WithName("") }, expected: &moduleConfig{}, }, { - name: "WithImport", + name: "WithName twice", with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "abort", "assemblyscript", "abort") + return c.WithName("wazero").WithName("wa0") }, expected: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, - }, - }, - { - name: "WithImport - empty to non-empty - module", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("", "abort", "assemblyscript", "abort") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{"\000abort": {"assemblyscript", "abort"}}, - }, - }, - { - name: "WithImport - non-empty to empty - module", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "abort", "", "abort") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"", "abort"}}, - }, - }, - { - name: "WithImport - empty to non-empty - name", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "", "assemblyscript", "abort") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{"env\000": {"assemblyscript", "abort"}}, - }, - }, - { - name: "WithImport - non-empty to empty - name", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "abort", "assemblyscript", "") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", ""}}, - }, - }, - { - name: "WithImport - override", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "abort", "assemblyscript", "abort"). - WithImport("env", "abort", "go", "exit") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"go", "exit"}}, - }, - }, - { - name: "WithImport - twice", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImport("env", "abort", "assemblyscript", "abort"). - WithImport("wasi_unstable", "proc_exit", "wasi_snapshot_preview1", "proc_exit") - }, - expected: &moduleConfig{ - replacedImports: map[string][2]string{ - "env\000abort": {"assemblyscript", "abort"}, - "wasi_unstable\000proc_exit": {"wasi_snapshot_preview1", "proc_exit"}, - }, - }, - }, - { - name: "WithImportModule", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImportModule("env", "assemblyscript") - }, - expected: &moduleConfig{ - replacedImportModules: map[string]string{"env": "assemblyscript"}, - }, - }, - { - name: "WithImportModule - empty to non-empty", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImportModule("", "assemblyscript") - }, - expected: &moduleConfig{ - replacedImportModules: map[string]string{"": "assemblyscript"}, - }, - }, - { - name: "WithImportModule - non-empty to empty", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImportModule("env", "") - }, - expected: &moduleConfig{ - replacedImportModules: map[string]string{"env": ""}, - }, - }, - { - name: "WithImportModule - override", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImportModule("env", "assemblyscript"). - WithImportModule("env", "go") - }, - expected: &moduleConfig{ - replacedImportModules: map[string]string{"env": "go"}, - }, - }, - { - name: "WithImportModule - twice", - with: func(c ModuleConfig) ModuleConfig { - return c.WithImportModule("env", "go"). - WithImportModule("wasi_unstable", "wasi_snapshot_preview1") - }, - expected: &moduleConfig{ - replacedImportModules: map[string]string{ - "env": "go", - "wasi_unstable": "wasi_snapshot_preview1", - }, + name: "wa0", }, }, } @@ -355,234 +282,6 @@ func TestModuleConfig(t *testing.T) { } } -func TestModuleConfig_replaceImports(t *testing.T) { - tests := []struct { - name string - config ModuleConfig - input *wasm.Module - expected *wasm.Module - expectSame bool - }{ - { - name: "no config, no imports", - config: &moduleConfig{}, - input: &wasm.Module{}, - expected: &wasm.Module{}, - expectSame: true, - }, - { - name: "no config", - config: &moduleConfig{}, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - expectSame: true, - }, - { - name: "replacedImportModules", - config: &moduleConfig{ - replacedImportModules: map[string]string{"wasi_unstable": "wasi_snapshot_preview1"}, - }, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasi_unstable", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasi_unstable", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - expected: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - }, - { - name: "replacedImportModules doesn't match", - config: &moduleConfig{ - replacedImportModules: map[string]string{"env": ""}, - }, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - expectSame: true, - }, - { - name: "replacedImports", - config: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, - }, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "env", Name: "abort", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "env", Name: "seed", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - expected: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "assemblyscript", Name: "abort", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "env", Name: "seed", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - }, - { - name: "replacedImports don't match", - config: &moduleConfig{ - replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, - }, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - }, - expectSame: true, - }, - { - name: "replacedImportModules and replacedImports", - config: &moduleConfig{ - replacedImportModules: map[string]string{"js": "wasm"}, - replacedImports: map[string][2]string{ - "wasm\000increment": {"go", "increment"}, - "wasm\000decrement": {"go", "decrement"}, - }, - }, - input: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "js", Name: "tbl", - Type: wasm.ExternTypeTable, - DescTable: &wasm.Table{Min: 4}, - }, - { - Module: "js", Name: "increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "js", Name: "decrement", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "js", Name: "wasm_increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "js", Name: "wasm_increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - }, - }, - expected: &wasm.Module{ - ImportSection: []*wasm.Import{ - { - Module: "wasm", Name: "tbl", - Type: wasm.ExternTypeTable, - DescTable: &wasm.Table{Min: 4}, - }, - { - Module: "go", Name: "increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "go", Name: "decrement", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasm", Name: "wasm_increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - { - Module: "wasm", Name: "wasm_increment", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, - }, - }, - }, - } - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - actual := tc.config.(*moduleConfig).replaceImports(tc.input) - if tc.expectSame { - require.Same(t, tc.input, actual) - } else { - require.NotSame(t, tc.input, actual) - require.Equal(t, tc.expected, actual) - } - }) - } -} - func TestModuleConfig_toSysContext(t *testing.T) { testFS := fstest.MapFS{} testFS2 := fstest.MapFS{} @@ -619,7 +318,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { ), }, { - name: "WithArgs - empty ok", // Particularly argv[0] can be empty, and we have no rules about others. + name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others. input: NewModuleConfig().WithArgs("", "bc"), expected: requireSysContext(t, math.MaxUint32, // max @@ -632,7 +331,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { ), }, { - name: "WithArgs - second call overwrites", + name: "WithArgs second call overwrites", input: NewModuleConfig().WithArgs("a", "bc").WithArgs("bc", "a"), expected: requireSysContext(t, math.MaxUint32, // max @@ -658,7 +357,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { ), }, { - name: "WithEnv - empty value", + name: "WithEnv empty value", input: NewModuleConfig().WithEnv("a", ""), expected: requireSysContext(t, math.MaxUint32, // max @@ -727,7 +426,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { ), }, { - name: "WithFS - overwrites", + name: "WithFS overwrites", input: NewModuleConfig().WithFS(testFS).WithFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max @@ -808,37 +507,37 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) { expectedErr string }{ { - name: "WithArgs - arg contains NUL", + name: "WithArgs arg contains NUL", input: NewModuleConfig().WithArgs("", string([]byte{'a', 0})), expectedErr: "args invalid: contains NUL character", }, { - name: "WithEnv - key contains NUL", + name: "WithEnv key contains NUL", input: NewModuleConfig().WithEnv(string([]byte{'a', 0}), "a"), expectedErr: "environ invalid: contains NUL character", }, { - name: "WithEnv - value contains NUL", + name: "WithEnv value contains NUL", input: NewModuleConfig().WithEnv("a", string([]byte{'a', 0})), expectedErr: "environ invalid: contains NUL character", }, { - name: "WithEnv - key contains equals", + name: "WithEnv key contains equals", input: NewModuleConfig().WithEnv("a=", "a"), expectedErr: "environ invalid: key contains '=' character", }, { - name: "WithEnv - empty key", + name: "WithEnv empty key", input: NewModuleConfig().WithEnv("", "a"), expectedErr: "environ invalid: empty key", }, { - name: "WithFS - nil", + name: "WithFS nil", input: NewModuleConfig().WithFS(nil), expectedErr: "FS for / is nil", }, { - name: "WithWorkDirFS - nil", + name: "WithWorkDirFS nil", input: NewModuleConfig().WithWorkDirFS(nil), expectedErr: "FS for . is nil", }, diff --git a/examples/replace-import/replace-import.go b/examples/replace-import/replace-import.go index 3a4e1353..99e89a13 100644 --- a/examples/replace-import/replace-import.go +++ b/examples/replace-import/replace-import.go @@ -30,21 +30,27 @@ func main() { } defer host.Close(ctx) - // Compile WebAssembly code that needs the function "env.abort". + // Compile the WebAssembly module, replacing the import "env.abort" with "assemblyscript.abort". + compileConfig := wazero.NewCompileConfig(). + WithImportRenamer(func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) { + if oldModule == "env" && oldName == "abort" { + return "assemblyscript", "abort" + } + return oldModule, oldName + }) + code, err := r.CompileModule(ctx, []byte(`(module $needs-import (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (export "abort" (func 0)) ;; exports the import for testing -)`)) +)`), compileConfig) if err != nil { log.Fatal(err) } defer code.Close(ctx) - // Instantiate the WebAssembly module, replacing the import "env.abort" - // with "assemblyscript.abort". - mod, err := r.InstantiateModuleWithConfig(ctx, code, wazero.NewModuleConfig(). - WithImport("env", "abort", "assemblyscript", "abort")) + // Instantiate the WebAssembly module. + mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig()) if err != nil { log.Fatal(err) } diff --git a/examples/wasi/cat.go b/examples/wasi/cat.go index 0105db18..76f2e82c 100644 --- a/examples/wasi/cat.go +++ b/examples/wasi/cat.go @@ -47,10 +47,17 @@ func main() { } defer wm.Close(ctx) - // InstantiateModuleFromCodeWithConfig runs the "_start" function which is what TinyGo compiles "main" to. + // Compile the WebAssembly module using the default configuration. + code, err := r.CompileModule(ctx, catWasm, wazero.NewCompileConfig()) + if err != nil { + log.Fatal(err) + } + defer code.Close(ctx) + + // InstantiateModule runs the "_start" function which is what TinyGo compiles "main" to. // * Set the program name (arg[0]) to "wasi" and add args to write "test.txt" to stdout twice. // * We use "/test.txt" or "./test.txt" because WithFS by default maps the workdir "." to "/". - cat, err := r.InstantiateModuleFromCodeWithConfig(ctx, catWasm, config.WithArgs("wasi", os.Args[1])) + cat, err := r.InstantiateModule(ctx, code, config.WithArgs("wasi", os.Args[1])) if err != nil { log.Fatal(err) } diff --git a/experimental/listener_test.go b/experimental/listener_test.go index d40a91f1..e4ae4523 100644 --- a/experimental/listener_test.go +++ b/experimental/listener_test.go @@ -66,14 +66,20 @@ func Example_listener() { } defer wm.Close(ctx) - cfg := wazero.NewModuleConfig().WithStdout(os.Stdout) - mod, err := r.InstantiateModuleFromCodeWithConfig(ctx, []byte(`(module $listener + // Compile the WebAssembly module using the default configuration. + code, err := r.CompileModule(ctx, []byte(`(module $listener (import "wasi_snapshot_preview1" "random_get" (func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32))) (func i32.const 0 i32.const 4 call 0 drop) ;; write 4 bytes of random data (memory 1 1) (start 1) ;; call the second function -)`), cfg) +)`), wazero.NewCompileConfig()) + if err != nil { + log.Fatal(err) + } + defer code.Close(ctx) + + mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout)) if err != nil { log.Fatal(err) } diff --git a/experimental/sys_test.go b/experimental/sys_test.go index 6ae9a3b0..3477caa0 100644 --- a/experimental/sys_test.go +++ b/experimental/sys_test.go @@ -43,14 +43,19 @@ func Example_sys() { } defer wm.Close(ctx) - cfg := wazero.NewModuleConfig().WithStdout(os.Stdout) - mod, err := r.InstantiateModuleFromCodeWithConfig(ctx, []byte(`(module + code, err := r.CompileModule(ctx, []byte(`(module (import "wasi_snapshot_preview1" "random_get" (func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32))) (func i32.const 0 i32.const 4 call 0 drop) ;; write 4 bytes of random data (memory 1 1) (start 1) ;; call the second function -)`), cfg) +)`), wazero.NewCompileConfig()) + if err != nil { + log.Fatal(err) + } + defer code.Close(ctx) + + mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout)) if err != nil { log.Fatal(err) } diff --git a/internal/asm/impl.go b/internal/asm/impl.go index 4fec0149..0659b664 100644 --- a/internal/asm/impl.go +++ b/internal/asm/impl.go @@ -31,7 +31,7 @@ func (a *BaseAssemblerImpl) AddOnGenerateCallBack(cb func([]byte) error) { // BuildJumpTable implements AssemblerBase.BuildJumpTable func (a *BaseAssemblerImpl) BuildJumpTable(table []byte, labelInitialInstructions []Node) { a.AddOnGenerateCallBack(func(code []byte) error { - // Build the offset table for each target. + // Compile the offset table for each target. base := labelInitialInstructions[0].OffsetInBinary() for i, nop := range labelInitialInstructions { if uint64(nop.OffsetInBinary())-uint64(base) >= JumpTableMaximumOffset { diff --git a/internal/integration_test/asm/golang_asm/golang_asm.go b/internal/integration_test/asm/golang_asm/golang_asm.go index 4a6bff13..1d9805d7 100644 --- a/internal/integration_test/asm/golang_asm/golang_asm.go +++ b/internal/integration_test/asm/golang_asm/golang_asm.go @@ -89,7 +89,7 @@ func (a *GolangAsmBaseAssembler) AddOnGenerateCallBack(cb func([]byte) error) { // BuildJumpTable implements the same method as documented on asm.AssemblerBase. func (a *GolangAsmBaseAssembler) BuildJumpTable(table []byte, labelInitialInstructions []asm.Node) { a.AddOnGenerateCallBack(func(code []byte) error { - // Build the offset table for each target. + // Compile the offset table for each target. base := labelInitialInstructions[0].OffsetInBinary() for i, nop := range labelInitialInstructions { if uint64(nop.OffsetInBinary())-uint64(base) >= asm.JumpTableMaximumOffset { diff --git a/internal/integration_test/bench/bench_test.go b/internal/integration_test/bench/bench_test.go index b381f3db..7de6a09e 100644 --- a/internal/integration_test/bench/bench_test.go +++ b/internal/integration_test/bench/bench_test.go @@ -50,14 +50,14 @@ func BenchmarkInitialization(b *testing.B) { } func runInitializationBench(b *testing.B, r wazero.Runtime) { - compiled, err := r.CompileModule(testCtx, caseWasm) + compiled, err := r.CompileModule(testCtx, caseWasm, wazero.NewCompileConfig()) if err != nil { b.Fatal(err) } defer compiled.Close(testCtx) b.ResetTimer() for i := 0; i < b.N; i++ { - mod, err := r.InstantiateModule(testCtx, compiled) + mod, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig()) if err != nil { b.Fatal(err) } diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index 8c3ae106..4c7927b2 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -15,11 +15,21 @@ import ( "github.com/tetratelabs/wazero/sys" ) -const memoryCapacityPages = 2 - // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") +var memoryCapacityPages = uint32(2) + +var compileConfig = wazero.NewCompileConfig(). + WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + if maxPages != nil { + return minPages, memoryCapacityPages, *maxPages + } + return minPages, memoryCapacityPages, memoryCapacityPages + }) + +var moduleConfig = wazero.NewModuleConfig() + var tests = map[string]func(t *testing.T, r wazero.Runtime){ "huge stack": testHugeStack, "unreachable": testUnreachable, @@ -45,9 +55,6 @@ func TestEngineInterpreter(t *testing.T) { } func runAllTests(t *testing.T, tests map[string]func(t *testing.T, r wazero.Runtime), config wazero.RuntimeConfig) { - config.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { - return memoryCapacityPages - }) for name, testf := range tests { name := name // pin testf := testf // pin @@ -369,7 +376,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { tc := tt t.Run(tc.name, func(t *testing.T) { - var importingCode, importedCode wazero.CompiledCode + var importingCode, importedCode wazero.CompiledModule var imported, importing api.Module var err error closeAndReturn := func(ctx context.Context, x uint32) uint32 { @@ -390,19 +397,19 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { // Create the host module, which exports the function that closes the importing module. importedCode, err = r.NewModuleBuilder(t.Name()+"-imported"). - ExportFunction("return_input", closeAndReturn).Build(testCtx) + ExportFunction("return_input", closeAndReturn).Compile(testCtx, compileConfig) require.NoError(t, err) - imported, err = r.InstantiateModule(testCtx, importedCode) + imported, err = r.InstantiateModule(testCtx, importedCode, moduleConfig) require.NoError(t, err) defer imported.Close(testCtx) // Import that module. source := callReturnImportSource(imported.Name(), t.Name()+"-importing") - importingCode, err = r.CompileModule(testCtx, source) + importingCode, err = r.CompileModule(testCtx, source, compileConfig) require.NoError(t, err) - importing, err = r.InstantiateModule(testCtx, importingCode) + importing, err = r.InstantiateModule(testCtx, importingCode, moduleConfig) require.NoError(t, err) defer importing.Close(testCtx) @@ -499,13 +506,13 @@ func testMultipleInstantiation(t *testing.T, r wazero.Runtime) { i64.store ) (export "store" (func $store)) - )`)) + )`), compileConfig) require.NoError(t, err) defer compiled.Close(testCtx) - // Instantiate multiple modules with the same source (*CompiledCode). + // Instantiate multiple modules with the same source (*CompiledModule). for i := 0; i < 100; i++ { - module, err := r.InstantiateModuleWithConfig(testCtx, compiled, wazero.NewModuleConfig().WithName(strconv.Itoa(i))) + module, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig().WithName(strconv.Itoa(i))) require.NoError(t, err) defer module.Close(testCtx) diff --git a/internal/integration_test/spectest/encoder_test.go b/internal/integration_test/spectest/encoder_test.go index 8f602243..bbeaece0 100644 --- a/internal/integration_test/spectest/encoder_test.go +++ b/internal/integration_test/spectest/encoder_test.go @@ -68,7 +68,7 @@ func TestBinaryEncoder(t *testing.T) { buf = requireStripCustomSections(t, buf) - mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryLimitPages) + mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemorySizer) require.NoError(t, err) encodedBuf := binary.EncodeModule(mod) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 6dc753c6..0dba7624 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -238,7 +238,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) { (func (param f64 f64) local.get 0 drop local.get 1 drop) (export "print_f64_f64" (func 6)) -)`), wasm.Features20191205, wasm.MemoryLimitPages) +)`), wasm.Features20191205, wasm.MemorySizer) require.NoError(t, err) // (global (export "global_i32") i32 (i32.const 666)) @@ -321,7 +321,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E case "module": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages) + mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemorySizer) require.NoError(t, err, msg) require.NoError(t, mod.Validate(enabledFeatures)) mod.AssignModuleID(buf) @@ -455,7 +455,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E } func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg string) { - mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryLimitPages) + mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemorySizer) if err != nil { return } diff --git a/internal/integration_test/vs/codec.go b/internal/integration_test/vs/codec.go index 629ce324..c23d4c37 100644 --- a/internal/integration_test/vs/codec.go +++ b/internal/integration_test/vs/codec.go @@ -55,7 +55,7 @@ func newExample() *wasm.Module { }}, {Body: []byte{wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}}, }, - MemorySection: &wasm.Memory{Min: 1, Max: three, IsMaxEncoded: true}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: three, IsMaxEncoded: true}, ExportSection: []*wasm.Export{ {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)}, {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(3)}, @@ -92,7 +92,7 @@ func newExample() *wasm.Module { func BenchmarkWat2Wasm(b *testing.B, vsName string, vsWat2Wasm func([]byte) error) { b.Run("wazero", func(b *testing.B) { for i := 0; i < b.N; i++ { - if m, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemoryLimitPages); err != nil { + if m, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemorySizer); err != nil { b.Fatal(err) } else { _ = binary.EncodeModule(m) diff --git a/internal/integration_test/vs/codec_test.go b/internal/integration_test/vs/codec_test.go index d77c4489..338ec059 100644 --- a/internal/integration_test/vs/codec_test.go +++ b/internal/integration_test/vs/codec_test.go @@ -14,13 +14,13 @@ import ( func TestExampleUpToDate(t *testing.T) { t.Run("binary.DecodeModule", func(t *testing.T) { - m, err := binary.DecodeModule(exampleBinary, wasm.Features20220419, wasm.MemoryLimitPages) + m, err := binary.DecodeModule(exampleBinary, wasm.Features20220419, wasm.MemorySizer) require.NoError(t, err) require.Equal(t, example, m) }) t.Run("text.DecodeModule", func(t *testing.T) { - m, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemoryLimitPages) + m, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemorySizer) require.NoError(t, err) require.Equal(t, example, m) }) @@ -49,7 +49,7 @@ func BenchmarkCodec(b *testing.B) { b.Run("binary.DecodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := binary.DecodeModule(exampleBinary, wasm.Features20220419, wasm.MemoryLimitPages); err != nil { + if _, err := binary.DecodeModule(exampleBinary, wasm.Features20220419, wasm.MemorySizer); err != nil { b.Fatal(err) } } @@ -63,7 +63,7 @@ func BenchmarkCodec(b *testing.B) { b.Run("text.DecodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemoryLimitPages); err != nil { + if _, err := text.DecodeModule(exampleText, wasm.Features20220419, wasm.MemorySizer); err != nil { b.Fatal(err) } } diff --git a/internal/integration_test/vs/runtime.go b/internal/integration_test/vs/runtime.go index 7a7cd796..28d104ec 100644 --- a/internal/integration_test/vs/runtime.go +++ b/internal/integration_test/vs/runtime.go @@ -54,7 +54,7 @@ type wazeroRuntime struct { config wazero.RuntimeConfig runtime wazero.Runtime logFn func([]byte) error - env, compiled wazero.CompiledCode + env, compiled wazero.CompiledModule } type wazeroModule struct { @@ -81,11 +81,11 @@ func (r *wazeroRuntime) Compile(ctx context.Context, cfg *RuntimeConfig) (err er if cfg.LogFn != nil { r.logFn = cfg.LogFn if r.env, err = r.runtime.NewModuleBuilder("env"). - ExportFunction("log", r.log).Build(ctx); err != nil { + ExportFunction("log", r.log).Compile(ctx, wazero.NewCompileConfig()); err != nil { return err } } - r.compiled, err = r.runtime.CompileModule(ctx, cfg.ModuleWasm) + r.compiled, err = r.runtime.CompileModule(ctx, cfg.ModuleWasm, wazero.NewCompileConfig()) return } @@ -102,13 +102,13 @@ func (r *wazeroRuntime) Instantiate(ctx context.Context, cfg *RuntimeConfig) (mo // Instantiate the host module, "env", if configured. if env := r.env; env != nil { - if m.env, err = r.runtime.InstantiateModule(ctx, env); err != nil { + if m.env, err = r.runtime.InstantiateModule(ctx, env, wazero.NewModuleConfig()); err != nil { return } } // Instantiate the module. - if m.mod, err = r.runtime.InstantiateModuleWithConfig(ctx, r.compiled, wazeroCfg); err != nil { + if m.mod, err = r.runtime.InstantiateModule(ctx, r.compiled, wazeroCfg); err != nil { return } diff --git a/internal/modgen/modgen_test.go b/internal/modgen/modgen_test.go index 1b2dccdc..afcd399f 100644 --- a/internal/modgen/modgen_test.go +++ b/internal/modgen/modgen_test.go @@ -49,7 +49,7 @@ func TestModGen(t *testing.T) { // Encode the generated module (*wasm.Module) as binary. bin := binary.EncodeModule(m) // Pass the generated binary into our compilers. - code, err := runtime.CompileModule(testCtx, bin) + code, err := runtime.CompileModule(testCtx, bin, wazero.NewCompileConfig()) require.NoError(t, err) err = code.Close(testCtx) require.NoError(t, err) diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 3c28e51b..8b3514f4 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -11,7 +11,11 @@ import ( // DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Binary Format // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-format%E2%91%A0 -func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryLimitPages uint32) (*wasm.Module, error) { +func DecodeModule( + binary []byte, + enabledFeatures wasm.Features, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) (*wasm.Module, error) { r := bytes.NewReader(binary) // Magic number. @@ -71,7 +75,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryLimitPages case wasm.SectionIDType: m.TypeSection, err = decodeTypeSection(enabledFeatures, r) case wasm.SectionIDImport: - if m.ImportSection, err = decodeImportSection(r, memoryLimitPages, enabledFeatures); err != nil { + if m.ImportSection, err = decodeImportSection(r, memorySizer, enabledFeatures); err != nil { return nil, err // avoid re-wrapping the error. } case wasm.SectionIDFunction: @@ -79,7 +83,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryLimitPages case wasm.SectionIDTable: m.TableSection, err = decodeTableSection(r, enabledFeatures) case wasm.SectionIDMemory: - m.MemorySection, err = decodeMemorySection(r, memoryLimitPages) + m.MemorySection, err = decodeMemorySection(r, memorySizer) case wasm.SectionIDGlobal: if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil { return nil, err // avoid re-wrapping the error. diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go index 10cbade2..f3f9f223 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -59,7 +59,7 @@ func TestDecodeModule(t *testing.T) { name: "table and memory section", input: &wasm.Module{ TableSection: []*wasm.Table{{Min: 3, Type: wasm.RefTypeFuncref}}, - MemorySection: &wasm.Memory{Min: 1, Max: 1, IsMaxEncoded: true}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true}, }, }, { @@ -80,7 +80,7 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemoryLimitPages) + m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemorySizer) require.NoError(t, e) require.Equal(t, tc.input, m) }) @@ -91,7 +91,7 @@ func TestDecodeModule(t *testing.T) { wasm.SectionIDCustom, 0xf, // 15 bytes in this section 0x04, 'm', 'e', 'm', 'e', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages) + m, e := DecodeModule(input, wasm.Features20191205, wasm.MemorySizer) require.NoError(t, e) require.Equal(t, &wasm.Module{}, m) }) @@ -106,24 +106,23 @@ func TestDecodeModule(t *testing.T) { subsectionIDModuleName, 0x07, // 7 bytes in this subsection 0x06, // the Module name simple is 6 bytes long 's', 'i', 'm', 'p', 'l', 'e') - m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages) + m, e := DecodeModule(input, wasm.Features20191205, wasm.MemorySizer) require.NoError(t, e) require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m) }) t.Run("data count section disabled", func(t *testing.T) { input := append(append(Magic, version...), wasm.SectionIDDataCount, 1, 0) - _, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages) + _, e := DecodeModule(input, wasm.Features20191205, wasm.MemorySizer) require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`) }) } func TestDecodeModule_Errors(t *testing.T) { tests := []struct { - name string - input []byte - memoryLimitPages uint32 - expectedErr string + name string + input []byte + expectedErr string }{ { name: "wrong magic", @@ -150,12 +149,9 @@ func TestDecodeModule_Errors(t *testing.T) { for _, tt := range tests { tc := tt - if tc.memoryLimitPages == 0 { - tc.memoryLimitPages = wasm.MemoryLimitPages - } t.Run(tc.name, func(t *testing.T) { - _, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryLimitPages) + _, e := DecodeModule(tc.input, wasm.Features20191205, wasm.MemorySizer) require.EqualError(t, e, tc.expectedErr) }) } diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index ef15f189..d561dd4f 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -8,7 +8,12 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) -func decodeImport(r *bytes.Reader, idx uint32, memoryLimitPages uint32, enabledFeatures wasm.Features) (i *wasm.Import, err error) { +func decodeImport( + r *bytes.Reader, + idx uint32, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), + enabledFeatures wasm.Features, +) (i *wasm.Import, err error) { i = &wasm.Import{} if i.Module, _, err = decodeUTF8(r, "import module"); err != nil { return nil, fmt.Errorf("import[%d] error decoding module: %w", idx, err) @@ -29,7 +34,7 @@ func decodeImport(r *bytes.Reader, idx uint32, memoryLimitPages uint32, enabledF case wasm.ExternTypeTable: i.DescTable, err = decodeTable(r, enabledFeatures) case wasm.ExternTypeMemory: - i.DescMem, err = decodeMemory(r, memoryLimitPages) + i.DescMem, err = decodeMemory(r, memorySizer) case wasm.ExternTypeGlobal: i.DescGlobal, err = decodeGlobalType(r) default: diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go index f26dfe7d..488c416e 100644 --- a/internal/wasm/binary/memory.go +++ b/internal/wasm/binary/memory.go @@ -9,20 +9,19 @@ import ( // decodeMemory returns the api.Memory decoded with the WebAssembly 1.0 (20191205) Binary Format. // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory -func decodeMemory(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) { +func decodeMemory( + r *bytes.Reader, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) (*wasm.Memory, error) { min, maxP, err := decodeLimitsType(r) if err != nil { return nil, err } - var max uint32 - var isMaxEncoded bool - if maxP != nil { - isMaxEncoded = true - max = *maxP - } - mem := &wasm.Memory{Min: min, Max: max, IsMaxEncoded: isMaxEncoded} - return mem, mem.ValidateMinMax(memoryLimitPages) + min, capacity, max := memorySizer(min, maxP) + mem := &wasm.Memory{Min: min, Cap: capacity, Max: max, IsMaxEncoded: maxP != nil} + + return mem, mem.Validate() } // encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format. diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go index 27f22827..23e44fdc 100644 --- a/internal/wasm/binary/memory_test.go +++ b/internal/wasm/binary/memory_test.go @@ -24,7 +24,7 @@ func TestMemoryType(t *testing.T) { expected: []byte{0x1, 0, 0x80, 0x80, 0x4}, }, { - name: "min 0 - default max", + name: "min 0 default max", input: &wasm.Memory{Max: wasm.MemoryLimitPages}, expected: []byte{0x0, 0}, }, @@ -35,7 +35,7 @@ func TestMemoryType(t *testing.T) { }, { name: "min=max", - input: &wasm.Memory{Min: 1, Max: 1, IsMaxEncoded: true}, + input: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true}, expected: []byte{0x1, 1, 1}, }, { @@ -45,7 +45,7 @@ func TestMemoryType(t *testing.T) { }, { name: "min largest max largest", - input: &wasm.Memory{Min: max, Max: max, IsMaxEncoded: true}, + input: &wasm.Memory{Min: max, Cap: max, Max: max, IsMaxEncoded: true}, expected: []byte{0x1, 0x80, 0x80, 0x4, 0x80, 0x80, 0x4}, }, } @@ -54,12 +54,12 @@ func TestMemoryType(t *testing.T) { tc := tt b := encodeMemory(tc.input) - t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { + t.Run(fmt.Sprintf("encode %s", tc.name), func(t *testing.T) { require.Equal(t, tc.expected, b) }) - t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { - binary, err := decodeMemory(bytes.NewReader(b), max) + t.Run(fmt.Sprintf("decode %s", tc.name), func(t *testing.T) { + binary, err := decodeMemory(bytes.NewReader(b), wasm.MemorySizer) require.NoError(t, err) require.Equal(t, binary, tc.input) }) @@ -68,10 +68,9 @@ func TestMemoryType(t *testing.T) { func TestDecodeMemoryType_Errors(t *testing.T) { tests := []struct { - name string - input []byte - memoryLimitPages uint32 - expectedErr string + name string + input []byte + expectedErr string }{ { name: "max < min", @@ -93,12 +92,8 @@ func TestDecodeMemoryType_Errors(t *testing.T) { for _, tt := range tests { tc := tt - if tc.memoryLimitPages == 0 { - tc.memoryLimitPages = wasm.MemoryLimitPages - } - t.Run(tc.name, func(t *testing.T) { - _, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryLimitPages) + _, err := decodeMemory(bytes.NewReader(tc.input), wasm.MemorySizer) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 7795036b..8c2817c7 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -24,7 +24,11 @@ func decodeTypeSection(enabledFeatures wasm.Features, r *bytes.Reader) ([]*wasm. return result, nil } -func decodeImportSection(r *bytes.Reader, memoryLimitPages uint32, enabledFeatures wasm.Features) ([]*wasm.Import, error) { +func decodeImportSection( + r *bytes.Reader, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), + enabledFeatures wasm.Features, +) ([]*wasm.Import, error) { vs, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("get size of vector: %w", err) @@ -32,7 +36,7 @@ func decodeImportSection(r *bytes.Reader, memoryLimitPages uint32, enabledFeatur result := make([]*wasm.Import, vs) for i := uint32(0); i < vs; i++ { - if result[i], err = decodeImport(r, i, memoryLimitPages, enabledFeatures); err != nil { + if result[i], err = decodeImport(r, i, memorySizer, enabledFeatures); err != nil { return nil, err } } @@ -76,7 +80,10 @@ func decodeTableSection(r *bytes.Reader, enabledFeatures wasm.Features) ([]*wasm return ret, nil } -func decodeMemorySection(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) { +func decodeMemorySection( + r *bytes.Reader, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) (*wasm.Memory, error) { vs, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("error reading size") @@ -85,7 +92,7 @@ func decodeMemorySection(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs) } - return decodeMemory(r, memoryLimitPages) + return decodeMemory(r, memorySizer) } func decodeGlobalSection(r *bytes.Reader, enabledFeatures wasm.Features) ([]*wasm.Global, error) { diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index cf753130..7d129057 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -92,7 +92,7 @@ func TestMemorySection(t *testing.T) { 0x01, // 1 memory 0x01, 0x02, 0x03, // (memory 2 3) }, - expected: &wasm.Memory{Min: 2, Max: three, IsMaxEncoded: true}, + expected: &wasm.Memory{Min: 2, Cap: 2, Max: three, IsMaxEncoded: true}, }, } @@ -100,7 +100,7 @@ func TestMemorySection(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryLimitPages) + memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemorySizer) require.NoError(t, err) require.Equal(t, tc.expected, memories) }) @@ -109,10 +109,9 @@ func TestMemorySection(t *testing.T) { func TestMemorySection_Errors(t *testing.T) { tests := []struct { - name string - input []byte - memoryLimitPages uint32 - expectedErr string + name string + input []byte + expectedErr string }{ { name: "min and min with max", @@ -128,12 +127,8 @@ func TestMemorySection_Errors(t *testing.T) { for _, tt := range tests { tc := tt - if tc.memoryLimitPages == 0 { - tc.memoryLimitPages = wasm.MemoryLimitPages - } - t.Run(tc.name, func(t *testing.T) { - _, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryLimitPages) + _, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemorySizer) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 55833cf8..f0ec9cf4 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -11,7 +11,7 @@ func TestModule_ValidateFunction_validateFunctionWithMaxStackValues(t *testing.T const max = 100 const valuesNum = max + 1 - // Build a function which has max+1 const instructions. + // Compile a function which has max+1 const instructions. var body []byte for i := 0; i < valuesNum; i++ { body = append(body, OpcodeI32Const, 1) diff --git a/internal/wasm/jit/jit_controlflow_test.go b/internal/wasm/jit/jit_controlflow_test.go index f3b64043..57d5b49b 100644 --- a/internal/wasm/jit/jit_controlflow_test.go +++ b/internal/wasm/jit/jit_controlflow_test.go @@ -860,7 +860,7 @@ func TestCompiler_returnFunction(t *testing.T) { t.Run("exit", func(t *testing.T) { env := newJITEnvironment() - // Build code. + // Compile code. compiler := env.requireNewCompiler(t, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) diff --git a/internal/wasm/jit/jit_numeric_test.go b/internal/wasm/jit/jit_numeric_test.go index b71f4042..16fea9b0 100644 --- a/internal/wasm/jit/jit_numeric_test.go +++ b/internal/wasm/jit/jit_numeric_test.go @@ -36,7 +36,7 @@ func TestCompiler_compileConsts(t *testing.T) { t.Run(fmt.Sprintf("0x%x", val), func(t *testing.T) { env := newJITEnvironment() - // Build code. + // Compile code. compiler := env.requireNewCompiler(t, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) diff --git a/internal/wasm/jit/jit_stack_test.go b/internal/wasm/jit/jit_stack_test.go index da548796..ee5c7caf 100644 --- a/internal/wasm/jit/jit_stack_test.go +++ b/internal/wasm/jit/jit_stack_test.go @@ -26,7 +26,7 @@ func TestCompiler_releaseRegisterToStack(t *testing.T) { t.Run(tc.name, func(t *testing.T) { env := newJITEnvironment() - // Build code. + // Compile code. compiler := env.requireNewCompiler(t, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) @@ -88,7 +88,7 @@ func TestCompiler_compileLoadValueOnStackToRegister(t *testing.T) { t.Run(tc.name, func(t *testing.T) { env := newJITEnvironment() - // Build code. + // Compile code. compiler := env.requireNewCompiler(t, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go index 9ef179b8..c695ce49 100644 --- a/internal/wasm/memory.go +++ b/internal/wasm/memory.go @@ -24,6 +24,15 @@ const ( MemoryPageSizeInBits = 16 ) +// MemorySizer is the default function that derives min, capacity and max pages from decoded source. The capacity +// returned is set to minPages and max defaults to MemoryLimitPages when maxPages is nil. +var MemorySizer api.MemorySizer = func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + if maxPages != nil { + return minPages, minPages, *maxPages + } + return minPages, minPages, MemoryLimitPages +} + // compile-time check to ensure MemoryInstance implements api.Memory var _ api.Memory = &MemoryInstance{} diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go index ee19e628..43d4f7b0 100644 --- a/internal/wasm/memory_test.go +++ b/internal/wasm/memory_test.go @@ -14,6 +14,21 @@ func TestMemoryPageConsts(t *testing.T) { require.Equal(t, MemoryLimitPages, uint32(1<<16)) } +func TestMemoryPages(t *testing.T) { + t.Run("cap=min, nil max", func(t *testing.T) { + min, capacity, max := MemorySizer(1, nil) + require.Equal(t, uint32(1), min) + require.Equal(t, uint32(1), capacity) + require.Equal(t, MemoryLimitPages, max) + }) + t.Run("cap=min, max", func(t *testing.T) { + min, capacity, max := MemorySizer(1, uint32Ptr(2)) + require.Equal(t, uint32(1), min) + require.Equal(t, uint32(1), capacity) + require.Equal(t, uint32(2), max) + }) +} + func Test_MemoryPagesToBytesNum(t *testing.T) { for _, numPage := range []uint32{0, 1, 5, 10} { require.Equal(t, uint64(numPage*MemoryPageSize), MemoryPagesToBytesNum(numPage)) diff --git a/internal/wasm/module.go b/internal/wasm/module.go index f2b06514..460b7a03 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -23,7 +23,11 @@ import ( // * result is the module parsed or nil on error // * err is a FormatError invoking the parser, dangling block comments or unexpected characters. // See binary.DecodeModule and text.DecodeModule -type DecodeModule func(source []byte, enabledFeatures Features, memoryLimitPages uint32) (result *Module, err error) +type DecodeModule func( + source []byte, + enabledFeatures Features, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) (result *Module, err error) // EncodeModule encodes the given module into a byte slice depending on the format of the implementation. // See binary.EncodeModule @@ -648,29 +652,25 @@ type Memory struct { IsMaxEncoded bool } -// ValidateMinMax ensures values assigned to Min and Max are within valid thresholds. -func (m *Memory) ValidateMinMax(memoryLimitPages uint32) error { - if !m.IsMaxEncoded { - m.Max = memoryLimitPages - } - min, max := m.Min, m.Max - if max > memoryLimitPages { - return fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)", max, PagesToUnitOfBytes(max), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages)) - } else if min > memoryLimitPages { - return fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)", min, PagesToUnitOfBytes(min), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages)) - } else if min > max { - return fmt.Errorf("min %d pages (%s) > max %d pages (%s)", min, PagesToUnitOfBytes(min), max, PagesToUnitOfBytes(max)) - } - return nil -} +// Validate ensures values assigned to Min, Cap and Max are within valid thresholds. +func (m *Memory) Validate() error { + min, capacity, max := m.Min, m.Cap, m.Max -// ValidateCap ensures the value assigned to Cap is within valid thresholds. -func (m *Memory) ValidateCap(memoryLimitPages uint32) error { - capacity, min := m.Cap, m.Min - if capacity < min { - return fmt.Errorf("capacity %d pages (%s) less than minimum %d pages (%s)", capacity, PagesToUnitOfBytes(capacity), min, PagesToUnitOfBytes(min)) - } else if capacity > memoryLimitPages { - return fmt.Errorf("capacity %d pages (%s) over limit of %d pages (%s)", capacity, PagesToUnitOfBytes(capacity), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages)) + if max > MemoryLimitPages { + return fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)", + max, PagesToUnitOfBytes(max), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages)) + } else if min > MemoryLimitPages { + return fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)", + min, PagesToUnitOfBytes(min), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages)) + } else if min > max { + return fmt.Errorf("min %d pages (%s) > max %d pages (%s)", + min, PagesToUnitOfBytes(min), max, PagesToUnitOfBytes(max)) + } else if capacity < min { + return fmt.Errorf("capacity %d pages (%s) less than minimum %d pages (%s)", + capacity, PagesToUnitOfBytes(capacity), min, PagesToUnitOfBytes(min)) + } else if capacity > MemoryLimitPages { + return fmt.Errorf("capacity %d pages (%s) over limit of %d pages (%s)", + capacity, PagesToUnitOfBytes(capacity), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages)) } return nil } @@ -908,46 +908,21 @@ func ValueTypeName(t ValueType) string { return api.ValueTypeName(t) } -// ExternType classifies imports and exports with their respective types. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0 -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#external-types%E2%91%A0 -type ExternType = byte +// ExternType is an alias of api.ExternType defined to simplify imports. +type ExternType = api.ExternType const ( - ExternTypeFunc ExternType = 0x00 - ExternTypeTable ExternType = 0x01 - ExternTypeMemory ExternType = 0x02 - ExternTypeGlobal ExternType = 0x03 + ExternTypeFunc = api.ExternTypeFunc + ExternTypeFuncName = api.ExternTypeFuncName + ExternTypeTable = api.ExternTypeTable + ExternTypeTableName = api.ExternTypeTableName + ExternTypeMemory = api.ExternTypeMemory + ExternTypeMemoryName = api.ExternTypeMemoryName + ExternTypeGlobal = api.ExternTypeGlobal + ExternTypeGlobalName = api.ExternTypeGlobalName ) -// The below are exported to consolidate parsing behavior for external types. -const ( - // ExternTypeFuncName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeFunc. - ExternTypeFuncName = "func" - // ExternTypeTableName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeTable. - ExternTypeTableName = "table" - // ExternTypeMemoryName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeMemory. - ExternTypeMemoryName = "memory" - // ExternTypeGlobalName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeGlobal. - ExternTypeGlobalName = "global" -) - -// ExternTypeName returns the name of the WebAssembly 1.0 (20191205) Text Format field of the given type. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#imports⑤ -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A4 -func ExternTypeName(et ExternType) string { - switch et { - case ExternTypeFunc: - return ExternTypeFuncName - case ExternTypeTable: - return ExternTypeTableName - case ExternTypeMemory: - return ExternTypeMemoryName - case ExternTypeGlobal: - return ExternTypeGlobalName - } - return fmt.Sprintf("%#x", et) +// ExternTypeName is an alias of api.ExternTypeName defined to simplify imports. +func ExternTypeName(t ValueType) string { + return api.ExternTypeName(t) } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 461daea6..f8453c09 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -68,29 +68,7 @@ func TestSectionIDName(t *testing.T) { } } -func TestExternTypeName(t *testing.T) { - tests := []struct { - name string - input ExternType - expected string - }{ - {"func", ExternTypeFunc, "func"}, - {"table", ExternTypeTable, "table"}, - {"mem", ExternTypeMemory, "memory"}, - {"global", ExternTypeGlobal, "global"}, - {"unknown", 100, "0x64"}, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.expected, ExternTypeName(tc.input)) - }) - } -} - -func TestMemory_ValidateCap(t *testing.T) { +func TestMemory_Validate(t *testing.T) { tests := []struct { name string mem *Memory @@ -98,58 +76,32 @@ func TestMemory_ValidateCap(t *testing.T) { }{ { name: "ok", - mem: &Memory{Min: 2, Cap: 2}, + mem: &Memory{Min: 2, Cap: 2, Max: 2}, }, { name: "cap < min", - mem: &Memory{Min: 2, Cap: 1}, + mem: &Memory{Min: 2, Cap: 1, Max: 2}, expectedErr: "capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)", }, { name: "cap > maxLimit", - mem: &Memory{Min: 2, Cap: 4}, - expectedErr: "capacity 4 pages (256 Ki) over limit of 3 pages (192 Ki)", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - err := tc.mem.ValidateCap(3) - if tc.expectedErr == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tc.expectedErr) - } - }) - } -} - -func TestMemory_ValidateMinMax(t *testing.T) { - tests := []struct { - name string - mem *Memory - expectedErr string - }{ - { - name: "ok", - mem: &Memory{Min: 2}, + mem: &Memory{Min: 2, Cap: math.MaxUint32, Max: 2}, + expectedErr: "capacity 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", }, { name: "max < min", - mem: &Memory{Min: 2, Max: 0, IsMaxEncoded: true}, + mem: &Memory{Min: 2, Cap: 2, Max: 0, IsMaxEncoded: true}, expectedErr: "min 2 pages (128 Ki) > max 0 pages (0 Ki)", }, { name: "min > limit", mem: &Memory{Min: math.MaxUint32}, - expectedErr: "min 4294967295 pages (3 Ti) over limit of 3 pages (192 Ki)", + expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", }, { name: "max > limit", mem: &Memory{Max: math.MaxUint32, IsMaxEncoded: true}, - expectedErr: "max 4294967295 pages (3 Ti) over limit of 3 pages (192 Ki)", + expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", }, } @@ -157,7 +109,7 @@ func TestMemory_ValidateMinMax(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - err := tc.mem.ValidateMinMax(3) + err := tc.mem.Validate() if tc.expectedErr == "" { require.NoError(t, err) } else { diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 7556ffe6..6b1faaac 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -390,7 +390,7 @@ func (s *Store) Instantiate( // Now all the validation passes, we are safe to mutate memory instances (possibly imported ones). m.applyData(module.DataSection) - // Build the default context for calls to this module. + // Compile the default context for calls to this module. m.CallCtx = NewCallContext(s, m, sys) // Execute the start function. diff --git a/internal/wasm/text/decoder.go b/internal/wasm/text/decoder.go index 2fb11e98..e0e36b21 100644 --- a/internal/wasm/text/decoder.go +++ b/internal/wasm/text/decoder.go @@ -104,7 +104,11 @@ type moduleParser struct { // DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Text Format // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0 -func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryLimitPages uint32) (result *wasm.Module, err error) { +func DecodeModule( + source []byte, + enabledFeatures wasm.Features, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) (result *wasm.Module, err error) { // TODO: when globals are supported, err on global vars if disabled // names are the wasm.Module NameSection @@ -114,7 +118,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryLimitPages // * LocalNames: nil when neither imported nor module-defined functions had named (param) fields. names := &wasm.NameSection{} module := &wasm.Module{NameSection: names} - p := newModuleParser(module, enabledFeatures, memoryLimitPages) + p := newModuleParser(module, enabledFeatures, memorySizer) p.source = source // A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this @@ -143,7 +147,11 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryLimitPages return module, nil } -func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryLimitPages uint32) *moduleParser { +func newModuleParser( + module *wasm.Module, + enabledFeatures wasm.Features, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), +) *moduleParser { p := moduleParser{module: module, enabledFeatures: enabledFeatures, typeNamespace: newIndexNamespace(module.SectionElementCount), funcNamespace: newIndexNamespace(module.SectionElementCount), @@ -152,7 +160,7 @@ func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryL p.typeParser = newTypeParser(enabledFeatures, p.typeNamespace, p.onTypeEnd) p.typeUseParser = newTypeUseParser(enabledFeatures, module, p.typeNamespace) p.funcParser = newFuncParser(enabledFeatures, p.typeUseParser, p.funcNamespace, p.endFunc) - p.memoryParser = newMemoryParser(memoryLimitPages, p.memoryNamespace, p.endMemory) + p.memoryParser = newMemoryParser(memorySizer, p.memoryNamespace, p.endMemory) return &p } @@ -451,8 +459,8 @@ func (p *moduleParser) endFunc(typeIdx wasm.Index, code *wasm.Code, name string, // endMemory adds the limits for the current memory, and increments memoryNamespace as it is shared across imported and // module-defined memories. Finally, this returns parseModule to prepare for the next field. -func (p *moduleParser) endMemory(min, max uint32, maxDecoded bool) tokenParser { - p.module.MemorySection = &wasm.Memory{Min: min, Max: max, IsMaxEncoded: maxDecoded} +func (p *moduleParser) endMemory(mem *wasm.Memory) tokenParser { + p.module.MemorySection = mem p.pos = positionModule return p.parseModule } diff --git a/internal/wasm/text/decoder_test.go b/internal/wasm/text/decoder_test.go index de411d0b..126dad7b 100644 --- a/internal/wasm/text/decoder_test.go +++ b/internal/wasm/text/decoder_test.go @@ -1226,14 +1226,14 @@ func TestDecodeModule(t *testing.T) { name: "memory", input: "(module (memory 1))", expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, }, }, { name: "memory ID", input: "(module (memory $mem 1))", expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, }, }, { @@ -1465,7 +1465,7 @@ func TestDecodeModule(t *testing.T) { (export "memory" (memory $mem)) )`, expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, ExportSection: []*wasm.Export{ {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}, }, @@ -1564,7 +1564,7 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, err := DecodeModule([]byte(tc.input), wasm.Features20220419, wasm.MemoryLimitPages) + m, err := DecodeModule([]byte(tc.input), wasm.Features20220419, wasm.MemorySizer) require.NoError(t, err) require.Equal(t, tc.expected, m) }) @@ -1573,9 +1573,8 @@ func TestDecodeModule(t *testing.T) { func TestParseModule_Errors(t *testing.T) { tests := []struct { - name, input string - memoryLimitPages uint32 - expectedErr string + name, input string + expectedErr string }{ { name: "forgot parens", @@ -1998,10 +1997,9 @@ func TestParseModule_Errors(t *testing.T) { expectedErr: "2:47: i32.trunc_sat_f32_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled in module.func[0]", }, { - name: "memory over max", - input: "(module (memory 1 4))", - memoryLimitPages: 3, - expectedErr: "1:19: max 4 pages (256 Ki) over limit of 3 pages (192 Ki) in module.memory[0]", + name: "memory over max", + input: "(module (memory 1 70000))", + expectedErr: "1:19: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi) in module.memory[0]", }, { name: "second memory", @@ -2145,17 +2143,14 @@ func TestParseModule_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - if tc.memoryLimitPages == 0 { - tc.memoryLimitPages = wasm.MemoryLimitPages - } - _, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryLimitPages) + _, err := DecodeModule([]byte(tc.input), wasm.Features20191205, wasm.MemorySizer) require.EqualError(t, err, tc.expectedErr) }) } } func TestModuleParser_ErrorContext(t *testing.T) { - p := newModuleParser(&wasm.Module{}, 0, 0) + p := newModuleParser(&wasm.Module{}, 0, wasm.MemorySizer) tests := []struct { input string pos parserPosition diff --git a/internal/wasm/text/memory_parser.go b/internal/wasm/text/memory_parser.go index e799b4a6..c5000a18 100644 --- a/internal/wasm/text/memory_parser.go +++ b/internal/wasm/text/memory_parser.go @@ -7,11 +7,15 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) -func newMemoryParser(memoryLimitPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser { - return &memoryParser{memoryLimitPages: memoryLimitPages, memoryNamespace: memoryNamespace, onMemory: onMemory} +func newMemoryParser( + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), + memoryNamespace *indexNamespace, + onMemory onMemory, +) *memoryParser { + return &memoryParser{memorySizer: memorySizer, memoryNamespace: memoryNamespace, onMemory: onMemory} } -type onMemory func(min, max uint32, maxDecoded bool) tokenParser +type onMemory func(*wasm.Memory) tokenParser // memoryParser parses an api.Memory from and dispatches to onMemory. // @@ -22,9 +26,7 @@ type onMemory func(min, max uint32, maxDecoded bool) tokenParser // Note: memoryParser is reusable. The caller resets via begin. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7 type memoryParser struct { - // memoryLimitPages is the limit of pages (not bytes) for each wasm.Memory. - memoryLimitPages uint32 - maxDecoded bool + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) memoryNamespace *indexNamespace @@ -32,9 +34,7 @@ type memoryParser struct { onMemory onMemory // currentMin is reset on begin and read onMemory - currentMin uint32 - // currentMax is reset on begin and read onMemory - currentMax uint32 + currentMemory *wasm.Memory } // begin should be called after reaching the wasm.ExternTypeMemoryName keyword in a module field. Parsing @@ -49,8 +49,7 @@ type memoryParser struct { // Ex. No memory ID `(memory 0)` // calls beginMin --^ func (p *memoryParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - p.currentMin = 0 - p.currentMax = p.memoryLimitPages + p.currentMemory = &wasm.Memory{} if tok == tokenID { // Ex. $mem if _, err := p.memoryNamespace.setID(tokenBytes); err != nil { return nil, err @@ -66,10 +65,14 @@ func (p *memoryParser) beginMin(tok tokenType, tokenBytes []byte, _, _ uint32) ( case tokenID: // Ex.(memory $rf32 $rf32 return nil, fmt.Errorf("redundant ID %s", tokenBytes) case tokenUN: - if i, overflow := decodeUint32(tokenBytes); overflow || i > p.memoryLimitPages { - return nil, fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryLimitPages, wasm.PagesToUnitOfBytes(p.memoryLimitPages)) + mem := p.currentMemory + if min, err := decodePages("min", tokenBytes); err != nil { + return nil, err } else { - p.currentMin = i + mem.Min, mem.Cap, mem.Max = p.memorySizer(min, nil) + if err = mem.Validate(); err != nil { + return nil, err + } } return p.beginMax, nil case tokenRParen: @@ -84,14 +87,16 @@ func (p *memoryParser) beginMin(tok tokenType, tokenBytes []byte, _, _ uint32) ( func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { switch tok { case tokenUN: - i, overflow := decodeUint32(tokenBytes) - if overflow || i > p.memoryLimitPages { - return nil, fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryLimitPages, wasm.PagesToUnitOfBytes(p.memoryLimitPages)) - } else if i < p.currentMin { - return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", p.currentMin, wasm.PagesToUnitOfBytes(p.currentMin), i, wasm.PagesToUnitOfBytes(i)) + mem := p.currentMemory + if max, err := decodePages("max", tokenBytes); err != nil { + return nil, err + } else { + mem.Min, mem.Cap, mem.Max = p.memorySizer(p.currentMemory.Min, &max) + mem.IsMaxEncoded = true + if err = mem.Validate(); err != nil { + return nil, err + } } - p.maxDecoded = true - p.currentMax = i return p.end, nil case tokenRParen: return p.end(tok, tokenBytes, line, col) @@ -100,11 +105,20 @@ func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint } } +func decodePages(fieldName string, tokenBytes []byte) (uint32, error) { + i, overflow := decodeUint32(tokenBytes) + if overflow { + return 0, fmt.Errorf("%s %d pages (%s) over limit of %d pages (%s)", fieldName, + i, wasm.PagesToUnitOfBytes(i), wasm.MemoryLimitPages, wasm.PagesToUnitOfBytes(wasm.MemoryLimitPages)) + } + return i, nil +} + // end increments the memory namespace and calls onMemory with the current limits func (p *memoryParser) end(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { if tok != tokenRParen { return nil, unexpectedToken(tok, tokenBytes) } p.memoryNamespace.count++ - return p.onMemory(p.currentMin, p.currentMax, p.maxDecoded), nil + return p.onMemory(p.currentMemory), nil } diff --git a/internal/wasm/text/memory_parser_test.go b/internal/wasm/text/memory_parser_test.go index 2f8b18c5..47c66cba 100644 --- a/internal/wasm/text/memory_parser_test.go +++ b/internal/wasm/text/memory_parser_test.go @@ -29,12 +29,12 @@ func TestMemoryParser(t *testing.T) { { name: "min largest", input: "(memory 65536)", - expected: &wasm.Memory{Min: max, Max: max}, + expected: &wasm.Memory{Min: max, Cap: max, Max: max}, }, { - name: "min largest - ID", + name: "min largest ID", input: "(memory $mem 65536)", - expected: &wasm.Memory{Min: max, Max: max}, + expected: &wasm.Memory{Min: max, Cap: max, Max: max}, expectedID: "mem", }, { @@ -45,12 +45,12 @@ func TestMemoryParser(t *testing.T) { { name: "min largest max largest", input: "(memory 65536 65536)", - expected: &wasm.Memory{Min: max, Max: max, IsMaxEncoded: true}, + expected: &wasm.Memory{Min: max, Cap: max, Max: max, IsMaxEncoded: true}, }, { - name: "min largest max largest - ID", + name: "min largest max largest ID", input: "(memory $mem 65536 65536)", - expected: &wasm.Memory{Min: max, Max: max, IsMaxEncoded: true}, + expected: &wasm.Memory{Min: max, Cap: max, Max: max, IsMaxEncoded: true}, expectedID: "mem", }, } @@ -162,11 +162,11 @@ func TestMemoryParser_Errors(t *testing.T) { func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.Memory, *memoryParser, error) { var parsed *wasm.Memory - var setFunc onMemory = func(min, max uint32, maxDecoded bool) tokenParser { - parsed = &wasm.Memory{Min: min, Max: max, IsMaxEncoded: maxDecoded} + var setFunc onMemory = func(mem *wasm.Memory) tokenParser { + parsed = mem return parseErr } - tp := newMemoryParser(wasm.MemoryLimitPages, memoryNamespace, setFunc) + tp := newMemoryParser(wasm.MemorySizer, memoryNamespace, setFunc) // memoryParser starts after the '(memory', so we need to eat it first! _, _, err := lex(skipTokens(2, tp.begin), []byte(input)) return parsed, tp, err diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go index ac7047f2..d5ab732c 100644 --- a/internal/wazeroir/compiler_test.go +++ b/internal/wazeroir/compiler_test.go @@ -558,7 +558,7 @@ func requireCompilationResult(t *testing.T, enabledFeatures wasm.Features, expec } func requireModuleText(t *testing.T, source string) *wasm.Module { - m, err := text.DecodeModule([]byte(source), wasm.Features20220419, wasm.MemoryLimitPages) + m, err := text.DecodeModule([]byte(source), wasm.Features20220419, wasm.MemorySizer) require.NoError(t, err) return m } diff --git a/wasi/example_test.go b/wasi/example_test.go index e8169095..3d92dcd4 100644 --- a/wasi/example_test.go +++ b/wasi/example_test.go @@ -27,11 +27,8 @@ func Example() { } defer wm.Close(testCtx) - // Override default configuration (which discards stdout). - config := wazero.NewModuleConfig().WithStdout(os.Stdout) - - // InstantiateModuleFromCodeWithConfig runs the "_start" function which is like a "main" function. - _, err = r.InstantiateModuleFromCodeWithConfig(ctx, []byte(` + // Compile the WebAssembly module using the default configuration. + code, err := r.CompileModule(ctx, []byte(` (module (import "wasi_snapshot_preview1" "proc_exit" (func $wasi.proc_exit (param $rval i32))) @@ -41,8 +38,18 @@ func Example() { ) (export "_start" (func $main)) ) -`), config.WithName("wasi-demo")) +`), wazero.NewCompileConfig()) + if err != nil { + log.Fatal(err) + } + defer code.Close(ctx) + // InstantiateModule runs the "_start" function which is like a "main" function. + // Override default configuration (which discards stdout). + mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithName("wasi-demo")) + if mod != nil { + defer mod.Close(ctx) + } // Print the exit code if exitErr, ok := err.(*sys.ExitError); ok { fmt.Printf("exit_code: %d\n", exitErr.ExitCode()) diff --git a/wasi/usage_test.go b/wasi/usage_test.go index cf8effd4..dc0e8b48 100644 --- a/wasi/usage_test.go +++ b/wasi/usage_test.go @@ -13,7 +13,7 @@ import ( //go:embed testdata/wasi_arg.wasm var wasiArg []byte -func TestInstantiateModuleWithConfig(t *testing.T) { +func TestInstantiateModule(t *testing.T) { r := wazero.NewRuntime() stdout := bytes.NewBuffer(nil) @@ -24,13 +24,13 @@ func TestInstantiateModuleWithConfig(t *testing.T) { require.NoError(t, err) defer wm.Close(testCtx) - compiled, err := r.CompileModule(testCtx, wasiArg) + compiled, err := r.CompileModule(testCtx, wasiArg, wazero.NewCompileConfig()) require.NoError(t, err) defer compiled.Close(testCtx) // Re-use the same module many times. for _, tc := range []string{"a", "b", "c"} { - mod, err := r.InstantiateModuleWithConfig(testCtx, compiled, sys.WithArgs(tc).WithName(tc)) + mod, err := r.InstantiateModule(testCtx, compiled, sys.WithArgs(tc).WithName(tc)) require.NoError(t, err) // Ensure the scoped configuration applied. As the args are null-terminated, we append zero (NUL). diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go index 8210b0bd..68a7dc56 100644 --- a/wasi/wasi_test.go +++ b/wasi/wasi_test.go @@ -2040,11 +2040,11 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo (memory 1 1) ;; just an arbitrary size big enough for tests (export "memory" (memory 0)) (export "%[1]s" (func $wasi.%[1]s)) -)`, wasifunction, wasiimport))) +)`, wasifunction, wasiimport)), wazero.NewCompileConfig()) require.NoError(t, err) defer compiled.Close(ctx) - mod, err := r.InstantiateModuleWithConfig(ctx, compiled, wazero.NewModuleConfig().WithName(t.Name())) + mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName(t.Name())) require.NoError(t, err) if sysCtx != nil { diff --git a/wasm.go b/wasm.go index 55596921..1696463f 100644 --- a/wasm.go +++ b/wasm.go @@ -14,16 +14,13 @@ import ( "github.com/tetratelabs/wazero/sys" ) -// Runtime allows embedding of WebAssembly 1.0 (20191205) modules. +// Runtime allows embedding of WebAssembly modules. // // Ex. // ctx := context.Background() // r := wazero.NewRuntime() -// compiled, _ := r.CompileModule(ctx, source) -// module, _ := r.InstantiateModule(ctx, compiled) +// module, _ := r.InstantiateModuleFromCode(ctx, source) // defer module.Close() -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/ type Runtime interface { // NewModuleBuilder lets you create modules out of functions defined in Go. // @@ -39,8 +36,8 @@ type Runtime interface { // Module returns exports from an instantiated module or nil if there aren't any. Module(moduleName string) api.Module - // CompileModule decodes the WebAssembly 1.0 (20191205) text or binary source or errs if invalid. - // Any pre-compilation done after decoding the source is dependent on the RuntimeConfig. + // CompileModule decodes the WebAssembly text or binary source or errs if invalid. + // Any pre-compilation done after decoding the source is dependent on RuntimeConfig or CompileConfig. // // There are two main reasons to use CompileModule instead of InstantiateModuleFromCode: // * Improve performance when the same module is instantiated multiple times under different names @@ -49,10 +46,9 @@ type Runtime interface { // Note: When the context is nil, it defaults to context.Background. // Note: The resulting module name defaults to what was binary from the custom name section. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 - CompileModule(ctx context.Context, source []byte) (CompiledCode, error) + CompileModule(ctx context.Context, source []byte, config CompileConfig) (CompiledModule, error) - // InstantiateModuleFromCode instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or - // errs if invalid. + // InstantiateModuleFromCode instantiates a module from the WebAssembly text or binary source or errs if invalid. // // Ex. // ctx := context.Background() @@ -62,59 +58,40 @@ type Runtime interface { // Note: When the context is nil, it defaults to context.Background. // Note: This is a convenience utility that chains CompileModule with InstantiateModule. To instantiate the same // source multiple times, use CompileModule as InstantiateModule avoids redundant decoding and/or compilation. + // Note: To avoid using configuration defaults, use InstantiateModule instead. InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error) - // InstantiateModuleFromCodeWithConfig is a convenience function that chains CompileModule to - // InstantiateModuleWithConfig. - // - // Ex. To only change the module name: - // ctx := context.Background() - // r := wazero.NewRuntime() - // wasm, _ := r.InstantiateModuleFromCodeWithConfig(ctx, source, wazero.NewModuleConfig(). - // WithName("wasm") - // ) - // defer wasm.Close() - // - // Note: When the context is nil, it defaults to context.Background. - InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) - // InstantiateModule instantiates the module namespace or errs if the configuration was invalid. // // Ex. // ctx := context.Background() // r := wazero.NewRuntime() - // compiled, _ := r.CompileModule(ctx, source) + // compiled, _ := r.CompileModule(ctx, source, wazero.NewCompileConfig()) // defer compiled.Close() - // module, _ := r.InstantiateModule(ctx, compiled) + // module, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("prod")) // defer module.Close() // - // While CompiledCode is pre-validated, there are a few situations which can cause an error: + // While CompiledModule is pre-validated, there are a few situations which can cause an error: // * The module name is already in use. // * The module has a table element initializer that resolves to an index outside the Table minimum size. // * The module has a start function, and it failed to execute. // - // Note: When the context is nil, it defaults to context.Background. - InstantiateModule(ctx context.Context, compiled CompiledCode) (api.Module, error) - - // InstantiateModuleWithConfig is like InstantiateModule, except you can override configuration such as the module - // name or ENV variables. - // - // For example, you can use this to define different args depending on the importing module. + // Configuration can also define different args depending on the importing module. // // ctx := context.Background() // r := wazero.NewRuntime() // wasi, _ := wasi.InstantiateSnapshotPreview1(r) - // compiled, _ := r.CompileModule(ctx, source) + // compiled, _ := r.CompileModule(ctx, source, wazero.NewCompileConfig()) // // // Initialize base configuration: // config := wazero.NewModuleConfig().WithStdout(buf) // // // Assign different configuration on each instantiation - // module, _ := r.InstantiateModuleWithConfig(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw")) + // module, _ := r.InstantiateModule(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw")) // - // Note: When the context is nil, it defaults to context.Background. // Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result. - InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config ModuleConfig) (api.Module, error) + // Note: When the context is nil, it defaults to context.Background. + InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error) } func NewRuntime() Runtime { @@ -128,19 +105,15 @@ func NewRuntimeWithConfig(rConfig RuntimeConfig) Runtime { panic(fmt.Errorf("unsupported wazero.RuntimeConfig implementation: %#v", rConfig)) } return &runtime{ - store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)), - enabledFeatures: config.enabledFeatures, - memoryLimitPages: config.memoryLimitPages, - memoryCapacityPages: config.memoryCapacityPages, + store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)), + enabledFeatures: config.enabledFeatures, } } // runtime allows decoupling of public interfaces from internal representation. type runtime struct { - enabledFeatures wasm.Features - store *wasm.Store - memoryLimitPages uint32 - memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32 + store *wasm.Store + enabledFeatures wasm.Features } // Module implements Runtime.Module @@ -149,11 +122,16 @@ func (r *runtime) Module(moduleName string) api.Module { } // CompileModule implements Runtime.CompileModule -func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCode, error) { +func (r *runtime) CompileModule(ctx context.Context, source []byte, cConfig CompileConfig) (CompiledModule, error) { if source == nil { return nil, errors.New("source == nil") } + config, ok := cConfig.(*compileConfig) + if !ok { + panic(fmt.Errorf("unsupported wazero.CompileConfig implementation: %#v", cConfig)) + } + if len(source) < 8 { // Ex. less than magic+version in binary or '(module)' in text return nil, errors.New("invalid source") } @@ -166,14 +144,7 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCod decoder = text.DecodeModule } - if r.memoryLimitPages > wasm.MemoryLimitPages { - return nil, fmt.Errorf("memoryLimitPages %d (%s) > specification max %d (%s)", - r.memoryLimitPages, wasm.PagesToUnitOfBytes(r.memoryLimitPages), - wasm.MemoryLimitPages, wasm.PagesToUnitOfBytes(wasm.MemoryLimitPages)) - } - - internal, err := decoder(source, r.enabledFeatures, r.memoryLimitPages) - + internal, err := decoder(source, r.enabledFeatures, config.memorySizer) if err != nil { return nil, err } else if err = internal.Validate(r.enabledFeatures); err != nil { @@ -182,17 +153,10 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCod return nil, err } - // Determine the correct memory capacity, if a memory was defined. - if mem := internal.MemorySection; mem != nil { - memoryName := "0" - for _, e := range internal.ExportSection { - if e.Type == wasm.ExternTypeMemory { - memoryName = e.Name - break - } - } - if err = r.setMemoryCapacity(memoryName, mem); err != nil { - return nil, err + // Replace imports if any configuration exists to do so. + if importRenamer := config.importRenamer; importRenamer != nil { + for _, i := range internal.ImportSection { + i.Module, i.Name = importRenamer(i.Type, i.Module, i.Name) } } @@ -205,38 +169,73 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCod return &compiledCode{module: internal, compiledEngine: r.store.Engine}, nil } +// +//func (c *compileConfig) replaceImports(compile *wasm.Compile) *wasm.Compile { +// if (c.replacedImportCompiles == nil && c.replacedImports == nil) || compile.ImportSection == nil { +// return compile +// } +// +// changed := false +// +// ret := *compile // shallow copy +// replacedImports := make([]*wasm.Import, len(compile.ImportSection)) +// copy(replacedImports, compile.ImportSection) +// +// // First, replace any import.Compile +// for oldCompile, newCompile := range c.replacedImportCompiles { +// for i, imp := range replacedImports { +// if imp.Compile == oldCompile { +// changed = true +// cp := *imp // shallow copy +// cp.Compile = newCompile +// replacedImports[i] = &cp +// } else { +// replacedImports[i] = imp +// } +// } +// } +// +// // Now, replace any import.Compile+import.Name +// for oldImport, newImport := range c.replacedImports { +// for i, imp := range replacedImports { +// nulIdx := strings.IndexByte(oldImport, 0) +// oldCompile := oldImport[0:nulIdx] +// oldName := oldImport[nulIdx+1:] +// if imp.Compile == oldCompile && imp.Name == oldName { +// changed = true +// cp := *imp // shallow copy +// cp.Compile = newImport[0] +// cp.Name = newImport[1] +// replacedImports[i] = &cp +// } else { +// replacedImports[i] = imp +// } +// } +// } +// +// if !changed { +// return compile +// } +// ret.ImportSection = replacedImports +// return &ret +//} + // InstantiateModuleFromCode implements Runtime.InstantiateModuleFromCode func (r *runtime) InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error) { - if compiled, err := r.CompileModule(ctx, source); err != nil { + if compiled, err := r.CompileModule(ctx, source, NewCompileConfig()); err != nil { return nil, err } else { // *wasm.ModuleInstance for the source cannot be tracked, so we release the cache inside this function. defer compiled.Close(ctx) - return r.InstantiateModule(ctx, compiled) - } -} - -// InstantiateModuleFromCodeWithConfig implements Runtime.InstantiateModuleFromCodeWithConfig -func (r *runtime) InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) { - if compiled, err := r.CompileModule(ctx, source); err != nil { - return nil, err - } else { - // *wasm.ModuleInstance for the source cannot be tracked, so we release the cache inside this function. - defer compiled.Close(ctx) - return r.InstantiateModuleWithConfig(ctx, compiled, config) + return r.InstantiateModule(ctx, compiled, NewModuleConfig()) } } // InstantiateModule implements Runtime.InstantiateModule -func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledCode) (mod api.Module, err error) { - return r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig()) -} - -// InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig -func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, mConfig ModuleConfig) (mod api.Module, err error) { +func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledModule, mConfig ModuleConfig) (mod api.Module, err error) { code, ok := compiled.(*compiledCode) if !ok { - panic(fmt.Errorf("unsupported wazero.CompiledCode implementation: %#v", compiled)) + panic(fmt.Errorf("unsupported wazero.CompiledModule implementation: %#v", compiled)) } config, ok := mConfig.(*moduleConfig) @@ -254,8 +253,6 @@ func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled Comp name = code.module.NameSection.ModuleName } - module := config.replaceImports(code.module) - var functionListenerFactory experimentalapi.FunctionListenerFactory if ctx != nil { // Test to see if internal code are using an experimental feature. if fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{}); fnlf != nil { @@ -263,7 +260,7 @@ func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled Comp } } - mod, err = r.store.Instantiate(ctx, module, name, sysCtx, functionListenerFactory) + mod, err = r.store.Instantiate(ctx, code.module, name, sysCtx, functionListenerFactory) if err != nil { return } @@ -283,16 +280,3 @@ func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled Comp } return } - -// setMemoryCapacity sets wasm.Memory cap using the function supplied by RuntimeConfig.WithMemoryCapacityPages. -func (r *runtime) setMemoryCapacity(name string, mem *wasm.Memory) error { - var max *uint32 - if mem.IsMaxEncoded { - max = &mem.Max - } - mem.Cap = r.memoryCapacityPages(mem.Min, max) - if err := mem.ValidateCap(r.memoryLimitPages); err != nil { - return fmt.Errorf("memory[%s] %v", name, err) - } - return nil -} diff --git a/wasm_test.go b/wasm_test.go index 06bb26c1..a141a143 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -36,28 +36,28 @@ func TestRuntime_CompileModule(t *testing.T) { expectedName string }{ { - name: "text - no name", + name: "text no name", source: []byte(`(module)`), }, { - name: "text - empty name", + name: "text empty name", source: []byte(`(module $)`), }, { - name: "text - name", + name: "text name", source: []byte(`(module $test)`), expectedName: "test", }, { - name: "binary - no name section", + name: "binary no name section", source: binary.EncodeModule(&wasm.Module{}), }, { - name: "binary - empty NameSection.ModuleName", + name: "binary empty NameSection.ModuleName", source: binary.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}), }, { - name: "binary - NameSection.ModuleName", + name: "binary NameSection.ModuleName", source: binary.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}), expectedName: "test", }, @@ -68,7 +68,7 @@ func TestRuntime_CompileModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, err := r.CompileModule(testCtx, tc.source) + m, err := r.CompileModule(testCtx, tc.source, NewCompileConfig()) require.NoError(t, err) code := m.(*compiledCode) defer code.Close(testCtx) @@ -79,30 +79,79 @@ func TestRuntime_CompileModule(t *testing.T) { }) } - t.Run("text - memory", func(t *testing.T) { - r := NewRuntimeWithConfig(NewRuntimeConfig(). - WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 2 })) + t.Run("WithMemorySizer", func(t *testing.T) { + source := []byte(`(module (memory 1))`) - source := []byte(`(module (memory 1 3))`) - - m, err := r.CompileModule(testCtx, source) + m, err := r.CompileModule(testCtx, source, NewCompileConfig(). + WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + return 1, 2, 3 + })) require.NoError(t, err) code := m.(*compiledCode) defer code.Close(testCtx) require.Equal(t, &wasm.Memory{ - Min: 1, - Cap: 2, // Uses capacity function - Max: 3, - IsMaxEncoded: true, + Min: 1, + Cap: 2, + Max: 3, }, code.module.MemorySection) }) + + t.Run("WithImportReplacements", func(t *testing.T) { + source := []byte(`(module + (import "js" "increment" (func $increment (result i32))) + (import "js" "decrement" (func $decrement (result i32))) + (import "js" "wasm_increment" (func $wasm_increment (result i32))) + (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) +)`) + + m, err := r.CompileModule(testCtx, source, NewCompileConfig(). + WithImportRenamer(func(externType api.ExternType, oldModule, oldName string) (string, string) { + if externType != api.ExternTypeFunc { + return oldModule, oldName + } + switch oldName { + case "increment", "decrement": + return "go", oldName + case "wasm_increment", "wasm_decrement": + return "wasm", oldName + default: + return oldModule, oldName + } + })) + require.NoError(t, err) + code := m.(*compiledCode) + defer code.Close(testCtx) + + require.Equal(t, []*wasm.Import{ + { + Module: "go", Name: "increment", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "go", Name: "decrement", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "wasm", Name: "wasm_increment", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + { + Module: "wasm", Name: "wasm_decrement", + Type: wasm.ExternTypeFunc, + DescFunc: 0, + }, + }, code.module.ImportSection) + }) } func TestRuntime_CompileModule_Errors(t *testing.T) { tests := []struct { name string - runtime Runtime + config CompileConfig source []byte expectedErr string }{ @@ -121,36 +170,30 @@ func TestRuntime_CompileModule_Errors(t *testing.T) { expectedErr: "1:2: unexpected field: modular", }, { - name: "RuntimeConfig.memoryLimitPages too large", - runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(math.MaxUint32)), - source: []byte(`(module)`), - expectedErr: "memoryLimitPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)", - }, - { - name: "memory has too many pages - text", - runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)), - source: []byte(`(module (memory 3))`), - expectedErr: "1:17: min 3 pages (192 Ki) over limit of 2 pages (128 Ki) in module.memory[0]", + name: "memory has too many pages text", + source: []byte(`(module (memory 70000))`), + expectedErr: "1:17: min 70000 pages (4 Gi) over limit of 65536 pages (4 Gi) in module.memory[0]", }, { name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go - runtime: NewRuntimeWithConfig(NewRuntimeConfig(). - WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })), + config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + return 3, 1, 3 + }), source: []byte(`(module (memory 3))`), - expectedErr: "memory[0] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)", + expectedErr: "1:17: capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki) in module.memory[0]", }, { - name: "memory cap < min - exported", // only one test to avoid duplicating tests in module_test.go - runtime: NewRuntimeWithConfig(NewRuntimeConfig(). - WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })), + name: "memory cap < min exported", // only one test to avoid duplicating tests in module_test.go + config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { + return 3, 2, 3 + }), source: []byte(`(module (memory 3) (export "memory" (memory 0)))`), - expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)", + expectedErr: "1:17: capacity 2 pages (128 Ki) less than minimum 3 pages (192 Ki) in module.memory[0]", }, { - name: "memory has too many pages - binary", - runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)), - source: binary.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}}), - expectedErr: "section memory: max 3 pages (192 Ki) over limit of 2 pages (128 Ki)", + name: "memory has too many pages binary", + source: binary.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}), + expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)", }, } @@ -158,59 +201,13 @@ func TestRuntime_CompileModule_Errors(t *testing.T) { for _, tt := range tests { tc := tt - if tc.runtime == nil { - tc.runtime = r - } - t.Run(tc.name, func(t *testing.T) { - _, err := tc.runtime.CompileModule(testCtx, tc.source) - require.EqualError(t, err, tc.expectedErr) - }) - } -} - -func TestRuntime_setMemoryCapacity(t *testing.T) { - tests := []struct { - name string - runtime *runtime - mem *wasm.Memory - expectedErr string - }{ - { - name: "cap ok", - runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { - return 3 - }, memoryLimitPages: 3}, - mem: &wasm.Memory{Min: 2}, - }, - { - name: "cap < min", - runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { - return 1 - }, memoryLimitPages: 3}, - mem: &wasm.Memory{Min: 2}, - expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)", - }, - { - name: "cap > maxLimit", - runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { - return 4 - }, memoryLimitPages: 3}, - mem: &wasm.Memory{Min: 2}, - expectedErr: "memory[memory] capacity 4 pages (256 Ki) over limit of 3 pages (192 Ki)", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - err := tc.runtime.setMemoryCapacity("memory", tc.mem) - if tc.expectedErr == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tc.expectedErr) + config := tc.config + if config == nil { + config = NewCompileConfig() } + _, err := r.CompileModule(testCtx, tc.source, config) + require.EqualError(t, err, tc.expectedErr) }) } } @@ -314,11 +311,11 @@ func TestModule_Global(t *testing.T) { r := NewRuntime().(*runtime) t.Run(tc.name, func(t *testing.T) { - var m CompiledCode + var m CompiledModule if tc.module != nil { m = &compiledCode{module: tc.module} } else { - m, _ = tc.builder(r).Build(testCtx) + m, _ = tc.builder(r).Compile(testCtx, NewCompileConfig()) } code := m.(*compiledCode) @@ -326,7 +323,7 @@ func TestModule_Global(t *testing.T) { require.NoError(t, err) // Instantiate the module and get the export of the above global - module, err := r.InstantiateModule(testCtx, code) + module, err := r.InstantiateModule(testCtx, code, NewModuleConfig()) require.NoError(t, err) defer module.Close(testCtx) @@ -382,7 +379,7 @@ func TestFunction_Context(t *testing.T) { defer closer(testCtx) // nolint // Instantiate the module and get the export of the above hostFn - module, err := r.InstantiateModuleFromCodeWithConfig(tc.ctx, source, NewModuleConfig().WithName(t.Name())) + module, err := r.InstantiateModuleFromCode(tc.ctx, source) require.NoError(t, err) defer module.Close(testCtx) @@ -413,12 +410,12 @@ func TestRuntime_InstantiateModule_UsesContext(t *testing.T) { code, err := r.CompileModule(testCtx, []byte(`(module $runtime_test.go (import "env" "start" (func $start)) (start $start) -)`)) +)`), NewCompileConfig()) require.NoError(t, err) defer code.Close(testCtx) // Instantiate the module, which calls the start function. This will fail if the context wasn't as intended. - m, err := r.InstantiateModule(testCtx, code) + m, err := r.InstantiateModule(testCtx, code, NewModuleConfig()) require.NoError(t, err) defer m.Close(testCtx) @@ -466,37 +463,37 @@ func TestRuntime_InstantiateModuleFromCode_UsesContext(t *testing.T) { require.True(t, calledStart) } -func TestInstantiateModuleWithConfig_PanicsOnWrongCompiledCodeImpl(t *testing.T) { - // It causes maintenance to define an impl of CompiledCode in tests just to verify the error when it is wrong. +func TestInstantiateModule_PanicsOnWrongCompiledCodeImpl(t *testing.T) { + // It causes maintenance to define an impl of CompiledModule in tests just to verify the error when it is wrong. // Instead, we pass nil which is implicitly the wrong type, as that's less work! r := NewRuntime() err := require.CapturePanic(func() { - _, _ = r.InstantiateModuleWithConfig(testCtx, nil, NewModuleConfig()) + _, _ = r.InstantiateModule(testCtx, nil, NewModuleConfig()) }) - require.EqualError(t, err, "unsupported wazero.CompiledCode implementation: ") + require.EqualError(t, err, "unsupported wazero.CompiledModule implementation: ") } -func TestInstantiateModuleWithConfig_PanicsOnWrongModuleConfigImpl(t *testing.T) { +func TestInstantiateModule_PanicsOnWrongModuleConfigImpl(t *testing.T) { r := NewRuntime() - code, err := r.CompileModule(testCtx, []byte(`(module)`)) + code, err := r.CompileModule(testCtx, []byte(`(module)`), NewCompileConfig()) require.NoError(t, err) defer code.Close(testCtx) // It causes maintenance to define an impl of ModuleConfig in tests just to verify the error when it is wrong. // Instead, we pass nil which is implicitly the wrong type, as that's less work! err = require.CapturePanic(func() { - _, _ = r.InstantiateModuleWithConfig(testCtx, code, nil) + _, _ = r.InstantiateModule(testCtx, code, nil) }) require.EqualError(t, err, "unsupported wazero.ModuleConfig implementation: ") } -// TestInstantiateModuleWithConfig_WithName tests that we can pre-validate (cache) a module and instantiate it under +// TestInstantiateModule_WithName tests that we can pre-validate (cache) a module and instantiate it under // different names. This pattern is used in wapc-go. -func TestInstantiateModuleWithConfig_WithName(t *testing.T) { +func TestInstantiateModule_WithName(t *testing.T) { r := NewRuntime() - base, err := r.CompileModule(testCtx, []byte(`(module $0 (memory 1))`)) + base, err := r.CompileModule(testCtx, []byte(`(module $0 (memory 1))`), NewCompileConfig()) require.NoError(t, err) defer base.Close(testCtx) @@ -504,14 +501,14 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) { // Use the same runtime to instantiate multiple modules internal := r.(*runtime).store - m1, err := r.InstantiateModuleWithConfig(testCtx, base, NewModuleConfig().WithName("1")) + m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1")) require.NoError(t, err) defer m1.Close(testCtx) require.Nil(t, internal.Module("0")) require.Equal(t, internal.Module("1"), m1) - m2, err := r.InstantiateModuleWithConfig(testCtx, base, NewModuleConfig().WithName("2")) + m2, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("2")) require.NoError(t, err) defer m2.Close(testCtx) @@ -519,7 +516,7 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) { require.Equal(t, internal.Module("2"), m2) } -func TestInstantiateModuleWithConfig_ExitError(t *testing.T) { +func TestInstantiateModule_ExitError(t *testing.T) { r := NewRuntime() start := func(ctx context.Context, m api.Module) {