Глобальные переменные и таблицы
Разбираем глобальные переменные и таблицы функций в Wasm.
Global — это переменная уровня модуля, а table — массив ссылок на функции для косвенных вызовов.
Глобальные переменные
Иногда нужно состояние, общее для всех функций модуля и живущее всё время работы. Для этого есть global. Глобаль бывает неизменяемой (константа) или изменяемой (mut). Объявим счётчик и функцию, которая его увеличивает:
(module
(global $counter (mut i32) (i32.const 0))
(func (export "inc") (result i32)
global.get $counter
i32.const 1
i32.add
global.set $counter ;; counter = counter + 1
global.get $counter)) ;; вернуть новое значение
Читается через global.get, пишется через global.set (только если mut). Неизменяемые глобали удобны для констант вроде версии или размера буфера.
Смоделируем счётчик на JS
let counter = 0; // как (global $counter (mut i32))
function inc() {
counter = counter + 1;
return counter;
}
console.log(inc());
console.log(inc());
console.log(inc());
Вывод:
1 2 3
Таблицы функций
А таблица (table) решает другую задачу — косвенный вызов. Прямой вызов call $add знает функцию заранее. Но что если нужно вызвать функцию по номеру, вычисленному во время работы — как указатель на функцию в C или массив колбэков? Тогда функции складывают в таблицу и вызывают через call_indirect по индексу:
(module
(table 2 funcref)
(func $add (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)
(func $sub (param i32 i32) (result i32) local.get 0 local.get 1 i32.sub)
(elem (i32.const 0) $add $sub) ;; таблица[0]=$add, таблица[1]=$sub
(type $binop (func (param i32 i32) (result i32)))
(func (export "apply") (param $op i32) (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
local.get $op
call_indirect (type $binop))) ;; вызвать таблица[$op]
Передавая $op = 0, мы вызовем $add, а $op = 1 вызовет $sub. Так Wasm поддерживает указатели на функции, виртуальные методы и колбэки из языков высокого уровня.
Как работает под капотом
Таблица — это массив ссылок на функции (funcref), отделённый от линейной памяти. Почему отдельно? Из соображений безопасности: если бы адреса функций лежали в обычной памяти, программа могла бы «подделать» указатель и прыгнуть в произвольный код. Таблица же хранит только валидные ссылки на реальные функции, а call_indirect вдобавок проверяет, что сигнатура (type) вызываемой функции совпадает с ожидаемой — иначе trap. Это защищает от вызова функции с неверными аргументами.
Частые ошибки
- Запись в неизменяемую глобаль —
global.setпо немутабельной глобали не пройдёт валидацию. - Несовпадение типа в call_indirect — если фактическая сигнатура функции не совпала с указанной
type, будет trap. - Выход за границу таблицы — индекс больше размера таблицы → trap.
Итоги
global— переменная модуля;mutделает её изменяемой.tableхранит ссылки на функции дляcall_indirectпо индексу.- Таблица отделена от памяти ради безопасности и проверяет сигнатуру вызова.
- Таблицы — это механизм указателей на функции и виртуальных методов.