前提として、自分の Rustの知識は 1年に1回ぐらい思い立った時にちょろっとやるぐらいで、基礎文法をググりながら、複雑なライフタイムとか書こうとすると手が止まる程度の知識。勘で書いてる。
前提
cargo build --target wasm-unknown-unknown
ができるようになるまでは省略。
調べた感じ、Rust の wasm ビルドでウェブの何かしらをやろうとすると、次のような選択肢がある。
- プレーンな wasm。基本的に数値(float)だけしか扱えない。ポインタの開始位置とサイズを返し、wasm メモリ空間のArrayBufferを自前でデコードする
- https://github.com/rustwasm/wasm-bindgen : ↑で生成された wasm の読み込みラッパーやTSの型定義、Rust 側からJSのメモリ空間を参照する諸々をやってくれるツール。
- js-sys: ↑の中で、より高水準なラッパー。開発中らしくドキュメントが少ない。追記: ECMA262 の API の Date や Console などを実装しようとしていた
- web-sys: もっと抽象度が高そうな何か?開発中らしくドキュメントがない。ちゃんと調べてない。追記: WebIDL から DOM API を生成しようとしていた。
- https://github.com/koute/stdweb wasm-bindgen とは別系列の高水準なJSラッパー。
生で使う以外は、主に stdweb と wasm-bindgen の2系列ある。 両方使ってみた感想だと、 stdweb はすべてを rust で管理しようとする感じで、 wasm-bindgen は webpack などの js エコシステムで連携しながら使うといった感じ。
Yew
stdweb に依存。Redux のパクリ元の Elm のアーキテクチャと仮想DOMを実装している。自分がReactに慣れてるので、これで遊んでみることにする。
カウンター作ってみた
こんなやつ
デプロイしておいた。IE以外は動く https://frosty-clarke-3d3c34.netlify.com
stdweb が emscripten 通せば asm.js ビルドをサポートしているので、頑張れば動きそう。今回は頑張らない。
コードはここ https://github.com/mizchi-sandbox/hello_cargo_web_yew
コード抜粋
#[macro_use]
extern crate yew;
use std::result::Result::{Err, Ok};
use yew::prelude::{App, Component, ComponentLink, Html, Renderable, ShouldRender};
use yew::services::ConsoleService;
struct Model {
console: ConsoleService,
value: i64,
adding_value_text: String,
}
enum Msg {
Increment,
AddByAddingValue,
SetAddingValue(String),
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Model {
console: ConsoleService::new(),
value: 0,
adding_value_text: "".to_string(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Increment => {
self.value = self.value + 1;
true
}
Msg::AddByAddingValue => {
match &self.adding_value_text.parse::<i64>() {
Ok(v) => {
self.value = self.value + v;
self.adding_value_text = "".to_string();
}
Err(_) => {
self.console.log("Parse error");
}
};
true
}
Msg::SetAddingValue(value) => {
self.adding_value_text = value;
true
}
}
}
}
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
<div>
<div>
<button onclick=|_| Msg::Increment,>{ "Increment" }</button>
</div>
<div>
<button onclick=|_| Msg::AddByAddingValue,>{ "Add" }</button>
<input
oninput=|e| Msg::SetAddingValue(e.value),
value=&self.adding_value_text,
/>
</div>
<p>{ self.value }</p>
</div>
}
}
}
fn main() {
yew::initialize();
App::<Model>::new().mount_to_body();
yew::run_loop();
}
ややこしかったのは、文字列をパースするときのパターンマッチで、ちょっと調べた。あと yew 0.5.0 が cargo にリリースされてなかったので、 Cargo.toml で GitHub を直接参照している。ドキュメント や examples が 0.5.0 のものしかない。
性能評価
こういうライブラリ、やたらライブラリサイズが大きかったりしないか?と調べてみた。
ビルドサイズ。
$ ls -l target/deploy
total 368
drwxr-xr-x 5 mz staff 160B 8 18 22:45 .
drwxr-xr-x 7 mz staff 224B 8 18 21:54 ..
-rw-r--r-- 1 mz staff 27K 8 18 22:45 counter.js
-rw-r--r-- 1 mz staff 151K 8 18 22:45 counter.wasm
-rw-r--r-- 1 mz staff 815B 8 18 22:45 index.html
180k。react + react-dom が 130k なのを考えると、問題なし。
実際の評価時間。
特に問題なさそう。
感想
JSXのマクロがあって、React/Elm知ってると、 yew の examples を参照すると素直に書ける感じ。特に戸惑うことはなかった。正直 Rust の数値変換とかパターンマッチの文法思い出すほうが時間かかった。
Rustエコシステム全般の話だが、 nightly じゃないと何かと動かない。だいたいマクロ拡張は nightly 使わないと駄目。wasm 周り特有の事情かどうかは推し量れてないが、 stable に全く人権がないのに、nightly を使ったら自己責任みたいな雰囲気がちょっとつらかった。stable と nightly の間のバージョンがほしい。
実際にウェブアプリ書く場合、 wasm-bindgen + js-sys と stdweb の目指してる世界が似てて、どっちが主流になるかわからない不安がある。できれば相互に乗り入れられるようになってほしい。
しかし思ったよりも現実的にアプリが書けそう、といった感触で、JS資産を参照しなくていい、グラフ可視化ツールみたいな複雑なアプリケーションなら思ってたより選択肢になりそう、といった感想。