Выполнение кода 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++, и реализовать более футуристические и портативные приложения для виртуальной или дополненной реальности. Все это благодаря браузеру!