Parcel+Rustで超お手軽WebAssembly体験

Last updated at

warning

この記事は最終更新から3年以上経っています。情報が古くなっている可能性が高いです。

Rust を import できるモジュールバンドラ

Parcelとは「Blazing fast, zero configuration web application bundler」を謳い文句にするフロントエンドのモジュールバンドラ。 index.htmlをエントリポイントとして多様なアセットを自動判別しバンドルすることができる。 Webpackが「JS 以外のファイルも import できる JS バンドラ」(1)なのに対して、こちらは「今のフロントエンドに必要なものをよしなに纏めてくれるモジュールバンドラ」といったところみたい。

この、「今のフロントエンドに必要なもの」はどうやら何でも取り込めるようにしたいようで、なんとバージョン 1.5.0 のリリースで wasm(WebAssembly)とRustがサポートされた。

(1) ... プラグインで他のファイルも吐けるが基本は JS ファイルのためのバンドラ

Rust って何よ

Rust とはなんぞやという人のために適当に説明すると、Rust とは C や C++といったプログラミング言語の置き換えを狙うシステムプログラミング言語である。 特徴としては「GC はないけどメモリの手動管理も不要」だとか「関数型のフレーバーが強く安全で高速なプログラムが書ける」とかいろいろある。 ただ、ぶっちゃけフロントエンド的には「WebAssembly を生成できる(2)プログラミング言語」ということさえ覚えておけばとりあえずは問題ない。

(2) ... ちなみに asm.js も吐ける

普通に WebAssembly を使うには...

Parcel を使わずに WebAssembly を使用するのは正直めんどくさい。 まず Rust や C なんかを.wasmに変換し、それを fetch API や XHR で読み込み、内容を WebAssembly API で生成して、取得したオブジェクトの中のプロパティを参照して...とする必要がある。 何よりも.wasmファイルという中間ファイルが必要になってくる上、開発環境のセットアップも必要になる(3)のがかったるい。 というか何より、今時のフロントエンドエンジニアなら JS で使う処理は JS で import したいよね!

(3) ... ぶっちゃけそこまで手間ではないが、複数人で開発するときは面倒になる

試してみる

ということで、Parcel を使って超お手軽に wasm 体験をしてみよう! ちなみに実際に動かすまでに叩くコマンドは 4 つだけ。やばい。

前提環境

  • Node.js(nvm でも)インストール済み(この記事では npx を使うため、npm@>=5.2.0を推奨)
  • Linux とか MacOS(他 UNIX 系 OS なら多分大丈夫)
    • Windows は知らない
  • 適当に作業用ディレクトリが切ってある

Rustup のインストール

Parcel の Rust サポートを使うために唯一必要な依存物。 Rust とかその周辺ツールを簡単にインストールしてくれる便利ツールで、Rust を使うならほぼ必須(らしい)。

  1. Rustup の公式サイトに書いてあるとおりに、表示されているコマンドをターミナルで叩く

これで事前準備はおわり。

Parcel のインストール

作業用ディレクトリでいつものように npm を初期化。

$ npm init
# 適当に質問に答える

parcel 本体をインストール。

$ npm i --save-dev parcel-bundler

アプリケーションを書く

まずエントリポイントとなるindex.htmlを適当に書く。

<html>
  <body>
    <div id="app"></div>
    <script src="./index.js"></script>
  </body>
</html>

次にindex.jsを書く。

import { add } from "./math.rs";

document.querySelector("#app").innerHTML = add(1, 2);

最後にmath.rsを書く。.rsは Rust のソースコード拡張子。

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

これで、「数値を足す wasm 内関数」を import して実行、表示するアプリケーションができた。

動かしてみる

ローカル npm パッケージを起動する npx を使って parcel を実行する。

$ npx parcel index.html

# npxがない場合は以下のコマンド
# node node_modules/.bin/parcel index.html

起動に成功するとServer running at http://localhost:1234みたいに出力されるため、ブラウザに表示されたアドレス(この場合はhttp://localhost:1234)を打ち込んでみると、作成したアプリケーションが起動しているのがわかるはずだ。

wasm の力を体感してみる

これだけだと処理速度もへったくれもないので重たい処理を動かして wasm の速さを体感してみよう。 お題として、フィボナッチ数の計算速度を Javascript と比較してみる。 計算方法は Wikipedia にのってるやつをそのまま使うものとする。

まず、先ほどのmath.rsに以下の内容を追記する。

#[no_mangle]
pub fn fibo(n: i32) -> i32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibo(n - 2) + fibo(n + 1)
    }
}

次にindex.jsの import 文で作成したfibo関数を読みこむようにし、

import { fibo, add } from "./math.rs";

以下の内容を追記する。 変数timesの値は PC スペックによっては下げた方がいいかも。

const fiboJS = (n) => {
  switch (n) {
    case 0:
      return 0;
    case 1:
      return 1;
    default:
      return fiboJS(n - 2) + fiboJS(n - 1);
  }
};

const times = 40;

console.time("wasm");
fibo(times);
console.timeEnd("wasm");

console.time("js");
fiboJS(times);
console.timeEnd("js");

ファイルを保存すると、ブラウザの内容が自動更新される。 ブラウザの開発者ツール(Ctrl+Shift+I)を開くと、コンソールタブに wasm 版関数の処理時間と js 版関数の処理時間が出ているはずだ。

ちなみに以下の環境で上記コードを実行すると wasm 版: 580ms700ms、js 版: 2100ms2200ms となった。

  • Firefox 58.0.2(64bit)
  • Core i5-7500
  • RAM 32GB
  • Linux(ElementaryOS)

実用できるか

wasm の面倒な手続きを踏まずに直接 import できるのは(個人的には)革命的に素晴らしい機能だ。 しかし、現状(1.6.2)では HMR(Hot Module Replacement)も Live reload も効かないので快適に作業できるかと言われたら微妙と答えてしまうだろう(4)。 また、wasm の関数に渡せるのは数値型のみであり、配列や文字列を扱おうとなると wasm の仮想メモリをいじることになり、お手軽さが失われてしまう。 なので、「単純だが処理の重い数値計算」という用途以外には現状では使えないと思う。

ただ、Rust には wasm 側(Rust 側)からブラウザ API を叩くことのできるstdwebというライブラリがあり、それを Parcel からも使えるようにする同じ作者のプラグイン(実験段階)もある。 もしかしたら将来的には Rust でゴリゴリのフロントエンドアプリケーションを簡単に書けるようになるのかもしれない。 stdweb の parcel プラグインについては試してみてから使えそうであればまた記事を書く予定。

(4) ... HMR に慣れすぎた人間の末路である

おまけ

Rust ソースコードの適当な解説

特に関数型言語を触ったことがない人にはいろいろと不思議なソースコードに見えるだろうからざっくりと解説。

#[no_mangle]

デフォルトだと関数名が最適化だかなんだかで勝手にいじくられてしまうため(mangle)、「そのままにしといて!!」とコンパイラにお願いする記述。 #[foo]というシンタックスはAttributesという言語機能で、JS のデコレータを強くしたものだと思っておけばいい。


pub fn add(a: i32, b: i32) -> i32 {
  a + b
}
  • pubエクスポート指定子。JS のexportみたいなもん。
  • fnは JS で言うところのfunction。ちなみに Go 言語はfuncだよ。
  • 関数名と引数名は特に問題はないかと思う。型指定は TypeScript と殆ど同じだけど、返り値の指定にシングルアロー演算子(->)を使うところが独特。
    • ちなみにi32は 32bit の整数型のこと。Integer(32bit)。
    • この関数は「32bit の整数を 2 つ受け取って 32bit の整数を返す公開関数」ということになる。
  • a + b ... Rust は関数ボディの最後にセミコロンなしで式文を書くことで、その値を返すことができる。明示的にreturnすることもできるけど、できるだけ暗黙の return を使ったほうがいいらしい。
    pub fn add(a: i32, b: i32) -> i32 {
        return a + b;
    }
    

match n {
    0 => 0,
    1 => 1,
    _ => fibo(n - 2) + fibo(n - 1)
}
  • matchは関数型言語でよく見るパターンマッチ式。JS のswitch文に似ているけれども、こっちは「式」なので、変数に代入したり値を返したりすることができる。
    • このコードだと暗黙の return でmatchの値を返している。

Rust が気になったあなたへ

上の説明で「Rust すごい!」とか「こんな説明じゃわかんねぇよ!」ってなった人は Rust のThe Book(英語)を読むと幸せになれる。 英語だけれどもかなりわかりやすいのでおすすめ。

日本語版もあるみたいだけどバージョンどうなってんのかとか更新状況とかは不明。英語読めるなら英語版を読んだほうがいいと思う。