Глобальные переменные и таблицы

Разбираем глобальные переменные и таблицы функций в 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 по индексу.
  • Таблица отделена от памяти ради безопасности и проверяет сигнатуру вызова.
  • Таблицы — это механизм указателей на функции и виртуальных методов.
Проверьте себя
1. Что нужно, чтобы глобальную переменную можно было изменять?
AОбъявить её с export
BОбъявить её как (mut i32)
CПоложить её в память
DГлобали всегда изменяемы
2. Для чего служит инструкция call_indirect?
AДля импорта функции
BДля вызова функции из таблицы по вычисленному индексу
CДля экспорта функции
DДля чтения памяти
3. Почему таблица функций отделена от линейной памяти?
AЧтобы экономить место
BРади безопасности: нельзя подделать указатель на функцию и прыгнуть в произвольный код
CПотому что память слишком мала
DЭто историческая случайность