Компиляция Rust в WebAssembly

Если уже вы написали некоторый код на Rust, вы можете скомпилировать его в WebAssembly! Из этого руководства вы узнаете всё, что вам нужно знать, чтобы скомпилировать проект на Rust в wasm и использовать его в существующем веб-приложении.

Примеры использования Rust и WebAssembly

Существует два основных варианта использования Rust и WebAssembly:

  • Чтобы создать целое приложение — целое веб-приложение, основанное на Rust!
  • Чтобы построить часть приложения — используйте Rust в существующем интерфейсе JavaScript.

На данный момент команда Rust фокусируется на последнем примере, его мы рассмотрим здесь. Для первого примера, посмотрите проекты, такие как yew.

В этом руководстве вы создадите npm-пакет, используя wasm-pack, инструмент построения npm-пакетов в Rust. Этот пакет будет содержать только код WebAssembly и JavaScript, так что его пользователям не нужен будет установщик Rust. Они могут даже не заметить, что он был написан на WebAssembly!

Настройка окружения Rust

Давайте пройдёмся по всем пунктам, необходимым для настройки нашего окружения.

Установка Rust

Чтобы установить Rust, посетите Install Rust страницу и проследуйте всем инструкциям. Так вы установите тулзу, называемую "rustup", которая позволит вам управлять несколькими версиями Rust. По умолчанию, она устанавливает последний стабильный релиз Rust, который вы будете использовать для стандартной разработки на Rust. Rustup устанавливает rustc, компилятор Rust, вместе с cargo, Rust-овским пакетным менеджером, rust-std, стандартной библиотекой Rust, и несколькими вспомогательными доками — rust-docs.

Примечание: Обратите внимание на пост-установочную заметку о необходимости добавить cargo bin директорию в список PATH. Она должна быть добавлена автоматически, но вам нужно будет перезапустить терминал, чтобы изменения вступили в силу.

wasm-pack

Чтобы собрать наш пакет, вам понадобится дополнительный инструмент, wasm-pack. Он поможет нам скомпилировать наш код в WebAssembly и создаст правильный контейнер для нашего пакета для npm. Чтобы скачать и установить, введите в терминале следующую команду:

bash
cargo install wasm-pack

Установка Node.js и получение npm-аккаунта

В этом руководстве мы будем собирать npm-пакет, поэтому вам понадобится установить Node.js и npm. Дополнительно, мы опубликуем наш пакет на npm, так что вам так же понадобится ваш npm-аккаунт. Они бесплатны! Технически, вы не обязаны ничего публиковать, но так будет проще, так что будем считать, что вы сделаете это в этом руководстве.

Чтобы получить Node.js и npm, посетите Get npm! страницу и проследуйте инструкциям. Когда настанет время выбрать версию, выберите любую, которая вам нравится; это руководство не зависит от версии.

Чтобы создать npm-аккаунт, посетите npm signup станицу и заполните форму.

Дальше запустите в командой строке npm adduser:

bash
> npm adduser
Username: yournpmusername
Password:
Email: (this IS public) you@example.com

Вам понадобится ввести своё пользовательское имя, пароль и email. Если все получится, вы увидите:

bash
Logged in as yournpmusername on https://registry.npmjs.org/.

Если что-то пойдёт не так, свяжитесь с командой npm, чтобы разобраться.

Создание WebAssembly npm-пакета

Хватит установок, давайте создадим новый пакет на Rust. Перейдите в любое место, где вы держите свои личные проекты, и сделайте следующее:

bash
$ cargo new --lib hello-wasm
     Создаст проектную библиотеку `hello-wasm`

Это создаст новую библиотеку в под-директории, называемой hello-wasm, со всем, что вам нужно:

+-- Cargo.toml
+-- src
    +-- lib.rs

Для начала, у нас есть Cargo.toml; с его помощью мы можем сконфигурировать наш билд. Если вы пользуетесь Gemfile из Bundler или package.json из npm, то вы почувствуете себя, как дома; Cargo работает аналогично обоим.

Дальше, Cargo сгенерировал кое-какой код для нас на Rust в src/lib.rs:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

Мы не будем использовать этот тестовый код вообще, так что можете просто удалить его.

Давайте попишем немного на Rust!

Вместо этого поместите этот код в src/lib.rs:

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

Это содержимое нашего проекта на Rust. У него есть три основные части, давайте пройдёмся по ним по очереди. Мы дадим здесь обобщённое пояснение и поясним некоторые детали; чтобы узнать больше о Rust, пожалуйста, просмотрите бесплатную online-книгу The Rust Programming Language.

Использование wasm-bindgen для коммуникации между Rust и JavaScript

Первая часть выглядит вот так:

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

Первая строка гласит: "эй, Rust, мы используем библиотеку, называемую wasm_bindgen." Библиотеки в Rust называются "crates" (контейнеры), а так как мы используем внешнюю, то "extern".

Поняли? Cargo поставляет контейнеры.

Третья строка содержит команду use, которая импортирует код из библиотеки в наш код. В нашем случае, мы импортируем все из модуля wasm_bindgen::prelude. Мы будем использовать его функции в следующей секции.

Прежде чем перейти к следующей секции, давайте поговорим немного о wasm-bindgen.

wasm-pack использует wasm-bindgen, другую тулзу, чтобы предоставить соединение между типами в JavaScript и Rust. Это позволяет JavaScript вызывать Rust-API со строками или функциям Rust перехватывать исключения JavaScript.

Мы будем использовать функциональность wasm-bindgen в нашем пакете. По факту, это следующая секция!

Вызов внешних функций JavaScript из Rust

Следующая часть выглядит так:

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

Частичка внутри #[] называется "атрибутом", и она кое-как модифицирует следующее за ней утверждение. В нашем случае, это утверждение extern, которое говорит Rust-у, что мы хотим вызвать некоторую функцию, определённую во внешнем пространстве. Атрибут говорит: "wasm-bindgen знает, как найти эти функции".

Третья строка это имя функции, написанной на Rust. Она говорит: "функция alert принимает один аргумент, строку с именем s."

У вас, возможно, есть предположение, что это за функция, и, возможно, ваше предположение верное: это функция alert, предоставляемая JavaScript! Мы будем вызывать эту функцию в следующей секции.

Когда бы вы не захотели вызвать новую функцию JavaScript, вы можете написать её здесь, и wasm-bindgen позаботится о том, чтобы настроить все для вас. Пока ещё поддерживается не все, но мы работаем над этим! Пожалуйста, сообщайте о проблемах, если что-то было упущено.

Создание функций Rust, который может вызывать JavaScript

Финальная часть следующая:

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

Ещё раз, мы видим #[wasm_bindgen] атрибут. В этом случае, он модифицирует не блок extern, а fn; это значит, что мы хотим, чтобы эта функция на Rust была доступна для JavaScript. Прямо противоположно extern: это не те функции, которые нам нужны, а те, что мы предоставляем миру!

Наша функция называется greet, и она принимает один аргумент, строку (пишется &str), name. Затем она вызывает функцию alert, которую мы запросили в блоке extern выше. Она передаёт вызов макросу format!, который позволяет нам соединить строки.

format! принимает два аргумента в нашем случае: форматируемую строку и переменную, которую должен в неё поместить. Форматируемая строка это "Hello, {}!" часть. Она содержит {}, куда будет вставлена переменная. Переменная, которую мы передаём, это name, аргумент функции, так что если мы вызовем greet("Steve"), то увидим "Hello, Steve!".

Все это передаётся в alert(), так что когда мы вызовем функцию, мы увидим алерт с "Hello, Steve!" внутри него!

Теперь, когда наша библиотека написана, давайте соберём её.

Компиляция кода в WebAssembly

Чтобы правильно скомпилить наш код, сначала нам надо сконфигурировать его с помощью Cargo.toml. Откройте этот файл и измените его так, чтобы он выглядел следующим образом:

toml
[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/hello-wasm"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Вам нужно будет ввести свой личный репозиторий, а Cargo заполнит authors, основываясь на информации git.

Главная часть находится внизу. Первая — [lib] — говорит Rust собрать cdylib версию нашего пакета; мы не будем вдаваться в то, что это значит в этом руководстве. Чтобы узнать больше, просмотрите Cargo и Rust Linkage документацию.

Вторая часть это секция [dependencies] . Тут мы говорим Cargo, от какой версии wasm-bindgen мы хотим зависеть; в нашем случае, это любая версия 0.2.z (но не 0.3.0 или выше).

Сборка пакета

Теперь, когда мы все установили, давайте соберём проект! Введите это в терминале:

bash
wasm-pack build --scope mynpmusername

Здесь мы сделали несколько вещей (и они займут много времени, особенно если вы запустили wasm-pack впервые). Чтобы изучить их детальней, прочитайте этот блог-пост на Mozilla Hacks. Вкратце, wasm-pack build:

  1. Компилирует ваш Rust-код в WebAssembly.
  2. Запускает wasm-bindgen с этим WebAssembly, генерируя JavaScript файл, который оборачивает WebAssembly файл в модуль. который может понять npm.
  3. Создаёт папку pkg, куда перемещает этот JavaScript файл и ваш код WebAssembly.
  4. Читает ваш Cargo.toml и создаёт эквивалентный package.json.
  5. Копирует ваш README.md (если есть) в пакет.

Конечный результат? У вас есть npm-пакет внутри папки pkg.

Отступление о размере кода

Если вы посмотрите на размер кода, сгенерированного для WebAssembly, это может быть около сотни килобайт. Мы вообще не инструктировали Rust оптимизировать размер, и он сильно его снизил. Это не является частью этого руководства, но если вам интересно, прочитайте документацию Rust WebAssembly Working Group на Shrinking .wasm Size.

Публикация нашего пакета на npm

Давайте опубликуем наш новый пакет на npm:

bash
cd pkg
npm publish --access=public

Теперь у нас есть npm-пакет, написанный на Rust, но скомпилированный в WebAssembly. Он готов к использованию из JavaScript, и его пользователь не нуждается в установке Rust; код внутри пакета написан на WebAssembly, не на Rust!

Использование пакета в web

Давайте создадим сайт, который будет использовать наш пакет! Многие пользуются пакетами npm с помощью разных сборщиков, и мы будем использовать один из них, webpack, в этом руководстве. Он только немного более усложнённый, но описывает более реалистичный вариант использования.

Давайте выйдем из нашей папки pkg и создадим новую, site, чтобы попробовать в ней следующее:

bash
cd ../..
mkdir site
cd site

Создайте новый файл, package.json, и поместите в него следующий код:

json
{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "@mynpmusername/hello-wasm": "^0.1.0"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

Заметьте, что вам нужно ввести своё пользовательское имя после @ в секции зависимостей.

Дальше нам нужно сконфигурировать Webpack. Создайте webpack.config.js и введите следующее:

js
const path = require("path");
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development",
};

Теперь нам нужен HTML-файл; создайте index.html и поместите в него:

html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

Наконец, создайте index.js, на который мы сослались в HTML-файле, и вставьте:

js
const js = import("./node_modules/@yournpmusername/hello-wasm/hello_wasm.js");
js.then((js) => {
  js.greet("WebAssembly");
});

Заметьте, что вам нужно будет снова ввести ваше имя для npm.

Так мы импортируем наш модуль из папки node_modules. Это не считается лучшей практикой, но это пример, так что пока сойдёт. Как только файл загрузится, он вызовет функцию greet из этого модуля, передав "WebAssembly", как строку. Обратите внимание, что здесь нет ничего особенного, и всё же мы вызываем код на Rust! Насколько JavaScript-код может судить, это просто обычный модуль.

Мы закончили! Давайте попробуем:

bash
npm install
npm run serve

Так мы запустим сервер. Откройте http://localhost:8080 и вы увидите алерт с надписью Hello, WebAssembly! в нем! Мы успешно обратились из JavaScript в Rust и из Rust в JavaScript.

Заключение

На этом руководство заканчивается, мы надеемся, что вы сочли его для себя полезным.

В этом направлении кипит бурная и при этом очень интересная деятельность, так что если вы бы хотели помочь что-то улучшить, то загляните в the Rust Webassembly Working Group.