Пример использования WebAssembly-модуля, скомпилированного из Rust, в React-приложении

от автора

Привет, друзья!

На днях прочитал интересную статью, в которой демонстрируется возможность использования WebAssembly-модулей (далее — Wasm), скомпилированных из Rust, в React-приложении.

Так вот, статья интересная, но автор толком ничего не объясняет, видимо, исходя из предположения, что читатели, как и он, владеют обоими языками программирования (JavaScript и Rust).

Поскольку я не отношусь к этой категории (пока не знаю Rust), но люблю как следует разбираться в интересующих меня вещах, представляю вашему вниманию собственную версию.

Исходный код проекта.

Если вам это интересно, прошу под кат.

Если вы впервые слышите о Wasm, вот статья, в которой освещаются некоторые связанные с ним общие вопросы.

Предполагается, что вы знакомы с React.js, имеете общее представление о Node.js и хотя бы раз настраивали какой-нибудь сборщик модулей типа Webpack (я буду использовать Snowpack).

Разумеется, на вашей машине должен быть установлен Node.js и Rust.

На Mac это делается так:

# устанавливаем Node.js brew install node@16 # lts версия # устанавливаем Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Подготовка проекта

Создаем шаблон React-проекта с помощью Snowpack:

# react-rust - название проекта # --template @snowpack/app-template-react - название используемого шаблона # --use-yarn - использовать yarn вместо npm для установки зависимостей, опционально yarn create snowpack-app react-rust --template @snowpack/app-template-react --use-yarn # или npx create-snowpack-app ...

Переходим в созданную директорию и инициализируем Rust-проект:

# переходим в директорию cd react-rust # инициализируем проект cargo init --lib

Cargo — это пакетный менеджер (package manager) Rust (аналог npm, входит в состав Rust). Он устанавливает зависимости, компилирует пакеты, создает распространяемые пакеты и загружает их в crates.io (реестр пакетов Rust, аналог npmjs.com).

Команда cargo init создает новый пакет в существующей директории. Флаг --lib создает пакет с целевой библиотекой (src/lib.rs, файлы с кодом на Rust, как правило, имеют расширение rs). Целевая библиотека — это «библиотека», которая может быть использована другими библиотеками и исполняемыми файлами (executables). Один пакет может иметь только одну библиотеку.

cargo не умеет компилировать Rust в Wasm. Для этого нам потребуется пакет wasm-bindgen (данный пакет входит в состав wasm-pack).

Он, в частности, позволяет импортировать «вещи» из JavaScript в Rust и экспортировать «вещи» из Rust в JavaScript (цитата из документации).

Также нам необходимо сообщить компилятору, что типом пакета является cdylib. Указание cdylib приводит к генерации динамической системной библиотеки (dynamic system library). Этот тип используется «при компиляции динамической библиотеки, загружаемой из другого языка программирования».

Редактируем Cargo.toml (аналог package.json, создается при инициализации Rust-проекта):

[package] name = "react-rust" version = "1.0.0" edition = "2021"  [lib] crate-type = ["cdylib"]  [dependencies] wasm-bindgen = "0.2"

Выполняем сборку Rust-приложения:

cargo build # данная команда компилирует пакеты и все их зависимости

Это приводит к генерации директории target. В ней пока нет ничего интересного, но скоро мы это исправим.

Для того, чтобы сборка содержала Wasm-файл, необходимо явно определить цель сборки:

rustup target add wasm32-unknown-unknown

rustup — это установщик набора инструментов (toolchain installer) Rust. target add позволяет определить цель компиляции.

Что означает wasm32-unknown-unknown? Первый unknown означает систему, в которой выполняется компиляция, второй — систему, для которой выполняется компиляция. wasm32 означает, что адресное пространство имеет размер 32 бита (источник).

Редактируем src/lib.rs (возьмем пример из документации wasm-bindgen):

// импорт пакета // https://doc.rust-lang.org/beta/reference/names/preludes.html // https://stackoverflow.com/questions/36384840/what-is-the-prelude use wasm_bindgen::prelude::*;  // импорт функции `window.alert` из "Веба" #[wasm_bindgen] extern "C" {  fn alert(s: &str); }  // экспорт функции `greet` в JavaScript #[wasm_bindgen] pub fn greet(name: &str) {  alert(&format!("Hello, {}!", name)); }

Выполняем сборку Rust-приложения, указывая нужную цель:

cargo build --target wasm32-unknown-unknown

Это приводит к генерации интересующего нас файла target/wasm32-unknown-unknown/debug/react_rust.wasm. debug означает, что мы выполнили сборку для разработки. Для создания продакш-сборки используется команда cargo build --release (выполнение этой команды приводит к генерации директории target/wasm32-unknown-unknown/release).

Устанавливаем плагин @emily-curry/snowpack-plugin-wasm-pack. Данный плагин генерирует обертку для Wasm, состоящую из набора JS и TS-файлов, в частности, index.js, экспортирующего функцию greet, которую мы будем использовать в React-приложении.

Редактируем snowpack.config.mjs:

export default {  mount: {    public: { url: '/', static: true },    src: { url: '/dist' },    // это позволяет импортировать файлы из директории pkg,    // находящейся за пределами директории src    pkg: { url: '/pkg' }  },  plugins: [    '@snowpack/plugin-react-refresh',    '@snowpack/plugin-dotenv',    // плагин для создания обертки    [      '@emily-curry/snowpack-plugin-wasm-pack',      {        // директория проекта, содержащая файл Cargo.toml        projectPath: '.'      }    ]  ],  // ...

Для работы плагина требуется cargo-watch и wasm-pack. wasm-pack устанавливается как зависимость wasm-bindgen.

cargo-watch выполняет соответствующие команды cargo при изменении файлов проекта (аналог nodemon). Устанавливаем его:

cargo install cargo-watch

Теперь займемся React-приложением.

Редактируем src/App.jsx:

import React, { useState } from 'react' // импортируем функцию инициализации и // нашу функцию `greet` import init, { greet } from '../pkg'  function App() {  // состояние для имени  const [name, setName] = useState('')   // функция изменения имени  const changeName = ({ target: { value } }) => setName(value)  // функция приветствия  const sayHello = async (e) => {    e.preventDefault()    const trimmed = name.trim()    if (!trimmed) return    // выполняем инициализацию    await init()    // вызываем нашу функцию    greet(name)  }   return (    <div className='app'>      <h1>React Rust</h1>      <form onSubmit={sayHello}>        <fieldset>          <label htmlFor='name'>Enter your name</label>          <input            type='text'            id='name'            value={name}            onChange={changeName}            autoFocus          />        </fieldset>        <button>Say hello</button>      </form>    </div>  ) }  export default App

Запускаем проект в режиме разработки:

yarn start # or npm start

Обратите внимание: здесь может возникнуть ошибка 404 Not Found, связанная с тем, что сервер для разработки запускается до генерации директории pkg, в которую помещаются файлы, скомпилированные с помощью плагина @emily-curry/snowpack-plugin-wasm-pack (из этой директории импортируется функция greet). В этом случае просто перезапустите сервер и все будет ок ?

Вводим имя и нажимаем кнопку Say hello:

Функция greet, написанная на Rust и скомпилированная в Wasm, работает в JS. Круто!

Выполняем сборку для продакшна:

yarn build # or npm run build

Это приводит к генерации директории build со всеми файлами проекта (настройка сборки для продакшна выполняется с помощью раздела buildOptions файла snowpack.config.js).

Поскольку типом скрипта, подключаемого в index.html, является module, запустить проект с помощью расширения VSCode типа Live Server не получится — сработает блокировка CORS.

Что делать? Писать сервер? Есть вариант получше.

Устанавливаем serve глобально:

yarn global add serve # or npm i -g serve

Запускаем проект:

# флаг -s или --single означает, что отсутствующие пути # будут перенаправляться к index.html serve -s build # or without install npx serve -s build

Получаем адрес сайта, переходим по нему, видим наше приложение.

Вводим имя, нажимаем Say hello, получаем приветствие. Да, мы сделали это!

Пожалуй, это все, чем я хотел поделиться с вами в этой статье.

Надеюсь, вам было интересно и вы не зря потратили время.

Благодарю за внимание и happy coding!



ссылка на оригинал статьи https://habr.com/ru/articles/594967/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *