Строки и массивы через память
Учимся передавать сложные данные — строки и массивы — через общую память.
Сложные данные (строки, массивы) пересекают границу не как значения, а через линейную память: передаются адрес и длина.
Почему нельзя просто передать строку
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.