github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/wasm/memory.go (about) 1 package wasm 2 3 import ( 4 "container/list" 5 "encoding/binary" 6 "fmt" 7 "math" 8 "reflect" 9 "sync" 10 "sync/atomic" 11 "time" 12 "unsafe" 13 14 "github.com/wasilibs/wazerox/api" 15 "github.com/wasilibs/wazerox/internal/internalapi" 16 "github.com/wasilibs/wazerox/internal/platform" 17 "github.com/wasilibs/wazerox/internal/wasmruntime" 18 ) 19 20 const ( 21 // MemoryPageSize is the unit of memory length in WebAssembly, 22 // and is defined as 2^16 = 65536. 23 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 24 MemoryPageSize = uint32(65536) 25 // MemoryLimitPages is maximum number of pages defined (2^16). 26 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem 27 MemoryLimitPages = uint32(65536) 28 // MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize". 29 MemoryPageSizeInBits = 16 30 ) 31 32 // compile-time check to ensure MemoryInstance implements api.Memory 33 var _ api.Memory = &MemoryInstance{} 34 35 type waiters struct { 36 mux sync.Mutex 37 l *list.List 38 } 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 internalapi.WazeroOnlyType 47 48 Buffer []byte 49 Min, Cap, Max uint32 50 Shared bool 51 // Mux is used to prevent overlapping calls to Grow and implement atomic instructions in interpreter 52 // mode when Go does not provide atomic APIs to use. 53 Mux sync.RWMutex 54 // definition is known at compile time. 55 definition api.MemoryDefinition 56 57 // waiters implements atomic wait and notify. It is implemented similarly to golang.org/x/sync/semaphore, 58 // with a fixed weight of 1 and no spurious notifications. 59 waiters sync.Map 60 61 closed bool 62 } 63 64 // NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory. 65 func NewMemoryInstance(memSec *Memory) *MemoryInstance { 66 min := MemoryPagesToBytesNum(memSec.Min) 67 capacity := MemoryPagesToBytesNum(memSec.Cap) 68 69 var buffer []byte 70 var cap uint32 71 if memSec.IsShared { 72 // TODO(anuraaga): Only use Mmap with compiler backend 73 max := MemoryPagesToBytesNum(memSec.Max) 74 b, err := platform.MmapMemory(int(max)) 75 if err != nil { 76 panic(err) 77 } 78 sp := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 79 sp.Len = int(MemoryPagesToBytesNum(memSec.Min)) 80 buffer = b 81 cap = memSec.Max 82 } else { 83 buffer = make([]byte, min, capacity) 84 cap = memSec.Cap 85 } 86 87 return &MemoryInstance{ 88 Buffer: buffer, 89 Min: memSec.Min, 90 Cap: cap, 91 Max: memSec.Max, 92 Shared: memSec.IsShared, 93 } 94 } 95 96 func (m *MemoryInstance) Close() error { 97 m.Mux.Lock() 98 defer m.Mux.Unlock() 99 100 if m.closed { 101 return nil 102 } 103 m.closed = true 104 if m.Shared { 105 return platform.MunmapCodeSegment(m.Buffer) 106 } 107 return nil 108 } 109 110 // Definition implements the same method as documented on api.Memory. 111 func (m *MemoryInstance) Definition() api.MemoryDefinition { 112 return m.definition 113 } 114 115 // Size implements the same method as documented on api.Memory. 116 func (m *MemoryInstance) Size() uint32 { 117 return m.size() 118 } 119 120 // ReadByte implements the same method as documented on api.Memory. 121 func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) { 122 if offset >= m.size() { 123 return 0, false 124 } 125 return m.Buffer[offset], true 126 } 127 128 // ReadUint16Le implements the same method as documented on api.Memory. 129 func (m *MemoryInstance) ReadUint16Le(offset uint32) (uint16, bool) { 130 if !m.hasSize(offset, 2) { 131 return 0, false 132 } 133 return binary.LittleEndian.Uint16(m.Buffer[offset : offset+2]), true 134 } 135 136 // ReadUint32Le implements the same method as documented on api.Memory. 137 func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) { 138 return m.readUint32Le(offset) 139 } 140 141 // ReadFloat32Le implements the same method as documented on api.Memory. 142 func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) { 143 v, ok := m.readUint32Le(offset) 144 if !ok { 145 return 0, false 146 } 147 return math.Float32frombits(v), true 148 } 149 150 // ReadUint64Le implements the same method as documented on api.Memory. 151 func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) { 152 return m.readUint64Le(offset) 153 } 154 155 // ReadFloat64Le implements the same method as documented on api.Memory. 156 func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) { 157 v, ok := m.readUint64Le(offset) 158 if !ok { 159 return 0, false 160 } 161 return math.Float64frombits(v), true 162 } 163 164 // Read implements the same method as documented on api.Memory. 165 func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { 166 if !m.hasSize(offset, uint64(byteCount)) { 167 return nil, false 168 } 169 return m.Buffer[offset : offset+byteCount : offset+byteCount], true 170 } 171 172 // WriteByte implements the same method as documented on api.Memory. 173 func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { 174 if offset >= m.size() { 175 return false 176 } 177 m.Buffer[offset] = v 178 return true 179 } 180 181 // WriteUint16Le implements the same method as documented on api.Memory. 182 func (m *MemoryInstance) WriteUint16Le(offset uint32, v uint16) bool { 183 if !m.hasSize(offset, 2) { 184 return false 185 } 186 binary.LittleEndian.PutUint16(m.Buffer[offset:], v) 187 return true 188 } 189 190 // WriteUint32Le implements the same method as documented on api.Memory. 191 func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool { 192 return m.writeUint32Le(offset, v) 193 } 194 195 // WriteFloat32Le implements the same method as documented on api.Memory. 196 func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool { 197 return m.writeUint32Le(offset, math.Float32bits(v)) 198 } 199 200 // WriteUint64Le implements the same method as documented on api.Memory. 201 func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { 202 return m.writeUint64Le(offset, v) 203 } 204 205 // WriteFloat64Le implements the same method as documented on api.Memory. 206 func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool { 207 return m.writeUint64Le(offset, math.Float64bits(v)) 208 } 209 210 // Write implements the same method as documented on api.Memory. 211 func (m *MemoryInstance) Write(offset uint32, val []byte) bool { 212 if !m.hasSize(offset, uint64(len(val))) { 213 return false 214 } 215 copy(m.Buffer[offset:], val) 216 return true 217 } 218 219 // WriteString implements the same method as documented on api.Memory. 220 func (m *MemoryInstance) WriteString(offset uint32, val string) bool { 221 if !m.hasSize(offset, uint64(len(val))) { 222 return false 223 } 224 copy(m.Buffer[offset:], val) 225 return true 226 } 227 228 // MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages. 229 func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) { 230 return uint64(pages) << MemoryPageSizeInBits 231 } 232 233 // Grow implements the same method as documented on api.Memory. 234 func (m *MemoryInstance) Grow(delta uint32) (result uint32, ok bool) { 235 // We take write-lock here as the following might result in a new slice 236 m.Mux.Lock() 237 defer m.Mux.Unlock() 238 239 currentPages := memoryBytesNumToPages(uint64(len(m.Buffer))) 240 if delta == 0 { 241 return currentPages, true 242 } 243 244 // If exceeds the max of memory size, we push -1 according to the spec. 245 newPages := currentPages + delta 246 if newPages > m.Max { 247 return 0, false 248 } else if newPages > m.Cap { // grow the memory. 249 if m.Shared { 250 panic("shared memory cannot be grown, this is a bug in wazero") 251 } 252 m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...) 253 m.Cap = newPages 254 return currentPages, true 255 } else { // We already have the capacity we need. 256 sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer)) 257 if math.MaxInt == math.MaxInt32 { 258 atomic.StoreInt32((*int32)(unsafe.Pointer(&sp.Len)), int32(MemoryPagesToBytesNum(newPages))) 259 } else { 260 atomic.StoreInt64((*int64)(unsafe.Pointer(&sp.Len)), int64(MemoryPagesToBytesNum(newPages))) 261 } 262 return currentPages, true 263 } 264 } 265 266 // PageSize returns the current memory buffer size in pages. 267 func (m *MemoryInstance) PageSize() (result uint32) { 268 return memoryBytesNumToPages(uint64(len(m.Buffer))) 269 } 270 271 // PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. e.g. 1 -> "64Ki" 272 // 273 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 274 func PagesToUnitOfBytes(pages uint32) string { 275 k := pages * 64 276 if k < 1024 { 277 return fmt.Sprintf("%d Ki", k) 278 } 279 m := k / 1024 280 if m < 1024 { 281 return fmt.Sprintf("%d Mi", m) 282 } 283 g := m / 1024 284 if g < 1024 { 285 return fmt.Sprintf("%d Gi", g) 286 } 287 return fmt.Sprintf("%d Ti", g/1024) 288 } 289 290 // Below are raw functions used to implement the api.Memory API: 291 292 // memoryBytesNumToPages converts the given number of bytes into the number of pages. 293 func memoryBytesNumToPages(bytesNum uint64) (pages uint32) { 294 return uint32(bytesNum >> MemoryPageSizeInBits) 295 } 296 297 // size returns the size in bytes of the buffer. 298 func (m *MemoryInstance) size() uint32 { 299 return uint32(len(m.Buffer)) // We don't lock here because size can't become smaller. 300 } 301 302 // hasSize returns true if Len is sufficient for byteCount at the given offset. 303 // 304 // Note: This is always fine, because memory can grow, but never shrink. 305 func (m *MemoryInstance) hasSize(offset uint32, byteCount uint64) bool { 306 return uint64(offset)+byteCount <= uint64(len(m.Buffer)) // uint64 prevents overflow on add 307 } 308 309 // readUint32Le implements ReadUint32Le without using a context. This is extracted as both ints and floats are stored in 310 // memory as uint32le. 311 func (m *MemoryInstance) readUint32Le(offset uint32) (uint32, bool) { 312 if !m.hasSize(offset, 4) { 313 return 0, false 314 } 315 return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true 316 } 317 318 // readUint64Le implements ReadUint64Le without using a context. This is extracted as both ints and floats are stored in 319 // memory as uint64le. 320 func (m *MemoryInstance) readUint64Le(offset uint32) (uint64, bool) { 321 if !m.hasSize(offset, 8) { 322 return 0, false 323 } 324 return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true 325 } 326 327 // writeUint32Le implements WriteUint32Le without using a context. This is extracted as both ints and floats are stored 328 // in memory as uint32le. 329 func (m *MemoryInstance) writeUint32Le(offset uint32, v uint32) bool { 330 if !m.hasSize(offset, 4) { 331 return false 332 } 333 binary.LittleEndian.PutUint32(m.Buffer[offset:], v) 334 return true 335 } 336 337 // writeUint64Le implements WriteUint64Le without using a context. This is extracted as both ints and floats are stored 338 // in memory as uint64le. 339 func (m *MemoryInstance) writeUint64Le(offset uint32, v uint64) bool { 340 if !m.hasSize(offset, 8) { 341 return false 342 } 343 binary.LittleEndian.PutUint64(m.Buffer[offset:], v) 344 return true 345 } 346 347 // Wait32 suspends the caller until the offset is notified by a different agent. 348 func (m *MemoryInstance) Wait32(offset uint32, exp uint32, timeout int64) uint64 { 349 w := m.getWaiters(offset) 350 w.mux.Lock() 351 352 addr := unsafe.Add(unsafe.Pointer(&m.Buffer[0]), offset) 353 cur := atomic.LoadUint32((*uint32)(addr)) 354 if cur != exp { 355 w.mux.Unlock() 356 return 1 357 } 358 359 return m.wait(w, timeout) 360 } 361 362 // Wait64 suspends the caller until the offset is notified by a different agent. 363 func (m *MemoryInstance) Wait64(offset uint32, exp uint64, timeout int64) uint64 { 364 w := m.getWaiters(offset) 365 w.mux.Lock() 366 367 addr := unsafe.Add(unsafe.Pointer(&m.Buffer[0]), offset) 368 cur := atomic.LoadUint64((*uint64)(addr)) 369 if cur != exp { 370 w.mux.Unlock() 371 return 1 372 } 373 374 return m.wait(w, timeout) 375 } 376 377 func (m *MemoryInstance) wait(w *waiters, timeout int64) uint64 { 378 if w.l == nil { 379 w.l = list.New() 380 } 381 382 // The specification requires a trap if the number of existing waiters + 1 == 2^32, so we add a check here. 383 // In practice, it is unlikely the application would ever accumulate such a large number of waiters as it 384 // indicates several GB of RAM used just for the list of waiters. 385 // https://github.com/WebAssembly/threads/blob/main/proposals/threads/Overview.md#wait 386 if uint64(w.l.Len()+1) == 1<<32 { 387 w.mux.Unlock() 388 // TODO(anuraaga): Handle this outside memory.go 389 panic(wasmruntime.ErrRuntimeTooManyWaiters) 390 } 391 392 ready := make(chan struct{}) 393 elem := w.l.PushBack(ready) 394 w.mux.Unlock() 395 396 if timeout < 0 { 397 <-ready 398 return 0 399 } else { 400 select { 401 case <-ready: 402 return 0 403 case <-time.After(time.Duration(timeout)): 404 // While we could see if the channel completed by now and ignore the timeout, similar to x/sync/semaphore, 405 // the Wasm spec doesn't specify this behavior, so we keep things simple by prioritizing the timeout. 406 w.mux.Lock() 407 w.l.Remove(elem) 408 w.mux.Unlock() 409 return 2 410 } 411 } 412 } 413 414 func (m *MemoryInstance) getWaiters(offset uint32) *waiters { 415 wAny, ok := m.waiters.Load(offset) 416 if !ok { 417 // The first time an address is waited on, simultaneous waits will cause extra allocations. 418 // Further operations will be loaded above, which is also the general pattern of usage with 419 // mutexes. 420 wAny, _ = m.waiters.LoadOrStore(offset, &waiters{}) 421 } 422 423 return wAny.(*waiters) 424 } 425 426 // Notify wakes up at most count waiters at the given offset. 427 func (m *MemoryInstance) Notify(offset uint32, count uint32) uint32 { 428 wAny, ok := m.waiters.Load(offset) 429 if !ok { 430 return 0 431 } 432 w := wAny.(*waiters) 433 434 w.mux.Lock() 435 defer w.mux.Unlock() 436 if w.l == nil { 437 return 0 438 } 439 440 res := uint32(0) 441 for num := w.l.Len(); num > 0 && res < count; num = w.l.Len() { 442 w := w.l.Remove(w.l.Front()).(chan struct{}) 443 close(w) 444 res++ 445 } 446 447 return res 448 }