[Rust] WebAssemblyを用いたウェブフロントエンド開発入門


本記事ではフロントエンド開発で Rust×WebAssembly (wasm) を用いる方法を説明します. 巷のチュートリアルの多くは Node.js の利用が前提で npm publish して終わっていますが, フロントエンドの実装の一部を Rust で行うだけならば Node.js はまったく必要なく, HTML/JS と Rust さえあれば十分なはずです. なので本記事に Node.js 関係のツールは登場しません1. 特に Apache や nginx などで動かしている静的なウェブページ (あるいは GitHub Pages) で Rust コードを動かそうと思った場合, この記事を読んでから取り組むとスムーズにいくと思います.

本記事の目標

Rust を WebAssembly にコンパイルしそれを HTML/JS 側から呼ぶことで, 普段 JS で書いている処理を Rust で実装する方法を学びます.

想定する読者像

  • Hello world ができる程度の HTML, JS, Rust の知識はある.
    • Rust にほとんど触ったことがない方のために随時🦀マークで参考となる文書へのリンクを貼っておきます.
  • ただちにプロダクションを作りたい訳ではなく, WebAssembly という技術に興味があり触ってみたい.

実務上は yew などのフレームワークを採用する可能性が高いと思いますが, ここでは wasm という技術に入門することが主眼なので, フレームワークなしのミニマルなアプローチでいきます. とはいえ, 本当にミニマルだと wasm と JS が直接やり取りできるのは数値だけで, あとはメモリ領域を通じて間接的にやり取りすることしかできません. それでは文字列の交換さえ一苦労です. しかしこの部分に関しては wasm-bindgen という事実上の標準クレート (ライブラリ) がサポートしてくれるので, このクレートを使用すれば解決します.

なおこの読者像はひと月前の私そのものです. 様々なチュートリアルなどを読んだものの頭の中が混沌としていたため, 知識を整理するためにチュートリアル形式でアウトプットしてできたのがこの記事です. 記事を書きあげてから考えると The wasm-bindgen Guide と内容が丸被りでこの記事の存在意義が若干疑わしいですが, 書いてしまった以上お蔵入りするのももったいないので公開します.

環境構築

まずは Rust をインストールします. 公式サイト の手順に従って rustup が使えるようにしてください. その後,

$ rustup target add wasm32-unknown-unknown

により wasm ターゲットを追加します. さらに, 最終生成物のコンパイルを簡単化してくれる wasm-pack というツールをインストールします.

$ cargo install wasm-pack

wasm-pack は Rust を wasm にコンパイルするだけでなく, それを JS から呼ぶグルーコードをも出力してくれます. wasm-bindgen を使用するなら事実上必須です.

また, 動作確認のためにローカルで動作するウェブサーバが必要ですが, 例えば VSCode の Live Server で大丈夫です.

Hello world

新しい技術に手を染めるときは最初に Hello world しないと落ち着かないですよね.

プロジェクト作成

任意の作業ディレクトリで hello-wasm というプロジェクトを作成し, その中に移動します.

$ cargo new hello-wasm --lib
$ cd hello-wasm

この時点で Cargo.toml, src/lib.rs というふたつのファイルが存在するはずです (git 関係は説明略). Cargo.toml を書き換えて crate-type を指定し, wasm-bindgen へ依存します 🦀.

Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Rust の実装: Hello world

それでは src/lib.rs を実装していきます. なおデフォルトで単体テストが書き込まれています🦀が必要ないので消して大丈夫です.

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace=console)]
    fn log(s: &str);
}

#[wasm_bindgen(start)]
pub fn run() {
    log("Hello, world!");
}

おおよそ見たままかと思いますが, 簡単に説明します. 最初の行で wasm-bindigen クレートから必要な諸々を導入しています. extern "C" の部分は JS 側で定義された関数が存在し, それを Rust から呼びますよ, という宣言です🦀. #[wasm_bindgen(js_namespace=console)] というアトリビュートをつけることで名前空間 consolelog という関数, つまりいつもの console.log が宣言されています (こちらもご確認ください).

そして #[wasm_bindgen(start)] アトリビュートつきの関数は, この wasm コードを読み込むときに自動的に実行されます. 今の場合, ブラウザのコンソールに Hello, world! と出力されるはずです.

Rust コードが完成したら, wasm-pack を用いてビルドします. いまフロントエンド開発でブラウザから呼び出す用の wasm が欲しいので --target web と指定します2.

$ wasm-pack build --target web

これにより新しくディレクトリ pkg が作成され, その中に hello_wasm_bg.d.ts, hello_wasm_bg.wasm, hello_wasm.d.ts, hello_wasm.js, package.json という 5 つのファイルが生成されます. .wasm ファイルが目標の wasm バイナリですがこれを直接用いるのではなく, .js ファイルを呼び出すことで間接的に呼ばれます.

HTML と JS の実装

カレントディレクトリに index.html を作成し, 以下の内容を書き込みます.

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Hello, WebAssembly!</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <div id="app"></div>

    <script type="module">
        import init from './pkg/hello_wasm.js';
        async function run() {
            await init();
        };
        run();
    </script>
</body>
</html>

見ての通り, pkg/hello_wasm.js を呼び, それを実行することで内部で wasm ファイルが呼ばれます. なお初期化関数は init という名称になります.

以上の実装が完成したらローカルサーバを起動し, index.html をブラウザから読み込みます. するとコンソールに Hello, world! と出力されているのが確認できるはずです. 以上で Hello world 編が終わりです.

Rust の関数を JS から呼ぶ

wasm の典型的な利用方法は, JS では計算に時間がかかる処理を wasm で実行することで高速化するというものです. 本節では Rust で実装した Fibonacci 数を計算する関数および FizzBuzz を JS から呼んでみます.

Rust 実装

Rust 実装は見ての通りで, 特に説明する点はないです (🦀 match, 🦀 to_string).

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        n => fibonacci(n-1) + fibonacci(n-2),
    }
}

#[wasm_bindgen]
pub fn fizzbuzz(n: u32) -> String {
    match (n%3, n%5) {
        (0, 0) => "FizzBuzz".to_string(),
        (0, _) => "Fizz".to_string(),
        (_, 0) => "Buzz".to_string(),
        (_, _) => n.to_string(),
    }
}

既に指摘したように, wasm の関数を JS から呼ぶときには本来は数値型しか扱えません. wasm-bindgen が良しなにグルーコードを生成してくれるため, String (あるいは Vec<i32> 🦀など) を返す Rust 関数を普通に実装すれば JS 側で扱えるようになるのです.

JS 実装

HTML 部分は変更がないため, JS 部分のみ示します.

import init, {fibonacci, fizzbuzz} from '/pkg/hello_wasm.js';
async function run() {
    await init();

    console.log( fibonacci(8) );
    console.log( fizzbuzz(10) );
};
run();

これも見た通りですね. wasm-pack で Rust コードをコンパイルして index.html をブラウザで表示すれば意図通りにコンソールに出力されているはずです.

std の wasm 対応状況

注意ですが, Rust の std のうち以下のものは wasm ではサポートされていません. コンパイルはできますが実行時エラーになります.

  • ファイル I/O std::{fs, io}
  • インターネット std::net
  • スレッド関係 std::thread

このうちマルチスレッドに関しては現在 wasm での対応が進められていますが, 2020年3月時点ではまだ実用段階には達していないようです.

Rust から JS の機能を呼ぶ

さて, せっかく Rust からあれこれできるようになったので, できるだけ JS を書かずに Rust ですべてを完結させたいという欲求が湧いてきます. web-sys というクレートを用いることで, JS 機能を Rust 側から呼ぶことができます. 様々な機能がありますが, 詳細は wasm-bindgen のドキュメント の 1.9 節以降を読んでもらうことにして, ここでは DOM API を通じた HTML 要素の操作だけを取り上げます.

テキストの書き換え

web-sys の機能はデフォルトで無効で, フィーチャーフラグを通じて明示的に有効化する必要があります. ここで必要なフラグは次の 3 個です.

Cargo.toml
[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "Element",
    "Window",
]

それでは Rust から ID "app" を持つ HTML 要素を取得し, そのテキストを書き換えてみます.

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn run() {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();

    let app = document.get_element_by_id("app").unwrap();
    app.set_inner_html("Hello from Rust!");
}

JS でよく見る記述を Rust に置き換えただけですが, これでうまくいきます.

HTML 要素を新規作成

次の例は Rust 側で新しく HTML 要素を作成し, DOM ツリーに付け加えるというものです. このためには HtmlElement フィーチャーが追加で必要です.

Cargo.toml
[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "Element",
    "HtmlElement",
    "Window",
]

Rust 実装はやはり JS の実装を素直に翻訳したものになります.

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn run() {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();

    let app = document.get_element_by_id("app").unwrap();

    let p = document.create_element("p").unwrap();
    p.set_inner_html("色は匂へど散りぬるを");
    app.append_child(&p).unwrap();
}

最終成果物の配信

本記事で作成したもののうち, 実際にブラウザに渡されているものは以下のものだけです.

  • index.html
  • pkg/hello_wasm.js
  • pkg/hello_wasm_bg.wasm

なので本記事に基づいて作成したプロジェクトを公開する場合, これらのファイルをウェブサーバに配置すれば正しく表示されるはずです (GitHub Pages のサンプル参照). なお wasm の MIME タイプは application/wasm ですが, 環境が古かったりすると MIME タイプが正しく指定されず動かない可能性があります. 気を付けてください.

終わりに

これで Rust コードを wasm にコンパイルし, ブラウザで利用することができるようになったと思います. この記事で説明したのはあくまで入門部分で, Rust をウェブ開発で使用するにはこの後様々な選択肢があります.

  • The Rust Wasm Book を読んでより高度な応用例を扱う (ライフゲームの実装).
  • yewSeed, Kagura などのフレームワークを用いてフロントエンド開発.

参考文献


  1. ただし Node.js でも wasm は動くので, Node.js から呼ぶサーバ側の処理を Rust で実装する場合にも本記事の知見は生かせるはずです. 

  2. Node.js から呼ぶ場合は --target nodejs です.