Курс СРС SRS19
до ЛР10 рівень 2 → «4»↔ Лабораторна 10↔ Лекція 10⌨️ команди 5
Самостійна робота · ~2.5 год

Межа Rust↔JS через wasm-bindgen: маршалінг типів і виклик з JavaScript

🌌 Міст між ядром і панеллю — Ядро С.І.Д. — те саме, що має оселитися в будь-якому браузері й стати невловимим, — написане на Rust, а панель охорони говорить мовою браузера; між ними міст: число перестрибує задарма, а кожне слово-команду доводиться розкладати по байтах на полиці Комірника й збирати заново по той бік. С.І.Д. давно опанував цей перехід і радо показує ранерові кожну його дошку — бо спотворений символ у команді в найгіршу мить коштує всієї операції, а такого він не допустить.

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

Чому «просто викликати функцію» на межі Rust↔JS — нетривіально

WebAssembly-модуль сам по собі вміє обмінюватися з JS-хостом лише примітивними числовими типами — i32, i64, f32, f64 — рівно тими самими типами, що лежать на стеці стекової машини з попередньої теми. Немає вбудованого поняття «рядок», «вектор» чи «об'єкт»: усе, що складніше за число, потрібно самостійно закодувати як послідовність байтів у лінійній пам'яті (linear memory) модуля й передати JS-стороні адресу та довжину цього буфера — а JS-стороні потрібно прочитати ці байти через WebAssembly.Memory/ArrayBuffer і декодувати відповідно до очікуваного формату (наприклад, UTF-8 для рядка). Це і є задача маршалінгу (marshalling) — перетворення значень високорівневого типу на межі виклику в узгоджений низькорівневий формат і назад.

Крейт wasm-bindgen автоматизує саме цю задачу: атрибут #[wasm_bindgen] над функцією fn upper(s: &str) -> String змушує макрос згенерувати код обгортки, що на Rust-стороні виділяє буфер у лінійній пам'яті модуля, кодує вхідний JS-рядок у UTF-8 і записує туди, а на виході — читає повернений буфер назад і декодує в JS-рядок; водночас генерується .d.ts-файл типів і невеликий JS-«клей» (glue code), що робить виклик з боку JavaScript таким же простим, як виклик звичайної функції: upper("hello") — уся складна робота маршалінгу схована всередині згенерованого коду.

Ключова причина, чому передача String/&str дорожча за передачу i32: примітивне число передається безпосередньо як один аргумент функції WASM (жодного доступу до пам'яті, жодного кодування) — рівно так само дешево, як звичайний виклик функції в нативному коді. Рядок же вимагає: (1) виділення пам'яті під буфер потрібного розміру всередині лінійної пам'яті модуля (виклик Rust-аллокатора); (2) копіювання байтів рядка з боку JS у цей буфер через TextEncoder/типізований масив; (3) передачі вказівника й довжини як пари i32-аргументів (сам рядок «під капотом» усе одно зводиться до двох чисел — це і є фактичний контракт функції на рівні WASM); (4) декодування UTF-8 на Rust-стороні; і для повернених рядків — симетричний процес у зворотному напрямку, включно зі звільненням тимчасового буфера. Кожен із цих кроків — реальна робота з копіюванням і виділенням пам'яті, на відміну від передачі числа «в чистому вигляді».

Ця різниця у вартості має пряме практичне значення для проєктування інтерфейсу між Rust-ядром і JS-панеллю: у гарячому шляху (функція, що викликається щокадру чи в циклі) варто віддавати перевагу примітивним числовим сигнатурам, а передачу великих структурованих даних (рядків, масивів, об'єктів) — мінімізувати частотою або переносити на рівень, де така вартість не критична (наприклад, одна команда на клік користувача, а не на кожен кадр анімації). Для складніших структур wasm-bindgen підтримує серіалізацію через serde у JSON (для довільних даних) чи прямий доступ до типізованих масивів Uint8Array/Float64Array для великих однорідних масивів — останнє суттєво дешевше за побайтову маршалізацію через JSON, бо дозволяє передавати сирі байти напряму без текстового кодування.

Варто окремо зауважити межі того, що wasm-bindgen автоматизує «безкоштовно»: складні власні структури Rust з методами експортуються через #[wasm_bindgen] на impl-блоці й на JS-стороні виглядають як звичайні класи, але кожен виклик методу все одно проходить через той самий механізм маршалінгу параметрів і повернених значень — це зручність синтаксису, а не усунення фундаментальної вартості переходу межі Rust↔JS.

📖 ОПРАЦЮВАТИ

Прочитати розділ «Passing high-level types» у книзі Rust and WebAssembly (rustwasm) — зокрема як передаються &str/String; написати мінімальну функцію #[wasm_bindgen] fn upper(s: &str) -> String і викликати її з DevTools console після збірки wasm-pack.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Створити мінімальний проєкт із wasm-bindgen, написати #[wasm_bindgen] fn upper(s: &str) -> String, зібрати через wasm-pack build --target web і викликати результат з DevTools console у браузері, передавши рядок і отримавши результат у верхньому регістрі.
  2. Написати другу функцію #[wasm_bindgen] fn add(a: i32, b: i32) -> i32 у тому самому модулі й порівняти згенерований JS-«клей»-код (файл *.js поруч зі скомпільованим *.wasm) для обох функцій, показавши, що виклик add не містить операцій з пам'яттю модуля, а виклик upper — містить.
  3. Виміряти орієнтовний час виконання 10000 викликів add(1, 2) і 10000 викликів upper("hello") у циклі через console.time/console.timeEnd у браузері й порівняти різницю, пояснивши її теорією маршалінгу.
  4. Написати функцію, що приймає Vec (через #[wasm_bindgen]) і повертає суму елементів, викликати її з JS, передавши Float64Array, і порівняти з альтернативним підходом передачі того самого масиву через JSON.stringify/serde_json (якщо доступно) за обсягом переданих даних.
  5. Спроєктувати (написати сигнатури функцій без реалізації) невеликий інтерфейс «панель керування ↔ ядро С.І.Д.» з трьома функціями різної частоти виклику (раз на клік, раз на кадр анімації, одноразово при завантаженні) і обґрунтувати письмово вибір типів параметрів для кожної з урахуванням вартості маршалінгу.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Які типи значень WebAssembly вміє передавати між модулем і хостом напряму, без жодного додаткового кодування?
  2. Чому передача String між Rust і JS дорожча за передачу i32, і з яких конкретних кроків складається ця додаткова вартість?
  3. Що саме генерує макрос #[wasm_bindgen] для функції з параметром &str — які два артефакти зʼявляються поруч зі скомпільованим .wasm?
  4. Як фактично представлений рядок на рівні контракту функції WASM, якщо сам WASM не має вбудованого типу рядка?
  5. Чому передача Uint8Array/Float64Array через wasm-bindgen зазвичай дешевша за передачу тих самих даних через serde_json та JSON.stringify?
  6. Яку практичну рекомендацію щодо вибору типів параметрів варто застосовувати для функції, яку планують викликати щокадру анімації?
  7. Чи усуває #[wasm_bindgen] на impl-блоці фундаментальну вартість маршалінгу при виклику методу з JS, чи лише спрощує синтаксис виклику?
✅ САМОПЕРЕВІРКА

Чому передача String між Rust і JS дорожча за передачу i32, і що саме відбувається «під капотом» під час виклику функції з параметром &str?

🥚 ПАСХАЛКА
Передай через міст рядок "Пиксель" і подивись, скільки байтів passStringToWasm0 викладе на полицю Комірника: кирилиця в UTF-8 займає по два байти на літеру, тож WASM_VECTOR_LEN буде 14, а не 7. С.І.Д. каже, що ім'я кота — перший рядок, який творці пропустили через цей міст, і що воно й досі «важить удвічі більше, ніж здається».
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 5

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

wasm-pack build
wasm-pack build --target web — компілює у wasm32 і кладе pkg/*_bg.wasm та pkg/*.js для підключення на сторінці.
wasm-pack
Збирає крейт Rust у пакет для браузера: .wasm + JS-обгортка (glue code) через wasm-bindgen.
ls
Показує список файлів у каталозі; ls -la — з правами, власником, розміром і прихованими файлами.
grep
Фільтрує текст: виводить лише рядки, що містять шаблон (ps aux | grep bash).
wasm-bindgen
Генерує JS-обгортку для модуля .wasm із Rust (його викликає wasm-pack; версія CLI має збігатися з крейтом wasm-bindgen).

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

← SRS18усі темиSRS20 →