Hanatare's PaPa

Make life a little richer.

Virtual Space of Hanatare's PaPa

人生をほんの少しだけ充実させる

【WASM】はじめてのWebAssembly:RustとWASMでブラウザーベースの画像フィルターアプリを作る①

早速ですがWASMってご存じでしょうか?私は、この技術を知りませんでした。たまたまnoteの記事を読んでいたらWASMについて書かれている記事に目に入り、興味を持ち、今回記事を書きながら、WASMがどういったものかを学んでみようと思いました。

今回の記事はWASM完全初心者の私が、WASMとは何か?といったところから、実際に開発をしてみるところまでを記事にしたいと思っています。少し長くなると思いますので、今回も何回かに分けてブログ記事にしていければと思います。

記事のポイント
  • WebAssemblyとは
  • Rustの開発環境構築
  • Rustでフィルターアプリ開発

はじめに

今回は2つの記事に分けて書いていきたいと思います。今回は、まずWebAssemblyの基本的な概念を理解し、なぜWASMを使うにあたって開発を行うのですが、その際数ある言語の中からなぜRustを選ぶのか、その理由を明らかにします。そして、開発環境をゼロから構築し、最終的には最初の画像フィルターである「グレースケール化」を実装するところまでを目指したいと思います。

WebAssemblyとは一体何者?

ここでは、WebAssembly(以下WASM)についての基本的な概念を整理したいと思います。

WASMとは?

WASMは、「モダンなウェブブラウザで実行できる、新しいタイプのコード」です 。これは、C++、RUST、Goなどのプログラミング言語から「コンパイル先」としてWASMという共通の変換形式に変換し、ブラウザに処理をしてもらうことになります。

WASMはJavaScriptの「置き換え」ではなく「相棒」

WASMを調べているとJavascriptのパフォーマンスの壁を超えるものがWASMと紹介されている記事が多数あります。ただ、WASM自体はJavascriptの置き換えというよりも、補完する機能だと考える方が良いです。

以下にWASMの設計思想が書かれています。 github.com

WASMはセキュリティ上の理由からWASM単体ではブラウザのDOM(ウェブページの構造)を直接操作したり、Web API(ブラウザの機能)を呼び出したりすることができません 。

これらの操作を行いたい場合は、JavaScriptを介して実行する必要があります。

そのため、WASMとJavaScriptの関係は以下のような形になると考えられます。

  • JavaScriptの役割: UIの操作、ユーザーからのイベント(クリックなど)の受け取り、サーバーとの通信、そしてWASMモジュールの呼び出しといった、「ウェブページ全体の司令塔」としての役割を担います。
  • WASMの役割: JavaScriptから依頼された、計算負荷の高い専門的なタスク(数学的な計算、画像データのピクセル単位の操作、物理シミュレーションなど)を、その圧倒的なスピードで黙々とこなす「専門技術者」の役割を担います。

WASMの基本要素

WASMを理解する上で以下の用語を抑えておく必要があります。

  • モジュール (Module): ブラウザによって実行可能な機械語にコンパイルされたWASMバイナリそのものです。
  • メモリ (Memory): WASMが読み書きする、サイズ変更可能なバイトの配列です。JavaScriptの世界とデータをやり取りするための共有スペースの役割も果たします。
  • テーブル (Table): 関数への参照などを格納する、サイズ変更可能な型付き配列です。
  • インスタンス (Instance): モジュールに、実行時に使用するメモリやテーブルなどの状態を組み合わせたものです。

WASMとJavaScriptの特徴比較

両社の違いをより明確にするために以下の表にまとめました。

特徴 WebAssembly (WASM) JavaScript (JS)
実行速度 ほぼネイティブ。事前コンパイル済みのバイナリ形式で高速。 高速だが、JITコンパイルに依存し、パフォーマンスが変動する可能性がある。
主な用途 計算集約的なタスク(画像/動画処理、ゲーム、物理演算、暗号化)。 UI操作、DOM操作、イベント処理、API通信、一般的なアプリケーションロジック。
言語 C/C++, Rust, Goなど多数の言語からコンパイル可能。 JavaScript (およびTypeScriptなどの派生言語)。
DOMアクセス 直接不可。JavaScriptを介して間接的に操作。 直接可能であり、主要な強み。
セキュリティ 強固なサンドボックス環境。静的型付けとバイナリ形式により一部の攻撃に耐性。 サンドボックス環境。動的言語の性質上、XSSなどの脆弱性に注意が必要。
エコシステム 成長中だが、JSに比べるとライブラリやツールはまだ少ない。 巨大で成熟。膨大な数のフレームワーク、ライブラリ、ツールが存在する。

RustとWASMの開発環境を構築する

それでは、ここからはWASMの開発環境を構築していきたいと思います。今回WASMの開発にあたって使うコンパイル可能な言語はRustを使うことを想定します。

なぜRustを選ぶのか?

WASMへのコンパイルが可能な言語はC++やGoがありますが、今回Rustを使う理由は、wasm-packと呼ばれるツール提供がある点になります。

wasm-packは、Rustコードのコンパイルから、JavaScriptとの連携に必要なグルーコード、TypeScriptの型定義ファイル、さらにはnpmパッケージとしての設定ファイル(package.json)まで、すべてを自動で生成してくれます。

そのため、WASMを初めて学ぶ私にとってRustが最もスムーズに開発体験ができると考えています。

開発環境の構築

それでは、開発環境を構築していきます。全て無料のツールで完結をします。

1. Rustのインストール

まずはRustの公式サイトにアクセスし、rustupというインストーラーを使ってRustをインストールします。

www.rust-lang.org

インストールを開始するとターミナルが立ち上がり以下の画面が表示されます。特別な理由がなければ、デフォルトのインストール設定である「1」を入力してEnterします。

これにより、Rustコンパイラ(rustc)やパッケージマネージャー(cargo)など、必要なものがすべて一括で手に入ります 。

2. wasm-packのインストール

ターミナルを起動し、以下のコマンドを実行してwasm-packをインストールします。

cargo install wasm-pack

wasm-packは、Rustコードを、JavaScriptが簡単に利用できるWASMパッケージへと変換してくれるツール群です。

3. プロジェクトの作成

今回の画像フィルターアプリ用のプロジェクトを作成します。ターミナルで任意の場所に移動し、以下のコマンドを実行してください。

cargo new --lib wasm-image-filters

wasm-image-filtersがプロジェクト名になります。

ここで重要なのが--libというフラグです。このフラグによって、これから作ろうとしているプログラムが単独で動く実行ファイルではなく、他のアプリケーション(今回はJavaScript)から呼び出される「ライブラリ」であることをcargoに伝えることになります。

Cargo.tomlを修正する

プロジェクトが作成されると、wasm-image-filtersというディレクトリの中にCargo.tomlというファイルが生成されています。これは、プロジェクトの設定や依存関係を管理する非常に重要なファイルです。

このCargo.tomlのファイルを以下のように置き換えます。

[package]
name = "wasm-image-filters"
version = "0.1.0"
authors = ["あなたの名前 <you@example.com>"]
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"
# 画像処理ライブラリ「image」を追加
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }

それぞれの項目は以下の通りです。

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

WASM開発における最重要設定の一つです。Rustコンパイラに対して、「他の言語から呼び出されるための動的ライブラリ」を生成するように指示します。WASMモジュールはまさにこの形式に当てはまります 。

wasm-bindgen = "0.2"

RustとJavaScriptの世界を繋ぐ、wasm-bindgenへの依存を宣言します。データ型の変換などを自動で行ってくれます 。

image = {... }

今回の画像処理の中心部で、Rust製の強力なライブラリです。最終的なWASMファイルのサイズを小さく保つために、default-features = falseで標準機能を無効にし、features = ["png", "jpeg"]でPNGとJPEG形式のサポートのみを明示的に有効にしています。

グレースケールフィルターをRustで実装する

次に実装を行います。最初に実装するものは画像を白黒にする「グレースケール」機能を実装していきます。

データの流れ

ソースコード書く前にJavaScriptで画像を受け取り、Rustで処理をして、再びJavaScriptに返すまでのデータの流れは以下のイメージです。

1. JavaScript側: 画像の受け取り

ユーザーがアップロードした画像ファイルを読み込み、ArrayBufferという生のバイナリデータ形式に変換します。

2. JavaScript側: 画像を8ビット整数の配列として扱えるようにする

ArrayBufferをUint8Arrayという「8ビット整数の配列」として扱えるようにします。これは単なるバイトの羅列です 。

3. JavaScript側: Rust関数に引数として8ビット整数の配列を渡す

Uint8Arrayを、WASMモジュールからエクスポートされたRust関数に引数として渡します。

4. Rust側: Rustのバイトスライス(&[u8])に変換

wasm-bindgenがこのUint8Arrayを自動的にRustのバイトスライス(&[u8])に変換します。

5. Rust側: グレースケール処理

受け取ったバイトスライスをimageクレート(ライブラリのこと)を使って画像として解釈し、グレースケール処理を施します。

6. Rust側: 処理後の画像データを返す

処理後の画像データを、再びバイトのコレクション(Vec)として関数から返します。

7. JavaScript側: Uint8Arrayに変換

wasm-bindgenがこのVecを、JavaScriptが扱えるUint8Arrayに変換します。

グレースケール関数の実装

それでは、グレースケール関数を実装していきたいと思います。 src/lib.rsファイルを編集していきます。

use wasm_bindgen::prelude::*;
use image::{io::Reader, ImageFormat};
use std::io::Cursor;

// JavaScriptのconsole.logをRustから呼び出すための設定(デバッグに便利!)
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn grayscale(image_data: &[u8]) -> Vec<u8> {
    log("Rust: grayscale関数が呼ばれました!");

    // 1. バイト配列から画像を読み込む
    // どんなフォーマット(PNG or JPEG)かを自動で推測してくれる
    let image_format = image::guess_format(image_data)
       .expect("画像フォーマットの判別に失敗しました");
    let mut reader = Reader::new(Cursor::new(image_data));
    reader.set_format(image_format);
    let img = reader.decode().expect("画像のデコードに失敗しました");

    // 2. グレースケール変換を実行(imageクレートがすべてやってくれる!)
    let grayscale_img = img.grayscale();

    // 3. 変換後の画像をPNG形式のバイト配列として書き出す
    let mut buf = Vec::new();
    grayscale_img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)
       .expect("画像のエンコードに失敗しました");
    
    log("Rust: グレースケール処理が完了しました。");
    buf
}

それでは、1行ずつ処理内容を見ていきます。

use...

必要なモジュールをインポートしています。

#[wasm_bindgen] extern "C"... log

JavaScriptのconsole.log関数をRust側から呼び出せるようにしています。これで、WASMモジュール内のデバッグが格段に楽になります 。

#[wasm_bindgen] pub fn grayscale...

'#[wasm_bindgen]属性を付けることで、このgrayscale関数がJavaScript側から呼び出せるよう公開(エクスポート)されます。引数としてバイトスライス(&[u8])を受け取り、バイトのベクター(Vec)を返す関数です。

image::guess_formatとReader

imageクレートは渡された生データがPNGなのかJPEGなのかを自動で判別し、適切に画像をデコード(解釈)してくれます。

img.grayscale()

この1行は、imageクレートが複雑なグレースケール変換の計算をすべて行います。

write_to

処理が完了した画像を、再びバイトのストリームに変換(エンコード)しています。ここでは、画質が劣化しないPNG形式で出力しています。このバイト配列が、最終的にJavaScriptへと返されます。

WASMモジュールのビルド

WASMモジュールをビルドします。ターミナルでプロジェクトのルートディレクトリ(wasm-image-filters)にいることを確認し、以下のコマンドを実行してください。

wasm-pack build --target web

成功すると、プロジェクト内にpkgというディレクトリが新しく作成されます。この中には、コンパイルされたwasm_image_filters_bg.wasmファイルと、それをJavaScriptから簡単に使うための「グルーコード」であるwasm_image_filters.jsファイルなどが格納されています。

まとめ

今回の記事はここまでとします。今回はWebAssemblyが何であるかを説明し、そのうえで開発環境をゼロからセットアップし、Rustで最初の画像フィルターを作成、コンパイルするところまでをご説明しました。次の記事では、実際に、ユーザーが画像をアップロードし、ボタンをくりっくしてフィルターがかかるUIを構築していこうと思います。ぜひ次回の記事も読んでいただけたら幸いです。 今回の記事がWASMを学びたい方の参考になっていれば幸いです。