Пример использования 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). В этом случае просто перезапустите сервер и все будет ок

Источник: https://habr.com/ru/company/timeweb/blog/594967/


Интересные статьи

Интересные статьи

Потоковое вещание с мобильных устройств превратилось из экзотики в надежный рабочий инструмент. В статье мы даём лучшие практики и рекомендации для мобильного вещания на примере нашего приложения...
Асинхронное программирование — мощный инструмент. Но экосистема Rust продолжает активно развиваться, и пока язык далёк от идеала. В частности, по этой причине многие считают, что асинхр...
Мысли использовать автомобильные АКБ с UPS бродят по просторам интернета очень давно. Плюсы очевидны - стоимость ампер*часа автомобильных АКБ на порядок ниже, чем у родны...
Вам приходилось сталкиваться с ситуацией, когда сайт или портал Битрикс24 недоступен, потому что на диске неожиданно закончилось место? Да, последний бэкап съел все место на диске в самый неподходящий...
Пользовательские интерфейсы современных прикладных приложений, как правило, сложны — зачастую необходимо реализовывать поддержку постраничной навигации, обрабатывать разного рода поля ввода, ...