Работа с памятью
Поскольку в WebAssembly всего 4 типа данных, причём только числовых, любые агрегатные типы данных нужно эмулировать. WebAssembly предоставляет для этого память — массив байт, которые можно использовать по своему усмотрению (например, часть — как регистры «процессора», часть — как стек значений, часть — как кучу для хранения данных).
Объявление памяти
Память объявляется похожим на функции образом:
(memory экспорт_или_импорт начальный_размер максимальный_размер)
Размер указывается в страницах, имеющих размер 65536 байт (64 КиБ). Максимальный размер можно не указывать, если не предполагается расширять память по ходу выполнения программы (если предполагается, то ограничение памяти сверху желательно как мера защиты от случайного исчерпания физической памяти компьютера).
Для импортируемой памяти начальный размер тоже является обязательным. Он должен быть не больше начального размера внешней памяти. Можно указать и максимальный: он должен быть не меньше, чем максимальный размер внешней памяти.
На практике стоит придерживаться следующей политики:
- если память импортируется, нужно указывать только минимальный размер
- в противном случае желательно указать максимальный размер
Также разрешается указать начальные значения для каких-то участков памяти (даже — для импортируемой). Делается это при помощи директив вида
(data смещение данные)
Смещение указывается в виде инструкции: на настоящий момент допускаются
только i32.const
и get_global
(про последнюю можно прочитать в
главе про интеграцию с Javascript). Данные указываются
в виде закавыченного текста (в кодировке UTF-8) с почти стандартным
набором экранирующих последовательностей.
Нестандартно указываются явные значения байт:
в виде \HH
, где H
— шестнадцатиричная цифра (в большинстве
языков программирования используется последовательность \xHH
).
Данные располагаются в памяти, начиная со смещения. Запись данных в
память происходит в порядке следования директив data
. Например
(data (i32.const 0) "foo")
(data (i32.const 1) "foo")
приведёт к тому, что первые 4 байта памяти будут заполнены ASCII-кодами символов f, f, o, o.
Работа с памятью
Основными инструкциями для работы с памятью являются тип.load
(загрузить из памяти) и тип.store
(сохранить в память). У них
такой же порядок аргументов, как у get_local
и set_local
,
за тем исключением, что первый аргумент является инструкцией,
а не непосредственным литералом.
Например:
;; кладёт в переменную $foo число, считанное с начальных 4 байт памяти
(set_local $foo (i32.load (i32.const 0)))
;; записывает значение переменной $bar в следующие 4 байта памяти
(i32.store (i32.const 4) (get_local $bar))
Все числа хранятся в памяти в формате Little-Endian (младшие байты сначала).
Кроме load
и store
есть инструкции, позволяющие считывать и записывать
по 1 и по 2 байта (см. справочник).
Также к любой из разновидностей load
и store
можно приписать
непосредственный аргумент вида offset=число
(без пробелов!),
число из которого прибавляется к входному смещению.
Расширение памяти
При желании память можно расширить инструкцией (memory.grow на_сколько)
.
Входное число имеет размерность страниц (по 64 КиБ). Эта инструкция
имеет две особенности:
- в случае успеха её результатом является старый размер памяти
- в случае неудачи её результатом является
-1
Не гарантируется, что память можно расширять в пределах размеров,
указанных в объявлении memory
. То есть следующая программа
(module
(func $print (import "io" "print") (param i32))
(memory 1 10)
(func (export "main")
(call $print (memory.grow (i32.const 1)))
)
)
вполне может напечатать -1
, несмотря на то, что запрошенный размер 2
меньше максимального размера 10.
Текущий размер памяти (опять же, в страницах) можно узнать
инструкцией (memory.size)
.
Массивный ввод-вывод в WebWASM
Стандартная библиотека учебного приложения предоставляет две функции, позволяющие считать/напечатать массив байт:
;; анонимные параметры можно объявлять одним S-выражением
(func $read (import "io" "read") (param i32 i32))
(func $write (import "io" "write") (param i32 i32))
Входы для этих функций имеют смысл смещения и размера.
Функция чтения read
считывает не более чем указанное количество байт
из UTF-8-представления текста в форме ввода данных и располагает
их в памяти по указанному смещению. Сторонним эффектом read
является поглощение считанных байт: следующий вызов read
считывает уже следующие байты.
Функция записи write
печатает в форме вывода текст, описываемый
указанным массивом байт (в кодировке UTF-8).
Все эти функции работают со внешней памятью, которую нужно не забыть импортировать директивой
(memory (import "io" "mem") 1)
Начальный размер этой памяти можно указать от 1 до 16. При меньшем чем 16 начальном размере гарантируется возможность её расширения до 16. При этом максимальный размер памяти ограничен сверху 256 (в программе это ограничение указывать не нужно).