Rust + WebAssembly実践ガイド - wasm-bindgen、wasm-pack、Webアプリ統合、パフォーマンス最適化
Rust + WebAssembly実践ガイド
RustとWebAssemblyを組み合わせることで、Webアプリケーションにネイティブレベルのパフォーマンスをもたらせます。
WebAssembly (WASM)とは
WebAssemblyは、ブラウザで動作するバイナリ形式の低レベル言語です。
利点
- 高速実行: JavaScriptより高速
- 言語非依存: Rust、C、C++、Goなどから生成可能
- セキュア: サンドボックス環境で実行
- 互換性: すべてのモダンブラウザで動作
Rustを使う理由
- メモリ安全性: ガベージコレクタなしでメモリ安全
- ゼロコスト抽象化: 高レベルコードでも高パフォーマンス
- 優れたツールチェーン: cargo、wasm-pack
- 小さいバイナリ: 最適化により軽量なWASMファイル生成
セットアップ
Rustのインストール
# Rustupのインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# WASMターゲットの追加
rustup target add wasm32-unknown-unknown
wasm-packのインストール
cargo install wasm-pack
基本的なプロジェクト
プロジェクト作成
cargo new --lib hello-wasm
cd hello-wasm
Cargo.toml設定
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "z" # サイズ最適化
lto = true # Link Time Optimization
codegen-units = 1 # 並列コンパイル無効化(サイズ削減)
基本的なRustコード
// src/lib.rs
use wasm_bindgen::prelude::*;
// JavaScriptから呼び出し可能な関数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// JavaScriptのコンソールにログ出力
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn say_hello() {
log("Hello from Rust!");
}
ビルド
# 開発ビルド
wasm-pack build --target web
# リリースビルド(最適化)
wasm-pack build --target web --release
生成されるファイル:
pkg/
├── hello_wasm.js
├── hello_wasm_bg.wasm
├── hello_wasm.d.ts
└── package.json
JavaScriptからの使用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello WASM</title>
</head>
<body>
<script type="module">
import init, { greet, add, say_hello } from './pkg/hello_wasm.js';
async function run() {
await init();
console.log(greet("World")); // "Hello, World!"
console.log(add(5, 3)); // 8
say_hello(); // "Hello from Rust!"
}
run();
</script>
</body>
</html>
JavaScriptとの相互運用
複雑な型の受け渡し
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
}
#[wasm_bindgen]
pub fn process_user(user_json: &str) -> Result<String, JsValue> {
let user: User = serde_json::from_str(user_json)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
// ユーザー処理
let result = format!("Processed user: {} (ID: {})", user.name, user.id);
Ok(result)
}
// 構造体を直接返す
#[wasm_bindgen]
pub fn create_user(id: u32, name: String, email: String) -> JsValue {
let user = User { id, name, email };
serde_wasm_bindgen::to_value(&user).unwrap()
}
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
import init, { process_user, create_user } from './pkg/hello_wasm.js';
await init();
const userJson = JSON.stringify({
id: 1,
name: "Alice",
email: "alice@example.com"
});
console.log(process_user(userJson));
const newUser = create_user(2, "Bob", "bob@example.com");
console.log(newUser); // { id: 2, name: "Bob", email: "bob@example.com" }
DOM操作
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement, Window};
#[wasm_bindgen]
pub fn create_element() -> Result<(), JsValue> {
let window: Window = web_sys::window().expect("no global `window`");
let document: Document = window.document().expect("no document");
let body = document.body().expect("document has no body");
// 新しい要素を作成
let div: Element = document.create_element("div")?;
div.set_text_content(Some("Created by Rust!"));
div.set_class_name("rust-element");
// DOMに追加
body.append_child(&div)?;
Ok(())
}
#[wasm_bindgen]
pub fn update_element(id: &str, text: &str) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
if let Some(element) = document.get_element_by_id(id) {
element.set_text_content(Some(text));
}
Ok(())
}
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
"Document",
"Element",
"HtmlElement",
"Window",
] }
イベントハンドラ
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Event, HtmlInputElement};
#[wasm_bindgen]
pub fn setup_event_listener() -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let input = document
.get_element_by_id("my-input")
.unwrap()
.dyn_into::<HtmlInputElement>()?;
let closure = Closure::wrap(Box::new(move |event: Event| {
let target = event.target().unwrap();
let input = target.dyn_into::<HtmlInputElement>().unwrap();
let value = input.value();
web_sys::console::log_1(&format!("Input value: {}", value).into());
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref())?;
// メモリリークを防ぐため、Closureを保持
closure.forget();
Ok(())
}
実用例
1. 画像処理
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::ImageData;
#[wasm_bindgen]
pub fn grayscale(data: &[u8], width: u32, height: u32) -> Vec<u8> {
let mut output = data.to_vec();
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32;
let g = data[i + 1] as f32;
let b = data[i + 2] as f32;
// グレースケール変換
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
output[i] = gray;
output[i + 1] = gray;
output[i + 2] = gray;
// Alpha値は維持
}
output
}
#[wasm_bindgen]
pub fn blur(data: &[u8], width: u32, height: u32, radius: u32) -> Vec<u8> {
// ガウシアンブラー実装
let mut output = data.to_vec();
// 簡易ボックスブラー
for y in 0..height {
for x in 0..width {
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let mut count = 0u32;
for dy in -(radius as i32)..=(radius as i32) {
for dx in -(radius as i32)..=(radius as i32) {
let nx = x as i32 + dx;
let ny = y as i32 + dy;
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
let idx = ((ny * width as i32 + nx) * 4) as usize;
r_sum += data[idx] as u32;
g_sum += data[idx + 1] as u32;
b_sum += data[idx + 2] as u32;
count += 1;
}
}
}
let idx = ((y * width + x) * 4) as usize;
output[idx] = (r_sum / count) as u8;
output[idx + 1] = (g_sum / count) as u8;
output[idx + 2] = (b_sum / count) as u8;
}
}
output
}
import init, { grayscale, blur } from './pkg/image_processor.js';
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 画像読み込み
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// グレースケール処理(Rustで実行)
const grayData = grayscale(imageData.data, canvas.width, canvas.height);
// 結果を表示
const newImageData = new ImageData(
new Uint8ClampedArray(grayData),
canvas.width,
canvas.height
);
ctx.putImageData(newImageData, 0, 0);
};
img.src = 'image.jpg';
2. 暗号化
use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
#[wasm_bindgen]
pub fn hash_password(password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let result = hasher.finalize();
format!("{:x}", result)
}
#[wasm_bindgen]
pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let nonce = Nonce::from_slice(b"unique nonce");
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(ciphertext)
}
[dependencies]
wasm-bindgen = "0.2"
sha2 = "0.10"
aes-gcm = "0.10"
3. データ処理・計算
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Statistics {
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
}
#[wasm_bindgen]
pub fn calculate_statistics(data: &[f64]) -> Statistics {
let len = data.len() as f64;
// 平均
let mean = data.iter().sum::<f64>() / len;
// 中央値
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = if sorted.len() % 2 == 0 {
(sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0
} else {
sorted[sorted.len() / 2]
};
// 標準偏差
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / len;
let std_dev = variance.sqrt();
// 最小・最大
let min = *data.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let max = *data.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
Statistics {
mean,
median,
std_dev,
min,
max,
}
}
#[wasm_bindgen]
pub fn matrix_multiply(a: &[f64], b: &[f64], rows_a: usize, cols_a: usize, cols_b: usize) -> Vec<f64> {
let mut result = vec![0.0; rows_a * cols_b];
for i in 0..rows_a {
for j in 0..cols_b {
for k in 0..cols_a {
result[i * cols_b + j] += a[i * cols_a + k] * b[k * cols_b + j];
}
}
}
result
}
パフォーマンス最適化
1. ビルド最適化
[profile.release]
opt-level = "z" # サイズ優先最適化
lto = true # Link Time Optimization
codegen-units = 1 # 並列化無効(サイズ削減)
strip = true # シンボル削除
panic = "abort" # パニック時abort
2. wasm-optの使用
# wasm-optのインストール
npm install -g wasm-opt
# 最適化
wasm-opt -Oz -o optimized.wasm input.wasm
3. メモリ管理
use wasm_bindgen::prelude::*;
// メモリ効率的な処理
#[wasm_bindgen]
pub fn process_large_data(data: &[u8]) -> Vec<u8> {
// イテレータを使って一度に全データをメモリに展開しない
data.iter()
.map(|&byte| byte.wrapping_add(1))
.collect()
}
// 可能な限りスライスを使う
#[wasm_bindgen]
pub fn sum_numbers(numbers: &[f64]) -> f64 {
numbers.iter().sum()
}
デバッグ
コンソールログ
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn log_u32(a: u32);
}
#[wasm_bindgen]
pub fn debug_example() {
log("Debug message from Rust");
log_u32(42);
}
web-sys::console
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn advanced_logging() {
web_sys::console::log_1(&"Hello".into());
web_sys::console::warn_1(&"Warning!".into());
web_sys::console::error_1(&"Error!".into());
}
まとめ
Rust + WebAssemblyで高パフォーマンスなWebアプリケーションを構築できます。
適用場面
- 画像・動画処理: フィルター、エンコード
- 暗号化: 高速な暗号処理
- データ分析: 大量データの統計処理
- ゲーム: 物理演算、レンダリング
- CAD/3D: 複雑な計算
ベストプラクティス
- JavaScript境界を最小化: 関数呼び出しのオーバーヘッド削減
- 大きなデータは共有メモリ: コピーを避ける
- リリースビルドで最適化: サイズとパフォーマンス両立
- 適材適所: すべてをWASM化する必要はない
Rustの安全性とパフォーマンスを、Webブラウザで活用しましょう。