RustでWebAssembly: spawn_local() へ渡すasync functionに状態を持ったObjectを渡したいときの抜け道


はじめに

spawn_local() は wasm-bindgen で、asyncな関数を実行する便利な(JS側を介さない)方法ですが、


pub fn spawn_local<F>(future: F) 
where
    F: Future<Output = ()> + 'static, 

という型になっていて Futureが使う参照は'static なlifetimeをもつものしか渡せません。
つまりこれは、以下のように書くことができないということになります。

impl MyApp {
  fn some_func(&mut self) {
     spawn_local(my_async_process(&mut self)); // &mut selfの lifetimeがstaticではないのでダメ
  }
}

この制約が厄介だなーと思っていたのですが、抜け道を見つけた(いや、最初からそこにあったのでしょうが)のでメモしておきます。Rust初心者にはちょっと気が付きにくいなぁと思ったので。

抜け道

'static MyApp を用意しておく

これは 前回 考えた方法で、まあ確かにこれならうまくいきます。
しかし、my_async_process の中に入れたいのが staticにできるものとは限りません。複数あるObjectとかだとこの方法は使いにくいです。(もちろん、なんとかはできますが)。

Rc<RefCell<MyApp>> を渡す

参照(&)を渡すことはできないですが、 Rc構造体を渡すことはできるようです。
なので、spawn_localしたい部分に、いつもの Rc<RefCell<MyApp>> みたいなのを持ち込むことがでていれば渡すことができます。
Localで作成した普通のObjectならこの方法が良いかもしれないです。Box<HogeHoge> とかでも良いような気がします。

// 仮にこれをselfが持っていれば渡せることになる
let x: Rc<RefCell<MyApp>> = Rc::new(RefCell::new(MyApp::new()));
spawn_local(my_async_process_with_ref_cell(x.clone(), self.clicks));

...

pub async fn my_async_process_with_ref_cell(my_app: Rc<RefCell<MyApp>>, param: u32) {
    // 何か await とかしたりできる(fetchとか)
    // spawn_local() で使えるのは static な 参照しかないが、 Rcに包まれたものは有効。
    my_app.borrow_mut().async_count += param;
}

Raw Pointerを渡す

Rustには 「参照(&, &mut)」とは別に「RawPointer(*)」というのがあります(というのを思い出した)。
https://doc.rust-lang.org/reference/types/pointer.html

「参照」の方は lifetime が管理されていたり、&mut が排他的な扱いをうけたりしていてRustの特別な感じを醸し出していますが、「RawPointer」にはそういうのがありません。代わりに Dereferenceするときは NULLかもしれないし、もう値がないかもしれないので unsafe {} で囲わないといけないことになっています。で、 spawn_local は 「'static ではない参照」を禁じているのであって、RawPointerについては何も言及していません。なので、実はRawPointerを渡すことができるようです。

つまり、こんな抜け道がありました。んー気が付かなかった。

spawn_local(my_async_process_with_raw_pointer(
    self as *mut MyApp,
    self.clicks,
));

...

pub async fn my_async_process_with_raw_pointer(my_app: *mut MyApp, param: u32) {
    // *mut MyApp は 「参照」ではなく 「RawPointer」
    // spawn_local() で使える「参照」は static な 参照しかないが、 Raw Pointerなら渡せる!!
    unsafe {
        (*my_app).async_count += param;
    }
}

さいごに

まあ、どの方法もケースバイケースで使い分けると良さそうです。
いろいろやり方はあるものですね。