💭

RustとWebAssemblyによるゲーム開発 ch.2

2024/06/19に公開

RustとWebAssemblyによるゲーム開発
の2章を読み進めて参ります
いつものように、有識者のまさかり待ってます

復習

上はビルド通る 下はエラー 何が違う?

Closure::once(move || {
    let _ = success_tx.send(());
};
Closure::once(move || success_tx.send(()));

上のコードと下のコードの違いは戻り値
下のコードでは戻り値がResult<(), T>となる
ここで、wasm_bindgen::closure::Closure::once()の定義を見てみると

pub fn once<F, A, R>(fn_once: F) -> Closure<F::FnMut>
where
    F:  'static + WasmClosureFnOnce<A, R>,

となっている
つまり

move || {
    let _ =success_tx.send(());
}

Fを満たし

move || {
    success_tx.send(())
}

Fを満たさないとのこと
ここでWasmClosureFnOnceの定義を見てみると

pub trait WasmClosureFnOnce<A, R>: 'static {
    type FnMut: ?Sized + 'static + WasmClosure;
    fn into_fn_mut(self) -> Box<Self::FnMut>;
    fn into_js_function(self) -> JsValue;
}

なるほどわからん
なぜ戻り値Result<(), T>があるとエラーになるのか全くわからないので有識者助けてください!

非同期処理を用い画像をロードし、ブラウザに表示する

コード

wasm_bindgen_futures::spawn_local(async move {
	let (success_tx, success_rx,) =
		futures::channel::oneshot::channel::<Result<(), JsValue,>,>();
	let success_tx =
		std::rc::Rc::new(std::sync::Mutex::new(Some(success_tx,),),);
	let error_tx = std::rc::Rc::clone(&success_tx,);
                                                                                               
	let img = web_sys::HtmlImageElement::new().unwrap();
                                                                                               
	let callback = Closure::once(move || {
		if let Some(success_tx,) =
			success_tx.lock().ok().and_then(|mut opt| opt.take(),)
		{
			let _ = success_tx.send(Ok((),),);
		}
	},);
	let err_callback = Closure::once(move |e| {
		if let Some(error_tx,) =
			error_tx.lock().ok().and_then(|mut opt| opt.take(),)
		{
			let _ = error_tx.send(Err(e,),);
		}
	},);
                                                                                               
	img.set_onload(Some(callback.as_ref().unchecked_ref(),),);
	img.set_onerror(Some(err_callback.as_ref().unchecked_ref(),),);
	img.set_src("Idle (1).png",);
                                                                                               
	let _ = success_rx.await;
	let _ = context.draw_image_with_html_image_element(&img, 0.0, 0.0,);
},);

各行が何故必要なのか その行がないと何が起こるか

まず最初の2行で非同期処理をするためにチャンネルを宣言している
oneshotチャンネルにしているのは、一度しか行わない処理(画像をロードする際のコールバック処理)だから
その次に、一つのチャンネルを2つのクロージャにmoveすることができない(スレッド安全性のため?)のでMutexを使ってラップしている Mutexでラップしているのはsuccess_txを含んだOption
それを、Rcを使って成功時用と失敗用の二つの参照を作っている

次に、htmlのimage elementを作る

let callback = ..
let err_callback = ..

で行っていることは、成功したらOkを返し、エラーが起こったらErrを返すようなコールバック処理を記述している
ここで、success_txOptionでラップしたことにより、success_txがクロージャ内にmoveされて起こるエラーがなくなる
どういうことかというと、success_txoneshotチャンネルなので一度だけ使われる
従ってClone, Copyを実装していない
なのでsuccess_txはクロージャ内にmoveされるのだが、Mutexが保持する所有権を奪うことはできないようになっているので、エラーが起こる
これをOptionでラップすることによりMutexが所有するのはOption自身となり、Mutexが何も所有していない状態をNoneで表現することができる

あとはイメージをロードして、success_rx.awaitして、描画する
以上!

image.set_onload(Some(callback.as_ref().uncheched_ref()));

にてas_refunchecked_refの必要性は?

image.set_onloadの引数がOption<&::js_sys::Function>なので参照を渡す必要がある
だからas_refを使っている
unchecked_refは、js関数として実行される際は型が動的に決まるので、js側に合わせるために使っている

wasm_bindgen_futuresは何を提供する?

jsのPromiseとRustのFutureの橋渡しをする機能を提供する

wasm_bindgen_futures::spawn_localを使う必要性

Rust標準のチャンネルを使うとチャンネルを呼び出してる間はメインスレッドの実行が止まってしまう
ここでいうメインスレッドとはブラウザのことなのでブラウザが停止してしまう
spawn_localを使ってasync/awaitをした場合、awaitが停止させるのはローカル実行で、プログラム全体は動作する

'staticってなんだっけ

プログラム実行中常に有効な値への参照のライフタイム。ライフタイムが表現する機関としては最大

調べていたら面白い(厄介な)ことがわかった
impl SomeTrait + 'aなどの型自身につくライフタイムは最長で'aの寿命をもてるという意味
&'a Tなどの参照につく場合は最短でも'aの間は生きていることを保証せよという意味

型につくか参照につくかでライフタイムの持つ意味合いが逆転!
厄介

futures::channel::oneshotチャンネルを含むクロージャの型

FnOnceトレイトを実装する
何故そうなるのか→oneshotは一回きりだから

std::sync::Mutexの役割 何を解決するのか

並行プログラミングを安全で楽に行えるように定義された型の一つ
チャンネルを複数のスレッドで共有して、一度に一つのスレッドだけがデータにアクセスできるようにする

and()and_then()の違い

andand_thenOptionのメソッド

andOptionNoneだった場合None Someだった場合andの引数を返す
and_thenは関数を引数に取り、OptionNoneだった場合None Someだった場合引数の戻り値を返す

oneshotがコピー出来ないのはなぜ

一度しか実行されなから

スプライトシートは何が嬉しいか

読み込むリソースの枚数が減り、ロード時間が短くなるらしい

気になったところ

下記のコードが望んだ結果を得られない理由となぜ自分はこのように書いたのか

if let Some(error_tx,) = error_tx.lock().err().take() {}

エラー時のコールバック処理なのでerror_tx.lock().err()としたが
error_tx.lock().err()の意味はerror_txを介してスレッドにアクセスしようとしたら失敗したという意味なので不適切

Base64でエンコーディングするとは

Base64はあらゆるデータを6bit(64種類の文字)を用いで表すエンコード方式
ファイルサイズが大きくなったり、エンコード・デコード分の処理負荷がかかったりというデメリットはあるがテキスト形式のファイルしか読み込めないプログラムに食わせたい時に使われることがあるらしい

ダブルバッファリングとは

画面描画分のメモリ領域をふたつ確保しておき、各領域のデータを交互に表示することによってチラつきや乱れを抑え高速化する手法

FnMut,Fn,FnOnceの違いは

兎にも角にもまず定義を見てみる

pub trait Fn<Args: Tuple>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Ars: Tuple>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args: Tuple> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

まず驚いたことに関数の引数はタプル型として扱われるらしい
次にextern "rust-call"という記述がある
これは、AbiDataによってAbi::RustCallに変換される

そうなんだが、正直何を言っているのかよくわからない
結論から言うと、関数の引数って個数が定められてないし、どんな型の引数も定義できるけど、それをABIに食わせる時にABIの食いやすい形にする必要がある その時のためにAbi::RustCallに変換して特殊な処理を施す
らしい 全くわからん

話を戻してFn, FnMut, FnOnceの違いを見てみると、callの第一引数が、それぞれ&self, &mut self, selfになっている
あとは、違いというか関係についてだが
FnFnMutFnMutFnOnceを継承?している(rustタームで言うと合成になるんだろうか?)

つまりFnFnMutでもFnOnceでもある
FnMutFnOnceでもある
ただ、場合分けとして、一度しか呼ばれない(そうであることが期待される)ものはFnOnce
変数を書き換えるものはFnMutFnOnce
それ以外はFn,FnMut,FnOnceを実装する

となるらしい

ABIって何

ABIって、言葉だけは知ってるけど正直よくわかってない
わかってないので調べてみた

ABIとは、どうやら、規約のようなものらしい
具体的には実際に何か関数を実行したり、データを持ったりする際にまずどう言った命令が呼び出されて、データはどのように配置して、サイズはいくらで、エンディアンは何で..
といった実際にプログラムを実行する際に必要となってくる低レベルでの処理の機能を提供するのがABI
..なんですか?

Discussion