0x90

一回休み

RustでWebフロントエンド開発はできるのか?

Rust Advent Calendarの13日めです。

イントロダクション

Rustを語るときしばしば「RustはWebフロントエンド開発もできる」ということが言われます。もちろん、RustはLLVMフロントエンドであり(LLVMから見てClangっぽく見えるように頑張っている)、emscriptenを使うことで用意にasm.js/wasmにできるわけですが、そこにとどまらずRust自体がオフィシャルにemscriptenバックエンドをサポートしています。更にごく最近、LLVM自体のwasm backendもサポートされました。

さて、では実際RustでWebフロントエンド開発はできるのでしょうか。もちろん、コストの掛かる何らかの特別な処理を行うためにJavascriptからRustの関数を呼び出したり、あるいはこちらの方がされているように、emscriptenOpenGL implementationを呼ぶことでOpenGLメインのアプリケーションを移植することなどは標準的機能の中で行うことができます。しかし、私を含め多くの人が「Webフロントエンド開発もできる」言語と聞いてイメージするのは、完全に今のJavascriptを置き換えるような言語ではないでしょうか。そこで不可欠なのは、DOM操作を始めとする、ブラウザのNative APIを呼ぶことであると思います。

前置きが長くなりましたが、この記事では「RustからJavascriptの関数を呼ぶことができるか」ということをまとめていきます。実はぼくも今年前半くらいはそんな感じのコードを書いたりしていたのですが、最近別のアプローチによるライブラリが出てきたりなどして界隈が結構活気づいてきているような感じもあるので、そんなところを含めてかけたらなあと思います。

C++からJavascriptの関数を呼ぶ - embind

Rust Advent CalendarにもかかわらずしょっぱなからC++の話をしてアレなのですが、emscripten + C++の環境では、すでに高レベルでC++Javascriptの連携ができる環境が整っています。それがemscriptenが提供するライブラリの一部であるembindと、それをうまく使うためのヘッダファイルであるval.hです。val.hを使えば、たとえば

var name = prompt("What is your name?");
alert("Hi, " + name + "!");

と等価なコードを

#include <string>
#include <emscripten/val.h>

using std::string;
using emscripten::val;

int main() {
  string name = val::global("prompt")(string("What is your name?")).as<string>();
  val::global("alert")("Hi, " + name + "!");
  return 0;
}

とかけます。コンパイル

$ emcc --bind -o valhtest.html valhtest.cpp

とします。オブジェクトの生成とかもできたりして、ここに使い方が若干書いてあります。

embindをRustでラップする

同様のことをRustでやりたい…!と思ったときにまず考えつくのは、embindをRustでラップすることです。embindの本体は上記のval.h及びbind.hそしてwire.hにあります。闇魔法templateを駆使しているコードなので慣れないと追いかけるのが大変ではありますが、結局のところval.h, bind.h内にある、emscripten::internal中の_em*関数を呼んでいるだけです。ですから、ffiを書く要領で、Rustによるラッパライブラリを作ることができます。幸いRustのgenericsもそこそこに強力なので、マクロと合わせてC++ templateで実装されたコードの大半はRustに問題なく移植することができます。これが、まさにぼくが書いていたこのコードがやっていたことでした。また他にもこの方なども書かれています。

embindがやっていることとその実装

さて、せっかくなので実際embindがどのような操作をやっていて、どうやってRustに翻訳できるかを少しまとめておきたいと思います。興味のある方はval.h, bind.hの、emscripten::internal名前空間あたりを一緒に見てください。ただ、この小節はおそらくほとんどの方にとってどうでもいい内容になることが予想されますので、適当に読み飛ばしていただいたほうがいいかもしれないという気もします。(ちなみにこの小節を書くのに自分のコードを見直していたのですが、すげえ見通しの悪いコードがたくさんあって少し嫌になりました…)

まず、bind.h中の_embind_register_*系の関数ですが、これはemscriptenに「型を登録する」関数群です。では、「型を登録する」というのはどういうことかを説明しておきましょう。最終的にやりたいことというのはJavascriptの関数をC++あるいはRustから呼ぶということなのでした。そのためには、val.h中の_emval_call*系の関数を呼ぶ必要があります。その際、関数の引数にどの型の変数を入れているのかを指定してやらなければなりません。なぜかというと、型ごとにC++/RustからJavascript上に翻訳する方法が違うからです。具体例を挙げましょう。ぼくのコードで言うとここJSSerializeトレイトの部分です。(余談ですがここは本当は#[repr(C)]なstructを使うべきだったのですがそうなっておらず、読みづらいです。すいません。)ここにある、EM_GENERIC_WIRE_TYPEというのが、64bit幅の値で、「C++/Rustの変数をJavascript上で表現したもの」になっています。isizef64などの数値型では、自分自身を浮動小数にキャストして詰めれば、すなわち「Javascript上での値」になります。一方、文字列の場合、

#[repr(C)]
struct SerializedStr {
  len: i32,
  pointer: *const char,
}

というように、長さと文字列実体へのポインタを格納した64bitの構造体が「Javascript上での値」になります。もちろん呼ばれる関数自体は普通のJavascriptの関数で、ポインタとかなんとかは知ったこっちゃないですから、emscriptenのライブラリがこの「Javascript上での値」を更に「Javascriptの値」に翻訳してやる必要があります。この例でわかるように、型ごとにうまく翻訳の必要があるので、C++/RustからJavascriptの関数を呼ぶ際は、「C++/Rust側がどの引数を渡したつもりになっているのか」ということを教えなくてはなりません。そのため、各型ごとに一意のIDが必要なのですが、そのIDを登録する関数が_embind_register_*系の関数なのです。上の型の登録とシリアライズ(=「Javascriptの値」への翻訳)さえできてしまえば関数を呼ぶところはかんたんで、_emval_get_method_callerなどを適当に順序よく呼んであげるだけです。

ちなみに、この実装をする中で一番おもしろかったのは無名クロージャシリアライズ部分でした。しかも、実はembindは無名クロージャシリアライズを提供していない(!)のです。みなさんもご存知のように、Javascriptというのは基本的にコールバックがないと何もできない言語ですので、RustのクロージャJavascriptクロージャに変換する必要があります。これは最初の方に書いた、「コストの掛かる何らかの特別な処理を行うためにJavascriptからRustの関数を呼び出したり」に対応している操作ですから、基本的には普通のasm.jsと同様にできそうな気がします。ところが、これはC++ラムダ式などでもそうですが、原理的には一度環境をキャプチャしてしまうとクロージャはただの関数ポインタではなく、キャプチャした環境の値と、関数ポインタをセットにした値にならなくてはいけません。つまり、クロージャFnなりなんなりの「呼び出し可能」traitを実装した、寿命を持つ構造体であるわけです。ですので、RustのクロージャJavascriptから呼ばせるためには、まずクロージャ構造体をヒープ上に移動し(Box<Fn(A1) -> B>など)、そのポインタ(ちなみに、Fnはfat pointerなので、Box<Box<Fn(..) -> ..>>としないとポインタサイズが2 * std::mem::size_of(usize)になってしまい、うまくいきません)を代わりにJavascriptへと翻訳します。呼ぶ際にポインタからFnのBoxに戻してやればよいわけです。幸いなことに(というか必要なのでw)、_embind_register_function中では関数を登録する際一緒にinvokerという関数ポインタを登録でき、そのinvoker内でポインタからBox<Box<Fn(..) -> ..>>に戻してあげれば良いわけです。長くなってきたのでそろそろやめますが、、、実装はここにあります。(ほんとうはこの問題を解決するのをサボっているので、複数回呼ばれるとうまく動かないですwやるだけなんですがモチベがわかず。。)

emval-rsを使ってどう書けるか

ということで、上で書いたJavascriptに対応するコードは、次のようになります。

extern crate emval;

use emval::*;

fn main() {
    let window = JSObj::global("window");
    // JSObj::global("prompt").call(args!("What is your name?")) でも可
    let name: String = window.call_prop("prompt", args!("What is your name?"));
    window.call_prop::<()>("alert", args!(format!("Hi, {}!", name)));
}

とかけます。大体一対一対応しているのがわかると思います。コンパイル

$ EMMAKEN_CFLAGS="--bind -s NO_EXIT_RUNTIME=1" cargo build --target=asmjs-unknown-emscripten

です。(embindのせいもあり、かなり遅いです。ぼくの環境(Core i7 6500U, 8GB RAM, WSL上のRust)で30秒弱かかります)コードはここにおいてみました。

embind相当のものをRustで実装する - stdweb

さて、上ではemscriptenの機能をRustから叩く方針について議論しました。ですが、wasm32-unknown-unknownのように、emscripten非依存のバックエンドがrustcに追加された今、このやりかたで果たしていいのだろうかという気がしてきます。そこで考えられるのが、embind相当の機能をRustで実装してしまおうという考え方です。メリットとしては、emscriptenに(あまり)依存しないこと、上のラッパのようにemscriptenのinternalなAPIを叩かなくてよいことが非常に大きいです。一方、デメリットとしては、まずメモリ管理を初め、厄介な生Javascriptを書かなくてはいけないことがあります。かなり気をつけないとメモリリークを起こすことになり、大変です。

このアプローチのライブラリとして最近話題(?)になっているのが、stdwebというライブラリです。stdwebの特徴としては、公開しているライブラリの機能として、JSを直接書けるようにしていることです。というか、stdwebの基本的な機能はむしろ、RustとJavascriptをシームレスに連携することだ、と言ってもよいのでしょう。具体的には、js!マクロを使って、

let hoge  = 1;

js! {
  alert( @{hoge} );
}

と書けます(上述のclosureなどにも対応しています)。実をいうと、この方法を見たときぼくは結構びっくりしました。というのも、拙作を含め、上で挙げた「embindのffiを作る方式」のライブラリ、そしてそもそものembind自体は基本的にJavascriptを駆逐することを目指していたからです。ですが、よくよく考えてみれば、最終的にJavascriptのnative APIのRustラッパを作るにせよ、あるいはJavascriptと連携するにせよ、「Javascriptを意識したRustコード」を書くよりは、「最低限のJavascriptとシームレスにJavascriptと連携できるRustコード」を書くほうが気持ちはいいし、素早く実装できるかもしれません。実際、すでに一部のnative APIに関してはRust wrapperが書かれていて、

extern crate stdweb;

use stdweb::web::*;
use stdweb::web::event::ClickEvent;

fn main() {
    stdweb::initialize();

    document().query_selector("#hoge").unwrap().add_event_listener(move |_: ClickEvent| {
        println!("Hello, world!");
    });

    stdweb::event_loop();
}

のようなコードがかけます。なお、tomlは

[dependencies]
stdweb = '*'

で、emscripten環境変数が設定されているとして

$ cargo build --target=asmjs-unknown-emscripten

でビルドできます。emscripten+wasm32にも対応しているようです。

Conclusion and Discussion

さて、ここまで「RustコードからJavascriptを呼ぶ」話をしてきましたが、いかがだったでしょうか。

JavascriptはRustで置き換えられるのか」ということに関しては、ぼくはまだ答えはノーであるし、今後もそこまで状況は大きく変わらないと思います。理由はいくつかありますが、正直に言ってRustで書くメリットがあまりないことが大きいです。一般的にRustという言語に期待することとしてはゼロコスト抽象と高速な実行速度を両立していることが大きいと思うのですが、ひとたびNative APIをいじりだすと後者に関するメリットはほとんどなくなります。embindを叩くにせよ、stdwebを叩くにせよ、結局内部で普通のJavascriptとして実行しているからです。というかむしろ、vanilla JSより絶対に遅いです。そうすると文法面での期待をするわけですが、まあ正直DOM操作はどの言語で書いてもあんまりかわらなさそうだよね…!という感じがあります(むしろコールバックを多用していくスタイルと所有権は相性がいまいちだなあと感じることも多いです)。まあ、そもそもがval.hがあるのにも関わらずJavascriptを置き換えていないmodern C++を見れば明らかなのかもしれませんが。

ただし一つ例外はあると思って、それはOpenGLをつかったWebアプリケーションなど、ほとんどRustの世界で閉じているアプリケーションでごく一部XHRなどのnative APIを使いたくなった場合です。このようなときはこれらのbinding libraryが重要になってくるでしょう。実際、val.hもぐぐるとこのような使い方がヒットします。

また、今後のasm.js/wasm方面での発展にも気を配る必要があります。例えばwasmやasm.js上の関数として、Javascript native APIに対応するAPIができればまた状況は変わってくるでしょう。ただまあこの手の話はぼくの知る限りないので、数年スパンくらいの長期で見てやらないとという感じです。そもそも需要があるか?という話もあります。さらに、そのようなものができたとしても、「書きやすい」形の言語を追い求めると結局Javascript的なものに収斂してしまうのではないかとも思います。(Promiseとかはもっとモナド色を全面に出していったほうがいいような気もしますが。)

ということで、2017年のRust Webフロントエンド開発の適当な感想としてはこのような感じになります。とりとめのない文章でしたが最後まで読んでくださりありがとうございました。アドベントカレンダー初めてなので緊張してます。質問、コメント、ご指摘等ありましたらぜひお願いします。きっとなにか勘違いをしているかもしれないので…。