Exposes api.Memory Grow (#535)

This exports an API which allows host-defined functions access to
increase the memory size.

Fixes #483

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-05-09 14:56:50 +08:00
committed by GitHub
parent 8f8c9ee205
commit 96bc7c8462
5 changed files with 50 additions and 23 deletions

View File

@@ -221,14 +221,21 @@ type MutableGlobal interface {
// Note: This includes all value types available in WebAssembly 1.0 (20191205) and all are encoded little-endian.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#storage%E2%91%A0
type Memory interface {
// Size returns the size in bytes available. Ex. If the underlying memory has 1 page: 65536
//
// Note: this will not grow during a host function call, even if the underlying memory can. Ex. If the underlying
// memory has min 0 and max 2 pages, this returns zero.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
Size(context.Context) uint32
// Grow increases memory by the delta in pages (65536 bytes per page). The return val is the previous memory size in
// pages, or false if the delta was ignored as it exceeds max memory.
//
// Note: This is the same as the "memory.grow" instruction defined in the WebAssembly Core Specification, except
// returns false instead of -1 on failure
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
// See MemorySizer
Grow(ctx context.Context, deltaPages uint32) (previousPages uint32, ok bool)
// IndexByte returns the index of the first instance of c in the underlying buffer at the offset or returns false if
// not found or out of range.
IndexByte(ctx context.Context, offset uint32, c byte) (uint32, bool)

View File

@@ -882,8 +882,11 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont
case wazeroir.OperationKindMemoryGrow:
{
n := ce.popValue()
res := memoryInst.Grow(ctx, uint32(n))
if res, ok := memoryInst.Grow(ctx, uint32(n)); !ok {
ce.pushValue(uint64(0xffffffff)) // = -1 in signed 32-bit integer.
} else {
ce.pushValue(uint64(res))
}
frame.pc++
}
case wazeroir.OperationKindConstI32, wazeroir.OperationKindConstI64,

View File

@@ -774,8 +774,11 @@ func (ce *callEngine) builtinFunctionGrowCallFrameStack() {
func (ce *callEngine) builtinFunctionMemoryGrow(ctx context.Context, mem *wasm.MemoryInstance) {
newPages := ce.popValue()
res := mem.Grow(ctx, uint32(newPages))
if res, ok := mem.Grow(ctx, uint32(newPages)); !ok {
ce.pushValue(uint64(0xffffffff)) // = -1 in signed 32-bit integer.
} else {
ce.pushValue(uint64(res))
}
// Update the moduleContext fields as they become stale after the update ^^.
bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&mem.Buffer))

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"math"
"reflect"
"sync"
"unsafe"
"github.com/tetratelabs/wazero/api"
@@ -44,6 +45,8 @@ var _ api.Memory = &MemoryInstance{}
type MemoryInstance struct {
Buffer []byte
Min, Cap, Max uint32
// mux is used to prevent overlapping calls to Grow.
mux sync.RWMutex
}
// NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory.
@@ -211,31 +214,31 @@ func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
return uint64(pages) << MemoryPageSizeInBits
}
// Grow extends the memory buffer by "newPages" * memoryPageSize.
// The logic here is described in https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem.
//
// 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, delta uint32) (result uint32) {
// Grow implements the same method as documented on api.Memory.
func (m *MemoryInstance) Grow(_ context.Context, delta uint32) (result uint32, ok bool) {
// Note: If you use the context.Context param, don't forget to coerce nil to context.Background()!
// We take write-lock here as the following might result in a new slice
m.mux.Lock()
defer m.mux.Unlock()
currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
if delta == 0 {
return currentPages
return currentPages, true
}
// If exceeds the max of memory size, we push -1 according to the spec.
newPages := currentPages + delta
if newPages > m.Max {
return 0xffffffff // = -1 in signed 32-bit integer.
return 0, false
} else if newPages > m.Cap { // grow the memory.
m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...)
m.Cap = newPages
return currentPages
return currentPages, true
} else { // We already have the capacity we need.
sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer))
sp.Len = int(MemoryPagesToBytesNum(newPages))
return currentPages
return currentPages, true
}
}
@@ -274,7 +277,7 @@ func memoryBytesNumToPages(bytesNum uint64) (pages uint32) {
// size returns the size in bytes of the buffer.
func (m *MemoryInstance) size() uint32 {
return uint32(len(m.Buffer))
return uint32(len(m.Buffer)) // We don't lock here because size can't become smaller.
}
// hasSize returns true if Len is sufficient for sizeInBytes at the given offset.

View File

@@ -66,22 +66,33 @@ func TestMemoryInstance_Grow_Size(t *testing.T) {
} else {
m = &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
}
require.Equal(t, uint32(0), m.Grow(ctx, 5))
res, ok := m.Grow(ctx, 5)
require.True(t, ok)
require.Equal(t, uint32(0), res)
require.Equal(t, uint32(5), m.PageSize(ctx))
// Zero page grow is well-defined, should return the current page correctly.
require.Equal(t, uint32(5), m.Grow(ctx, 0))
res, ok = m.Grow(ctx, 0)
require.True(t, ok)
require.Equal(t, uint32(5), res)
require.Equal(t, uint32(5), m.PageSize(ctx))
require.Equal(t, uint32(5), m.Grow(ctx, 4))
res, ok = m.Grow(ctx, 4)
require.True(t, ok)
require.Equal(t, uint32(5), res)
require.Equal(t, uint32(9), 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)))
_, ok = m.Grow(ctx, 2)
require.False(t, ok)
require.Equal(t, uint32(9), m.PageSize(ctx))
// But growing one page is still permitted.
require.Equal(t, uint32(9), m.Grow(ctx, 1))
res, ok = m.Grow(ctx, 1)
require.True(t, ok)
require.Equal(t, uint32(9), res)
// Ensure that the current page size equals the max.
require.Equal(t, max, m.PageSize(ctx))