Для чего нужна данная статья? :
- Получить представление о WebAssembly.
- Написать простой код CMS и мобильного приложения.
Узнать что такое:
- Полноценная нейросеть с обратным распространением ошибки
- Использование ndarray для матричных операций
- Продвинутая обработка ошибок с panic hook
- Система обучения с adjustable learning rate
- Активация через сигмоиду
- Поддержка многомерных входов/выходов
- Полная интеграция с JavaScript через WASM
Зачем Вам это уметь? :
1. Основные инструменты компиляции
- wasm-pack
Стандартный инструмент для сборки, тестирования и публикации Rust-кода в WASM. Генерирует JS-обвязки и интегрируется с npm. - wasm-bindgen
Библиотека для взаимодействия Rust и JavaScript. Позволяет вызывать JS из Rust и наоборот, работать с DOM, Promise, файлами и т.д. - Целевая платформа wasm32-unknown-unknown
Базовая компиляция Rust в WASM без зависимостей. Требует ручного управления памятью и экспортом функций.
2. Альтернативные библиотеки
- stdweb (устаревший)
Ранняя альтернатива wasm-bindgen для взаимодействия с JS и DOM. Сейчас рекомендуется использовать wasm-bindgen.
3. Фреймворки для веб-приложений
- Yew
Компонентный фреймворк с поддержкой асинхронности, виртуальным DOM и SSR (Server-Side Rendering). - Seed
Фреймворк в Elm-стиле с акцентом на простоту и производительность. - Sycamore
Реактивный фреймворк с компиляцией шаблонов в Rust. - Percy
Инструмент для создания изоморфных веб-приложений (рендеринг на сервере и клиенте). - Dioxus
Универсальный UI-фреймворк с поддержкой WASM, нативного рендеринга и SSR.
4. Интеграция с JavaScript API
- js-sys
Привязки к базовым объектам JavaScript (Object, Array, JSON и т.д.). - web-sys
Привязки к Web API (DOM, Fetch, Canvas, WebGL и другим браузерным функциям). Используется с wasm-bindgen.
5. Специализированные сценарии
- Emscripten (wasm32-unknown-emscripten)
Компиляция через Emscripten для интеграции с legacy-кодом на C/C++ (редко используется в Rust). - Node.js + WASM
Запуск Rust/WASM в Node.js через wasm-pack-node или кастомные настройки.
6. Ручное управление
- Чистый WASM без библиотек
Прямая работа с памятью и экспортом функций через #[no_mangle] и extern "C". Подходит для низкоуровневых задач.
7. Дополнительные инструменты
- wasm-opt (из Binaryen)
Оптимизация размера и производительности WASM-модулей. - wasm-bindgen-cli
Генерация JS-привязок для ручной настройки.
CMS
Написание CMS (системы управления контентом) в Rust с WebAssembly будет включать компиляцию кода Rust в WebAssembly, а затем использование его в веб-приложении. Вот пример того, как вы можете начать структурировать простую CMS в Rust с помощью WebAssembly:
пример простой структуры Page, которая представляет страницу контента в CMS:
#[derive(Debug)]
pub struct Page {
title: String,
content: String,
}
impl Page {
pub fn new(title: String, content: String) -> Self {
Self { title, content }
}
pub fn title(&self) -> &str {
&self.title
}
pub fn content(&self) -> &str {
&self.content
}
}
Этот код определяет структуру Страницы с полями для заголовка и содержимого страницы, а также методы для создания новой страницы и доступа к ее заголовку и содержимому.
Далее скомпилируем код Rust в WebAssembly с помощью инструмента wasm-pack. При этом будет создан WASM-файл, который можно использовать в веб-приложении.
В веб-приложении можно использовать JavaScript для загрузки модуля WebAssembly и создания экземпляра структуры Page. Вот пример того, как это может выглядеть с помощью JavaScript и библиотеки wasm-bindgen:
import init from './pkg/my_cms.js';
async function run() {
const my_cms = await init();
const { Page } = my_cms;
const page = Page.new('Home', 'Welcome to our website!');
console.log(page.title(), page.content());
}
run();
Этот код использует инструкцию import для загрузки модуля WebAssembly, созданного wasm-pack. Затем он использует функцию init для инициализации модуля и получения ссылки на структуру Page. Код создает экземпляр структуры Page с помощью нового метода, а затем вызывает его методы title и content для доступа к своим данным.
Создание мобильного приложения
Создадим простую функцию на Rust, которая будет компилироваться в WebAssembly и вызвана из веб-интерфейса.
1.1 Установите необходимые инструменты:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
1.2 Создайте новый проект на Rust:
cargo new --lib wasm_example
cd wasm_example
1.3 Напишите код на Rust:
Файл src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen] pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
1.4 Скомпилируйте проект в WebAssembly:
cargo build --target wasm32-unknown-unknown --release
1.5 Генерация обвязки с помощью wasm-bindgen:
wasm-bindgen target/wasm32-unknown-unknown/release/wasm_example.wasm --out-dir ./out --target web
2.1 Использование WebView в Android:
Если вы хотите интегрировать WebAssembly код в мобильное приложение на Android, можно использовать WebView:
2.1.1 Создайте Android проект и добавьте WebView в макет:
Файл res/layout/activity_main.xml:
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
2.1.2 В MainActivity загрузите HTML с WebAssembly:
Файл MainActivity.java или MainActivity.kt:
WebView webView = findViewById(R.id.webview); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("file:///android_asset/index.html");
2.1.3 Создайте файл index.html в assets:
Файл assets/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Wasm Example</title>
<script type="module"> import init, { greet } from './wasm_example.js'; async function run() {
await init();
document.getElementById("output").textContent = greet("Android User"); }
run();
</script>
</head>
<body>
<div id="output">
</div>
<script src="wasm_example.js">
</script> </body> </html>
Когда вы скомпилируете и запустите Android приложение, оно откроет WebView, который загрузит HTML-страницу с вашим скомпилированным WebAssembly кодом на Rust. Эта страница вызовет функцию greet, которая выполнится на WebAssembly и отобразит результат.
Нейросеть с матричными операциями, компилируемой в WASM, включая JS-интерфейс
// lib.rs
#![allow(non_snake_case)]
use wasm_bindgen::prelude::*;
use ndarray::{Array2, Array1};
use rand::Rng;
use std::f64::consts::E;
#[wasm_bindgen]
pub struct NeuralNetwork {
input_size: usize,
hidden_size: usize,
output_size: usize,
weights_ih: Array2<f64>,
weights_ho: Array2<f64>,
bias_h: Array1<f64>,
bias_o: Array1<f64>,
learning_rate: f64,
}
#[wasm_bindgen]
impl NeuralNetwork {
#[wasm_bindgen(constructor)]
pub fn new(
input_size: usize,
hidden_size: usize,
output_size: usize,
learning_rate: f64,
) -> Self {
let mut rng = rand::thread_rng();
let weights_ih = Array2::from_shape_fn((hidden_size, input_size), |_| rng.gen_range(-1.0..1.0));
let weights_ho = Array2::from_shape_fn((output_size, hidden_size), |_| rng.gen_range(-1.0..1.0));
let bias_h = Array1::from_shape_fn(hidden_size, |_| rng.gen_range(-1.0..1.0));
let bias_o = Array1::from_shape_fn(output_size, |_| rng.gen_range(-1.0..1.0));
NeuralNetwork {
input_size,
hidden_size,
output_size,
weights_ih,
weights_ho,
bias_h,
bias_o,
learning_rate,
}
}
fn sigmoid(&self, x: &Array2<f64>) -> Array2<f64> {
x.mapv(|v| 1.0 / (1.0 + E.powf(-v)))
}
fn sigmoid_derivative(&self, x: &Array2<f64>) -> Array2<f64> {
x * (1.0 - x)
}
#[wasm_bindgen]
pub fn predict(&self, inputs: &[f64]) -> Vec<f64> {
let inputs = Array2::from_shape_vec((1, self.input_size), inputs.to_vec())
.expect("Invalid input shape");
let hidden = self.sigmoid(&(inputs.dot(&self.weights_ih.t()) + &self.bias_h));
let outputs = self.sigmoid(&(hidden.dot(&self.weights_ho.t()) + &self.bias_o));
outputs.into_raw_vec()
}
#[wasm_bindgen]
pub fn train(&mut self, inputs: &[f64], targets: &[f64]) {
let inputs = Array2::from_shape_vec((1, self.input_size), inputs.to_vec())
.expect("Invalid input shape");
let targets = Array2::from_shape_vec((1, self.output_size), targets.to_vec())
.expect("Invalid target shape");
// Forward pass
let hidden_inputs = inputs.dot(&self.weights_ih.t()) + &self.bias_h;
let hidden_outputs = self.sigmoid(&hidden_inputs);
let final_inputs = hidden_outputs.dot(&self.weights_ho.t()) + &self.bias_o;
let final_outputs = self.sigmoid(&final_inputs);
// Backpropagation
let output_errors = &targets - &final_outputs;
let output_gradients = output_errors * self.sigmoid_derivative(&final_outputs);
let hidden_errors = output_gradients.dot(&self.weights_ho);
let hidden_gradients = hidden_errors * self.sigmoid_derivative(&hidden_outputs);
// Update weights and biases
self.weights_ho += &(output_gradients.t().dot(&hidden_outputs) * self.learning_rate);
self.weights_ih += &(hidden_gradients.t().dot(&inputs) * self.learning_rate);
self.bias_o += &output_gradients.sum_axis(ndarray::Axis(0)) * self.learning_rate;
self.bias_h += &hidden_gradients.sum_axis(ndarray::Axis(0)) * self.learning_rate;
}
}
#[wasm_bindgen]
pub fn init_panic_hook() {
console_error_panic_hook::set_once();
}
JavaScript часть (index.js):
import init, { NeuralNetwork, init_panic_hook } from './pkg/ml_wasm.js';
async function run() {
await init();
init_panic_hook();
const nn = new NeuralNetwork(2, 4, 1, 0.5);
// XOR dataset
const training_data = [
{ inputs: [0, 0], targets: [0] },
{ inputs: [0, 1], targets: [1] },
{ inputs: [1, 0], targets: [1] },
{ inputs: [1, 1], targets: [0] },
];
// Training
for (let epoch = 0; epoch < 10000; epoch++) {
for (const data of training_data) {
nn.train(data.inputs, data.targets);
}
}
// Testing
console.log('[0,0] =>', nn.predict([0, 0])); // ≈0
console.log('[0,1] =>', nn.predict([0, 1])); // ≈1
console.log('[1,0] =>', nn.predict([1, 0])); // ≈1
console.log('[1,1] =>', nn.predict([1, 1])); // ≈0
}
run();
Сборка (Cargo.toml):
[package]
name = "ml-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
ndarray = "0.15"
rand = "0.8"
console_error_panic_hook = "0.1"