Замикання лінії курсу
Після презентації теми — самостійна робота в аудиторії над двома темами. Вони ж готують до захисту та розширення ЛР10. Матеріали публікуються в Google Classroom разом із лекцією.
.wat до компільованого кодуУ Лекції 9 ми писали .wat вручну — корисно для розуміння моделі, але непридатно для реальної програми в кілька тисяч рядків. Лекція 10 замикає лінію курсу: замість людини, що вручну розставляє local.get/i32.add, цю роботу виконує компілятор — той самий rustc, яким ми користувалися для нативного x86-64 у Модулях 1–2, тепер спрямований на іншу ціль компіляції. Rust тут — не випадковий вибір: система володіння й запозичень (Лекції 5–7) дає гарантії безпеки пам'яті на етапі компіляції, а це саме те, що потрібно для коду, який піде виконуватися в чужому, потенційно ворожому браузерному host-середовищі (host — рушій, що запускає модуль; сам модуль для нього — guest) без окремого рантайму збирача сміття.
wasm32-unknown-unknownRust визначає ціль компіляції як триплет (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-типів;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.
<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 усередині лінійної пам'яті.
Проблема, добре знайома з Модуля 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 прибирають дрейф інструментарію;Верифікація — тривіальна: sha256sum pkg/*.wasm на двох незалежних машинах має дати однаковий хеш.
Весь курс — це послідовне занурення на три різні межі ізоляції та перевірки:
| Межа | Модуль | Що ізолює/перевіряє |
|---|---|---|
| Процес усередині контейнера | Модуль 1 | простори імен, cgroups — те саме ядро Linux, окрема view на систему |
| Безпечний доступ до пам'яті | Модуль 2 | володіння й запозичення Rust — компілятор, а не рантайм, ловить помилки пам'яті |
| Переносний набір інструкцій | Модуль 3 | від x86-64 до WASM — той самий алгоритм, дві різні машини, валідація типів при завантаженні |
Спільний мотив усіх трьох: ізоляція й перевірка відбуваються якомога раніше — на етапі ядра (namespaces), компілятора (borrow checker) чи завантаження модуля (WASM validation), а не «десь у рантаймі, якщо пощастить».
wasm-bindgen — версія крейта в Cargo.toml і версія CLI, встановленого глобально, повинні збігатися, інакше збірка падає з незрозумілою помилкою генерації.#[wasm_bindgen] переданий напряму в експортовану сигнатуру — помилка компіляції про те, що тип не реалізує потрібний трейт маршалінгу..free() для JS-обгорнутих Rust-структур — тихий витік пам'яті лінійної області, який не покаже жоден JS-профайлер (адже сам JS-об'єкт малий, «важка» пам'ять прихована в .wasm-хіпі).file:// замість HTTP-сервера — браузери відмовляються завантажувати .wasm через fetch з локального файлу через політику CORS; потрібен хоча б python3 -m http.server.console_error_panic_hook, підключений на старті, перенаправляє паніку в console.error браузера.wasm-pack чи rustc «в фоні» на CI-машині рано чи пізно дасть інший бінарник з того самого коду.Компільований у 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 розв'язує ту саму задачу компіляції, що й gcc -S у Л8, лише для іншої машини
Той самий байт-код на виході; змінюється лише те, хто його пише — людина чи компілятор
Той самий rustc, що збирав x86-64 у Модулях 1–2, — інша ціль компіляції
| Частина | Значення | Що означає |
|---|---|---|
| архітектура | wasm32 | 32-бітна адресація лінійної пам'яті |
| виробник | unknown | для віртуальної машини не має сенсу |
| ОС | unknown | немає ОС: жодного libc, жодних системних викликів |
| сусідня ціль | wasm32-wasi (wasip1/wasip2) | POSIX-подібні виклики через WASI — поза браузером |
rustup target add wasm32-unknown-unknown — одноразово. Курс тримається unknown-unknown, бо мета — сторінка в браузері, а не окремий бінарник
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Вітаю, {name}! Ядро на зв'язку.")
}Атрибут #[wasm_bindgen] позначає межу з JavaScript
Це і є маршалінг (marshalling) — перетворення значень при перетині межі між WASM-модулем (guest) і JS-хостом (host). i32/f64 проходять напряму — вони й так типи WASM
i32/f64 — типи самої машини WASM; JS-рядок у модуль не входить — входить лише (ptr, len)
wasm-pack — те саме, що cargo build, тільки для цілі wasm32 з додатковою генерацією обгортки
| Файл | Що це | Хто використовує |
|---|---|---|
sid_core_bg.wasm | сам бінарник модуля | браузер через init() |
sid_core.js | ES-модуль: init(), greet(), класи | <script type="module"> |
sid_core.d.ts | типи експортів | TypeScript, редактор |
package.json | метадані пакета | npm, бандлери |
--target web — ES-модуль без бандлера; --target bundler — Webpack/Vite; --target nodejs — CommonJS
<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-8 | TextEncoder/TextDecoder |
Vec<T> з числами | вказівник + довжина → TypedArray | одна копія через ArrayBuffer |
struct з #[wasm_bindgen] | непрозорий JS-клас із вказівником | геттери/сеттери від макроса |
| enum з даними, вкладені типи | серіалізація (serde-wasm-bindgen, JSON) | найдорожче |
Правило гарячого шляху: чим простіший тип на межі, тим дешевший виклик. Порівняти з Л8: там межею була угода ABI, тут — контракт wasm-bindgen
У WASM немає збирача сміття: JS-об'єкт зникне, а пам'ять усередині модуля — ні. Жоден JS-профайлер цей витік не покаже
Очікувано: Клік по кнопці друкує рядок, згенерований у Rust, скомпільований у WASM, викликаний із JS
Це замикає лінію курсу: контейнер (Модуль 1) знову стає інструментом гарантії, тепер для збірки WASM
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 — три складові відтворюваності
Дрібниця, яку легко забути й довго шукати в проді
Очікувано: Хеші .wasm-файлів з обох запусків збігаються побайтово — доказ відтворюваності
| Межа | Модуль | Що ізолює / перевіряє | Хто перевіряє |
|---|---|---|---|
| Процес у контейнері | М1 | простори імен, cgroups — те саме ядро Linux | ядро (namespaces) |
| Безпечний доступ до пам'яті | М2 | володіння й запозичення Rust | компілятор (borrow checker) |
| Переносний набір інструкцій | М3 | від x86-64 до WASM: один алгоритм, дві машини | рушій (валідація модуля) |
Спільний мотив: ізоляція й перевірка відбуваються якомога раніше — в ядрі, компіляторі або при завантаженні модуля, а не «десь у рантаймі, якщо пощастить»
Підсумковий слайд усього курсу, не лише лекції
Не можна довіряти коду, який ви не створили повністю самі. Жоден обсяг перевірки чи аналізу вихідного тексту не захистить вас від використання недовіреного коду. — Кен Томпсон, «Reflections on Trusting Trust» (Тюрінгівська лекція, 1984)
Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
wasm-pack buildновеwasm-pack build --target web — компілює у wasm32 і кладе pkg/*_bg.wasm та pkg/*.js для підключення на сторінці.wasm-pack.wasm + JS-обгортка (glue code) через wasm-bindgen.sha256sumновеrustup targetновеrustup target add wasm32-unknown-unknown — щоб збирати у WebAssembly).rustupcargo buildtarget/debug/; --release — з оптимізаціями у target/release/.cargowasm-pack testнове--headless --chrome).wasm-optновеwasm-opt -Oz зменшує розмір модуля .wasm.rustcrustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.wasm-bindgen.wasm із Rust (його викликає wasm-pack; версія CLI має збігатися з крейтом wasm-bindgen).python3python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер.cargo checkcargo 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.