FORCIA CUBEフォルシアの情報を多面的に発信するブログ

wasm-bindgenを触ってみよう!RustとWebAssemblyが同時に学べる

2019.12.25

アドベントカレンダー2019 Rust

FORCIAアドベントカレンダー2019 25日目の記事です。

FORCIAアドベントカレンダー最終回を担当します、エンジニアの武田です。

WebAssemblyについて、今まで触ったことがなかったのでこの機会に学んでみました。

業務でRustを書く機会があるためwasm-bindgenを利用してみましたが、こちらのドキュメントのexamplesが非常に良かったためそのご紹介をします。

WebAssemblyとは

高速、安全で効率良く動作することを目指して提案されたWebの標準規格です。詳しくはW3CのSpecificationのDesign Goalsを参照してください。

2019年12月5日にW3Cの勧告となり、HTML、CSS、JavaScriptに次いで4番目のブラウザ上で動作する標準の言語として認められました。

基本的に直接WebAssemblyのコードを書くことはなく、他言語からコンパイルして作成されます。
CやC++、Rust、Go、KotlinやTypeScript(AssemblyScript)などから生成でき、今後さらにサポートする言語は増えていくと考えられます。

高速に実行できる、というメリットは非常に大きいですが、バイナリフォーマットで軽量なため、構造解析とコンパイルが高速という点も魅力的です。

実際のユースケースについてはこちらにまとめられています。やはり画像/動画処理やゲーム、科学シミュレーションなど計算量を必要とされるところがメインの使いどころになりそうです。

wasm-bindgenについて

WebAssemblyとJavaScriptの間のデータの受け渡しをwrapしてくれるツール/ライブラリです。
js-sys(JavaScriptのAPIが利用できる)やweb-sys(documentオブジェクトやwindowオブジェクトなどが利用できる)といったクレートが含まれています。現時点でも非常に多くのAPIが利用可能となっており、フロントの実装すべてをRustで書く、ということも不可能ではなさそうです。

wasm-bindgenのexamplesについて

今回紹介したかったのはwasm-bindgenのドキュメントです。examplesから始まっており、実際に動かして試してみることができます。

wasm-bindgen-cliのインストール

rustはインストール済みの前提です(rustupというツールから簡単にインストールできます)。

$ cargo install wasm-bindgen-cli # こちらも既にインストール済みの場合は不要です

以降のexamplesでnpm run build、もしくはnpm run serveをした場合に

  • wasm-packの有無をチェックしてなければインストール
  • コンパイルターゲットにwasm32-unknown-unknownがなければ追加

自動で実行してくれるようです。事前準備はrustとwasm-bindgen-cliのインストールのみでWebAssemblyが動かせます!

hello-worldを動かす

$ git clone https://github.com/rustwasm/wasm-bindgen.git
$ cd wasm-bindgen/examples/hello_world
$ npm install
$ npm run serve

この状態でhttp://localhost:8080にアクセスしたときにHello, World!!のalertが画面に表示されれば成功です。WebAssemblyでは文字列を扱うのにも工夫が必要ですが、この辺りはwasm-bindgenがWebAssembly、JavaScript間のデータのやり取りをwrapしてくれています。

canvasを触ってみる

おまけでweb-sysクレートを利用しているexamplesであるcanvasを触ってみます。実行するとニコちゃんマークが表示されます。

今回はcanvasのコードを少しいじって別の絵を表示するようにしました。

<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <canvas id="canvas" height="300" width="300" />
  </body>
</html>
use std::f64;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

fn write_some_object(ctx: &web_sys::CanvasRenderingContext2d, x: f64, y: f64) {
    ctx.begin_path();
    let mut rot = f64::consts::PI / 2.0 * 3.0;
    let step = f64::consts::PI / 5.0;
    let outer = 20.0;
    let inner = 10.0;
    ctx.move_to(x, y - outer);
    for _i in 0..5 {
        ctx.line_to(x + rot.cos() * outer, y + rot.sin() * outer);
        rot = rot + step;
        ctx.line_to(x + rot.cos() * inner, y + rot.sin() * inner);
        rot = rot + step;
    }
    ctx.line_to(x, y - outer);
    ctx.close_path();
    ctx.set_line_width(5.0);
    ctx.set_stroke_style(&JsValue::from("gold"));
    ctx.stroke();
    ctx.set_fill_style(&JsValue::from("yellow"));
    ctx.fill();
}

#[wasm_bindgen(start)]
pub fn start() {
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document.get_element_by_id("canvas").unwrap();
    let canvas: web_sys::HtmlCanvasElement = canvas
        .dyn_into::()
        .map_err(|_| ())
        .unwrap();

    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::()
        .unwrap();

    context.begin_path();
    context.move_to(80.0, 130.0);
    context.line_to(150.0, 70.0);
    context.line_to(220.0, 130.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("green"));
    context.fill();

    context.begin_path();
    context.move_to(60.0, 170.0);
    context.line_to(150.0, 90.0);
    context.line_to(240.0, 170.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("green"));
    context.fill();

    context.begin_path();
    context.move_to(50.0, 210.0);
    context.line_to(150.0, 130.0);
    context.line_to(250.0, 210.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("green"));
    context.fill();

    context.begin_path();
    context.move_to(130.0, 210.0);
    context.line_to(170.0, 210.0);
    context.line_to(170.0, 240.0);
    context.line_to(130.0, 240.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("brown"));
    context.fill();

    context.begin_path();
    context.move_to(130.0, 210.0);
    context.line_to(170.0, 210.0);
    context.line_to(170.0, 240.0);
    context.line_to(130.0, 240.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("brown"));
    context.fill();

    context.begin_path();
    context.move_to(130.0, 210.0);
    context.line_to(170.0, 210.0);
    context.line_to(170.0, 240.0);
    context.line_to(130.0, 240.0);
    context.close_path();
    context.set_fill_style(&JsValue::from("brown"));
    context.fill();

    write_some_object(&context, 150.0, 80.0);
}

今までcanvasは利用したことがなかったのですが、位置を移動する、線を引く、線を閉じる、など面白いAPIですね。

JavaScriptでも書けるコードではありますが、ぜひ上のコードをwasm-bindgenを利用して動かしてみて、どんな絵が表示されるか確認してみてください。

さいごに

wasm-bindgenを利用することで比較的簡単にRustのコードをブラウザ上でWebAssemblyとして動作させることができます。

今ある別言語のソースコードからWebAssemblyにコンパイルして、Webで再利用できるようになります。
また、今までWebで使えなかったソースコードをWebで再活用することができます。これからさらにエコシステムも発展していき、WebAssemblyを利用しやすい環境が整ってくるでしょう。

フォルシアではWebAssemblyをプロダクション環境で採用した事例はまだありませんが、高速で快適なWebを目指して、パフォーマンスの問題が起きたときなどに取れる強力な選択肢の一つとしてWebAssemblyの利用も検討していければと考えています。

FORCIAアドベントカレンダー2019は本日で終了となります。皆さん、メリークリスマス&よいお年を!

この記事を書いた人

武田 陽一郎

2012年度新卒入社。旅行会社のシステムを担当。
家でPCを使っていると1歳半の娘がキーボードを叩きに来ることが最近の悩み。