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

использованная литература