Rustで作ったインタプリタをブラウザで動かしてみた
この記事はCA21 Advent Calender 2020の8日目の記事です。
Sammary
最近、Go言語でつくるインタプリタという本を参考にRustでMonkey Languageという仮想の言語を作りながら、インタプリタを実装していました。今回はそれをブラウザで動かして遊んでいたので、その話をできればと思います。
また、wasmを使ってインタプリタをブラウザで動かしたのですが、その中でもYewというフレームワークを使って実装してみたので、現状の使用感や悩んだところなどを残せたらと思います。
成果物について
今回作ったのは以下のようなサイトです。
RUNボタンを押すと処理が走り、一番下のResultと書かれている部分に実行結果が表示されます。
現状の対応状況は以下の通りです。
- String
- Integer
- Array
- 組み込み関数: first,last,rest,push
 
- 組み込み関数: 
- HashMap
- 組み込み関数: insert,remove
 
- 組み込み関数: 
- 共通組み込み関数: puts,len
- Function
- 
if式
- 
let文
- 
return文
具体的には以下のようなコードが動きます。
let map = fn(arr, f) {
  let iter = fn(arr, acc) {
    if(len(arr) == 0) {
      acc
    } else {
      iter(rest(arr), push(acc, f(first(arr))));
    }
  };
  iter(arr, []);
}
map([1, 2, 3], fn(v) { v + 1 });
基本的にはMonkey Languageと同じように動作します。
詳しい文法については、README.mdを参照してください。
インタプリタとは
詳しい仕組みについては、Rustでインタプリタというスクラップにまとめているので、今回はざっくりとした説明のみとさせていただきます。
インタプリタは大きく分けて3つの機能によってコードを解釈していきます。
1つ目が字句解析器(Lexer)というものです。これはただの文字列である入力されたコードを意味のあるTokenに変換していきます。意味のあるTokenというのは、例えば、letという文字列が与えられた時に、LETという定数に変換し、プログラミング言語上での処理を行いやすくします。
2つ目が構文解析器(Parser)というものです。これは抽象構文木(AST)というものに変換する機能を持ちます。ASTというのはLexerで得たTokenを元に正しい順番でTokenを並べていき、1つの木構造の構造体を作るものです。例えば、let x = 1 + 2;というコードがあった時に、LexerはLET IDENTIFIER("x") ASSIGN INTEGER(1) PLUS INTEGER(2) SEMICOLONというToken列に変換します。これを正しい順番に並べて木構造にしていきます。今回の場合は以下のようなASTに変換されます。
LetStatement {
  ident: Identifier { value: "x" },
  value: Expression {
    left: Integer { value: 1 },
    operator: PLUS,
    right: Integer { value: 2 }
  }
}
実際はもう少し複雑ですが、上記のようなASTが作られます。ちなみに今回の場合はASSIGNがない時にエラーを発生させて、処理を止めるといったようなこともParserで行われます。
3つ目が評価(Evaluate)です。これはParserよりも単純で、ASTを元にそれぞれの処理を実際のプログラムに置き換えていきます。例えば1 + 2というコードは"+"を実際のプログラム中の+に置き換えて加算処理を行います。ここは基本的には置き換えていくだけなので簡単です。変数については変数名をkeyにもつHashMapを作成し、そこから値を取り出すことで、変数を実現しています。
実際はGCなどの処理も必要ですが、今回はそこまでやっていません。ですが、インタプリタのかなり詳しい部分について理解できてとても勉強になりました。
wasmについて
wasmとは、ブラウザ上でCやRustなどの言語を扱うことができる技術で、実行速度の向上が期待されています。そのため、今回作ったインタプリタを実行してみるとかなり速く動作しているのがわかると思います。
今回は勉強のために、全てwasmで作っていますが、現状の実際のユースケースとしては、部分的に画像や音声などをフロントエンドで圧縮してから、Requestを送ることで、転送量を減らしたり、ゲームのような複雑なアニメーションを実現するために使用するといったことが考えられます。
Yewについて
ここからが本題です。今回インタプリタをブラウザで動かすにあたり、Yewを使用しました.Yewを使用した理由は、wasm系のフレームワークを使ったことがなかったので興味があったことが大きいです。
Yewは12月08日現在でv0.17.3で、まだまだ発展途上のフレームワークです。そのため、ドキュメントもほとんど完成しておらず、ちょっと凝ったことをする場合はYewのExampleを見ることになります。
開発環境
フロントエンドを開発するときはHMRを使用して、コードを修正した時には自動的に再buildして欲しいです。webpack + wasm-packを使っていれば@wasm-tool/wasm-pack-pluginを使って、YewでもHMRを行うことができます。
実際のプロダクトでもwebpackと合わせて@wasm-tool/wasm-pack-pluginを使うことで、wasmとESMを共通でbuildできるのでとても便利です。
使い方は簡単で、以下のように設定するだけです。
module.exports = {
  // ...
  plugins: [
    // ...
    new WasmPackPlugin({
      crateDirectory: path.resolve(__dirname, "crate"),
      outDir: path.resolve(__dirname, "src", "wasm"),
    }),
    // ...
  ]
  // ...
};
crateDirectoryにwasmのプロジェクトのpathを指定します。outDirには出力するディレクトリのpathを指定します。指定したファイルを好きなファイルから読み込む形になります。ファイル名はデフォルトでindex.jsになります。
import("./wasm").then(mod => mod.run_wasm());
wasmのビルドについて
今回はwasm-packを使ってビルドしました。
wasmは i32・i64・f32・f64の数値しか受け取ることができません。そのため、通常であれば、Uint8Arrayを使ってメモリからJavaScriptのデータを受け取ってそれをやり取りすることになるのですが、その辺りの処理をいい感じに出力してくれるのがwasm-packです。
悩んだところ
今回Yewを使ってみた中で悩んだところなどを雑にまとめていけたらと思います。
1. CSSをどのように定義していくか
これは、今後もしwasmが一般化した時にも悩みの種になりそうな部分だと感じました。今回はCSSはwasm部分と分けて、CSSファイルに書き、BEM風のclass名を直接指定する形で対応しました。今後CSS in JS(CSS in Rust)のような記法もYew側でサポートされるかもしれないので今後の動きに期待です。
CSS Modulesのようなものを実現するとしたらどのような感じになるのかも気になります。
2. 大きな値をPropsとして渡すとパフォーマンスに影響を及ぼす
これは悩んだ部分ではないのですが、仕組み的な話で、Yewのpropsは所有権を受け取る必要があるので、値を渡す時にcloneを行います。そのためVecやHashMap、Stringのような大きい値がpropsとして渡されると、cloneがオーバーヘッドになる可能性があります。これを回避するために、std::rc::Rcを使ってcloneするようにして、データを変更する時にはstd::rc::Rc::make_mute()を使って親コンポーネントでデータを変更し、std::rc::Rc::ptr_eq()で変更を確認しすることができます。(Using smart pointers effectively)
また、関連した話で、何度もpropsを渡すような、リスト型のUIでアイテムを繰り返し表示するようなケースで、propsをそのまま渡してしまうと多くの値がcloneされることになるので、これもパフォーマンスに影響を及ぼす可能性があります。この場合はstd::rc::Rcを使ってRc::clone()をするようにすることでパフォーマンスを改善できます。(Memory/speed overhead of using Properties - Yew)
もしRcを使えない、データを修正したいと言った状況のときはNested ListのExampleのようにlinkをpropsとして渡して、イベントを通じてデータを変更する方法もあるようです。
3. ドキュメントが充実していない
公式ドキュメントは入門の内容の部分はしっかりと書かれているのですが、途中から空白のページもあったりするので、Exampleを読むのが大変でした。これについては時間があればコントリビュートとかできればいいなと思ったりしています。
4. バイナリーサイズ最適化
最適化の手法はReducing binary sizes - Yew、Shrinking .wasm Code Size - Rust and WebAssemblyが参考になります。
少しだけ気になったものをまとめてみます。
- wee_alloc: デフォルトのallocatorを小さいものに置き換えることでバイナリーサイズを減らしてくれるライブラリです。allocatorを置き換えてしまうため、runtimeの速度が落ちてしまうのでトレードオフを考えながら使う感じになりそうです。allocator周りの話はRustでwasm用カスタムアロケータを書くという記事が参考になりました。
- 
wasm-opt: binaryenというwasmのtoolkitに含まれるwasm-optというものを使うことでコードサイズをさらに15〜20%節約できそうです。さらにruntime速度も向上させることができそうです。
- Twiggy: これはwasmのバイナリを分析して、バイナリサイズや、関数がバイナリに含まれる理由、使われていない関数を削除した時のサイズなどを出力してくれます。
- このほかにもCargo.tomlのreleaseビルドの設定を変更することで、buildスピードと引き換えに様々な最適化を施すことができそうです。
現状だと圧縮なしで221kbあるのでもう少し小さくなって欲しいなと思っています(インタプリタがでかいのかもしれない)。今回はバイナリサイズを減らすところまでできなかったので。今後上記の最適化を取り入れてみて、改めてまとめてみたいです。
5. Hooks周りも今後に期待
Hooks周りについてもissueが上がっていたので今後に期待したいところです。
まとめ
ここ最近作っていたインタプリタやwasmについてまとめてみました。Rustは個人的にとても難しかったですが、後半の方は慣れてきてスムーズにコードを書くことができたので、今後も少しづつ書いていき、慣れていきたいなと思います。
次はRustでHTML Rendering Engineを作ってみようと思っているので、完成次第また記事を書きたいと思います(多分)。
最後まで読んでいただきありがとうございました。
PR
内定先のCyberAgentでは22卒のエンジニア採用を行っています!


Discussion