Rust Elective 01: WebAssembly - Web向けRustの実践

課題説明

概要

WebAssembly (WASM) は、Web上でネイティブに近いパフォーマンスでコードを実行するための技術です。本課題では、Rustのコードをコンパイルしてブラウザで動作させ、JavaScriptと連携するWebアプリケーションを構築します。

背景と動機

WebAssemblyの重要性:

  • パフォーマンス: ブラウザ上でネイティブに近い速度でコードを実行
  • 言語の多様性: C/C++、Rust、Goなど、複数の言語から生成可能
  • セキュリティ: サンドボックス環境で安全に実行
  • 相互運用性: JavaScriptと双方向に連携可能

Rustの優位性:

  • メモリ安全性保証(ガベージコレクションなし)
  • 小さなバイナリサイズ
  • wasm-bindgenによる型安全なJavaScript連携
  • wasm-packによる簡単なビルド
  • 課題要件

    以下の機能を持つWASMアプリケーションを実装してください:

  • 画像処理ライブラリ:
- グレースケール変換 - セピア変換 - ぼかし効果(Gaussian Blur) - エッジ検出(Sobel Filter) - 各処理のパフォーマンス計測

  • JavaScript連携:
- wasm-bindgenを使った型安全なインターフェース - JavaScriptからRust関数の呼び出し - RustからJavaScriptコールバックの実行 - DOM操作の実装

  • パフォーマンス比較:
- 同じ処理をJavaScriptでも実装 - WASM版とJS版のパフォーマンス比較 - 処理時間のベンチマーク表示

  • Webアプリ統合:
- HTMLキャンバスでの画像表示 - ファイルアップロード機能 - リアルタイムプレビュー - 処理結果のダウンロード

制約条件

  • wasm-bindgen、wasm-packを使用すること
  • JavaScriptとの連携は型安全に実装すること
  • エラーハンドリングを適切に実装すること
  • 大きな画像(4K以上)でもスムーズに動作すること
  • モダンなブラウザ(Chrome、Firefox、Safari)で動作すること

---

想定解答

プロジェクト構造

rust_wasm/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── image_processor.rs
│   ├── filters.rs
│   └── utils.rs
├── www/
│   ├── index.html
│   ├── index.js
│   ├── style.css
│   └── package.json
├── tests/
│   └── integration_tests.rs
└── README.md

Cargo.toml

[package]
name = "rust_wasm"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
    "console",
    "Document",
    "Element",
    "HtmlCanvasElement",
    "CanvasRenderingContext2d",
    "ImageData",
    "Window",
    "Performance",
] }

[dev-dependencies]
wasm-bindgen-test = "0.3"

[profile.release]
opt-level = 3
lto = true

src/lib.rs

use wasm_bindgen::prelude::*;
use web_sys::{console, ImageData};

mod filters;
mod image_processor;
mod utils;

// JavaScriptのconsole.logを使うためのマクロ
#[macro_export]
macro_rules! log {
    ($($t:tt)*) => {
        web_sys::console::log_1(&format!($($t)*).into());
    }
}

/// WASMモジュールの初期化
#[wasm_bindgen(start)]
pub fn init() {
    utils::set_panic_hook();
    log!("WASM module initialized");
}

/// メモリアドレスを返す(パフォーマンステスト用)
#[wasm_bindgen]
pub fn get_memory_buffer() -> *const u8 {
    wasm_bindgen::memory().as_ptr()
}

/// 画像処理クラス
#[wasm_bindgen]
pub struct ImageProcessor {
    width: u32,
    height: u32,
    data: Vec<u8>,
}

#[wasm_bindgen]
impl ImageProcessor {
    /// 新しいImageProcessorインスタンスを作成
    #[wasm_bindgen(constructor)]
    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<ImageProcessor, JsValue> {
        if data.len() != (width * height * 4) as usize {
            return Err(JsValue::from_str("Invalid data size"));
        }

        Ok(ImageProcessor {
            width,
            height,
            data,
        })
    }

    /// ImageDataから作成
    pub fn from_image_data(image_data: &ImageData) -> Result<ImageProcessor, JsValue> {
        let width = image_data.width();
        let height = image_data.height();
        let data = image_data.data().0;

        Ok(ImageProcessor {
            width,
            height,
            data,
        })
    }

    /// ImageDataに変換
    pub fn to_image_data(&self) -> Result<ImageData, JsValue> {
        ImageData::new_with_u8_clamped_array_and_sh(
            wasm_bindgen::Clamped(&self.data),
            self.width,
            self.height,
        )
    }

    /// グレースケール変換
    pub fn grayscale(&mut self) -> Result<(), JsValue> {
        filters::grayscale(&mut self.data);
        Ok(())
    }

    /// セピア変換
    pub fn sepia(&mut self) -> Result<(), JsValue> {
        filters::sepia(&mut self.data);
        Ok(())
    }

    /// ぼかし効果(3x3 Gaussian Blur)
    pub fn blur(&mut self) -> Result<(), JsValue> {
        let blurred = filters::gaussian_blur(&self.data, self.width, self.height);
        self.data = blurred;
        Ok(())
    }

    /// エッジ検出(Sobel Filter)
    pub fn edge_detect(&mut self) -> Result<(), JsValue> {
        let edges = filters::sobel_filter(&self.data, self.width, self.height);
        self.data = edges;
        Ok(())
    }

    /// 画像データのクローンを返す
    pub fn clone_data(&self) -> Vec<u8> {
        self.data.clone()
    }

    /// 幅を返す
    pub fn width(&self) -> u32 {
        self.width
    }

    /// 高さを返す
    pub fn height(&self) -> u32 {
        self.height
    }
}

/// パフォーマンステスト用の関数群
#[wasm_bindgen]
pub struct PerformanceTest;

#[wasm_bindgen]
impl PerformanceTest {
    /// 大量の計算を実行(ベンチマーク用)
    pub fn heavy_computation(n: u32) -> f64 {
        let mut sum = 0.0;
        for i in 0..n {
            sum += (i as f64).sqrt().sin().cos();
        }
        sum
    }

    /// メモリアクセステスト
    pub fn memory_access_test(size: usize) -> Vec<u8> {
        let mut data = vec![0u8; size];
        for i in 0..size {
            data[i] = (i % 256) as u8;
        }
        data
    }
}

src/filters.rs

/// グレースケール変換
pub fn grayscale(data: &mut [u8]) {
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;

        // 輝度を計算(ITU-R BT.709規格)
        let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;

        pixel[0] = gray;
        pixel[1] = gray;
        pixel[2] = gray;
        // Alpha値はそのまま
    }
}

/// セピア変換
pub fn sepia(data: &mut [u8]) {
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;

        // セピア変換行列
        let tr = (0.393 * r + 0.769 * g + 0.189 * b).min(255.0) as u8;
        let tg = (0.349 * r + 0.686 * g + 0.168 * b).min(255.0) as u8;
        let tb = (0.272 * r + 0.534 * g + 0.131 * b).min(255.0) as u8;

        pixel[0] = tr;
        pixel[1] = tg;
        pixel[2] = tb;
    }
}

/// Gaussian Blur(3x3カーネル)
pub fn gaussian_blur(data: &[u8], width: u32, height: u32) -> Vec<u8> {
    let mut result = vec![0u8; data.len()];

    // 3x3 Gaussianカーネル(正規化済み)
    const KERNEL: [[f32; 3]; 3] = [
        [1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
        [2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0],
        [1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
    ];

    for y in 1..(height - 1) {
        for x in 1..(width - 1) {
            for c in 0..3 {
                // RGB各チャンネルに対して畳み込み
                let mut sum = 0.0;

                for ky in 0..3 {
                    for kx in 0..3 {
                        let px = (x + kx - 1) as usize;
                        let py = (y + ky - 1) as usize;
                        let idx = (py * width as usize + px) * 4 + c;

                        sum += data[idx] as f32 * KERNEL[ky][kx];
                    }
                }

                let idx = (y * width + x) as usize * 4 + c;
                result[idx] = sum as u8;
            }

            // Alpha値をコピー
            let idx = (y * width + x) as usize * 4;
            result[idx + 3] = data[idx + 3];
        }
    }

    // 端の処理(そのままコピー)
    for y in 0..height {
        for x in 0..width {
            if y == 0 || y == height - 1 || x == 0 || x == width - 1 {
                let idx = (y * width + x) as usize * 4;
                for c in 0..4 {
                    result[idx + c] = data[idx + c];
                }
            }
        }
    }

    result
}

/// Sobelフィルタによるエッジ検出
pub fn sobel_filter(data: &[u8], width: u32, height: u32) -> Vec<u8> {
    let mut result = vec![0u8; data.len()];

    // Sobelカーネル(X方向とY方向)
    const SOBEL_X: [[i32; 3]; 3] = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
    const SOBEL_Y: [[i32; 3]; 3] = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];

    // まずグレースケール化
    let mut gray = data.to_vec();
    grayscale(&mut gray);

    for y in 1..(height - 1) {
        for x in 1..(width - 1) {
            let mut gx = 0.0;
            let mut gy = 0.0;

            for ky in 0..3 {
                for kx in 0..3 {
                    let px = (x + kx - 1) as usize;
                    let py = (y + ky - 1) as usize;
                    let idx = (py * width as usize + px) * 4;
                    let intensity = gray[idx] as f32;

                    gx += intensity * SOBEL_X[ky][kx] as f32;
                    gy += intensity * SOBEL_Y[ky][kx] as f32;
                }
            }

            // 勾配の大きさ
            let magnitude = (gx * gx + gy * gy).sqrt().min(255.0) as u8;

            let idx = (y * width + x) as usize * 4;
            result[idx] = magnitude;
            result[idx + 1] = magnitude;
            result[idx + 2] = magnitude;
            result[idx + 3] = 255;
        }
    }

    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_grayscale() {
        let mut data = vec![100, 150, 200, 255]; // RGBA
        grayscale(&mut data);

        // すべてのRGB値が同じになるはず
        assert_eq!(data[0], data[1]);
        assert_eq!(data[1], data[2]);
        assert_eq!(data[3], 255); // Alphaは変わらない
    }

    #[test]
    fn test_sepia() {
        let mut data = vec![100, 100, 100, 255];
        sepia(&mut data);

        // セピア調になっているはず(赤みがかる)
        assert!(data[0] >= data[1]);
        assert!(data[1] >= data[2]);
    }
}

src/utils.rs

use wasm_bindgen::prelude::*;

/// パニック時のスタックトレースを表示
pub fn set_panic_hook() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

/// JavaScriptのconsole.timeを使った計測
#[wasm_bindgen]
pub struct Timer {
    label: String,
}

#[wasm_bindgen]
impl Timer {
    #[wasm_bindgen(constructor)]
    pub fn new(label: &str) -> Timer {
        web_sys::console::time_with_label(label);
        Timer {
            label: label.to_string(),
        }
    }

    pub fn end(&self) {
        web_sys::console::time_end_with_label(&self.label);
    }
}

www/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rust WASM Image Processor</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>🦀 Rust WASM Image Processor</h1>

        <div class="controls">
            <input type="file" id="imageInput" accept="image/*">

            <div class="filters">
                <button id="grayscaleBtn">Grayscale</button>
                <button id="sepiaBtn">Sepia</button>
                <button id="blurBtn">Blur</button>
                <button id="edgeBtn">Edge Detect</button>
                <button id="resetBtn">Reset</button>
            </div>

            <div class="benchmark">
                <button id="benchmarkBtn">Run Benchmark</button>
                <div id="benchmarkResults"></div>
            </div>
        </div>

        <div class="canvas-container">
            <div class="canvas-wrapper">
                <h3>Original</h3>
                <canvas id="originalCanvas"></canvas>
            </div>
            <div class="canvas-wrapper">
                <h3>Processed (WASM)</h3>
                <canvas id="processedCanvas"></canvas>
            </div>
        </div>

        <div class="info">
            <h3>Performance Metrics</h3>
            <div id="metrics">
                <p>Load an image to start</p>
            </div>
        </div>
    </div>

    <script type="module" src="./index.js"></script>
</body>
</html>

www/index.js

import init, { ImageProcessor, PerformanceTest } from '../pkg/rust_wasm.js';

let wasmModule;
let originalImageData;
let currentProcessor;

// WASM初期化
async function initWasm() {
    wasmModule = await init();
    console.log('WASM module loaded successfully');
}

// 画像読み込み
function loadImage(file) {
    const reader = new FileReader();

    reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
            // オリジナルキャンバスに描画
            const originalCanvas = document.getElementById('originalCanvas');
            const ctx = originalCanvas.getContext('2d');

            originalCanvas.width = img.width;
            originalCanvas.height = img.height;
            ctx.drawImage(img, 0, 0);

            // ImageDataを取得
            originalImageData = ctx.getImageData(0, 0, img.width, img.height);

            // 処理用キャンバスも初期化
            const processedCanvas = document.getElementById('processedCanvas');
            processedCanvas.width = img.width;
            processedCanvas.height = img.height;
            const procCtx = processedCanvas.getContext('2d');
            procCtx.putImageData(originalImageData, 0, 0);

            updateMetrics('Image loaded', img.width, img.height);
        };
        img.src = e.target.result;
    };

    reader.readAsDataURL(file);
}

// フィルタ適用
async function applyFilter(filterName) {
    if (!originalImageData) {
        alert('Please load an image first');
        return;
    }

    const start = performance.now();

    try {
        // ImageProcessorを作成
        currentProcessor = ImageProcessor.from_image_data(originalImageData);

        // フィルタ適用
        switch(filterName) {
            case 'grayscale':
                currentProcessor.grayscale();
                break;
            case 'sepia':
                currentProcessor.sepia();
                break;
            case 'blur':
                currentProcessor.blur();
                break;
            case 'edge':
                currentProcessor.edge_detect();
                break;
        }

        // 結果を表示
        const resultImageData = currentProcessor.to_image_data();
        const canvas = document.getElementById('processedCanvas');
        const ctx = canvas.getContext('2d');
        ctx.putImageData(resultImageData, 0, 0);

        const end = performance.now();
        const duration = (end - start).toFixed(2);

        updateMetrics(
            `${filterName} applied`,
            canvas.width,
            canvas.height,
            `${duration}ms (WASM)`
        );

    } catch (error) {
        console.error('Filter error:', error);
        alert('Error applying filter: ' + error);
    }
}

// JavaScript版フィルタ(比較用)
function applyFilterJS(imageData, filterName) {
    const data = imageData.data;
    const start = performance.now();

    switch(filterName) {
        case 'grayscale':
            for (let i = 0; i < data.length; i += 4) {
                const gray = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
                data[i] = data[i + 1] = data[i + 2] = gray;
            }
            break;
        case 'sepia':
            for (let i = 0; i < data.length; i += 4) {
                const r = data[i], g = data[i + 1], b = data[i + 2];
                data[i] = Math.min(255, 0.393 * r + 0.769 * g + 0.189 * b);
                data[i + 1] = Math.min(255, 0.349 * r + 0.686 * g + 0.168 * b);
                data[i + 2] = Math.min(255, 0.272 * r + 0.534 * g + 0.131 * b);
            }
            break;
    }

    const end = performance.now();
    return (end - start).toFixed(2);
}

// ベンチマーク実行
async function runBenchmark() {
    if (!originalImageData) {
        alert('Please load an image first');
        return;
    }

    const results = [];
    const filters = ['grayscale', 'sepia'];

    for (const filter of filters) {
        // WASM版
        const wasmStart = performance.now();
        const processor = ImageProcessor.from_image_data(originalImageData);
        if (filter === 'grayscale') processor.grayscale();
        if (filter === 'sepia') processor.sepia();
        const wasmEnd = performance.now();
        const wasmTime = wasmEnd - wasmStart;

        // JavaScript版
        const jsImageData = new ImageData(
            new Uint8ClampedArray(originalImageData.data),
            originalImageData.width,
            originalImageData.height
        );
        const jsTime = applyFilterJS(jsImageData, filter);

        const speedup = (jsTime / wasmTime).toFixed(2);
        results.push({
            filter,
            wasm: wasmTime.toFixed(2),
            js: jsTime,
            speedup
        });
    }

    // 結果表示
    const resultsDiv = document.getElementById('benchmarkResults');
    resultsDiv.innerHTML = `
        <table>
            <tr>
                <th>Filter</th>
                <th>WASM (ms)</th>
                <th>JavaScript (ms)</th>
                <th>Speedup</th>
            </tr>
            ${results.map(r => `
                <tr>
                    <td>${r.filter}</td>
                    <td>${r.wasm}</td>
                    <td>${r.js}</td>
                    <td>${r.speedup}x</td>
                </tr>
            `).join('')}
        </table>
    `;
}

// メトリクス更新
function updateMetrics(action, width, height, time = '') {
    const metricsDiv = document.getElementById('metrics');
    const pixels = width && height ? (width * height).toLocaleString() : '';

    metricsDiv.innerHTML = `
        <p><strong>Action:</strong> ${action}</p>
        ${width ? `<p><strong>Dimensions:</strong> ${width} x ${height} (${pixels} pixels)</p>` : ''}
        ${time ? `<p><strong>Processing Time:</strong> ${time}</p>` : ''}
    `;
}

// リセット
function reset() {
    if (originalImageData) {
        const canvas = document.getElementById('processedCanvas');
        const ctx = canvas.getContext('2d');
        ctx.putImageData(originalImageData, 0, 0);
        updateMetrics('Reset to original', canvas.width, canvas.height);
    }
}

// イベントリスナー設定
document.addEventListener('DOMContentLoaded', async () => {
    await initWasm();

    document.getElementById('imageInput').addEventListener('change', (e) => {
        if (e.target.files[0]) {
            loadImage(e.target.files[0]);
        }
    });

    document.getElementById('grayscaleBtn').addEventListener('click', () => applyFilter('grayscale'));
    document.getElementById('sepiaBtn').addEventListener('click', () => applyFilter('sepia'));
    document.getElementById('blurBtn').addEventListener('click', () => applyFilter('blur'));
    document.getElementById('edgeBtn').addEventListener('click', () => applyFilter('edge'));
    document.getElementById('resetBtn').addEventListener('click', reset);
    document.getElementById('benchmarkBtn').addEventListener('click', runBenchmark);
});

www/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    background: white;
    border-radius: 12px;
    padding: 30px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 30px;
}

.controls {
    margin-bottom: 30px;
}

input[type="file"] {
    display: block;
    margin-bottom: 20px;
    padding: 10px;
    border: 2px dashed #667eea;
    border-radius: 8px;
    width: 100%;
    cursor: pointer;
}

.filters {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

button {
    padding: 12px 24px;
    border: none;
    border-radius: 6px;
    background: #667eea;
    color: white;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.3s;
}

button:hover {
    background: #5568d3;
}

button:active {
    transform: translateY(1px);
}

#resetBtn {
    background: #e74c3c;
}

#resetBtn:hover {
    background: #c0392b;
}

#benchmarkBtn {
    background: #27ae60;
}

#benchmarkBtn:hover {
    background: #229954;
}

.canvas-container {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-bottom: 30px;
}

.canvas-wrapper {
    text-align: center;
}

.canvas-wrapper h3 {
    margin-bottom: 10px;
    color: #555;
}

canvas {
    max-width: 100%;
    height: auto;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.info {
    background: #f8f9fa;
    padding: 20px;
    border-radius: 8px;
}

#metrics p {
    margin: 8px 0;
    color: #555;
}

.benchmark table {
    width: 100%;
    margin-top: 15px;
    border-collapse: collapse;
}

.benchmark th,
.benchmark td {
    padding: 10px;
    text-align: left;
    border-bottom: 1px solid #ddd;
}

.benchmark th {
    background: #667eea;
    color: white;
    font-weight: 600;
}

@media (max-width: 768px) {
    .canvas-container {
        grid-template-columns: 1fr;
    }
}

www/package.json

{
  "name": "rust-wasm-image-processor",
  "version": "0.1.0",
  "description": "Image processing with Rust and WebAssembly",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack serve --mode development",
    "serve": "http-server dist"
  },
  "devDependencies": {
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.0",
    "webpack-dev-server": "^4.15.0",
    "http-server": "^14.1.1"
  }
}

ビルドスクリプト

#!/bin/bash
# build.sh

echo "Building WASM module..."
wasm-pack build --target web

echo "Building web assets..."
cd www
npm install
npm run build

echo "Done! Open www/index.html in a browser"

---

解説

実装のポイント

1. wasm-bindgenの活用

型安全なインターフェース:

#[wasm_bindgen]
pub struct ImageProcessor {
    width: u32,
    height: u32,
    data: Vec<u8>,
}

  • #[wasm_bindgen]マクロでJavaScript互換の型を自動生成
  • RustとJavaScript間で安全に値を受け渡し
  • エラーはResultで適切にハンドリング

2. メモリ管理

効率的なデータ転送:

pub fn to_image_data(&self) -> Result<ImageData, JsValue> {
    ImageData::new_with_u8_clamped_array_and_sh(
        wasm_bindgen::Clamped(&self.data),
        self.width,
        self.height,
    )
}

  • データのコピーを最小化
  • Clamped型で安全な値の範囲を保証
  • 大きな画像でもメモリ効率が良い

3. 画像処理アルゴリズム

Sobelフィルタ(エッジ検出):

const SOBEL_X: [[i32; 3]; 3] = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const SOBEL_Y: [[i32; 3]; 3] = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];

  • 畳み込み演算による画像処理
  • X方向とY方向の勾配を計算
  • WASMの高速な数値計算を活用

4. パフォーマンス最適化

最適化設定:

[profile.release]
opt-level = 3
lto = true

  • LTO(Link Time Optimization)で最大限の最適化
  • サイズとパフォーマンスのバランス
  • JavaScriptより2-5倍高速
  • 設計判断

  • ImageDataとの連携: ブラウザ標準のImageDataを使うことで、キャンバスAPIと直接やり取りできる
  • フィルタの分離: 各フィルタを独立した関数にすることで、テストとメンテナンスが容易
  • エラーハンドリング: Result型を使って、失敗の可能性がある操作を明示的に扱う
  • ベンチマーク機能: WASMとJavaScriptの比較で、パフォーマンス向上を実感できる
  • 代替案

  • 並列処理: Rayon + wasm-bindgen-rayonで並列化(複雑度が上がる)
  • GPU処理: WebGPUを使った実装(より高速だが対応ブラウザが限定的)
  • ストリーム処理: 大きな画像をチャンク単位で処理(実装が複雑)
  • ---

    学習の意図

    習得する概念

  • WebAssemblyの基礎:
- WASMバイナリの仕組み - JavaScript FFI(Foreign Function Interface) - メモリモデルの理解

  • Rustとブラウザの連携:
- wasm-bindgenの使い方 - web-sys APIの活用 - 型安全なインターフェース設計

  • 画像処理アルゴリズム:
- ピクセル操作 - 畳み込み演算 - カラースペース変換

  • パフォーマンスチューニング:
- プロファイリング - ベンチマーク - 最適化手法

CSの基礎との関連

コンパイラとランタイム:

  • WASMはスタックマシン
  • AOT(Ahead-of-Time)コンパイル
  • サンドボックス実行環境

メモリ管理:

  • 線形メモリモデル
  • ヒープとスタックの分離
  • ガベージコレクションなしの効率性

並行処理:

  • Web Workersとの組み合わせ
  • メインスレッドをブロックしない設計

数値計算:

  • SIMD命令の活用
  • 浮動小数点演算
  • アルゴリズムの計算量
  • ---

    テスト方法

    単体テスト

    # Rustユニットテスト
    cargo test
    
    # WASMテスト
    wasm-pack test --headless --firefox
    

    統合テスト

    // tests/integration_tests.rs
    use wasm_bindgen_test::*;
    use rust_wasm::ImageProcessor;
    
    #[wasm_bindgen_test]
    fn test_image_processor_creation() {
        let data = vec![0u8; 400]; // 10x10 RGBA
        let processor = ImageProcessor::new(10, 10, data);
        assert!(processor.is_ok());
    }
    
    #[wasm_bindgen_test]
    fn test_grayscale_filter() {
        let mut data = vec![100, 150, 200, 255];
        data.extend_from_slice(&[100, 150, 200, 255]);
    
        let mut processor = ImageProcessor::new(1, 2, data).unwrap();
        assert!(processor.grayscale().is_ok());
    }
    

    パフォーマンステスト

  • 小さい画像(640x480)でフィルタ処理
  • 中サイズ画像(1920x1080)でベンチマーク
  • 大きい画像(4K)でメモリ使用量確認
  • JavaScript版との比較で高速化を確認
  • ブラウザテスト

  • Chrome(最新版)
  • Firefox(最新版)
  • Safari(最新版)
  • モバイルブラウザ(iOS Safari、Chrome Mobile)
  • ---

    評価基準

    必須要件(60点)

  • [ ] WASMモジュールが正しくビルドできる(10点)
  • [ ] 画像をロードして表示できる(10点)
  • [ ] 4つのフィルタすべてが動作する(20点)
  • [ ] JavaScript連携が型安全に実装されている(10点)
  • [ ] エラーハンドリングが適切(10点)
  • 標準要件(30点)

  • [ ] パフォーマンスベンチマークが実装されている(10点)
  • [ ] JavaScriptより高速に動作する(10点)
  • [ ] UIが使いやすくデザインされている(5点)
  • [ ] モバイルでも動作する(5点)
  • 発展要件(10点)

  • [ ] 追加のフィルタ実装(ヒストグラム均等化など)(3点)
  • [ ] ダウンロード機能の実装(2点)
  • [ ] 複数フィルタの連続適用(2点)
  • [ ] リアルタイムプレビュー(3点)
  • ボーナス(+10点)

  • [ ] Web Workersでバックグラウンド処理(+5点)
  • [ ] WebGPUとの比較実装(+5点)
  • ---

    参考資料

    公式ドキュメント

  • Rust and WebAssembly Book
  • wasm-bindgen Guide
  • web-sys Documentation
  • 学習リソース

  • MDN WebAssembly Concepts
  • WebAssembly Specification
  • Lin Clark's Cartoon Intro to WebAssembly
  • コミュニティ

  • Rust WASM Working Group
  • WebAssembly Community Group