Adds Runtime.WithCapacityPages to avoid allocations during runtime. (#514)
`Runtime.WithMemoryCapacityPages` is a function that determines memory
capacity in pages (65536 bytes per page). The inputs 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:
```golang
c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
if maxPages != nil {
return *maxPages
}
return minPages
})
```
Note: This applies at compile time, ModuleBuilder.Build or Runtime.CompileModule.
Fixes #500
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
28
builder.go
28
builder.go
@@ -30,8 +30,10 @@ import (
|
||||
// env2, _ := r.InstantiateModuleWithConfig(ctx, env, NewModuleConfig().WithName("env.2"))
|
||||
// defer env2.Close(ctx)
|
||||
//
|
||||
// Note: Builder methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
|
||||
// Note: Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
|
||||
// Notes:
|
||||
// * ModuleBuilder is mutable. WithXXX functions return the same instance for chaining.
|
||||
// * WithXXX methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
|
||||
// * Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
|
||||
type ModuleBuilder interface {
|
||||
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
|
||||
|
||||
@@ -91,7 +93,7 @@ type ModuleBuilder interface {
|
||||
// // (memory (export "memory") 1)
|
||||
// builder.ExportMemory(1)
|
||||
//
|
||||
// Note: This is allowed to grow to RuntimeConfig.WithMemoryMaxPages (4GiB). To bound it, use ExportMemoryWithMax.
|
||||
// Note: This is allowed to grow to RuntimeConfig.WithMemoryLimitPages (4GiB). To bound it, use ExportMemoryWithMax.
|
||||
// Note: If a memory is already exported with the same name, this overwrites it.
|
||||
// Note: Version 1.0 (20191205) of the WebAssembly spec allows at most one memory per module.
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
|
||||
@@ -103,7 +105,7 @@ type ModuleBuilder interface {
|
||||
// // (memory (export "memory") 1 1)
|
||||
// builder.ExportMemoryWithMax(1, 1)
|
||||
//
|
||||
// Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryMaxPages
|
||||
// Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryLimitPages
|
||||
ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder
|
||||
|
||||
// ExportGlobalI32 exports a global constant of type api.ValueTypeI32.
|
||||
@@ -195,13 +197,17 @@ func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) Mod
|
||||
|
||||
// ExportMemory implements ModuleBuilder.ExportMemory
|
||||
func (b *moduleBuilder) ExportMemory(name string, minPages uint32) ModuleBuilder {
|
||||
b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: b.r.memoryMaxPages}
|
||||
mem := &wasm.Memory{Min: minPages, Max: b.r.memoryLimitPages}
|
||||
mem.Cap = b.r.memoryCapacityPages(mem.Min, nil)
|
||||
b.nameToMemory[name] = mem
|
||||
return b
|
||||
}
|
||||
|
||||
// ExportMemoryWithMax implements ModuleBuilder.ExportMemoryWithMax
|
||||
func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder {
|
||||
b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: maxPages}
|
||||
mem := &wasm.Memory{Min: minPages, Max: maxPages, IsMaxEncoded: true}
|
||||
mem.Cap = b.r.memoryCapacityPages(mem.Min, &maxPages)
|
||||
b.nameToMemory[name] = mem
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -246,11 +252,13 @@ func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder {
|
||||
// Build implements ModuleBuilder.Build
|
||||
func (b *moduleBuilder) Build(ctx context.Context) (*CompiledCode, error) {
|
||||
// Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule
|
||||
maxLimit := b.r.memoryMaxPages
|
||||
memoryLimitPages := b.r.memoryLimitPages
|
||||
for name, mem := range b.nameToMemory {
|
||||
if mem.Max > maxLimit {
|
||||
max := mem.Max
|
||||
return nil, fmt.Errorf("memory[%s] max %d pages (%s) outside range of %d pages (%s)", name, max, wasm.PagesToUnitOfBytes(max), maxLimit, wasm.PagesToUnitOfBytes(maxLimit))
|
||||
if err := mem.ValidateMinMax(memoryLimitPages); err != nil {
|
||||
return nil, fmt.Errorf("memory[%s] %v", name, err)
|
||||
}
|
||||
if err := b.r.setMemoryCapacity(name, mem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
|
||||
return r.NewModuleBuilder("").ExportMemory("memory", 1)
|
||||
},
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -171,7 +171,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
|
||||
return r.NewModuleBuilder("").ExportMemory("memory", 1).ExportMemory("memory", 2)
|
||||
},
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 2, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: wasm.MemoryLimitPages},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -183,7 +183,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
|
||||
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1)
|
||||
},
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: 1},
|
||||
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -195,7 +195,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
|
||||
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1).ExportMemoryWithMax("memory", 1, 2)
|
||||
},
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: 2},
|
||||
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 2, IsMaxEncoded: true},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -362,22 +362,26 @@ func TestNewModuleBuilder_Build(t *testing.T) {
|
||||
func TestNewModuleBuilder_Build_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input func(Runtime) ModuleBuilder
|
||||
input func(*RuntimeConfig) ModuleBuilder
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "memory max > limit",
|
||||
input: func(r Runtime) ModuleBuilder {
|
||||
return r.NewModuleBuilder("").ExportMemory("memory", math.MaxUint32)
|
||||
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)
|
||||
},
|
||||
expectedErr: "memory[memory] min 4294967295 pages (3 Ti) > max 65536 pages (4 Gi)",
|
||||
expectedErr: "memory[memory] min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
||||
},
|
||||
{
|
||||
name: "memory min > limit",
|
||||
input: func(r Runtime) ModuleBuilder {
|
||||
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, math.MaxUint32)
|
||||
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)
|
||||
},
|
||||
expectedErr: "memory[memory] max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
|
||||
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -385,7 +389,7 @@ func TestNewModuleBuilder_Build_Errors(t *testing.T) {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, e := tc.input(NewRuntime()).Build(testCtx)
|
||||
_, e := tc.input(NewRuntimeConfig()).Build(testCtx)
|
||||
require.EqualError(t, e, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
||||
64
config.go
64
config.go
@@ -15,24 +15,29 @@ import (
|
||||
)
|
||||
|
||||
// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig
|
||||
//
|
||||
// Note: RuntimeConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
|
||||
type RuntimeConfig struct {
|
||||
enabledFeatures wasm.Features
|
||||
newEngine func(wasm.Features) wasm.Engine
|
||||
memoryMaxPages uint32
|
||||
enabledFeatures wasm.Features
|
||||
newEngine func(wasm.Features) wasm.Engine
|
||||
memoryLimitPages uint32
|
||||
memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32
|
||||
}
|
||||
|
||||
// engineLessConfig helps avoid copy/pasting the wrong defaults.
|
||||
var engineLessConfig = &RuntimeConfig{
|
||||
enabledFeatures: wasm.Features20191205,
|
||||
memoryMaxPages: wasm.MemoryMaxPages,
|
||||
enabledFeatures: wasm.Features20191205,
|
||||
memoryLimitPages: wasm.MemoryLimitPages,
|
||||
memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { return minPages },
|
||||
}
|
||||
|
||||
// clone ensures all fields are coped even if nil.
|
||||
// clone ensures all fields are copied even if nil.
|
||||
func (c *RuntimeConfig) clone() *RuntimeConfig {
|
||||
return &RuntimeConfig{
|
||||
enabledFeatures: c.enabledFeatures,
|
||||
newEngine: c.newEngine,
|
||||
memoryMaxPages: c.memoryMaxPages,
|
||||
enabledFeatures: c.enabledFeatures,
|
||||
newEngine: c.newEngine,
|
||||
memoryLimitPages: c.memoryLimitPages,
|
||||
memoryCapacityPages: c.memoryCapacityPages,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,19 +58,41 @@ func NewRuntimeConfigInterpreter() *RuntimeConfig {
|
||||
return ret
|
||||
}
|
||||
|
||||
// WithMemoryMaxPages reduces the maximum number of pages a module can define from 65536 pages (4GiB) to a lower value.
|
||||
// WithMemoryLimitPages limits the maximum number of pages a module can define from 65536 pages (4GiB) to a lower value.
|
||||
//
|
||||
// Notes:
|
||||
// * If a module defines no memory max limit, Runtime.CompileModule sets max to this value.
|
||||
// * If a module defines a memory max larger than this amount, it will fail to compile (Runtime.CompileModule).
|
||||
// * 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
|
||||
func (c *RuntimeConfig) WithMemoryMaxPages(memoryMaxPages uint32) *RuntimeConfig {
|
||||
func (c *RuntimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) *RuntimeConfig {
|
||||
ret := c.clone()
|
||||
ret.memoryMaxPages = memoryMaxPages
|
||||
ret.memoryLimitPages = memoryLimitPages
|
||||
return ret
|
||||
}
|
||||
|
||||
// WithMemoryCapacityPages is a function that determines memory capacity in pages (65536 bytes per page). The inputs 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.
|
||||
func (c *RuntimeConfig) WithMemoryCapacityPages(maxCapacityPages func(minPages uint32, maxPages *uint32) uint32) *RuntimeConfig {
|
||||
if maxCapacityPages == nil {
|
||||
return c // Instead of erring.
|
||||
}
|
||||
ret := c.clone()
|
||||
ret.memoryCapacityPages = maxCapacityPages
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -183,12 +210,15 @@ func (c *CompiledCode) Close(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (ex via StartWASICommandWithConfig), so that the same module can
|
||||
// be safely instantiated multiple times.
|
||||
// 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.
|
||||
//
|
||||
// Note: While wazero supports Windows as a platform, host functions using ModuleConfig follow a UNIX dialect.
|
||||
// See RATIONALE.md for design background and relationship to WebAssembly System Interfaces (WASI).
|
||||
//
|
||||
// TODO: This is accidentally mutable. A follow-up PR should change it to be immutable as that's how baseline
|
||||
// configuration can be used safely in modules instantiated on different goroutines.
|
||||
type ModuleConfig struct {
|
||||
name string
|
||||
startFunctions []string
|
||||
|
||||
@@ -18,12 +18,12 @@ func TestRuntimeConfig(t *testing.T) {
|
||||
expected *RuntimeConfig
|
||||
}{
|
||||
{
|
||||
name: "WithMemoryMaxPages",
|
||||
name: "WithMemoryLimitPages",
|
||||
with: func(c *RuntimeConfig) *RuntimeConfig {
|
||||
return c.WithMemoryMaxPages(1)
|
||||
return c.WithMemoryLimitPages(1)
|
||||
},
|
||||
expected: &RuntimeConfig{
|
||||
memoryMaxPages: 1,
|
||||
memoryLimitPages: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -83,6 +83,24 @@ func TestRuntimeConfig(t *testing.T) {
|
||||
require.Equal(t, &RuntimeConfig{}, input)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("WithMemoryCapacityPages", func(t *testing.T) {
|
||||
c := NewRuntimeConfig()
|
||||
|
||||
// Test default returns min
|
||||
require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil))
|
||||
|
||||
// Nil ignored
|
||||
c = c.WithMemoryCapacityPages(nil)
|
||||
require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil))
|
||||
|
||||
// Assign a valid function
|
||||
c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
|
||||
return 2
|
||||
})
|
||||
// Returns updated value
|
||||
require.Equal(t, uint32(2), c.memoryCapacityPages(1, nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRuntimeConfig_FeatureToggle(t *testing.T) {
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestBinaryEncoder(t *testing.T) {
|
||||
|
||||
buf = requireStripCustomSections(t, buf)
|
||||
|
||||
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryMaxPages)
|
||||
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
|
||||
encodedBuf := binary.EncodeModule(mod)
|
||||
|
||||
@@ -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.MemoryMaxPages)
|
||||
)`), wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
|
||||
// (global (export "global_i32") i32 (i32.const 666))
|
||||
@@ -267,6 +267,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
|
||||
mod.TableSection = &wasm.Table{Min: 10, Max: &tableLimitMax}
|
||||
mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable})
|
||||
|
||||
maybeSetMemoryCap(mod)
|
||||
err = store.Engine.CompileModule(testCtx, mod)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -274,7 +275,14 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Run runs all the test inside of the testDataFS file system where all the cases are descirbed
|
||||
// maybeSetMemoryCap assigns wasm.Memory Cap to Min, which is what wazero.CompileModule would do.
|
||||
func maybeSetMemoryCap(mod *wasm.Module) {
|
||||
if mem := mod.MemorySection; mem != nil {
|
||||
mem.Cap = mem.Min
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs all the test inside the testDataFS file system where all the cases are described
|
||||
// via JSON files created from wast2json.
|
||||
func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.Engine, enabledFeatures wasm.Features) {
|
||||
files, err := testDataFS.ReadDir("testdata")
|
||||
@@ -313,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.MemoryMaxPages)
|
||||
mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err, msg)
|
||||
require.NoError(t, mod.Validate(enabledFeatures))
|
||||
mod.AssignModuleID(buf)
|
||||
@@ -329,6 +337,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
|
||||
}
|
||||
}
|
||||
|
||||
maybeSetMemoryCap(mod)
|
||||
err = store.Engine.CompileModule(testCtx, mod)
|
||||
require.NoError(t, err, msg)
|
||||
|
||||
@@ -446,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.MemoryMaxPages)
|
||||
mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryLimitPages)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -458,6 +467,7 @@ func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg
|
||||
|
||||
mod.AssignModuleID(buf)
|
||||
|
||||
maybeSetMemoryCap(mod)
|
||||
err = store.Engine.CompileModule(testCtx, mod)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -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.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
|
||||
if m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages); err != nil {
|
||||
b.Fatal(err)
|
||||
} else {
|
||||
_ = binary.EncodeModule(m)
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
func TestExampleUpToDate(t *testing.T) {
|
||||
t.Run("binary.DecodeModule", func(t *testing.T) {
|
||||
m, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryMaxPages)
|
||||
m, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, example, m)
|
||||
})
|
||||
|
||||
t.Run("text.DecodeModule", func(t *testing.T) {
|
||||
m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages)
|
||||
m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages)
|
||||
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.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
|
||||
if _, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryLimitPages); 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.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
|
||||
if _, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func (g *generator) genImportSection() {
|
||||
|
||||
if memoryImported == 0 {
|
||||
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
|
||||
max := g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min
|
||||
max := g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min
|
||||
|
||||
imp.Type = wasm.ExternTypeMemory
|
||||
imp.DescMem = &wasm.Memory{
|
||||
@@ -180,7 +180,7 @@ func (g *generator) genImportSection() {
|
||||
|
||||
if tableImported == 0 {
|
||||
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
|
||||
max := uint32(g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min)
|
||||
max := uint32(g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min)
|
||||
|
||||
imp.Type = wasm.ExternTypeTable
|
||||
tableImported = 1
|
||||
@@ -215,7 +215,7 @@ func (g *generator) genTableSection() {
|
||||
}
|
||||
|
||||
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
|
||||
max := uint32(g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min)
|
||||
max := uint32(g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min)
|
||||
g.m.TableSection = &wasm.Table{Min: uint32(min), Max: &max}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ func (g *generator) genMemorySection() {
|
||||
return
|
||||
}
|
||||
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
|
||||
max := g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min
|
||||
max := g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min
|
||||
g.m.MemorySection = &wasm.Memory{Min: uint32(min), Max: uint32(max), IsMaxEncoded: true}
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ func (g *generator) newCode() *wasm.Code {
|
||||
wasm.OpcodeEnd}}
|
||||
}
|
||||
|
||||
// genDataSection generates random data section if memory is declared and its minums is not zero.
|
||||
// genDataSection generates random data section if memory is declared and its min is not zero.
|
||||
func (g *generator) genDataSection() {
|
||||
_, _, mem, _, err := g.m.AllDeclarations()
|
||||
if err != nil {
|
||||
|
||||
@@ -703,15 +703,15 @@ func TestGenerator_dataSection(t *testing.T) {
|
||||
{
|
||||
numData: 1,
|
||||
ints: []int{
|
||||
int(wasm.MemoryMaxPages) - 1, // offset
|
||||
1, // size of inits
|
||||
int(wasm.MemoryLimitPages) - 1, // offset
|
||||
1, // size of inits
|
||||
},
|
||||
bufs: [][]byte{{0x1}},
|
||||
exps: []*wasm.DataSegment{
|
||||
{
|
||||
OffsetExpression: &wasm.ConstantExpression{
|
||||
Opcode: wasm.OpcodeI32Const,
|
||||
Data: leb128.EncodeUint32(uint32(wasm.MemoryMaxPages) - 1),
|
||||
Data: leb128.EncodeUint32(uint32(wasm.MemoryLimitPages) - 1),
|
||||
},
|
||||
Init: []byte{0x1},
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ 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, memoryMaxPages uint32) (*wasm.Module, error) {
|
||||
func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryLimitPages uint32) (*wasm.Module, error) {
|
||||
r := bytes.NewReader(binary)
|
||||
|
||||
// Magic number.
|
||||
@@ -71,7 +71,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages u
|
||||
case wasm.SectionIDType:
|
||||
m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
|
||||
case wasm.SectionIDImport:
|
||||
if m.ImportSection, err = decodeImportSection(r, memoryMaxPages); err != nil {
|
||||
if m.ImportSection, err = decodeImportSection(r, memoryLimitPages); err != nil {
|
||||
return nil, err // avoid re-wrapping the error.
|
||||
}
|
||||
case wasm.SectionIDFunction:
|
||||
@@ -79,7 +79,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages u
|
||||
case wasm.SectionIDTable:
|
||||
m.TableSection, err = decodeTableSection(r)
|
||||
case wasm.SectionIDMemory:
|
||||
m.MemorySection, err = decodeMemorySection(r, memoryMaxPages)
|
||||
m.MemorySection, err = decodeMemorySection(r, memoryLimitPages)
|
||||
case wasm.SectionIDGlobal:
|
||||
if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil {
|
||||
return nil, err // avoid re-wrapping the error.
|
||||
|
||||
@@ -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.MemoryMaxPages)
|
||||
m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
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.MemoryMaxPages)
|
||||
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
require.NoError(t, e)
|
||||
require.Equal(t, &wasm.Module{}, m)
|
||||
})
|
||||
@@ -106,24 +106,24 @@ 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.MemoryMaxPages)
|
||||
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
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.MemoryMaxPages)
|
||||
_, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
|
||||
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
|
||||
memoryMaxPages uint32
|
||||
expectedErr string
|
||||
name string
|
||||
input []byte
|
||||
memoryLimitPages uint32
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "wrong magic",
|
||||
@@ -150,12 +150,12 @@ func TestDecodeModule_Errors(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
if tc.memoryMaxPages == 0 {
|
||||
tc.memoryMaxPages = wasm.MemoryMaxPages
|
||||
if tc.memoryLimitPages == 0 {
|
||||
tc.memoryLimitPages = wasm.MemoryLimitPages
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryMaxPages)
|
||||
_, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryLimitPages)
|
||||
require.EqualError(t, e, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
)
|
||||
|
||||
func decodeImport(r *bytes.Reader, idx uint32, memoryMaxPages uint32) (i *wasm.Import, err error) {
|
||||
func decodeImport(r *bytes.Reader, idx uint32, memoryLimitPages uint32) (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 +29,7 @@ func decodeImport(r *bytes.Reader, idx uint32, memoryMaxPages uint32) (i *wasm.I
|
||||
case wasm.ExternTypeTable:
|
||||
i.DescTable, err = decodeTable(r)
|
||||
case wasm.ExternTypeMemory:
|
||||
i.DescMem, err = decodeMemory(r, memoryMaxPages)
|
||||
i.DescMem, err = decodeMemory(r, memoryLimitPages)
|
||||
case wasm.ExternTypeGlobal:
|
||||
i.DescGlobal, err = decodeGlobalType(r)
|
||||
default:
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestEncodeImport(t *testing.T) {
|
||||
Type: wasm.ExternTypeMemory,
|
||||
Module: "my",
|
||||
Name: "memory",
|
||||
DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages, IsMaxEncoded: false},
|
||||
DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages, IsMaxEncoded: false},
|
||||
},
|
||||
expected: []byte{
|
||||
0x02, 'm', 'y',
|
||||
|
||||
@@ -2,7 +2,6 @@ package binary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
)
|
||||
@@ -10,7 +9,7 @@ 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, memoryMaxPages uint32) (*wasm.Memory, error) {
|
||||
func decodeMemory(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) {
|
||||
min, maxP, err := decodeLimitsType(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -21,17 +20,9 @@ func decodeMemory(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error)
|
||||
if maxP != nil {
|
||||
isMaxEncoded = true
|
||||
max = *maxP
|
||||
} else {
|
||||
max = memoryMaxPages
|
||||
}
|
||||
if max > memoryMaxPages {
|
||||
return nil, fmt.Errorf("max %d pages (%s) outside range of %d pages (%s)", max, wasm.PagesToUnitOfBytes(max), memoryMaxPages, wasm.PagesToUnitOfBytes(memoryMaxPages))
|
||||
} else if min > memoryMaxPages {
|
||||
return nil, fmt.Errorf("min %d pages (%s) outside range of %d pages (%s)", min, wasm.PagesToUnitOfBytes(min), memoryMaxPages, wasm.PagesToUnitOfBytes(memoryMaxPages))
|
||||
} else if min > max {
|
||||
return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", min, wasm.PagesToUnitOfBytes(min), max, wasm.PagesToUnitOfBytes(max))
|
||||
}
|
||||
return &wasm.Memory{Min: min, Max: max, IsMaxEncoded: isMaxEncoded}, nil
|
||||
mem := &wasm.Memory{Min: min, Max: max, IsMaxEncoded: isMaxEncoded}
|
||||
return mem, mem.ValidateMinMax(memoryLimitPages)
|
||||
}
|
||||
|
||||
// encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format.
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestMemoryType(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
max := wasm.MemoryMaxPages
|
||||
max := wasm.MemoryLimitPages
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -20,12 +20,12 @@ func TestMemoryType(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "min 0",
|
||||
input: &wasm.Memory{Max: wasm.MemoryMaxPages, IsMaxEncoded: true},
|
||||
input: &wasm.Memory{Max: wasm.MemoryLimitPages, IsMaxEncoded: true},
|
||||
expected: []byte{0x1, 0, 0x80, 0x80, 0x4},
|
||||
},
|
||||
{
|
||||
name: "min 0 - default max",
|
||||
input: &wasm.Memory{Max: wasm.MemoryMaxPages},
|
||||
input: &wasm.Memory{Max: wasm.MemoryLimitPages},
|
||||
expected: []byte{0x0, 0},
|
||||
},
|
||||
{
|
||||
@@ -68,10 +68,10 @@ func TestMemoryType(t *testing.T) {
|
||||
|
||||
func TestDecodeMemoryType_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
memoryMaxPages uint32
|
||||
expectedErr string
|
||||
name string
|
||||
input []byte
|
||||
memoryLimitPages uint32
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "max < min",
|
||||
@@ -81,24 +81,24 @@ func TestDecodeMemoryType_Errors(t *testing.T) {
|
||||
{
|
||||
name: "min > limit",
|
||||
input: []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xf},
|
||||
expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
|
||||
expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
||||
},
|
||||
{
|
||||
name: "max > limit",
|
||||
input: []byte{0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf},
|
||||
expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
|
||||
expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
if tc.memoryMaxPages == 0 {
|
||||
tc.memoryMaxPages = wasm.MemoryMaxPages
|
||||
if tc.memoryLimitPages == 0 {
|
||||
tc.memoryLimitPages = wasm.MemoryLimitPages
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryMaxPages)
|
||||
_, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryLimitPages)
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func decodeTypeSection(enabledFeatures wasm.Features, r *bytes.Reader) ([]*wasm.
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import, error) {
|
||||
func decodeImportSection(r *bytes.Reader, memoryLimitPages uint32) ([]*wasm.Import, error) {
|
||||
vs, _, err := leb128.DecodeUint32(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get size of vector: %w", err)
|
||||
@@ -32,7 +32,7 @@ func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import
|
||||
|
||||
result := make([]*wasm.Import, vs)
|
||||
for i := uint32(0); i < vs; i++ {
|
||||
if result[i], err = decodeImport(r, i, memoryMaxPages); err != nil {
|
||||
if result[i], err = decodeImport(r, i, memoryLimitPages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func decodeTableSection(r *bytes.Reader) (*wasm.Table, error) {
|
||||
return decodeTable(r)
|
||||
}
|
||||
|
||||
func decodeMemorySection(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error) {
|
||||
func decodeMemorySection(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) {
|
||||
vs, _, err := leb128.DecodeUint32(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading size")
|
||||
@@ -75,7 +75,7 @@ func decodeMemorySection(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory,
|
||||
return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs)
|
||||
}
|
||||
|
||||
return decodeMemory(r, memoryMaxPages)
|
||||
return decodeMemory(r, memoryLimitPages)
|
||||
}
|
||||
|
||||
func decodeGlobalSection(r *bytes.Reader, enabledFeatures wasm.Features) ([]*wasm.Global, error) {
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestMemorySection(t *testing.T) {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryMaxPages)
|
||||
memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, memories)
|
||||
})
|
||||
@@ -93,10 +93,10 @@ func TestMemorySection(t *testing.T) {
|
||||
|
||||
func TestMemorySection_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
memoryMaxPages uint32
|
||||
expectedErr string
|
||||
name string
|
||||
input []byte
|
||||
memoryLimitPages uint32
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "min and min with max",
|
||||
@@ -112,12 +112,12 @@ func TestMemorySection_Errors(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
if tc.memoryMaxPages == 0 {
|
||||
tc.memoryMaxPages = wasm.MemoryMaxPages
|
||||
if tc.memoryLimitPages == 0 {
|
||||
tc.memoryLimitPages = wasm.MemoryLimitPages
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryMaxPages)
|
||||
_, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryLimitPages)
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
@@ -15,9 +17,9 @@ const (
|
||||
// and is defined as 2^16 = 65536.
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0
|
||||
MemoryPageSize = uint32(65536)
|
||||
// MemoryMaxPages is maximum number of pages defined (2^16).
|
||||
// MemoryLimitPages is maximum number of pages defined (2^16).
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
|
||||
MemoryMaxPages = uint32(65536)
|
||||
MemoryLimitPages = uint32(65536)
|
||||
// MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize".
|
||||
MemoryPageSizeInBits = 16
|
||||
)
|
||||
@@ -31,8 +33,8 @@ var _ api.Memory = &MemoryInstance{}
|
||||
// wasm.Store Memories index zero: `store.Memories[0]`
|
||||
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
|
||||
type MemoryInstance struct {
|
||||
Buffer []byte
|
||||
Min, Max uint32
|
||||
Buffer []byte
|
||||
Min, Cap, Max uint32
|
||||
}
|
||||
|
||||
// Size implements the same method as documented on api.Memory.
|
||||
@@ -193,17 +195,25 @@ func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
|
||||
//
|
||||
// Returns -1 if the operation resulted in exceeding the maximum memory pages.
|
||||
// Otherwise, returns the prior memory size after growing the memory buffer.
|
||||
func (m *MemoryInstance) Grow(_ context.Context, newPages uint32) (result uint32) {
|
||||
func (m *MemoryInstance) Grow(_ context.Context, delta uint32) (result uint32) {
|
||||
// Note: If you use the context.Context param, don't forget to coerce nil to context.Background()!
|
||||
|
||||
currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
|
||||
if delta == 0 {
|
||||
return currentPages
|
||||
}
|
||||
|
||||
// If exceeds the max of memory size, we push -1 according to the spec.
|
||||
if currentPages+newPages > m.Max {
|
||||
newPages := currentPages + delta
|
||||
if newPages > m.Max {
|
||||
return 0xffffffff // = -1 in signed 32-bit integer.
|
||||
} else {
|
||||
// Otherwise, grow the memory.
|
||||
m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(newPages))...)
|
||||
} else if newPages > m.Cap { // grow the memory.
|
||||
m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...)
|
||||
m.Cap = newPages
|
||||
return currentPages
|
||||
} else { // We already have the capacity we need.
|
||||
sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer))
|
||||
sp.Len = int(MemoryPagesToBytesNum(newPages))
|
||||
return currentPages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func TestMemoryPageConsts(t *testing.T) {
|
||||
require.Equal(t, MemoryPageSize, uint32(1)<<MemoryPageSizeInBits)
|
||||
require.Equal(t, MemoryPageSize, uint32(1<<16))
|
||||
require.Equal(t, MemoryMaxPages, uint32(1<<16))
|
||||
require.Equal(t, MemoryLimitPages, uint32(1<<16))
|
||||
}
|
||||
|
||||
func Test_MemoryPagesToBytesNum(t *testing.T) {
|
||||
@@ -27,28 +27,56 @@ func Test_MemoryBytesNumToPages(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryInstance_Grow_Size(t *testing.T) {
|
||||
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
|
||||
max := uint32(10)
|
||||
m := &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
|
||||
require.Equal(t, uint32(0), m.Grow(ctx, 5))
|
||||
require.Equal(t, uint32(5), m.PageSize(ctx))
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
capEqualsMax bool
|
||||
}{
|
||||
{name: "nil context"},
|
||||
{name: "context", ctx: testCtx},
|
||||
{name: "nil context, capEqualsMax", capEqualsMax: true},
|
||||
{name: "context, capEqualsMax", ctx: testCtx, capEqualsMax: true},
|
||||
}
|
||||
|
||||
// Zero page grow is well-defined, should return the current page correctly.
|
||||
require.Equal(t, uint32(5), m.Grow(ctx, 0))
|
||||
require.Equal(t, uint32(5), m.PageSize(ctx))
|
||||
require.Equal(t, uint32(5), m.Grow(ctx, 4))
|
||||
require.Equal(t, uint32(9), m.PageSize(ctx))
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
// At this point, the page size equal 9,
|
||||
// so trying to grow two pages should result in failure.
|
||||
require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
|
||||
require.Equal(t, uint32(9), m.PageSize(ctx))
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := tc.ctx
|
||||
max := uint32(10)
|
||||
maxBytes := MemoryPagesToBytesNum(max)
|
||||
var m *MemoryInstance
|
||||
if tc.capEqualsMax {
|
||||
m = &MemoryInstance{Cap: max, Max: max, Buffer: make([]byte, 0, maxBytes)}
|
||||
} else {
|
||||
m = &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
|
||||
}
|
||||
require.Equal(t, uint32(0), m.Grow(ctx, 5))
|
||||
require.Equal(t, uint32(5), m.PageSize(ctx))
|
||||
|
||||
// But growing one page is still permitted.
|
||||
require.Equal(t, uint32(9), m.Grow(ctx, 1))
|
||||
// Zero page grow is well-defined, should return the current page correctly.
|
||||
require.Equal(t, uint32(5), m.Grow(ctx, 0))
|
||||
require.Equal(t, uint32(5), m.PageSize(ctx))
|
||||
require.Equal(t, uint32(5), m.Grow(ctx, 4))
|
||||
require.Equal(t, uint32(9), m.PageSize(ctx))
|
||||
|
||||
// Ensure that the current page size equals the max.
|
||||
require.Equal(t, max, m.PageSize(ctx))
|
||||
// At this point, the page size equal 9,
|
||||
// so trying to grow two pages should result in failure.
|
||||
require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
|
||||
require.Equal(t, uint32(9), m.PageSize(ctx))
|
||||
|
||||
// But growing one page is still permitted.
|
||||
require.Equal(t, uint32(9), m.Grow(ctx, 1))
|
||||
|
||||
// Ensure that the current page size equals the max.
|
||||
require.Equal(t, max, m.PageSize(ctx))
|
||||
|
||||
if tc.capEqualsMax { // Ensure the capacity isn't more than max.
|
||||
require.Equal(t, maxBytes, uint64(cap(m.Buffer)))
|
||||
} else { // Slice doubles, so it should have a higher capacity than max.
|
||||
require.True(t, maxBytes < uint64(cap(m.Buffer)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +158,7 @@ func TestPagesToUnitOfBytes(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "max memory",
|
||||
pages: MemoryMaxPages,
|
||||
pages: MemoryLimitPages,
|
||||
expected: "4 Gi",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ 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, memoryMaxPages uint32) (result *Module, err error)
|
||||
type DecodeModule func(source []byte, enabledFeatures Features, memoryLimitPages uint32) (result *Module, err error)
|
||||
|
||||
// EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
|
||||
// See binary.EncodeModule
|
||||
@@ -559,9 +559,12 @@ func paramNames(localNames IndirectNameMap, funcIdx uint32, paramLen int) []stri
|
||||
func (m *Module) buildMemory() (mem *MemoryInstance) {
|
||||
memSec := m.MemorySection
|
||||
if memSec != nil {
|
||||
min := MemoryPagesToBytesNum(memSec.Min)
|
||||
capacity := MemoryPagesToBytesNum(memSec.Cap)
|
||||
mem = &MemoryInstance{
|
||||
Buffer: make([]byte, MemoryPagesToBytesNum(memSec.Min)),
|
||||
Buffer: make([]byte, min, capacity),
|
||||
Min: memSec.Min,
|
||||
Cap: memSec.Cap,
|
||||
Max: memSec.Max,
|
||||
}
|
||||
}
|
||||
@@ -652,11 +655,38 @@ type limitsType struct {
|
||||
|
||||
// Memory describes the limits of pages (64KB) in a memory.
|
||||
type Memory struct {
|
||||
Min, Max uint32
|
||||
// IsMaxEncoded true if the Max is encoded in the orignial source (binary or text).
|
||||
Min, Cap, Max uint32
|
||||
// IsMaxEncoded true if the Max is encoded in the original source (binary or text).
|
||||
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
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GlobalType struct {
|
||||
ValType ValueType
|
||||
Mutable bool
|
||||
|
||||
@@ -90,6 +90,83 @@ func TestExternTypeName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemory_ValidateCap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mem *Memory
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
mem: &Memory{Min: 2, Cap: 2},
|
||||
},
|
||||
{
|
||||
name: "cap < min",
|
||||
mem: &Memory{Min: 2, Cap: 1},
|
||||
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},
|
||||
},
|
||||
{
|
||||
name: "max < min",
|
||||
mem: &Memory{Min: 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)",
|
||||
},
|
||||
{
|
||||
name: "max > limit",
|
||||
mem: &Memory{Max: math.MaxUint32, IsMaxEncoded: true},
|
||||
expectedErr: "max 4294967295 pages (3 Ti) over limit of 3 pages (192 Ki)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.mem.ValidateMinMax(3)
|
||||
if tc.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModule_allDeclarations(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
module *Module
|
||||
@@ -730,7 +807,7 @@ func TestModule_buildMemoryInstance(t *testing.T) {
|
||||
t.Run("non-nil", func(t *testing.T) {
|
||||
min := uint32(1)
|
||||
max := uint32(10)
|
||||
m := Module{MemorySection: &Memory{Min: min, Max: max}}
|
||||
m := Module{MemorySection: &Memory{Min: min, Cap: min, Max: max}}
|
||||
mem := m.buildMemory()
|
||||
require.Equal(t, min, mem.Min)
|
||||
require.Equal(t, max, mem.Max)
|
||||
|
||||
@@ -28,16 +28,16 @@ func TestModuleInstance_Memory(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "memory not exported",
|
||||
input: &Module{MemorySection: &Memory{Min: 1}},
|
||||
input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
|
||||
},
|
||||
{
|
||||
name: "memory not exported, one page",
|
||||
input: &Module{MemorySection: &Memory{Min: 1}},
|
||||
input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
|
||||
},
|
||||
{
|
||||
name: "memory exported, different name",
|
||||
input: &Module{
|
||||
MemorySection: &Memory{Min: 1},
|
||||
MemorySection: &Memory{Min: 1, Cap: 1},
|
||||
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
|
||||
},
|
||||
},
|
||||
@@ -52,7 +52,7 @@ func TestModuleInstance_Memory(t *testing.T) {
|
||||
{
|
||||
name: "memory exported, one page",
|
||||
input: &Module{
|
||||
MemorySection: &Memory{Min: 1},
|
||||
MemorySection: &Memory{Min: 1, Cap: 1},
|
||||
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
|
||||
},
|
||||
expected: true,
|
||||
@@ -61,7 +61,7 @@ func TestModuleInstance_Memory(t *testing.T) {
|
||||
{
|
||||
name: "memory exported, two pages",
|
||||
input: &Module{
|
||||
MemorySection: &Memory{Min: 2},
|
||||
MemorySection: &Memory{Min: 2, Cap: 2},
|
||||
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
|
||||
},
|
||||
expected: true,
|
||||
@@ -156,7 +156,7 @@ func TestStore_CloseModule(t *testing.T) {
|
||||
_, err := s.Instantiate(testCtx, &Module{
|
||||
TypeSection: []*FunctionType{{}},
|
||||
ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
|
||||
MemorySection: &Memory{Min: 1},
|
||||
MemorySection: &Memory{Min: 1, Cap: 1},
|
||||
GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
|
||||
TableSection: &Table{Min: 10},
|
||||
}, importingModuleName, nil, nil)
|
||||
@@ -205,7 +205,7 @@ func TestStore_hammer(t *testing.T) {
|
||||
TypeSection: []*FunctionType{{}},
|
||||
FunctionSection: []uint32{0},
|
||||
CodeSection: []*Code{{Body: []byte{OpcodeEnd}}},
|
||||
MemorySection: &Memory{Min: 1},
|
||||
MemorySection: &Memory{Min: 1, Cap: 1},
|
||||
GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
|
||||
TableSection: &Table{Min: 10},
|
||||
ImportSection: []*Import{
|
||||
@@ -352,7 +352,7 @@ func TestCallContext_ExportedFunction(t *testing.T) {
|
||||
importing, err := s.Instantiate(testCtx, &Module{
|
||||
TypeSection: []*FunctionType{{}},
|
||||
ImportSection: []*Import{{Type: ExternTypeFunc, Module: "host", Name: "host_fn", DescFunc: 0}},
|
||||
MemorySection: &Memory{Min: 1},
|
||||
MemorySection: &Memory{Min: 1, Cap: 1},
|
||||
ExportSection: []*Export{{Type: ExternTypeFunc, Name: "host.fn", Index: 0}},
|
||||
}, "test", nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -636,10 +636,10 @@ func TestStore_resolveImports(t *testing.T) {
|
||||
})
|
||||
t.Run("minimum size mismatch", func(t *testing.T) {
|
||||
s := newStore()
|
||||
importMemoryType := &Memory{Min: 2}
|
||||
importMemoryType := &Memory{Min: 2, Cap: 2}
|
||||
s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
|
||||
Type: ExternTypeMemory,
|
||||
Memory: &MemoryInstance{Min: importMemoryType.Min - 1},
|
||||
Memory: &MemoryInstance{Min: importMemoryType.Min - 1, Cap: 2},
|
||||
}}, Name: moduleName}
|
||||
_, _, _, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}})
|
||||
require.EqualError(t, err, "import[0] memory[test.target]: minimum size mismatch: 2 > 1")
|
||||
@@ -650,7 +650,7 @@ func TestStore_resolveImports(t *testing.T) {
|
||||
importMemoryType := &Memory{Max: max}
|
||||
s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
|
||||
Type: ExternTypeMemory,
|
||||
Memory: &MemoryInstance{Max: MemoryMaxPages},
|
||||
Memory: &MemoryInstance{Max: MemoryLimitPages},
|
||||
}}, Name: moduleName}
|
||||
_, _, _, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}})
|
||||
require.EqualError(t, err, "import[0] memory[test.target]: maximum size mismatch: 10 < 65536")
|
||||
|
||||
@@ -104,7 +104,7 @@ 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, memoryMaxPages uint32) (result *wasm.Module, err error) {
|
||||
func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryLimitPages 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 +114,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages u
|
||||
// * 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, memoryMaxPages)
|
||||
p := newModuleParser(module, enabledFeatures, memoryLimitPages)
|
||||
p.source = source
|
||||
|
||||
// A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this
|
||||
@@ -143,7 +143,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages u
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryMaxPages uint32) *moduleParser {
|
||||
func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryLimitPages uint32) *moduleParser {
|
||||
p := moduleParser{module: module, enabledFeatures: enabledFeatures,
|
||||
typeNamespace: newIndexNamespace(module.SectionElementCount),
|
||||
funcNamespace: newIndexNamespace(module.SectionElementCount),
|
||||
@@ -152,7 +152,7 @@ func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryM
|
||||
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(memoryMaxPages, p.memoryNamespace, p.endMemory)
|
||||
p.memoryParser = newMemoryParser(memoryLimitPages, p.memoryNamespace, p.endMemory)
|
||||
return &p
|
||||
}
|
||||
|
||||
|
||||
@@ -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.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory ID",
|
||||
input: "(module (memory $mem 1))",
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1420,7 +1420,7 @@ func TestDecodeModule(t *testing.T) {
|
||||
(export "foo" (memory 0))
|
||||
)`,
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "foo", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -1433,7 +1433,7 @@ func TestDecodeModule(t *testing.T) {
|
||||
(memory 0)
|
||||
)`,
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages},
|
||||
ExportSection: []*wasm.Export{
|
||||
{Name: "foo", Type: wasm.ExternTypeMemory, Index: 0},
|
||||
},
|
||||
@@ -1465,7 +1465,7 @@ func TestDecodeModule(t *testing.T) {
|
||||
(export "memory" (memory $mem))
|
||||
)`,
|
||||
expected: &wasm.Module{
|
||||
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
|
||||
MemorySection: &wasm.Memory{Min: 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.FeaturesFinished, wasm.MemoryMaxPages)
|
||||
m, err := DecodeModule([]byte(tc.input), wasm.FeaturesFinished, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, m)
|
||||
})
|
||||
@@ -1573,9 +1573,9 @@ func TestDecodeModule(t *testing.T) {
|
||||
|
||||
func TestParseModule_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, input string
|
||||
memoryMaxPages uint32
|
||||
expectedErr string
|
||||
name, input string
|
||||
memoryLimitPages uint32
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "forgot parens",
|
||||
@@ -1998,10 +1998,10 @@ 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))",
|
||||
memoryMaxPages: 3,
|
||||
expectedErr: "1:19: max 4 pages (256 Ki) outside range of 3 pages (192 Ki) in module.memory[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: "second memory",
|
||||
@@ -2145,10 +2145,10 @@ func TestParseModule_Errors(t *testing.T) {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.memoryMaxPages == 0 {
|
||||
tc.memoryMaxPages = wasm.MemoryMaxPages
|
||||
if tc.memoryLimitPages == 0 {
|
||||
tc.memoryLimitPages = wasm.MemoryLimitPages
|
||||
}
|
||||
_, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryMaxPages)
|
||||
_, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryLimitPages)
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
)
|
||||
|
||||
func newMemoryParser(memoryMaxPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser {
|
||||
return &memoryParser{memoryMaxPages: memoryMaxPages, memoryNamespace: memoryNamespace, onMemory: onMemory}
|
||||
func newMemoryParser(memoryLimitPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser {
|
||||
return &memoryParser{memoryLimitPages: memoryLimitPages, memoryNamespace: memoryNamespace, onMemory: onMemory}
|
||||
}
|
||||
|
||||
type onMemory func(min, max uint32, maxDecooded bool) tokenParser
|
||||
type onMemory func(min, max uint32, maxDecoded bool) tokenParser
|
||||
|
||||
// memoryParser parses a api.Memory from and dispatches to onMemory.
|
||||
// memoryParser parses an api.Memory from and dispatches to onMemory.
|
||||
//
|
||||
// Ex. `(module (memory 0 1024))`
|
||||
// starts here --^ ^
|
||||
@@ -22,9 +22,9 @@ type onMemory func(min, max uint32, maxDecooded 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 {
|
||||
// memoryMaxPages is the limit of pages (not bytes) for each wasm.Memory.
|
||||
memoryMaxPages uint32
|
||||
maxDecoded bool
|
||||
// memoryLimitPages is the limit of pages (not bytes) for each wasm.Memory.
|
||||
memoryLimitPages uint32
|
||||
maxDecoded bool
|
||||
|
||||
memoryNamespace *indexNamespace
|
||||
|
||||
@@ -50,7 +50,7 @@ type memoryParser struct {
|
||||
// calls beginMin --^
|
||||
func (p *memoryParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
|
||||
p.currentMin = 0
|
||||
p.currentMax = p.memoryMaxPages
|
||||
p.currentMax = p.memoryLimitPages
|
||||
if tok == tokenID { // Ex. $mem
|
||||
if _, err := p.memoryNamespace.setID(tokenBytes); err != nil {
|
||||
return nil, err
|
||||
@@ -66,8 +66,8 @@ 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.memoryMaxPages {
|
||||
return nil, fmt.Errorf("min %d pages (%s) outside range of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryMaxPages, wasm.PagesToUnitOfBytes(p.memoryMaxPages))
|
||||
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))
|
||||
} else {
|
||||
p.currentMin = i
|
||||
}
|
||||
@@ -85,8 +85,8 @@ func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint
|
||||
switch tok {
|
||||
case tokenUN:
|
||||
i, overflow := decodeUint32(tokenBytes)
|
||||
if overflow || i > p.memoryMaxPages {
|
||||
return nil, fmt.Errorf("max %d pages (%s) outside range of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryMaxPages, wasm.PagesToUnitOfBytes(p.memoryMaxPages))
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func TestMemoryParser(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
max := wasm.MemoryMaxPages
|
||||
max := wasm.MemoryLimitPages
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
@@ -122,12 +122,12 @@ func TestMemoryParser_Errors(t *testing.T) {
|
||||
{
|
||||
name: "min > limit",
|
||||
input: "(memory 4294967295)",
|
||||
expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
|
||||
expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
||||
},
|
||||
{
|
||||
name: "max > limit",
|
||||
input: "(memory 0 4294967295)",
|
||||
expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
|
||||
expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.Memor
|
||||
parsed = &wasm.Memory{Min: min, Max: max, IsMaxEncoded: maxDecoded}
|
||||
return parseErr
|
||||
}
|
||||
tp := newMemoryParser(wasm.MemoryMaxPages, memoryNamespace, setFunc)
|
||||
tp := newMemoryParser(wasm.MemoryLimitPages, 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
|
||||
|
||||
@@ -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.FeaturesFinished, wasm.MemoryMaxPages)
|
||||
m, err := text.DecodeModule([]byte(source), wasm.FeaturesFinished, wasm.MemoryLimitPages)
|
||||
require.NoError(t, err)
|
||||
return m
|
||||
}
|
||||
|
||||
52
wasm.go
52
wasm.go
@@ -124,17 +124,19 @@ func NewRuntime() Runtime {
|
||||
// NewRuntimeWithConfig returns a runtime with the given configuration.
|
||||
func NewRuntimeWithConfig(config *RuntimeConfig) Runtime {
|
||||
return &runtime{
|
||||
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
|
||||
enabledFeatures: config.enabledFeatures,
|
||||
memoryMaxPages: config.memoryMaxPages,
|
||||
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
|
||||
enabledFeatures: config.enabledFeatures,
|
||||
memoryLimitPages: config.memoryLimitPages,
|
||||
memoryCapacityPages: config.memoryCapacityPages,
|
||||
}
|
||||
}
|
||||
|
||||
// runtime allows decoupling of public interfaces from internal representation.
|
||||
type runtime struct {
|
||||
enabledFeatures wasm.Features
|
||||
store *wasm.Store
|
||||
memoryMaxPages uint32
|
||||
enabledFeatures wasm.Features
|
||||
store *wasm.Store
|
||||
memoryLimitPages uint32
|
||||
memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32
|
||||
}
|
||||
|
||||
// Module implements Runtime.Module
|
||||
@@ -160,13 +162,14 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCo
|
||||
decoder = text.DecodeModule
|
||||
}
|
||||
|
||||
if r.memoryMaxPages > wasm.MemoryMaxPages {
|
||||
return nil, fmt.Errorf("memoryMaxPages %d (%s) > specification max %d (%s)",
|
||||
r.memoryMaxPages, wasm.PagesToUnitOfBytes(r.memoryMaxPages),
|
||||
wasm.MemoryMaxPages, wasm.PagesToUnitOfBytes(wasm.MemoryMaxPages))
|
||||
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.memoryMaxPages)
|
||||
internal, err := decoder(source, r.enabledFeatures, r.memoryLimitPages)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if err = internal.Validate(r.enabledFeatures); err != nil {
|
||||
@@ -175,6 +178,20 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCo
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
internal.AssignModuleID(source)
|
||||
|
||||
if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
|
||||
@@ -252,3 +269,16 @@ func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled *Com
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
97
wasm_test.go
97
wasm_test.go
@@ -18,9 +18,10 @@ import (
|
||||
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
||||
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
||||
|
||||
func TestRuntime_DecodeModule(t *testing.T) {
|
||||
func TestRuntime_CompileModule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
runtime Runtime
|
||||
source []byte
|
||||
expectedName string
|
||||
}{
|
||||
@@ -66,9 +67,27 @@ func TestRuntime_DecodeModule(t *testing.T) {
|
||||
require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("text - memory", func(t *testing.T) {
|
||||
r := NewRuntimeWithConfig(NewRuntimeConfig().
|
||||
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 2 }))
|
||||
|
||||
source := []byte(`(module (memory 1 3))`)
|
||||
|
||||
code, err := r.CompileModule(testCtx, source)
|
||||
require.NoError(t, err)
|
||||
defer code.Close(testCtx)
|
||||
|
||||
require.Equal(t, &wasm.Memory{
|
||||
Min: 1,
|
||||
Cap: 2, // Uses capacity function
|
||||
Max: 3,
|
||||
IsMaxEncoded: true,
|
||||
}, code.module.MemorySection)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRuntime_DecodeModule_Errors(t *testing.T) {
|
||||
func TestRuntime_CompileModule_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
runtime Runtime
|
||||
@@ -90,22 +109,36 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) {
|
||||
expectedErr: "1:2: unexpected field: modular",
|
||||
},
|
||||
{
|
||||
name: "RuntimeConfig.memoryMaxPage too large",
|
||||
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(math.MaxUint32)),
|
||||
name: "RuntimeConfig.memoryLimitPages too large",
|
||||
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(math.MaxUint32)),
|
||||
source: []byte(`(module)`),
|
||||
expectedErr: "memoryMaxPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
|
||||
expectedErr: "memoryLimitPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
|
||||
},
|
||||
{
|
||||
name: "memory has too many pages - text",
|
||||
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
|
||||
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)),
|
||||
source: []byte(`(module (memory 3))`),
|
||||
expectedErr: "1:17: min 3 pages (192 Ki) outside range of 2 pages (128 Ki) in module.memory[0]",
|
||||
expectedErr: "1:17: min 3 pages (192 Ki) over limit of 2 pages (128 Ki) 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 })),
|
||||
source: []byte(`(module (memory 3))`),
|
||||
expectedErr: "memory[0] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
|
||||
},
|
||||
{
|
||||
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 })),
|
||||
source: []byte(`(module (memory 3) (export "memory" (memory 0)))`),
|
||||
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
|
||||
},
|
||||
{
|
||||
name: "memory has too many pages - binary",
|
||||
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
|
||||
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) outside range of 2 pages (128 Ki)",
|
||||
expectedErr: "section memory: max 3 pages (192 Ki) over limit of 2 pages (128 Ki)",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -124,6 +157,52 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
|
||||
func TestModule_Memory(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
||||
Reference in New Issue
Block a user