WebAssembly/CallbackとかPromiseとか
無名関数が飛び交う
JavaScriptでは、関数もオブジェクトとして存在していて、あまりの使い勝手の良さにfunction(){}とか、今なら()=>{}とか、何度も何度もタイプしてきました。また、JavaScriptがブラウザのGUI操作を担当したり、サーバーサイドとの通信を担当したりすることの多さから、非同期処理を設計の前提とすることも多かったです。昔はCallback地獄なんて揶揄されてましたが、Promiseが導入されてからソースコードもかなりすっきりしてきた印象です。
さて、RustのWASMではこの辺どうなっているのでしょう。ちょっとよそのプロジェクトの実装を覗いてみたところ、何が行われているのかさっぱりわからなくてそっと閉じました。
Fetchぐらいしたい
でも、自分の開発が進むにつれて、データをソースコードに直書きしていくのに限界を感じてきました。データは外の別ファイルに置きたい!しかし、WASMを使うこのプロダクトはWEBサーバー上で動いていて、データファイルはまた別途通信して取得する必要があります。この通信処理は協調するJavaScriptにお願いして、データの解析だけRust(WASM)側で処理するのでもいいかなって思いました。ただ、ちょっとだけ好奇心がなくもないので、わからないなりにFetch API相当のことをやってみることにしました。
まず、Rust側の知識が圧倒的に足りなくて、知らなきゃいけないことに一つずつぶつかっていきました。とりあえず、Rustにもawaitとasyncのキーワードがあるようです。Rustは若い言語でawait,asyncの安定した仕様が決まって日も浅く、ネット上には古い情報も多いようです。こうして自分のつけているこの記録もすぐに古くなって、混乱しか呼ばないダメな存在になるのだろうなぁ。Rustの記事の頭に実装時のバージョンを明記している人はちゃんとしてて偉いなぁ。以下、ザックリとした私の理解です。
async
asyncはブロックの修飾子です。
async { // 非同期処理がこのブロックには書ける。 future.await; }
私の場合、まずブロックってなんだよ、ってとこからでした。ブロックは{}で囲まれた範囲のことで、式であり、変数のスコープです。ブロック内で定義された変数はそのブロックの所有となるので、ブロックの外に出ると変数は解放されます。へ~、unsafeなんかもブロックの修飾子なのかぁ。moveっていう修飾子をつけると関数みたいにブロックの外側の変数の所有権を内側に移動できるのか~。(脱線)
それでasyncブロックは内側に非同期処理が入っていることを明示しているのですね。明示しておくと、awaitキーワードが使えるようになると。asyncブロックが式つぃて評価されると、その実態はFutureトレイトが実装したオブジェクトになるらしい。
await
awaitキーワードがasyncブロックにくっつくと、文字通り処理が終わるまで待ってくれて、すなわち同期されるとのこと。非同期処理がまるで連続した処理のようにかける優れモノ。awaitはasyncブロックの中でしか使えないので、どこでも使えるようにしたい気持ちにかられますが、仕組み上それは無理です。ただ、WASMをブラウザ上で走らせる分には、ブラウザがUIスレッドやら通信スレッドを持っているので、きっと大きな問題ではない、多分。
Futureトレイト
futuresクレートが非同期処理に関するあれこれを定義していて、その策定に紆余曲折があったと聞きました。Futureトレイトを実装すると非同期処理が「まだやってない」「もうやった」みたいな現状を管理してくれるわけですね。asyncブロックの本質がこいつなんですね。この非同期処理は定義しただけでは誰も実行してくれないので、別に執行者(Executer)を誰かにやってもらう必要があります。
Promise
JavaScriptのPromiseが丁度Futureトレイトみたいなことをやってくれていると思います。そしてjs-sysがこれに対応するPromiseを用意してくれています。Promiseには非同期処理を実行した結果「成功した!」「しくじった・・・」という状態も管理してくれます。そして、その成否に対してその後の処理を進める次のPromiseも用意してくれます。
結果 | 呼ばれるメソッド | その結果が渡される非同期処理を登録するメソッド |
成功 | resolve | then |
失敗 | reject | catch |
非同期処理Aが成功したら非同期処理Bを行ってそれも成功したら非同期処理Cを行って・・・と順番に実行することを順番通りに書けるのがPromiseの醍醐味ですね。これがない時代は処理があっちこっちに散逸してたものでした。このシリアルな(チェーンな、直列な)処理は先の処理の結果を次の処理に引数として渡すことで進んでいきます。Promiseの引数なので、値はJsValue型となります。
JsFuture
でも、PromiseをJavaScriptに渡さないんだったら、Rust側でFutureとしても書けるようにしてあるみたい。JsFuture::fromでPromiseをFutureトレイトの実装されたJsFutureに変身させて、and_thenなどで非同期処理をつないでいけます。
let future = JsFuture::from(promise) .and_then(|value| async { // 結果のvalueをどうこうする }); let promise = future_to_promise(future);
wasm_bindgen_futures::future_to_promise を最後に呼んで、できあがったfutureをPromiseに変換しておきました。future_to_promise を使ってPromise化すると実行スケジュールが組まれるそうです。この場合、非同期処理の執行者は誰になるんでしょう?ブラウザが何かしてくれるのかな?とりあえず実行されるようになったのでヨシとしておきます。
closure
Rustの無名関数は || {} と簡単に書けます。JavaScriptだと昔はfunction(){}ってずっと書いてたやつですね。今はアロー関数式(=>)で簡単にかけちゃうかな。(厳密にはちょっと動作が違うけど)非同期処理を定義するときはやはりasyncブロックにするとのこと。|| async {}
Rustのクロージャの正体はFn,Fnmut,FnOnceトレイトを実装したオブジェクトだそうです。このトレイト名、ただちに意識しなくてもよさそうですが、前にキーボードイベントのリスナーを書いたときに名前をみかけたなぁ。
wasm_bindgenにはJavaScriptのクロージャに対応するClosureがありました。どこかで使う時がくるのかな?
Fetch API
let mut opts = RequestInit::new(); opts.method("GET"); opts.mode(RequestMode::Cors); let url = "https://soft.candychip.net/nanikano-file.txt"; let request = Request::new_with_str_and_init(&url, &opts).unwrap(); let window = web_sys::window().unwrap(); let request_promise = window.fetch_with_request(&request); let future = JsFuture::from(request_promise) .and_then(|resp_value| async { assert!(resp_value.is_instance_of::<Response>()); let resp:Response = resp_value.dyn_into().unwrap(); resp.text() }) .and_then(|text:Promise| { JsFuture::from(text) }) .and_then(|text| async move { nanika_kansuu(text.as_string().unwrap()); Ok(text) }); future_to_promise(future)
こうしてできあがったのがFetch APIを使ったこんな処理でした。fetch_with_requestにRequestオブジェクトを渡すと、「結果(Response)」を返すという未来の約束(futureトレイトオブジェクト)が返ってきて、future_to_promiseでPromiseオブジェクト化する際に約束の履行スケジュールが立つという内容になっているはずです。
そしてこの非同期処理の連続の中に直接関係ないオブジェクトの更新工程を織り込もうとしたら、オブジェクトのlifetimeに絡んだまた別の魔窟につながっていたのでした・・・。