Линейная память: единый массив байт

Знакомимся с главным хранилищем данных Wasm — линейной памятью.

Линейная память (linear memory) — это один непрерывный массив байт, который Wasm-модуль использует для всех данных сложнее простых чисел.

Зачем отдельная память

Стек операндов годится для арифметики, но где хранить массив из миллиона чисел или текст? Для этого у модуля есть линейная память — большой плоский массив байт, адресуемый от нуля. Это как чистый лист оперативной памяти, выданный модулю. Объявляется память так:

(module
  (memory 1)   ;; 1 страница = 64 КБ
  (export "memory" (memory 0)))

Единица измерения — страница, ровно 64 КБ (65536 байт). (memory 1) выделяет одну страницу. Память можно нарастить во время работы инструкцией memory.grow.

Чтение и запись

В память пишут инструкцией store, читают — load. Адрес — это просто число (смещение в байтах от начала). Запишем число 99 по адресу 0 и прочитаем обратно:

(func $demo (result i32)
  i32.const 0        ;; адрес
  i32.const 99       ;; значение
  i32.store          ;; память[0..3] = 99
  i32.const 0        ;; адрес снова
  i32.load)          ;; читаем -> 99

Поскольку i32 занимает 4 байта, число по адресу 0 ляжет в байты 0–3, следующее целое логично класть по адресу 4, потом 8 и так далее.

Память общая для модуля и хоста

Ключевая идея: эту же память видит и JavaScript. Когда память экспортирована, JS получает к ней доступ как к ArrayBuffer и может читать и писать те же байты. Смоделируем эту картину на JS, используя ArrayBuffer и DataView — так работают и реальные Wasm-store/load:

const memory = new ArrayBuffer(16);   // как линейная память Wasm
const view = new DataView(memory);
view.setInt32(0, 99, true);           // i32.store по адресу 0 (little-endian)
view.setInt32(4, 42, true);           // i32.store по адресу 4
console.log("по адресу 0:", view.getInt32(0, true));
console.log("по адресу 4:", view.getInt32(4, true));

Вывод:

по адресу 0: 99
по адресу 4: 42

Как работает под капотом

Линейная память — это, по сути, выделенный движком ArrayBuffer. Wasm-инструкции load/store компилируются в обычные обращения к этому буферу по смещению. Числа хранятся в порядке little-endian (младший байт первым). Каждое обращение проверяется на выход за границу: если адрес больше размера памяти, исполнение прерывается ловушкой (trap) — выйти за пределы своего буфера в чужую память нельзя.

Частые ошибки

  • Перепутали байты и элементы — соседнее i32 лежит по адресу +4, а не +1.
  • Выход за границу — запись по адресу больше размера памяти вызывает trap.
  • Забыли про little-endian — при ручном разборе байтов важен порядок.

Итоги

  • Линейная память — непрерывный массив байт, адресуется от нуля.
  • Единица — страница 64 КБ; растёт через memory.grow.
  • store пишет, load читает; адрес — смещение в байтах.
  • Экспортированную память видит и JS как ArrayBuffer.
Проверьте себя
1. Чему равна одна страница линейной памяти Wasm?
A1 КБ
B64 КБ (65536 байт)
C1 МБ
D4 байта
2. Где соседнее 32-битное целое лежит после числа по адресу 0?
AПо адресу 1
BПо адресу 4
CПо адресу 32
DВ стеке, а не в памяти
3. Что произойдёт при записи за пределы выделенной памяти?
AПамять автоматически расширится
BВозникнет ловушка (trap) — исполнение прервётся
CЗапись уйдёт в память соседнего модуля
DНичего, запись проигнорируется молча