LoginSignup
19
13

More than 1 year has passed since last update.

Rust + seed でWebAssemblyを体験してみる。

Last updated at Posted at 2022-02-07

背景

社内でいろいろ勉強会が開かれているうちの一つ、Rust勉強会に参加しています。
Rust By Example 日本語版を題材に進めているのですが、なんかぼやっと理解している感が強い状態で復習必須という感じです。また、せっかく勉強しているので何か使ってみたいという気持ちも出てきました。

そんな日々に、ふと家の中にあったルービックキューブに目が留まり、YouTubeの解説動画を見ながらそろえました。その後は揃える方法を覚え、分単位の時間で揃えられるようにはなりました。ただ、こちらもステップとその対応を覚えただけです。何故そう回すと揃うのか完全理解している訳では無いです。回した時の色の動きを全面で見たりして理解したいという思いが強くなりました。

そうして、「そうだ、これを題材にしてRustを使って、ルービックキューブが遊べたり動作確認が出来るプログラム作ればいいじゃないか」という結論になったわけです。そう、私は実際に使ってみたりしないと技術などが身につかないタイプです。

この記事はそれにあたって技術調査した記録になります。

まとめ

結構長くなってしまったので、冒頭のここにまとめておきます。

今回、RustでGUIアプリが実現できる技術を色々調べましたが、ほとんどがバージョン1.0以下という状況でした。その中でUbuntu環境下で基本的動作が確認できたseedというWebassemblyフレームワークを使おうと判断しました。ドキュメントだけでなくサンプルがそろっているのが良かったです。サンプルを通して、以下に羅列するseedと関係するリンクの情報も見つけられて、自分のしたい事が出来そうと思える事も出来ました。
この調査の中で、表題の通り最近話に上がっているWebassemblyの概念や基本的動作を体感出来ました。

以後、その結論に至った経緯を紹介していきます。

GUI技術調査

まず、Rustを使ってGUIを実現するにはどのような構成が可能なのか調べます。

ネイディブアプリ系

各OSで実行可能な形式にビルドする方向です。

web-view

ネイティブアプリのUIコンポーネントとして、webページを表示する部品を使う形になるかと思います。
[Rust] web-viewでGUIアプリをつくる という記事が有用でした。
こちらの記事にも書かれてますが、UIコンポーネントとしてはウェブページの物を使うので、UIコンポーネントの使い方などをキャッチアップしなくても良い利点があります。
ただ、自分はUbuntuで試していたのですが、実行は出来たものの、ライブラリのインストールか何かがうまく行っていないのか、js側で、externalオブジェクトにアクセスする所で処理が止まってしまい、断念しました。

各種ネイティブコンポーネント

RustでGUI 〜GUIライブラリ比較編〜 という記事で細かく調べられています。2019年の記事(2020年更新)の記事なので現状はまた違っているかもしれませんが、この記事を読む限り群雄割拠時代の様です。
使い方としては、ソース上で、UIコンポーネントを生成したり設定する命令を繰り返して画面構築する感じの様です。
初心者が手を出すとトラブル解消出来ず詰まる危険性が高いと思い、断念しました。

ゲーミングフレームワーク

最初は噂に名高いUnityというゲーム用フレームワークでやれたら面白いかもと思いましたが、C++を使っているそうで、Rustでは使えなさそうでした。

コミュニティか、公式かは解らなかったですが、Are we game yet? というページで、ゲーム用ライブラリのリストがありました。ただ、バージョンを見ると1.0以上のものがほとんど見当たらなかったです。こちらも、初心者が手を出す状態ではないと思い断念しました。

WebAssembly系

最近耳にする様になった技術です。ブラウザアプリ形式になります。
公式ページに詳細はお任せしますが、中間バイナリモジュール(以後wasm)を生成し、それをブラウザで処理する事により、高速処理が出来るというものです。その中間モジュールは各種プログラミング言語で生成可能みたいです。
そこで使用される言語としてRustが人気の様です。
後述しますが、JavaScriptからwasmを呼び出す事も、wasmからJavaScriptを呼び出す事も可能な様です。

公式チュートリアル

MDN Web Docのページ「Rust から WebAssembly にコンパイルする」に基本的なチュートリアルがありました。
JavaScriptのalertメソッドを呼び出す関数をwasmに作成して、wasmに作成した関数をJavaScriptから呼び出すという事をやっていました。
このサンプルだけ見ると、JavaScriptとwasmの連携インターフェースは少なくした方が可視性などで良さそうで、何か計算量が多いブロックをwasmで作るというのが基本的な使い方のように感じました。DOMの生成や処理などはJavaScriptでやるというイメージを受けます。

WebAssemblyフレームワーク群

そんな計算量が多い処理をやりたいのは大規模システムになるかと思いますが、それを前述のチュートリアル的な方法でやっていたら大変になりそうです。各種フレームワークが群雄割拠しているようです。

Rust web framework comparison という、各フレームワークの比較リストを作っているリポジトリもありました。こちらも、バージョン1.0以下のものばかりで、まだまだ群雄割拠時代という感じです。Webassemblyに関してちょっと体験したいという気持ちがあり、試す事にしました。話題になってるぐらいなので今後発展する可能性も高そうと思ったという事もあります。結果として2つ程試しました。

yew

前述比較サイトの数字からすると一番活発の様です。実際にやってみた方の記事もありました。
Rust + Yew = WebAssembly でかんばんライクなタスク管理アプリを作ってみました。
[Rust] YewでWebAssemblyを使ったWebアプリのフロントを作ってみる

ただ、残念ながら、日本語ドキュメントから飛べる「始める」のリンクが切れていて、ちょっと使ってみようかという時に挫折しました。前述の記事も参考にしましたが、何かのライブラリインストールなどでミスったのか足りなかったかビルドエラーが出て、自分の知識では解消できませんでした。
gitの更新自体は活発っぽいので、今はコミュニティの皆さんがガシガシ更新している状況かなと想像します。今の時点では初心者の私が手を出さない方が良さそうな状況と思いました。

seed

前述比較サイトではGithub Starsが2番目に多いものです。実際に使った方のQiita記事もありました。
SeedってRustのフロントエンドフレームワークが最高だったので紹介したい

Seed Quickstartページもあり、後述している通り、実際に動く状態にも出来ました。
WebAssembly技術にも興味があった所でした。
既に題名にも使っていますが、こちらのseedを使って始めていく事にします。

Seed Quickstart トレース

ボタンを押してページ内にあるカウンタの数字を増やしていくページを作成します。

インストール

基本ライブラリ

途中で追加インストールしたものです。後述インストールやビルドでSSLモジュールに関するエラーが出たら対応するぐらいで良いと思います。

sudo apt install libssl-dev

Rust、Cargo

公式ページに従います。
Rust本体と、CargoというRust用のパッケージマネージャーがインストールされます。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Cargo関係モジュール

cargo install cargo-make
cargo install cargo-generate

テンプレートから新規プロジェクト作成

cargo generate --git https://github.com/seed-rs/seed-quickstart.git --name cubetrain
cd cubetrain

ビルド&サービスを開発モード起動

cargo make build_release
cargo make serve

※開発モードでの起動にはビルドは必要なさそうですが、一度ビルドしておかないとサービス起動できませんでした。

アクセス

http://localhost:8000/へアクセスすると以下画面が出てきます。※ボタンを3回クリックした状態です。
image.png

この時点でのhtml要素は以下の様になっていました。

image.png

プロジェクトのフォルダには以下のファイルが出来ていました。勝手な想像だと、package.jsがwasmファイルとjsのインターフェースを担っている感じだと思います。

./pkg/package_bg.wasm
./pkg/package.js

seedの基本要素確認

Quickstartのソースを見て、どのような構造になっているのか想像してみます。間違ってたら指摘いただけると有り難いです。

index.html

前述の実際のhtml要素からすると、id=appのsectionタグの配下にDOM要素が挿入されている事が解ります。

bodyだけ抜き出し
<body>
    <section id="app"></section>
    <script type="module">
        import init from '/pkg/package.js';
        init('/pkg/package_bg.wasm');
    </script>
</body>

src/lib.rs

処理のフローの順番に追ってみます。

start関数

コメントに書いてくれています。JavaScript側のinit関数により呼ばれる入り口ですね。fn startという関数名を変えても動きましたが、wasm_bindgen(start)の方は変えたらビルドエラーになりました。脳死でこのままで良いのだと思います。
最初の引数appに関してもコメントで説明してくれていますね。htmlのid=appの要素にマウントするという事ですね。

// (This function is invoked by `init` function in `index.html`.)
#[wasm_bindgen(start)]
pub fn start() {
    // Mount the `app` to the element with the `id` "app".
    App::start("app", init, update, view);
}

init関数

コメントにも書いてありますが、appが開始した時に処理される関数ですね。UrlとOrdersという情報を受け取れるようです。
そして返却するのがModelという事です。アプリ内で使用する変数の様です。
前述記事「SeedってRustのフロントエンドフレームワークが最高だったので紹介したい」によると、

Component毎にローカルな状態を持つことはなく、基本的にグローバル変数一つで状態を管理します。

との事で、ここで宣言したModelが全ての情報という事になるのだと思います。

// `init` describes what should happen when your app started.
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
    Model { counter: 0 }
}

update関数

コメントからすると、何かメッセージ(Msg)が起こった時の処理を記載する部分の様です。
QuickStartではIncrementというメッセージを定義して、それが発生した時にModelのcounterを1増加するという処理を記載してます。

// (Remove the line below once any of your `Msg` variants doesn't implement `Copy`.)
#[derive(Copy, Clone)]
// `Msg` describes the different events you can modify state with.
enum Msg {
    Increment,
}

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Increment => model.counter += 1,
    }
}

view関数

こう見てみると、いわゆるMVCモデル的な事を実現しているのだと思います。そしてこれはそのView部分ですね。
Modelを受け取って、その表示方法を定義するという形ですね。
div![]button![]部分がDOMのhtmlタグに対応していそうです。C![]はDOMのclassを指定していると思われます。
[]内は、文字型なら配下のtext要素、C![]ならクラス、buttonなどのhtml要素なら配下DOMと自動判別していると思います。
ev()がイベント指定していて、Clickイベントが来たら、イベントメッセージとして、Msg::Incrementを送信、という流れの様です。
そしてMsg::Incrementの送信に応じて、前述update関数が呼ばれて処理されるという事ですね。
Webassemblyの公式サンプルではDOM要素はhtmlファイル側で定義されていましたが、seedではDOM要素をhtml文としてではなく、固有の仮想DOMの情報として定義していくというポイントがありそうです。
とはいえ、Nodeのメソッドを見ると、htmlから仮想DOMを作る事も出来そうです。

// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
    div![
        "This is a counter: ",
        C!["counter"],
        button![model.counter, ev(Ev::Click, |_| Msg::Increment),],
    ]
}

必要技術確認

今回やろうとしている事に必要な技術要素を確認してみます。

  • キャンバス(やっぱりグラフィカルな事をやる時には必要になると思います)
  • イベント(ボタンなど以外でもクリックイベントが拾えるかなど確認したい)
  • css(使えないと痛いです)
  • アイコンなどの画像(リソースファイルの扱い)

SeedのExamplesページに色々な例が載っているのでそれぞれ確認していきます。

# これは一度だけ
git clone https://github.com/seed-rs/seed.git
cd seed/samples/{それぞれのフォルダ}
cargo make build_release
# このサンプルでは make serve ではない様です。
cargo make start

今回は使わないと思いますが、component_builderという例もあり、拡張コンポーネントなどを独自に作れそうです。

キャンバス 機能確認

こんな画面が出てきました。「Change color」ボタンを押して色を変えています。
image.png

Quickstart から変わった部分確認を確認します。

宣言部

web_sysは標準モジュールの様です。Cargo.tomlのdependencyに特に追加の記載ありませんでした。
コメントの中には、公式ページCanvasRenderingContext2dへのアドレスが記載されていました。ざっと見た限りキャンバスで必要な一通りのメソッドは準備されていそうです。

use web_sys::HtmlCanvasElement;

init関数

QuickStartでは使っていなかったordersが出てきています。

公式説明ページ Trait seed::app::orders::Ordersを読むと、レンダリングが終わったのちに実行するCallbackを指定する関数の様です。Renderedと指定したメッセージを発生させるようにしているという事ですね。

fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
    orders.after_next_render(|_| Msg::Rendered);
    Model::default()
}

update関数

initで指定していたRenderedメッセージを受け取り処理してます。他のイベントはModelの値を変えて、その結果Renderが走った後に、最終的にRenderedメッセージが発生して、drawする。その時はその後のRenderはしないという事の様です。おそらくOrdersはこのように処理の順番を制御できるトレイトなのだと思います。
draw関数内部は他の言語でも見てるような処理をしていました。言語やフレームワークで特徴を出すような場所ではなさそうです。
冒頭のallow(clippy::needless_pass_by_value)Clippy Lintsによると使ってない変数をチェックするものらしいので、それを許可するという事だと思われます。

#[allow(clippy::needless_pass_by_value)]
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::Rendered => {
            draw(&model.canvas, model.fill_color, model.zoom);
            // We want to call `.skip` to prevent infinite loop.
            // (However infinite loops are useful for animations.)
            orders.after_next_render(|_| Msg::Rendered).skip();
        }
        Msg::ChangeColor => {
            model.fill_color = if model.fill_color == Color::A {
                Color::B
            } else {
                Color::A
            };
        }
        Msg::Zoom(zoom) => {
            model.zoom += match zoom {
                Zoom::In => -0.1,
                Zoom::Out => 0.1,
            };
        }
    }
}

view関数

canvas要素がdivの内側に設定されてますね。model.canvasはDefaultでcanvas: ElRef::<HtmlCanvasElement>::default()と指定されていました。全てのhtmlタグがそのまま使えるのでなく、el_refを使う必要があるものもあるという事でしょう。style![]を使う事で、css指定も出来るという事ですね。wheel_evで、マウスホイールイベントを処理しています。ZoomメッセージをZoom種類の引数で呼んでるという事ですね。

enum Zoom {
    In,
    Out,
}

enum Msg {
    Rendered,
    ChangeColor,
    Zoom(Zoom),
}

fn view(model: &Model) -> impl IntoNodes<Msg> {
    div![
        style! {St::Display => "flex"},
        canvas![
            el_ref(&model.canvas),
            attrs![
                At::Width => px(400),
                At::Height => px(200),
            ],
            style![
                St::Border => "1px solid black",
            ],
            wheel_ev(Ev::Wheel, |event| {
                let delta_y = event.delta_y();
                (delta_y != 0.0).then(|| {
                    event.prevent_default();
                    Msg::Zoom(if delta_y < 0.0 { Zoom::In } else { Zoom::Out })
                })
            }),
        ],
        button!["Change color", ev(Ev::Click, |_| Msg::ChangeColor)],
    ]
}

イベント 機能確認

こんな画面が出てきました。ボタンを押すとイベント処理のオン・オフが出来て、オンの状態だとマウスの位置と押したキーのコードが表示される形です。
image.png

update関数

Msgで使うイベントを定義しておいて、ToggleWatchingの中でmodel.event_streamsにハンドラを指定している感じでしょうか。event_streamsのデータ型はVec<StreamHandle>の様です。
ここでも出てきたOrdersに、stream_with_handleがあり、そこにリスナーが追加されていくというイメージでしょうか。
Evのリストがありました。ほとんど網羅していると思います。

enum Msg {
    ToggleWatching,
    MouseMoved(web_sys::MouseEvent),
    KeyPressed(web_sys::KeyboardEvent),
}

fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::ToggleWatching => {
            if model.event_streams.is_empty() {
                model.event_streams = vec![
                    orders.stream_with_handle(streams::window_event(Ev::MouseMove, |event| {
                        Msg::MouseMoved(event.unchecked_into())
                    })),
                    orders.stream_with_handle(streams::window_event(Ev::KeyDown, |event| {
                        Msg::KeyPressed(event.unchecked_into())
                    })),
                ];
            } else {
                model.event_streams.clear();
            }
        }
        Msg::MouseMoved(ev) => {
            model.point = Point {
                x: ev.client_x(),
                y: ev.client_y(),
            }
        }
        Msg::KeyPressed(ev) => model.key_code = ev.key_code(),
    }
}

view関数

htmlとしては以下の画像の様になっていました。vecではその要素は作成されず、配下の要素が、html側で直接id=appの要素に追加されるようです。関数の戻り型もQuickStartではNode<Msg>となっていたのがVec<Node<Msg>>となっていますね。
image.png

fn view(model: &Model) -> Vec<Node<Msg>> {
    vec![
        h2![format!("X: {}, Y: {}", model.point.x, model.point.y)],
        h2![format!("Last key pressed: {}", model.key_code)],
        button![
            ev(Ev::Click, |_| Msg::ToggleWatching),
            if model.event_streams.is_empty() {
                "Start watching"
            } else {
                "Stop watching"
            }
        ],
    ]
}

css機能調査

これは要素にクラス指定が出来るので、htmlファイル側で、cssファイルを読み込んでおけば問題なさそうです。
また、Canvasの例の時に、style![]を使っていたという方法でも大丈夫そうです。
Stのリストもありました。ほとんど網羅していると思います。

アイコンなどの画像機能調査

これもstyle![]を使ってbackground-imageなどの指定を使えば背景画像とかは問題なさそうです。
imgタグのDOMが作成出来るか確認したい所です。

Tagのリストもありました。ほとんど網羅していると思います。Imgもありました。
web_sysの各種HtmlElementリストもありました。HtmlImageElementもあったので、canvasの時と同じように出来そうです。サンプルにはなさそうだったので実際に使った時に何かあるかもですが、多分大丈夫でしょう。

これを探している時に仮想DOM関係の説明ページがありましたので今後使えそうなので備忘録として残しておきます。
Node
El

次に向けて

seedで、技術的にこれが出来なくて詰まりそう、という事は無さそうです。
冒頭に上げたルービックキューブのアプリを作っていくうえで、何か技術ポイントで興味深いものに出会ったら記事にしていこうと思います。

19
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
13