Parcel+Rustで超お手軽WebAssembly体験

#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(英語)を読むと幸せになれる。
英語だけれどもかなりわかりやすいのでおすすめ。

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