Выполнение кода Rust из JavaScript - Wasm
Вводные
Предполагая, что у вас есть и NPM (для JS), и Cargo (для Rust), еще одним предварительным условием, которое нам нужно для его установки, является wasm-pack:
> cargo install wasm-pack
Код ржавчины
Давайте создадим новый проект Rust для «Hello world»:
> cargo new helloworld --lib
Cargo.toml мы собираемся добавить следующее:
[package] name = "helloworld" version = "0.1.0" authors = ["Aral Roca Gomez <contact@aralroca.com>"] edition = "2018" ## new things... [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2.67" [package.metadata.wasm-pack.profile.release] wasm-opt = ["-Oz", "--enable-mutable-globals"]
cdylib библиотека для wasm финальных артефактов. зависимость wasm-bindgen для облегчения высокоуровневого взаимодействия между модулями Wasm и JavaScript.
- Примечание. Последняя часть о --enable-mutable-globals в принципе не нужна в следующих выпусках wasm-bindgen, но для этого руководства она необходима. Иначе мы не можем работать со строками.
WebAssembly поддерживает только типы i32, u32, i64 и u64. Если вы хотите работать с другими типами, такими как String или Objects, вы обычно должны сначала закодировать их. Однако wasm-bindgen делает эти привязки за нас. Больше не нужно об этом беспокоиться. Тем не менее, давайте создадим нашу функцию helloworld для возврата строки в src/lib.rs:
use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn helloworld() -> String { String::from("Hello world from Rust!") }
- Сборник
Давайте скомпилируем код Rust с помощью:
> wasm-pack build --target web
Мы используем веб-цель, однако есть разные цели, которые мы можем использовать в зависимости от того, как мы хотим использовать этот файл wasm:
— целевой сборщик — для таких сборщиков, как Webpack, Parcel или Rollup. — целевой веб-сайт — для веб-сайта в виде модуля ECMAScript. — таргетинг без модулей — для Интернета без модуля ECMAScript. — целевые узлы nodejs — для Node.js
После выполнения вышеуказанной команды будет создан каталог pkg с нашей библиотекой JavaScript, содержащей код, который мы создали в Rust! Он даже генерирует файлы типов TypeScript.
> ls -l pkg total 72 -rw-r--r-- 1 aralroca staff 929 Aug 15 13:38 helloworld.d.ts -rw-r--r-- 1 aralroca staff 3210 Aug 15 13:38 helloworld.js -rw-r--r-- 1 aralroca staff 313 Aug 15 13:38 helloworld.wasm -rw-r--r-- 1 aralroca staff 268 Aug 15 13:38 helloworld_bg.d.ts -rw-r--r-- 1 aralroca staff 15160 Aug 15 13:38 helloworld_bg.wasm -rw-r--r-- 1 aralroca staff 289 Aug 15 13:38 package.json
Теперь он готов как пакет JavaScript, поэтому мы можем использовать его в нашем проекте или даже загрузить пакет в NPM, как мы увидим позже.
Файл .js содержит необходимый «склеивающий» код, чтобы не беспокоиться о работе за пределами pkg с буферами, декодерами текста и т. д.
Используйте скомпилированный код в нашем JS-проекте Чтобы использовать файл wasm в нашем JavaScript, мы можем импортировать сгенерированный модуль pkg в наш проект. Чтобы проверить это, мы можем создать index.html в корне проекта Rust следующим образом:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>"Hello world" in Rust + Webassembly</title> <script type="module"> import init, { helloworld } from './pkg/helloworld.js' async function run() { await init() document.body.textContent = helloworld() } run() </script> </head> <body></body> </html>
Как видите, перед использованием функции helloworld важно вызвать асинхронную функцию init, чтобы загрузить файл wasm. Тогда мы сможем более легко использовать общедоступные функции Rust!
Чтобы проверить это, вы можете сделать:
npx serve и открыть http://localhost:5000.
Выполнение кода JavaScript из Rust
В Rust можно использовать код JavaScript, например, использовать переменные window, писать в DOM или вызывать внутренние функции, такие как console.log. Все, что нам нужно сделать, это объявить привязки JavaScript, которые мы хотим использовать внутри extern C.
В качестве примера мы будем использовать функцию console.log внутри Rust:
use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } #[wasm_bindgen] pub fn example() { log("Log from rust"); }
Как мы видим, внутри extern "C" мы должны указать js_namespace (консоль), объявляющую функцию, которую мы будем использовать внутри пространства имен (журнал). В этом случае мы поместили только одну строку в качестве параметра, но если бы мы хотели выполнить console.log с несколькими параметрами, их нужно было бы объявить.
- И в нашем JS:
import init, { example } from './pkg/helloworld.js' async function run() { await init() example() // This will log "Log from rust" to the console } run()
- Производительность — JavaScript против Rust
Давайте сравним немного более дорогую функцию, такую как функция fibonacci, чтобы увидеть, как она работает как в Rust, так и в JavaScript:
use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn fibonacci(n: u32) -> u32 { match n { 0 | 1 => n, _ => fibonacci(n - 1) + fibonacci(n - 2), } }
Используя функцию console.time, мы можем измерить производительность каждого из них:
import init, { fibonacci } from './pkg/helloworld.js' function fibonacciInJs(n) { if (n <= 1) return n return fibonacciInJs(n - 1) + fibonacciInJs(n - 2) } async function run() { await init() const num = 20 console.time('Fibonnaci in rust') const fibRust = fibonacci(num) console.timeEnd('Fibonnaci in rust') console.time('Fibonnaci in JS') const fibJS = fibonacciInJs(num) console.timeEnd('Fibonnaci in JS') document.body.textContent = `Fib ${num}: Rust ${fibRust} - JS ${fibJS}` } run()
В ржавчине: 0,13 мс In JS: 1.28ms Примерно в 10 раз быстрее в Rust, чем в JS!
Однако важно отметить, что не все функции, которые мы реализуем в Rust, будут быстрее, чем в JavaScript. Но многие из них, которые требуют рекурсии или циклов, будут значительно улучшены.
Отладка
Если в devtools -> source мы заглянем внутрь наших файлов в поисках нашего файла .wasm, мы увидим, что вместо двоичного файла он показывает нам файл WAT, более читабельный и поддающийся отладке.
Для удобства отладки вы можете использовать флаг --debug для отображения имен функций, которые вы использовали в Rust.
> wasm-pack build --target web --debug
На данный момент с wasm-bindgen невозможно использовать исходные карты для отображения кода в Rust на devtools. Но я полагаю, что в будущем это будет доступно.
Публикация в NPM
Как только мы сгенерируем наш каталог pkg, мы можем упаковать его с помощью:
> wasm-pack pack myproject/pkg
И опубликуйте его в npm с помощью:
> wasm-pack publish
Они работают так же, как npm pack и npm publish, поэтому мы можем использовать те же флаги, что и
wasm-pack publish --tag next.
Код из статьи
Я загрузил код, использованный в этой статье, на свой GitHub.
Выводы
В этой статье мы немного рассмотрели, что такое WebAssembly и что необходимо для начала создания веб-приложений на Rust.
Мы использовали Rust, потому что это один из лучших интегрированных языков, но можно использовать и многие другие языки. Таким образом, мы можем вернуть к жизни старые приложения, созданные на таких языках, как C или C++, и реализовать более футуристические и портативные приложения для виртуальной или дополненной реальности. Все это благодаря браузеру!
Использованная литература
- rust-lang
- wasm-pack
- rustwasm
- webassembly-how-and-why
- Изучаем WebAssembly с помощью Rust
- Используем клиентский процессор по максимуму. Часть 1: Rust + WebAssembly