Выполнение кода Rust из JavaScript - Wasm

Материал из support.qbpro.ru

Вводные

Предполагая, что у вас есть и 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.

Wasm1.png

Выполнение кода 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()

И результат:
Wasm2.png

В ржавчине: 0,13 мс In JS: 1.28ms Примерно в 10 раз быстрее в Rust, чем в JS!

Однако важно отметить, что не все функции, которые мы реализуем в Rust, будут быстрее, чем в JavaScript. Но многие из них, которые требуют рекурсии или циклов, будут значительно улучшены.

Отладка

Если в devtools -> source мы заглянем внутрь наших файлов в поисках нашего файла .wasm, мы увидим, что вместо двоичного файла он показывает нам файл WAT, более читабельный и поддающийся отладке.
Wasm3.png Для удобства отладки вы можете использовать флаг --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++, и реализовать более футуристические и портативные приложения для виртуальной или дополненной реальности. Все это благодаря браузеру!

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