Строки и массивы через память

Учимся передавать сложные данные — строки и массивы — через общую память.

Сложные данные (строки, массивы) пересекают границу не как значения, а через линейную память: передаются адрес и длина.

Почему нельзя просто передать строку

Wasm-функция принимает только числа (i32, f64 и т.п.). Строки в её сигнатуре быть не может. Решение: строку (последовательность байт) кладут в линейную память, а в функцию передают два числа — адрес начала и длину. Внутри модуль читает байты по этому адресу. Это та же идея, что указатель + длина в C.

Кодируем строку в байты

Строки в памяти Wasm обычно хранят в UTF-8. Со стороны JS строку превращают в байты через TextEncoder, копируют в память Wasm и зовут функцию с адресом и длиной. Смоделируем кодирование и подсчёт длины на чистом JS:

const text = "Привет";
const bytes = new TextEncoder().encode(text);  // UTF-8 байты
console.log("символов:", text.length);
console.log("байт UTF-8:", bytes.length);
console.log("первый байт:", bytes[0]);

Вывод:

символов: 6
байт UTF-8: 12
первый байт: 208

Обратите внимание: 6 кириллических символов заняли 12 байт — в UTF-8 кириллица кодируется двумя байтами на символ. Это важно: длину для Wasm считают в байтах, а не в символах.

Схема передачи строки

JS:  "Привет" --TextEncoder--> [208,159,...] (12 байт)
        |
        | копируем в линейную память по адресу ptr
        v
память Wasm: [ ... байты строки ... ]
        |
        | зовём функцию(ptr, len=12)
        v
Wasm: читает len байт начиная с ptr

Обратно: из Wasm в JS

Если Wasm возвращает строку, он отдаёт адрес (и обычно длину), а JS читает байты из памяти и декодирует обратно через TextDecoder:

// имитация: байты в "памяти", JS их декодирует
const memoryBytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const str = new TextDecoder().decode(memoryBytes);
console.log("декодировано:", str);

Вывод:

декодировано: Hello

Массивы — так же

Массив чисел передаётся аналогично: данные кладут в память подряд, передают адрес и количество элементов. Зная тип (i32 = 4 байта), Wasm вычисляет смещение каждого элемента: элемент i лежит по адресу ptr + i*4.

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

Тонкость в том, кто выделяет место в памяти. Обычно скомпилированный модуль экспортирует свой malloc: JS вызывает его, получает свободный адрес, копирует туда байты строки, зовёт целевую функцию, а потом (если язык без GC) вызывает free. Именно эту рутину автоматизируют инструменты вроде wasm-bindgen для Rust или Emscripten для C — вручную это писать утомительно, поэтому такой «клей» генерируют.

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

  • Длина в символах, а не в байтах — для UTF-8 кириллица это 2 байта на символ, эмодзи — 4.
  • Забыли выделить память — нельзя писать в произвольный адрес; сначала malloc.
  • Утечка — выделили под строку память и не освободили (в языках без GC).

Итоги

  • Строки и массивы передаются через линейную память: адрес + длина.
  • Строки кодируют в UTF-8 (TextEncoder/TextDecoder); длина — в байтах.
  • Память под данные обычно выделяет экспортированный malloc модуля.
  • Этот «клей» автоматизируют wasm-bindgen и Emscripten.
Проверьте себя
1. Как строка передаётся из JS в Wasm-функцию?
AПрямо как аргумент-строка
BБайты кладут в линейную память, в функцию передают адрес и длину
CЧерез глобальную переменную типа string
DСтроки передать нельзя в принципе
2. В чём измеряют длину строки для передачи в Wasm?
AВ символах
BВ байтах (UTF-8)
CВ словах
DВ страницах памяти
3. Кто обычно выделяет место в памяти под передаваемую строку?
AБраузер сам
BЭкспортированный модулем malloc, который вызывает JS
CОперационная система
DПамять не выделяется, пишут в адрес 0