Исходная статья: https://aralroca.com/blog/first-steps-webassembly-rust
Мы увидим, как запускать нативный код в браузере, создавать более быстрые веб-приложения, иметь возможность повторно использовать старый код, например ретро-видеоигры, и в то же время узнавать будущее веб-разработки.
Что такое веб-сборка?
Во всех современных браузерах есть механизм JavaScript, который интерпретирует и выполняет код. Это позволило нам реализовать очень многофункциональные веб-приложения, потому что JavaScript становится лучше и совершеннее с каждым днем. Тем не менее, это язык высокого уровня, но он все еще не идеален для некоторых задач, потому что он не был разработан как быстрый язык с высокой производительностью.
WebAssembly (WASM) — это новый переносимый формат двоичного кода, который можно запускать в современных браузерах. Он дополнен текстовым форматом (WAT), чтобы сделать его более читаемым/отлаживаемым для нас, кроме того, чтобы мы могли напрямую писать код в своего рода ассемблерном коде. Это открытый стандарт W3C, который все еще находится в разработке и позволяет нам писать быстрый и эффективный код для Интернета на языках, отличных от JavaScript, и работает с производительностью, аналогичной родному языку. Он здесь не для замены JavaScript, а для его дополнения.
Еще одна цель WebAssembly — обеспечить безопасность, легкость и скорость работы в Интернете, сохраняя небольшой .wasm
размер файла и всегда сохраняя назад- совместимость с новыми функциями WASM, чтобы сеть не сломалась.
Для WebAssembly существует более 40 поддерживаемых языков, наиболее распространенными из которых являются C, C++ и Rust из-за их производительности и зрелости, хотя вы также можете писать код для WASM на языках высокого уровня, таких как Python, PHP или даже JavaScript!
Некоторые практические применения WebAssembly:
- Шифрование
- Игры, которые требуют много ресурсов
- Редактирование изображений и видео
- P2P
- Высокопроизводительные алгоритмы
- VR, AR
- Визуализации и симуляции
- Большой и т.д…
Почему в Русте?
Возможно, вам интересно, почему мы выбрали Rust, когда у нас есть так много языков, доступных с WebAssembly. Тому есть несколько причин:
- Производительность: Rust свободен от недетерминированной сборки мусора и дает программистам контроль над косвенностью, мономорфизацией и расположением памяти.
- Небольшой размер
.wasm
: в Rust отсутствует среда выполнения, что позволяет использовать небольшой размер.wasm
, потому что в него не включено дополнительное раздувание, такое как сборщик мусора. Следовательно, вы платите только за размер кода для тех функций, которые вы используете. - Интеграция: Rust и Webassembly интегрируются с существующими инструментами JavaScript (npm, Webpack…).
Выполнение кода Rust из JavaScript
Предполагая, что у вас есть и 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 <[email protected]>"] 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++, и реализовать более футуристические и портативные приложения для виртуальной или дополненной реальности. Все это благодаря браузеру!