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