WebAssemblyを使ってRustをウェブブラウザで動かす


WebAssemblyとは

javascriptのように、ウェブブラウザ上で動くプログラミング言語です。

ウェブブラウザ上で動作するプログラミング言語といえば、javascriptが代表格だと思います。(というか他に思いつかない)
ですが、元々文字だけの静的なページにアニメーションなど、動的にwebページを装飾したいという動機で作られたスクリプト言語なので、開発が複雑かつ大規模になるにつれ、実行速度に影響が出るという弱点を抱えています。

その問題を背景として登場したのがWebAssembly(WASM)です。

この言語の特徴として挙げられるのは、WebAssemblyという言語を直接記述するわけではなく、C/C++/Rustなど、他言語からコンパイルすることによって生成される、という点です。
つまり他言語で培われた資源をそのまま活用でき、生成されるコードはアセンブリのような低水準言語になるので実行速度が速いという、ちょうどjavascriptの苦手な部分を補完するような立ち位置となっています。

Rust -> WebAssembly

RustからWebAssemblyへ変換してブラウザ上で動かしてみたいと思います。

Rustのインストール

$ brew install rust

WebAssembly用のbuildpackをインストール

$ cargo install wasm-pack

cargoはRustのパッケージマネージャ兼ビルドシステムである便利屋さんです。

テンプレートをcargoを使って生成する

$ cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name rust2wasm

リポジトリはこちらです。

もしcargo-generateのインストールが済んでいない場合は

$ cargo install cargo-generate

をしてください。

生成されたファイルの中で、src/lib.rsがプログラムのメイン部分になります。

lib.rs
mod utils;

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, rust2wasm!");
}

Rustからjsのalert関数を呼び出す想定です。
wasm_bindgenによってjsからRustで書かれた関数を呼び出したり、Rustがjsの例外をキャッチできるようになったり、と橋渡し役を担っているライブラリです。

pub fn greet() {
    alert("Hello, rust2wasm!");
}

定義したgreetをjavascriptから呼び出すことでalertを出すイメージです。

ビルドする

$ wasm-pack build

ビルドをすると、プロジェクトにpkgというディレクトリが生えます。
/pkg/rust2wasm_bg.wasm というファイルが、RustからコンパイルされたWebAssemblyファイルで、これらをjsでラップしているファイルがpkg/rust2wasm.jsです。

pkg/rust2wasm_bg.wasm
// 省略
export function greet() {
    wasm.greet();
}

jsのgreetという関数が、wasm(WebAssembly)へ変換された元Rustのgreetを呼び出していますね。

ブラウザ上で動かしてみる

本記事ではnpmでwebプロジェクトを生成します。
create-wasm-appというWebAssemblyをブラウザ上で動かすための雛形プロジェクトを利用しました。
プロジェクト内で以下を実行します。appは生成ターゲットのディレクトリなので、適宜変えることが可能です。

$ npm init wasm-app app

appディレクトリに入って、パッケージのインストールをします。

$ cd app
$ npm install

app/index.htmlから、app/bootstrap.jsを介して、app/index.jsをインポートする仕組みなっています。

index.js
import * as wasm from "hello-wasm-pack";

wasm.greet();

hello-wasm-packnpm installでインストールされるパッケージで、WebAssemblyファイルと、それのラッパーであるjsファイルが配置されています。
つまりデフォルトのindex.htmlでは、hello-wasm-packにあるhello_wasm_pack_bg.wasmを呼び出しているということです。npm run startをしてみると、以下のように表示されます。



これをRust側の関数で定義した、"Hello, rust2wasm!"が表示されるようにしたい、というのが目標です。

方針として、Rustから生成したwasmファイルをローカル内でリンクさせて、そこから呼び出すという形を取ります。

まず、pkgディレクトリ内で

$ npm link

を実行します。
その後、appディレクトリで、リンクしたパッケージを利用できるようにします。

$ npm link rust2wasm

これで、ローカルに存在するnpmパッケージを、appにあるプロジェクト内で利用できるようになります。

最後にindex.jsを書き換えます。

index.js
import * as wasm from "rust2wasm";

wasm.greet();

ここまでやると、

ということで、Rustで書かれた関数をjsを介して呼び出すことができました。

Rustの関数を書き換える -> wasm-pack build というサイクルを回すことで開発を進めていくことができます。

まとめ

RustからWebAssemblyにコンパイルして、それをラップしたjsを介して呼び出すことで、Rustで書かれたコードをブラウザ上で動かすことができました。
コンパイルが若干煩雑なところがありますが、現存している言語資産を活用しつつ、js以外でフロント側の開発ができる可能性を秘めているという点で、とても面白みがあって、今後の発展を期待したい技術だと思っています。