スプライトをエンドレスに描画する - kossy0701/walk-the-dog GitHub Wiki

入門

以下のコードで何を行なっているかを理解したい。

wasm_bindgen_futures::spawn_local(async move {
    let json = fetch_json("rhb.json")
        .await
        .expect("Could not fetch rhb.json");
    let sheet: Sheet = json
        .into_serde()
        .expect("Could not convert rhb.json into a Sheet structure");

    let image = web_sys::HtmlImageElement::new().unwrap();
    let (success_tx, success_rx) = futures::channel::oneshot::channel::<Result<(), JsValue>>();
    let success_tx = Rc::new(Mutex::new(Some(success_tx)));
    let error_tx = Rc::clone(&success_tx);

    let callback = Closure::once(move || {
        if let Some(success_tx) = success_tx.lock().ok().and_then(|mut opt| opt.take()) {
            success_tx.send(Ok(()));
        }
    });

    let error_callback = Closure::once(move |err| {
        if let Some(error_tx) = error_tx.lock().ok().and_then(|mut opt| opt.take()) {
            error_tx.send(Err(err));
        }
    });

    image.set_onload(Some(callback.as_ref().unchecked_ref()));
    image.set_onload(Some(error_callback.as_ref().unchecked_ref()));
    image.set_src("rhb.png");

    success_rx.await;

    let mut frame = -1;
    let interval_callback = Closure::wrap(Box::new(move || {
        frame = (frame + 1) % 8;
        let frame_name = format!("Run ({}).png", frame + 1);
        let sprite = sheet.frames.get(&frame_name).expect("Cell not found");

        context.clear_rect(0.0, 0.0, 600.0, 600.0);
        context.draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
            &image,
            sprite.frame.x.into(),
            sprite.frame.y.into(),
            sprite.frame.w.into(),
            sprite.frame.h.into(),
            300.0,
            300.0,
            sprite.frame.w.into(),
            sprite.frame.h.into(),
        );
    }) as Box<dyn FnMut()>);

    window.set_interval_with_callback_and_timeout_and_arguments_0(
        interval_callback.as_ref().unchecked_ref(),
        50,
    );
    interval_callback.forget();
});

move

ドキュメントだったりChatGPTの説明を聞いてもいまいち言語化できないのでそのまま引用

move

move キーワードは Rust のクロージャと非同期ブロックに関連して使用されるキーワードで、クロージャや非同期ブロックが変数をキャプチャする方法を変更します。

デフォルトでは、クロージャは環境内の変数を自動的に参照またはミュータブル参照としてキャプチャします。しかし、moveキーワードを使用することで、クロージャは環境の変数を所有権と共にキャプチャします。これはクロージャが変数の所有権を移動("move")するためです。

以下は、moveキーワードの使用例とその挙動の違いを示すコードです:

let s = String::from("hello");

// move を使わないクロージャ
let print_s = || println!("{}", s);
print_s();  // hello
println!("{}", s);  // これはエラーにならない

let s = String::from("world");

// move を使うクロージャ
let print_s_move = move || println!("{}", s);
print_s_move();  // world
// println!("{}", s);  // これはエラーになる。s の所有権はクロージャに移動したため

特に、非同期ブロックやスレッドを扱う場合に、moveキーワードは非常に重要です。 これは、非同期ブロックやスレッドが変数にアクセスするタイミングが、その変数が存在するかどうかが不明確になることがあるからです。 このような状況で、変数の所有権を明確にクロージャや非同期ブロックに移動させることで、ライフタイムの問題やデータの競合を回避することができます。

wasm_bindgen_futures::spawn_local

こちらも解説を読んでも書籍の説明を読んでもピンと来ていないのでそのまま引用

wasm_bindgen_futures::spawn_local

この関数は、wasm_bindgen_futuresクレートに由来するもので、非同期タスクをWebAssemblyの環境で実行するためのものです。具体的には、Rustの非同期関数を、JavaScriptのイベントループ内で実行するためのブリッジ役を果たします。

基本的な考え方として、Rustの非同期タスクやFutureは通常、特定のエグゼキュータ上で動作する必要があります。エグゼキュータは、非同期タスクをスケジューリングして実行する役目を持ちます。Rustの通常の実行環境では、tokioやasync-stdなどのエグゼキュータがよく使用されます。しかし、WebAssemblyの環境ではこれらのエグゼキュータをそのまま使うことができません。

この問題を解決するために、wasm_bindgen_futures::spawn_localは、非同期タスクを受け取り、そのタスクをJavaScriptのイベントループ上でスケジューリングします。これにより、非同期のRustコードが、WebAssembly環境内で期待通りに動作するようになります。

先ほどのコードで言えば、wasm_bindgen_futures::spawn_local(async move { ... })の部分は、中括弧 {...} の中に書かれた非同期コードを、WebAssemblyの環境で実行するためのものです。

この関数を使うことで、Rustで書いた非同期コードを、ブラウザのイベントループとシームレスに統合し、WebAssemblyを介してブラウザで動作させることができるようになります。

expect()

  • RustのOption型やResult型にある関数
  • 値が存在しない場合やエラーが発生した場合に対処するためのもの
  • エラーが発生した場合には即座にプログラムを終了することで、エラーの原因を明確にするためのものなので、production環境で運用するときには使わないようにするのがベスト
  • 例外処理の仕組みを整えるのが最適