Repository: wazero/wazero (Go)

Issue:
Initialize memory from limit (with WithMemoryLimitPages + WithMemoryCapacityFromMax)
**Is your feature request related to a problem? Please describe.**
I would like to prevent re-allocation, so I tried to use `WithMemoryLimitPages(256).WithMemoryCapacityFromMax(true)`, in that
case it would allocate 256 pages and never change.

However, that is not what happens. Since the WASM-module is not defining `max memory`, the `WithMemoryCapacityFromMax(true)` is useless.

**Describe the solution you'd like**
Maybe exposing the memory, in general, so we can provide any custom Memory implementation, or provide more option to avoid re-allocation.

**Describe alternatives you've considered**
I tried `WithMemoryLimitPages(255).WithMemoryCapacityFromMax(true)`, but it doesn't allocates 255 ahead-of-time.



Discussion:
https://github.com/tetratelabs/wazero/pull/1335



Patch:
diff --git a/config.go b/config.go
index 6e4f32bbae..2a8848a8b7 100644
--- a/config.go
+++ b/config.go
@@ -48,8 +48,8 @@ type RuntimeConfig interface {
 	WithCoreFeatures(api.CoreFeatures) RuntimeConfig
 
 	// WithMemoryLimitPages overrides the maximum pages allowed per memory. The
-	// default is 65536, allowing 4GB total memory per instance. Setting a
-	// value larger than default will panic.
+	// default is 65536, allowing 4GB total memory per instance if the maximum is
+	// not encoded in a Wasm binary. Setting a value larger than default will panic.
 	//
 	// This example reduces the largest possible memory size from 4GB to 128KB:
 	//	rConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(2)
@@ -67,6 +67,9 @@ type RuntimeConfig interface {
 	//	rConfig = wazero.NewRuntimeConfig().WithMemoryCapacityFromMax(true)
 	//
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
+	//
+	// Note: if the memory maximum is not encoded in a Wasm binary, this
+	// results in allocating 4GB. See the doc on WithMemoryLimitPages for detail.
 	WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig
 
 	// WithDebugInfoEnabled toggles DWARF based stack traces in the face of
diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go
index 4035ca7a90..7387c85f1b 100644
--- a/internal/wasm/binary/decoder.go
+++ b/internal/wasm/binary/decoder.go
@@ -35,7 +35,7 @@ func DecodeModule(
 		return nil, ErrInvalidVersion
 	}
 
-	memorySizer := newMemorySizer(memoryLimitPages, memoryCapacityFromMax)
+	memSizer := newMemorySizer(memoryLimitPages, memoryCapacityFromMax)
 
 	m := &wasm.Module{}
 	var info, line, str, abbrev, ranges []byte // For DWARF Data.
@@ -106,7 +106,7 @@ func DecodeModule(
 		case wasm.SectionIDType:
 			m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
 		case wasm.SectionIDImport:
-			m.ImportSection, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, err = decodeImportSection(r, memorySizer, memoryLimitPages, enabledFeatures)
+			m.ImportSection, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures)
 			if err != nil {
 				return nil, err // avoid re-wrapping the error.
 			}
@@ -115,7 +115,7 @@ func DecodeModule(
 		case wasm.SectionIDTable:
 			m.TableSection, err = decodeTableSection(r, enabledFeatures)
 		case wasm.SectionIDMemory:
-			m.MemorySection, err = decodeMemorySection(r, memorySizer, memoryLimitPages)
+			m.MemorySection, err = decodeMemorySection(r, memSizer, memoryLimitPages)
 		case wasm.SectionIDGlobal:
 			if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil {
 				return nil, err // avoid re-wrapping the error.
@@ -185,6 +185,9 @@ func newMemorySizer(memoryLimitPages uint32, memoryCapacityFromMax bool) memoryS
 			}
 			return minPages, minPages, *maxPages
 		}
+		if memoryCapacityFromMax {
+			return minPages, memoryLimitPages, memoryLimitPages
+		}
 		return minPages, minPages, memoryLimitPages
 	}
 }


Test patch:
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index c7f46b9548..6eeadf72fb 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -16,15 +16,11 @@ import (
 	"time"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental/gojs"
 	"github.com/tetratelabs/wazero/internal/fstest"
 	internalgojs "github.com/tetratelabs/wazero/internal/gojs"
 	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/gojs/run"
-	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
-	"github.com/tetratelabs/wazero/internal/wasm"
-	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
 type newConfig func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config)
@@ -35,8 +31,11 @@ func defaultConfig(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *conf
 
 func compileAndRun(ctx context.Context, arg string, config newConfig) (stdout, stderr string, err error) {
 	rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
-		// https://github.com/tetratelabs/wazero/issues/992
+		// In order to avoid race condition on scheduleTimeoutEvent, we need to set the memory max
+		// and WithMemoryCapacityFromMax(true) above. See #992.
 		WithMemoryCapacityFromMax(true).
+		// Set max to a high value, e.g. so that Test_stdio_large can pass.
+		WithMemoryLimitPages(1024). // 64MB
 		WithCompilationCache(cache))
 	return compileAndRunWithRuntime(ctx, rt, arg, config) // use global runtime
 }
@@ -113,20 +112,6 @@ func TestMain(m *testing.M) {
 		log.Panicln(err)
 	}
 
-	// In order to avoid race condition on scheduleTimeoutEvent, we need to set the memory max
-	// and WithMemoryCapacityFromMax(true) above.
-	// https://github.com/tetratelabs/wazero/issues/992
-	//
-	// TODO: Maybe add WithMemoryMax API?
-	parsed, err := binaryformat.DecodeModule(testBin, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, false)
-	if err != nil {
-		log.Panicln(err)
-	}
-	// Set max to a high value, e.g. so that Test_stdio_large can pass
-	parsed.MemorySection.Max = 1024 // 64MB
-	parsed.MemorySection.IsMaxEncoded = true
-	testBin = binaryencoding.EncodeModule(parsed)
-
 	// Seed wazero's compilation cache to see any error up-front and to prevent
 	// one test from a cache-miss performance penalty.
 	r := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go
index 1c2c7840a8..8cff4c6207 100644
--- a/internal/wasm/binary/memory_test.go
+++ b/internal/wasm/binary/memory_test.go
@@ -13,32 +13,36 @@ import (
 func Test_newMemorySizer(t *testing.T) {
 	zero := uint32(0)
 	one := uint32(1)
-	limit := wasm.MemoryLimitPages
+	defaultLimit := wasm.MemoryLimitPages
 
 	tests := []struct {
 		name                                       string
 		memoryCapacityFromMax                      bool
+		limit                                      uint32
 		min                                        uint32
 		max                                        *uint32
 		expectedMin, expectedCapacity, expectedMax uint32
 	}{
 		{
 			name:             "min 0",
+			limit:            defaultLimit,
 			min:              zero,
-			max:              &limit,
+			max:              &defaultLimit,
 			expectedMin:      zero,
 			expectedCapacity: zero,
-			expectedMax:      limit,
+			expectedMax:      defaultLimit,
 		},
 		{
-			name:             "min 0 defaults max to limit",
+			name:             "min 0 defaults max to defaultLimit",
+			limit:            defaultLimit,
 			min:              zero,
 			expectedMin:      zero,
 			expectedCapacity: zero,
-			expectedMax:      limit,
+			expectedMax:      defaultLimit,
 		},
 		{
 			name:             "min 0, max 0",
+			limit:            defaultLimit,
 			min:              zero,
 			max:              &zero,
 			expectedMin:      zero,
@@ -47,6 +51,7 @@ func Test_newMemorySizer(t *testing.T) {
 		},
 		{
 			name:             "min 0, max 1",
+			limit:            defaultLimit,
 			min:              zero,
 			max:              &one,
 			expectedMin:      zero,
@@ -55,6 +60,7 @@ func Test_newMemorySizer(t *testing.T) {
 		},
 		{
 			name:                  "min 0, max 1 memoryCapacityFromMax",
+			limit:                 defaultLimit,
 			memoryCapacityFromMax: true,
 			min:                   zero,
 			max:                   &one,
@@ -62,8 +68,26 @@ func Test_newMemorySizer(t *testing.T) {
 			expectedCapacity:      one,
 			expectedMax:           one,
 		},
+		{
+			name:             "min 10, no max",
+			limit:            200,
+			min:              10,
+			expectedMin:      10,
+			expectedCapacity: 10,
+			expectedMax:      200,
+		},
+		{
+			name:                  "min 10, no max memoryCapacityFromMax",
+			memoryCapacityFromMax: true,
+			limit:                 200,
+			min:                   10,
+			expectedMin:           10,
+			expectedCapacity:      200,
+			expectedMax:           200,
+		},
 		{
 			name:             "min=max",
+			limit:            defaultLimit,
 			min:              one,
 			max:              &one,
 			expectedMin:      one,
@@ -75,7 +99,7 @@ func Test_newMemorySizer(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			sizer := newMemorySizer(limit, tc.memoryCapacityFromMax)
+			sizer := newMemorySizer(tc.limit, tc.memoryCapacityFromMax)
 			min, capacity, max := sizer(tc.min, tc.max)
 			require.Equal(t, tc.expectedMin, min)
 			require.Equal(t, tc.expectedCapacity, capacity)