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:
13
api/wasm.go
13
api/wasm.go
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
ce.pushValue(uint64(res))
|
||||
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,
|
||||
|
||||
@@ -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))
|
||||
ce.pushValue(uint64(res))
|
||||
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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user