github.com/taubyte/vm-wasm-utils@v1.0.2/memory.go (about) 1 package wasm 2 3 import ( 4 "context" 5 "encoding/binary" 6 "fmt" 7 "math" 8 "reflect" 9 "sync" 10 "unsafe" 11 12 "github.com/tetratelabs/wazero/api" 13 ) 14 15 const ( 16 // MemoryPageSize is the unit of memory length in WebAssembly, 17 // and is defined as 2^16 = 65536. 18 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 19 MemoryPageSize = uint32(65536) 20 // MemoryLimitPages is maximum number of pages defined (2^16). 21 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem 22 MemoryLimitPages = uint32(65536) 23 // MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize". 24 MemoryPageSizeInBits = 16 25 ) 26 27 // MemorySizer is the default function that derives min, capacity and max pages from decoded wasm. The capacity 28 // returned is set to minPages and max defaults to MemoryLimitPages when maxPages is nil. 29 30 var MemorySizer = func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) { 31 if maxPages != nil { 32 return minPages, minPages, *maxPages 33 } 34 return minPages, minPages, MemoryLimitPages 35 } 36 37 // compile-time check to ensure MemoryInstance implements api.Memory 38 var _ api.Memory = &MemoryInstance{} 39 40 // MemoryInstance represents a memory instance in a store, and implements api.Memory. 41 // 42 // Note: In WebAssembly 1.0 (20191205), there may be up to one Memory per store, which means the precise memory is always 43 // wasm.Store Memories index zero: `store.Memories[0]` 44 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0. 45 type MemoryInstance struct { 46 Buffer []byte 47 Min, Cap, Max uint32 48 // mux is used to prevent overlapping calls to Grow. 49 mux sync.RWMutex 50 } 51 52 // NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory. 53 func NewMemoryInstance(memSec *Memory) *MemoryInstance { 54 min := MemoryPagesToBytesNum(memSec.Min) 55 capacity := MemoryPagesToBytesNum(memSec.Cap) 56 return &MemoryInstance{ 57 Buffer: make([]byte, min, capacity), 58 Min: memSec.Min, 59 Cap: memSec.Cap, 60 Max: memSec.Max, 61 } 62 } 63 64 // Size implements the same method as documented on api.Memory. 65 func (m *MemoryInstance) Size() uint32 { 66 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 67 68 return m.size() 69 } 70 71 func (m *MemoryInstance) Definition() api.MemoryDefinition { 72 // TODO: Not necessary at the moment, but need to implement 73 return nil 74 } 75 76 // ReadByte implements the same method as documented on api.Memory. 77 func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) { 78 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 79 80 if offset >= m.size() { 81 return 0, false 82 } 83 return m.Buffer[offset], true 84 } 85 86 // ReadUint16Le implements the same method as documented on api.Memory. 87 func (m *MemoryInstance) ReadUint16Le(offset uint32) (uint16, bool) { 88 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 89 90 if !m.hasSize(offset, 2) { 91 return 0, false 92 } 93 return binary.LittleEndian.Uint16(m.Buffer[offset : offset+2]), true 94 } 95 96 // ReadUint32Le implements the same method as documented on api.Memory. 97 func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) { 98 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 99 100 return m.readUint32Le(offset) 101 } 102 103 // ReadFloat32Le implements the same method as documented on api.Memory. 104 func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) { 105 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 106 107 v, ok := m.readUint32Le(offset) 108 if !ok { 109 return 0, false 110 } 111 return math.Float32frombits(v), true 112 } 113 114 // ReadUint64Le implements the same method as documented on api.Memory. 115 func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) { 116 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 117 118 return m.readUint64Le(offset) 119 } 120 121 // ReadFloat64Le implements the same method as documented on api.Memory. 122 func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) { 123 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 124 125 v, ok := m.readUint64Le(offset) 126 if !ok { 127 return 0, false 128 } 129 return math.Float64frombits(v), true 130 } 131 132 // Read implements the same method as documented on api.Memory. 133 func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { 134 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 135 136 if !m.hasSize(offset, byteCount) { 137 return nil, false 138 } 139 return m.Buffer[offset : offset+byteCount : offset+byteCount], true 140 } 141 142 // WriteByte implements the same method as documented on api.Memory. 143 func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { 144 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 145 146 if offset >= m.size() { 147 return false 148 } 149 m.Buffer[offset] = v 150 return true 151 } 152 153 // WriteUint16Le implements the same method as documented on api.Memory. 154 func (m *MemoryInstance) WriteUint16Le(offset uint32, v uint16) bool { 155 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 156 157 if !m.hasSize(offset, 2) { 158 return false 159 } 160 binary.LittleEndian.PutUint16(m.Buffer[offset:], v) 161 return true 162 } 163 164 // WriteUint32Le implements the same method as documented on api.Memory. 165 func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool { 166 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 167 168 return m.writeUint32Le(offset, v) 169 } 170 171 // WriteFloat32Le implements the same method as documented on api.Memory. 172 func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool { 173 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 174 175 return m.writeUint32Le(offset, math.Float32bits(v)) 176 } 177 178 // WriteUint64Le implements the same method as documented on api.Memory. 179 func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { 180 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 181 182 return m.writeUint64Le(offset, v) 183 } 184 185 // WriteFloat64Le implements the same method as documented on api.Memory. 186 func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool { 187 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 188 189 return m.writeUint64Le(offset, math.Float64bits(v)) 190 } 191 192 // Write implements the same method as documented on api.Memory. 193 func (m *MemoryInstance) Write(offset uint32, val []byte) bool { 194 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 195 196 if !m.hasSize(offset, uint32(len(val))) { 197 return false 198 } 199 copy(m.Buffer[offset:], val) 200 return true 201 } 202 203 func (m *MemoryInstance) WriteString(offset uint32, v string) bool { 204 if !m.hasSize(offset, uint32(len(v))) { 205 return false 206 } 207 208 copy(m.Buffer[offset:], []byte(v)) 209 return true 210 } 211 212 // MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages. 213 func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) { 214 return uint64(pages) << MemoryPageSizeInBits 215 } 216 217 // Grow implements the same method as documented on api.Memory. 218 func (m *MemoryInstance) Grow(delta uint32) (result uint32, ok bool) { 219 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 220 221 // We take write-lock here as the following might result in a new slice 222 m.mux.Lock() 223 defer m.mux.Unlock() 224 225 currentPages := memoryBytesNumToPages(uint64(len(m.Buffer))) 226 if delta == 0 { 227 return currentPages, true 228 } 229 230 // If exceeds the max of memory size, we push -1 according to the spec. 231 newPages := currentPages + delta 232 if newPages > m.Max { 233 return 0, false 234 } else if newPages > m.Cap { // grow the memory. 235 m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...) 236 m.Cap = newPages 237 return currentPages, true 238 } else { // We already have the capacity we need. 239 sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer)) 240 sp.Len = int(MemoryPagesToBytesNum(newPages)) 241 return currentPages, true 242 } 243 } 244 245 // PageSize returns the current memory buffer size in pages. 246 func (m *MemoryInstance) PageSize(_ context.Context) (result uint32) { 247 // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! 248 249 return memoryBytesNumToPages(uint64(len(m.Buffer))) 250 } 251 252 // PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. Ex. 1 -> "64Ki" 253 // 254 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 255 func PagesToUnitOfBytes(pages uint32) string { 256 k := pages * 64 257 if k < 1024 { 258 return fmt.Sprintf("%d Ki", k) 259 } 260 m := k / 1024 261 if m < 1024 { 262 return fmt.Sprintf("%d Mi", m) 263 } 264 g := m / 1024 265 if g < 1024 { 266 return fmt.Sprintf("%d Gi", g) 267 } 268 return fmt.Sprintf("%d Ti", g/1024) 269 } 270 271 // Below are raw functions used to implement the api.Memory API: 272 273 // memoryBytesNumToPages converts the given number of bytes into the number of pages. 274 func memoryBytesNumToPages(bytesNum uint64) (pages uint32) { 275 return uint32(bytesNum >> MemoryPageSizeInBits) 276 } 277 278 // size returns the size in bytes of the buffer. 279 func (m *MemoryInstance) size() uint32 { 280 return uint32(len(m.Buffer)) // We don't lock here because size can't become smaller. 281 } 282 283 // hasSize returns true if Len is sufficient for byteCount at the given offset. 284 // 285 // Note: This is always fine, because memory can grow, but never shrink. 286 func (m *MemoryInstance) hasSize(offset uint32, byteCount uint32) bool { 287 return uint64(offset)+uint64(byteCount) <= uint64(len(m.Buffer)) // uint64 prevents overflow on add 288 } 289 290 // readUint32Le implements ReadUint32Le without using a context. This is extracted as both ints and floats are stored in 291 // memory as uint32le. 292 func (m *MemoryInstance) readUint32Le(offset uint32) (uint32, bool) { 293 if !m.hasSize(offset, 4) { 294 return 0, false 295 } 296 return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true 297 } 298 299 // readUint64Le implements ReadUint64Le without using a context. This is extracted as both ints and floats are stored in 300 // memory as uint64le. 301 func (m *MemoryInstance) readUint64Le(offset uint32) (uint64, bool) { 302 if !m.hasSize(offset, 8) { 303 return 0, false 304 } 305 return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true 306 } 307 308 // writeUint32Le implements WriteUint32Le without using a context. This is extracted as both ints and floats are stored 309 // in memory as uint32le. 310 func (m *MemoryInstance) writeUint32Le(offset uint32, v uint32) bool { 311 if !m.hasSize(offset, 4) { 312 return false 313 } 314 binary.LittleEndian.PutUint32(m.Buffer[offset:], v) 315 return true 316 } 317 318 // writeUint64Le implements WriteUint64Le without using a context. This is extracted as both ints and floats are stored 319 // in memory as uint64le. 320 func (m *MemoryInstance) writeUint64Le(offset uint32, v uint64) bool { 321 if !m.hasSize(offset, 8) { 322 return false 323 } 324 binary.LittleEndian.PutUint64(m.Buffer[offset:], v) 325 return true 326 }