Курс Лекція 10
▶ СлайдиСРС 19+20⌨️ команди 14 · 5 нових
Конспект · Лекція 10

RUST У WASM, МЕЖА З HOST-СЕРЕДОВИЩЕМ

Замикання лінії курсу

Ключова ідея. Останню межу курсу — межу набору інструкцій — перетинає не рука, що пише .wat вручну, а компілятор: Rust компілюється у WASM, а wasm-bindgen генерує міст до JavaScript.
Демо. Rust-функція, позначена #[wasm_bindgen], компілюється через wasm-pack build; на сторінці HTML кнопка викликає її з JS, а рядок-аргумент передається через межу мов у консоль браузера.
🧩 СРС ДО ЦІЄЇ ЛЕКЦІЇ · 2-га година заняття

Після презентації теми — самостійна робота в аудиторії над двома темами. Вони ж готують до захисту та розширення ЛР10. Матеріали публікуються в Google Classroom разом із лекцією.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Вступ: від ручного .wat до компільованого коду

У Лекції 9 ми писали .wat вручну — корисно для розуміння моделі, але непридатно для реальної програми в кілька тисяч рядків. Лекція 10 замикає лінію курсу: замість людини, що вручну розставляє local.get/i32.add, цю роботу виконує компілятор — той самий rustc, яким ми користувалися для нативного x86-64 у Модулях 1–2, тепер спрямований на іншу ціль компіляції. Rust тут — не випадковий вибір: система володіння й запозичень (Лекції 5–7) дає гарантії безпеки пам'яті на етапі компіляції, а це саме те, що потрібно для коду, який піде виконуватися в чужому, потенційно ворожому браузерному host-середовищі (host — рушій, що запускає модуль; сам модуль для нього — guest) без окремого рантайму збирача сміття.

Ціль компіляції wasm32-unknown-unknown

Rust визначає ціль компіляції як триплет (target triple) архітектура-виробник-ОС. Для WASM у браузері використовується wasm32-unknown-unknown:

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
  • wasm32 — 32-бітна адресація лінійної пам'яті (специфікація WASM MVP; wasm64 існує, але поки що експериментальний);
  • перше unknown — виробник не заданий (нема сенсу для віртуальної машини);
  • друге unknownнемає конкретної операційної системи: жодного libc, жодних системних викликів. Це «голий метал» у сенсі відсутності ОС, майже як ціль для мікроконтролера.

Існує і сусідня ціль — wasm32-wasi (нині wasm32-wasip1/wasip2), яка натомість надає POSIX-подібний інтерфейс системних викликів через стандарт WASI (WebAssembly System Interface) — придатна для запуску WASM поза браузером, наприклад у Wasmtime як заміну контейнера. У курсі ми зосереджені на wasm32-unknown-unknown, бо мета — сторінка в браузері, а не автономний бінарник.

wasm-bindgen: що генерується під капотом

Ручний WASM (Лекція 9) обмінюється з хостом лише числами (i32, f64) — це все, що розуміє специфікація WASM. Rust-типи (String, &str, struct, Vec<T>) складніші, і саме тут вступає wasm-bindgen:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Вітаю, {name}! Ядро на зв'язку.")
}

Атрибут #[wasm_bindgen] — процедурний макрос, який під час компіляції генерує код по обидва боки межі:

  • усередині .wasm — додаткові extern "C"-функції, що вміють приймати/повертати «сирі» числові дескриптори замість Rust-типів;
  • зовні — JS-файл (glue code), що ховає цю роботу за звичайним, ергономічним викликом функції.

Маршалінг типів через межу

Маршалінг (marshalling) — перетворення значення при перетині межі мов. Складність різна залежно від типу:

Rust-тип Що передається фактично Вартість
i32, u32, f64, bool напряму, як число WASM нуль — це вже нативний тип WASM
&str, String пара (вказівник, довжина) в лінійну пам'ять + копія байтів UTF-8 TextEncoder/TextDecoder у glue-коді
Vec<T> (числа) вказівник + довжина, зчитується як TypedArray (Uint8Array тощо) одна копія через ArrayBuffer
struct з #[wasm_bindgen] непрозорий JS-клас, що всередині зберігає лише вказівник на Rust-об'єкт у лінійній пам'яті геттери/сеттери генеруються макросом
складні вкладені типи, enum з даними здебільшого через серіалізацію (serde-wasm-bindgen, JSON) найдорожче — повна серіалізація/десеріалізація

Правило, яке варто запам'ятати: чим простіший тип на межі, тим дешевший виклик. Гарячий шлях (функція, яку викликають щокадру в анімації чи грі) варто проєктувати на прості числові типи; складні структури краще передавати одноразово або пакетно.

wasm-pack: конвеєр збірки в один виклик

wasm-pack build --target web

Ця одна команда виконує послідовність, яку інакше довелося б збирати вручну: cargo build --target wasm32-unknown-unknown --release, потім запуск CLI wasm-bindgen для генерації JS-обгортки з метаданих, які компілятор вклав у .wasm, і пакування результату в теку pkg/:

pkg/
├── sid_core_bg.wasm     # сам бінарник
├── sid_core.js           # JS/ES-модуль з експортованими функціями
├── sid_core.d.ts          # типи для TypeScript
└── package.json           # метадані для npm/бандлерів

Прапорець --target web готує ES-модуль, готовий до прямого <script type="module"> без бандлера; --target bundler — варіант для Webpack/Vite; --target nodejs — для CommonJS-середовища Node.js.

Виклик із HTML/JS

<script type="module">
  import init, { greet } from './pkg/sid_core.js';
  await init();                          // підвантажити й інстанціювати .wasm
  document.querySelector('#btn').onclick = () => {
    console.log(greet('С.І.Д.'));
  };
</script>

init() — асинхронна функція: завантажує .wasm по мережі (fetch), компілює й інстанціює модуль (та сама операція WebAssembly.instantiateStreaming, яку ми викликали вручну в Лекції 9), і лише після її завершення greet готова до виклику як звичайна JS-функція.

Пам'ять через межу: хто відповідає за звільнення

Лінійна пам'ять модуля з боку JS видима як instance.exports.memory.buffer — звичайний ArrayBuffer. Rust-об'єкти (struct, обгорнуті #[wasm_bindgen]), передані в JS, насправді залишаються всередині лінійної пам'яті WASM; JS-сторона тримає лише непрозорий дескриптор (вказівник). Оскільки в WASM немає збирача сміття, і Rust звільняє пам'ять детерміновано через Drop лише коли є явний виклик, а не автоматично з боку JS, wasm-bindgen генерує для кожного такого класу метод .free(). Забути викликати .free() на JS-стороні — це витік пам'яті: сам об'єкт живий, доки JS тримає посилання, але навіть після втрати посилання пам'ять у WASM-модулі не звільняється, бо збирач сміття JS не знає нічого про купу (heap) Rust усередині лінійної пам'яті.

Відтворювані збірки (reproducible builds)

Проблема, добре знайома з Модуля 1: «на моїй машині збирається інший .wasm». Причини — різні версії rustc, wasm-bindgen, транзитивних крейтів, навіть різний порядок символів через непослідовну ітерацію HashMap під час кодогенерації. Для системного й безпекового коду це критично: без відтворюваності неможливо довести, що опублікований бінарник відповідає опублікованому вихідному коду — а це саме та властивість, яку перевіряє аудит перед тим, як довірити модулю виконання в браузері користувача.

Практика:

FROM rust:1.82-slim
RUN rustup target add wasm32-unknown-unknown
RUN cargo install wasm-pack --version 0.13.1 --locked
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN wasm-pack build --target web --release
  • Cargo.lock фіксує точні версії всіх залежностей — команда cargo build --locked відмовляється збирати, якщо Cargo.lock розходиться з Cargo.toml;
  • фіксована версія образу (rust:1.82-slim, а не rust:latest) і фіксована версія wasm-pack прибирають дрейф інструментарію;
  • збірка всередині контейнера ізолює процес від локального стану машини розробника — той самий принцип ізоляції, що в Модулі 1, тепер працює на гарантію байт-у-байт відтворюваності, а не на безпеку виконання.

Верифікація — тривіальна: sha256sum pkg/*.wasm на двох незалежних машинах має дати однаковий хеш.

Три межі курсу

Весь курс — це послідовне занурення на три різні межі ізоляції та перевірки:

Межа Модуль Що ізолює/перевіряє
Процес усередині контейнера Модуль 1 простори імен, cgroups — те саме ядро Linux, окрема view на систему
Безпечний доступ до пам'яті Модуль 2 володіння й запозичення Rust — компілятор, а не рантайм, ловить помилки пам'яті
Переносний набір інструкцій Модуль 3 від x86-64 до WASM — той самий алгоритм, дві різні машини, валідація типів при завантаженні

Спільний мотив усіх трьох: ізоляція й перевірка відбуваються якомога раніше — на етапі ядра (namespaces), компілятора (borrow checker) чи завантаження модуля (WASM validation), а не «десь у рантаймі, якщо пощастить».

Підводні камені

  • Розсинхронізовані версії wasm-bindgen — версія крейта в Cargo.toml і версія CLI, встановленого глобально, повинні збігатися, інакше збірка падає з незрозумілою помилкою генерації.
  • Складний Rust-тип без #[wasm_bindgen] переданий напряму в експортовану сигнатуру — помилка компіляції про те, що тип не реалізує потрібний трейт маршалінгу.
  • Забутий .free() для JS-обгорнутих Rust-структур — тихий витік пам'яті лінійної області, який не покаже жоден JS-профайлер (адже сам JS-об'єкт малий, «важка» пам'ять прихована в .wasm-хіпі).
  • file:// замість HTTP-сервера — браузери відмовляються завантажувати .wasm через fetch з локального файлу через політику CORS; потрібен хоча б python3 -m http.server.
  • Панічна поведінка Rust у WASM без обробника — паніка може мовчки «повісити» модуль без діагностики; крейт console_error_panic_hook, підключений на старті, перенаправляє паніку в console.error браузера.
  • Недетермінована збірка через непінований інструментарій — оновлення wasm-pack чи rustc «в фоні» на CI-машині рано чи пізно дасть інший бінарник з того самого коду.

Тестування WASM-модуля

Компільований у WASM код не звільняє від дисципліни тестування — навпаки, помилка маршалінгу чи невірний тип на межі виявляється лише під час фактичного виклику з JS, а не на етапі cargo check. wasm-bindgen постачає крейт wasm-bindgen-test, що дозволяє писати тести, які реально виконуються в середовищі WASM (headless-браузер або Node.js), а не лише в нативному cargo test:

use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn greet_contains_name() {
    assert!(greet("С.І.Д.").contains("С.І.Д."));
}
wasm-pack test --headless --chrome

Це важливо саме тому, що cargo test за замовчуванням компілює й запускає тести під нативну ціль (x86-64), а не під wasm32-unknown-unknown — легко написати код, що коректно проходить нативні тести, але падає лише при фактичному виконанні в браузерному WASM-рушії через розбіжності в поведінці (наприклад, недоступність частини std під wasm32-unknown-unknown, зокрема мереж і файлової системи).

Розмір бінарника: wasm-opt і wee_alloc

Розмір .wasm-файлу напряму впливає на час першого завантаження сторінки, тому для продакшн-збірки типова практика — прогнати бінарник через wasm-opt (частина binaryen), який виконує додаткові оптимізації розміру й швидкості поза тим, що вже зробив LLVM у rustc:

wasm-opt -Oz -o pkg/sid_core_bg.wasm pkg/sid_core_bg.wasm

Прапорець -Oz пріоритизує розмір над швидкістю — доречний вибір для коду, що завантажується по мережі й виконується нечасто. Додатково стандартний алокатор Rust у wasm32-unknown-unknown можна замінити на компактніший (наприклад, wee_alloc чи, у сучасних збірках, вбудований dlmalloc з тюнінгом) — компроміс між швидкістю алокацій і розміром згенерованого коду, який варто вимірювати, а не вгадувати.

Підсумок

Rust компілюється в WASM тим самим rustc, який ми використовували для нативного коду, — просто з іншою ціллю компіляції та без операційної системи під капотом. wasm-bindgen бере на себе найважчу частину — маршалінг типів через межу, від тривіальних чисел до рядків, що фізично живуть у лінійній пам'яті модуля; wasm-pack пакує весь конвеєр (компіляція + генерація обгортки + метадані) в один виклик. Відтворювані збірки в контейнері перетворюють Rust→WASM з «якось працює в мене» на властивість, яку можна довести криптографічно. А сама тема курсу — три межі, три різні техніки ізоляції й перевірки — тут замикається: від процесу в контейнері, через пам'ять під контролем компілятора, до набору інструкцій, що виконується безпечно в чужому середовищі.

Розбір за слайдами

План лекції

Фокус: WASM як компільований бекенд, а не мова для ручного письма

Навіщо компілювати Rust у WASM

Компілятор Rust розв'язує ту саму задачу компіляції, що й gcc -S у Л8, лише для іншої машини

Ручний .wat (Л9) проти Rust → WASM (Л10)

Ручний .wat
  • Людина розставляє local.get / i32.add
  • На межі лише i32, i64, f32, f64
  • Кожен export пишеться вручну
  • Навчальна модель стекової машини
Rust → wasm32-unknown-unknown
  • rustc + LLVM генерують інструкції
  • String, struct, Vec — через wasm-bindgen
  • Експорти — з атрибута #[wasm_bindgen]
  • Гарантії володіння (Л5–Л7) у браузері

Той самий байт-код на виході; змінюється лише те, хто його пише — людина чи компілятор

Rust → WASM у цифрах

1
команда збірки
wasm-pack build --target web
32
біти адреси лінійної пам'яті
ціль wasm32-unknown-unknown
4
файли у pkg/
.wasm · .js · .d.ts · package.json
1
хеш для двох незалежних збірок
sha256sum — доказ відтворюваності

Той самий rustc, що збирав x86-64 у Модулях 1–2, — інша ціль компіляції

Ціль компіляції: триплет wasm32-unknown-unknown

ЧастинаЗначенняЩо означає
архітектураwasm3232-бітна адресація лінійної пам'яті
виробникunknownдля віртуальної машини не має сенсу
ОСunknownнемає ОС: жодного libc, жодних системних викликів
сусідня цільwasm32-wasi (wasip1/wasip2)POSIX-подібні виклики через WASI — поза браузером

rustup target add wasm32-unknown-unknown — одноразово. Курс тримається unknown-unknown, бо мета — сторінка в браузері, а не окремий бінарник

Rust-функція для WASM

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Вітаю, {name}! Ядро на зв'язку.")
}

Атрибут #[wasm_bindgen] позначає межу з JavaScript

Що генерує #[wasm_bindgen]

Усередині .wasm (guest)
  • extern "C"-обгортки для експортованих функцій
  • Приймають і повертають лише числові дескриптори
  • Рядок живе в лінійній пам'яті: (вказівник, довжина)
У .js — glue code (host)
  • TextEncoder/TextDecoder копіює байти UTF-8
  • Звичайний виклик greet(name) ховає роботу з пам'яттю
  • Для структур — JS-класи з методом .free()

Це і є маршалінг (marshalling) — перетворення значень при перетині межі між WASM-модулем (guest) і JS-хостом (host). i32/f64 проходять напряму — вони й так типи WASM

Ціна перетину межі: числа й рядки

МЕЖА JS ↔ WASM: ЧИСЛА ЙДУТЬ, РЯДКИ КОПІЮЮТЬСЯ JS · HOST (браузер) 3.14 звичайне число greet('С.І.Д.') аргумент — JS-рядок МЕЖА напряму, без копії типи самого WASM WASM-BINDGEN · GLUE копія на виклик TextEncoder → UTF-8 у пам'ять модуля у WASM входять лише (ptr, len) результат — так само копія, потім free WASM · GUEST лінійна пам'ять f64 нативний тип WASM (ptr, len) → &str Rust читає з пам'яті числа безкоштовні, рядки — маршалінг: проєктуй API так, щоб рядки йшли рідко

i32/f64 — типи самої машини WASM; JS-рядок у модуль не входить — входить лише (ptr, len)

wasm-pack: конвеєр збірки

wasm-pack — те саме, що cargo build, тільки для цілі wasm32 з додатковою генерацією обгортки

RUST → WEBASSEMBLY → БРАУЗЕР .rs Rust код wasm32 cargo build wasm-bindgen JS-міст браузер .wasm+.js wasm-pack пакує модуль + типізований JS-обгортку для сторінки

Що лежить у pkg/ після збірки

ФайлЩо цеХто використовує
sid_core_bg.wasmсам бінарник модулябраузер через init()
sid_core.jsES-модуль: init(), greet(), класи<script type="module">
sid_core.d.tsтипи експортівTypeScript, редактор
package.jsonметадані пакетаnpm, бандлери

--target web — ES-модуль без бандлера; --target bundler — Webpack/Vite; --target nodejs — CommonJS

Виклик з HTML/JS

html
<script type="module">
  import init, { greet } from './pkg/sid_core.js';
  await init();
  document.querySelector('#btn').onclick = () => {
    console.log(greet('С.І.Д.'));
  };
</script>

init() підвантажує і інстанціює .wasm; greet — вже звичайна JS-функція

Маршалінг типів: що дешево, що дорого

Rust-типЩо передається фактичноВартість
i32, u32, f64, boolнапряму, як число WASMнуль — нативний тип
&str, String(вказівник, довжина) + копія UTF-8TextEncoder/TextDecoder
Vec<T> з числамивказівник + довжина → TypedArrayодна копія через ArrayBuffer
struct з #[wasm_bindgen]непрозорий JS-клас із вказівникомгеттери/сеттери від макроса
enum з даними, вкладені типисеріалізація (serde-wasm-bindgen, JSON)найдорожче

Правило гарячого шляху: чим простіший тип на межі, тим дешевший виклик. Порівняти з Л8: там межею була угода ABI, тут — контракт wasm-bindgen

Пам'ять через межу: хто звільняє

Rust structживе в лінійній пам'яті .wasm
#[wasm_bindgen]генерує JS-клас з вказівником
JS тримає дескрипторmemory.buffer видимий як ArrayBuffer
.free()явний виклик → Drop у Rust
забули .free()витік: купа модуля зайнята

У WASM немає збирача сміття: JS-об'єкт зникне, а пам'ять усередині модуля — ні. Жоден JS-профайлер цей витік не покаже

Живе демо: виклик Rust-функції зі сторінки

  1. wasm-pack build --target web у теці з Cargo-проєктом
  2. python3 -m http.server у теці з index.html (WASM вимагає http, не file://)
  3. Відкрити сторінку, натиснути кнопку
  4. DevTools → Console: рядок від greet('С.І.Д.')
  5. DevTools → Network: розмір .wasm поруч із розміром glue .js

Очікувано: Клік по кнопці друкує рядок, згенерований у Rust, скомпільований у WASM, викликаний із JS

Відтворювані збірки (reproducible builds)

🎯
Проблема«на моїй машині інший .wasm»: інші версії rustc/wasm-bindgen — інші байти
📦
Ізольована збіркаDocker-контейнер з фіксованими інструментами — принцип Модуля 1
📌
Фіксовані версіїCargo.lock + FROM rust:1.XX-slim — детермінований набір залежностей
🔏
Доказsha256sum двох незалежних збірок збігається — бінарник відповідає коду

Це замикає лінію курсу: контейнер (Модуль 1) знову стає інструментом гарантії, тепер для збірки WASM

Dockerfile для відтворюваної збірки

dockerfile
FROM rust:1.82-slim
RUN rustup target add wasm32-unknown-unknown
RUN cargo install wasm-pack --version 0.13.1 --locked
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN wasm-pack build --target web --release

Фіксовані версії образу, wasm-pack і Cargo.lock — три складові відтворюваності

Панічна поведінка Rust у WASM

Дрібниця, яку легко забути й довго шукати в проді

Живе демо: перевірка відтворюваності збірки

  1. docker build -t sid-core-build . — зібрати образ з Dockerfile
  2. docker run --rm -v $(pwd)/out1:/build/pkg sid-core-build
  3. docker run --rm -v $(pwd)/out2:/build/pkg sid-core-build — та сама збірка, окремий запуск
  4. sha256sum out1/*.wasm out2/*.wasm — порівняти хеші двох незалежних збірок

Очікувано: Хеші .wasm-файлів з обох запусків збігаються побайтово — доказ відтворюваності

Три межі курсу — підсумок

МежаМодульЩо ізолює / перевіряєХто перевіряє
Процес у контейнеріМ1простори імен, cgroups — те саме ядро Linuxядро (namespaces)
Безпечний доступ до пам'ятіМ2володіння й запозичення Rustкомпілятор (borrow checker)
Переносний набір інструкційМ3від x86-64 до WASM: один алгоритм, дві машинирушій (валідація модуля)

Спільний мотив: ізоляція й перевірка відбуваються якомога раніше — в ядрі, компіляторі або при завантаженні модуля, а не «десь у рантаймі, якщо пощастить»

Три межі — одна картина

ПРОЦЕС межа середовища ПАМʼЯТЬ межа безпеки ІНСТРУКЦІЇ межа набору Docker · namespaces Rust · borrow checker asm → WebAssembly ТРИ МЕЖІ МІЖ ПРОГРАМОЮ І МАШИНОЮ

Підсумковий слайд усього курсу, не лише лекції

Не можна довіряти коду, який ви не створили повністю самі. Жоден обсяг перевірки чи аналізу вихідного тексту не захистить вас від використання недовіреного коду. — Кен Томпсон, «Reflections on Trusting Trust» (Тюрінгівська лекція, 1984)

Підсумок

Джерела

🥚 ПАСХАЛКА
У демо-терміналі хеш зібраного модуля починається з c1d0…: перші три шістнадцяткові цифри — 0xC1D, тобто 3101. Це число супроводжувало приклади від перших лекцій; тут воно замикає коло.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 14 · 5 нових

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

wasm-pack buildнове
wasm-pack build --target web — компілює у wasm32 і кладе pkg/*_bg.wasm та pkg/*.js для підключення на сторінці.
wasm-pack
Збирає крейт Rust у пакет для браузера: .wasm + JS-обгортка (glue code) через wasm-bindgen.
sha256sumнове
Обчислює хеш SHA-256 файлу — «відбиток», за яким видно, чи два файли байт-у-байт однакові.
rustup targetнове
Додає ціль компіляції для іншої платформи (rustup target add wasm32-unknown-unknown — щоб збирати у WebAssembly).
rustup
Встановлювач і менеджер версій Rust: тулчейни, цілі (targets), компоненти.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
wasm-pack testнове
Запускає тести Rust усередині справжнього браузера (--headless --chrome).
wasm-optнове
Оптимізатор Binaryen: wasm-opt -Oz зменшує розмір модуля .wasm.
rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.
wasm-bindgen
Генерує JS-обгортку для модуля .wasm із Rust (його викликає wasm-pack; версія CLI має збігатися з крейтом wasm-bindgen).
python3
Інтерпретатор Python; тут — python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер.
cargo check
Швидко перевіряє код компілятором без створення бінарника — для швидкого циклу «правка → помилки».
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.