RustでWebAssembly: MyAppの'static 参照を作る方法


はじめに

前回は、アプリケーション状態のベースとなる MyApprequestAnimationFrame() に渡すClosureにキャプチャさせるのでDropされないという構造になっていました。

これとは別に MyApp 自体をstaticな変数に置いてしまうという方法も考えられます。安易にstaticを使うというのは大抵アンチパターンな気もしますが、WebBrower上のアプリケーションであれば、まあ動作するアプリケーションは1つであろうし、JSでそういうやり方になっているものも珍しくないでしょう(というか、普通windowとかに保存することでそうなっている)。
そして、現状のwasm-bindgenだとHTTPでのfetchなどの非同期関数を任意のタイミングで実行しようと思うと wasm_bindgen_futures::spawn_local() を使うしかないのかなと思いますが、この spawn_local() に渡せる async function は 'static な参照しか使えないことになっています(というより、stackに関連したものを渡せないということらしいですが)。そうなると fetch の結果を MyAPP とかに渡したいというときに &'static MyApp とかになっているとやりやすいです。

まあ、そういうわけで 「MyAppの'static参照を作る方法」について考えてみました。
ちゃんとした複雑なアプリケーションで使ったことはないので、この後どういうTrapがあるかまだわかりませんが、とりあえずは問題なく動作しました。

MyAppの'static参照を作る方法

実装方法1: Rc<RefCell<MyApp>> を使う

ポイントになるのは、この辺りかなと思います。
static mut な変数へのアクセスには unsafe で囲む必要があるようです。
毎回 unsafe とか使うと心理的に負担なので、簡単なwrapperを用意しておきます。

lib.rs

static mut MY_APP: Option<Rc<RefCell<MyApp>>> = None;

pub fn init_my_app() {
    unsafe {
        MY_APP = Some(Rc::new(RefCell::new(MyApp::new())));
    }
}

pub fn my_app() -> Ref<'static, MyApp> {
    unsafe { MY_APP.as_ref().unwrap().borrow() }
}

pub fn my_app_mut() -> RefMut<'static, MyApp> {
    unsafe { MY_APP.as_ref().unwrap().borrow_mut() }
}

上記は、本来できないような下記のコードがコンパイルを通ってしまいますが、実行時に panic になります。なので、使う時には普段より一層の注意が必要になります。

    let mut x = my_app_mut();
    let y = my_app();

実装方法2: Box<MyApp> を使う

こういう形でも動くようです。


static mut MY_APP: Option<Box<MyApp>> = None;

pub fn init_my_app() {
    unsafe {
        MY_APP = Some(Box::new(MyApp::new()));
    }
}

pub fn my_app() -> &'static MyApp {
    unsafe { MY_APP.as_ref().unwrap() }
}

pub fn my_app_mut() -> &'static mut MyApp {
    unsafe { MY_APP.as_mut().unwrap() }
}

これのポイントは以下のようなRust的掟破りのコードがコンパイルも実行時も通ることです。
WebBrowserのゆるいアプリケーションの場合、だいたいこれくらいゆるさで良い気もするし、これは意外と使えるかもしれない。

    let mut x = my_app_mut();
    x.clicks += 1;
    let y = my_app();
    log!("clicks={}", y.clicks);

使い方

これらは以下のように使うことができます。
ポイントは pub async fn my_async_process(param: u32) みたいな MyAppへの参照をもたせられない関数の中でもアクセスできることです。
まあ、JSやwasm内では単一Threadなので、MyAppを使ってすぐにしまっておけば大抵は問題にならないとは思うのですが。

lib.rs
#[wasm_bindgen]
pub fn start() {
    console_error_panic_hook::set_once();
    log!("start");
    init_my_app();
    let closure_captured = Rc::new(RefCell::new(None));
    let closure_cloned = Rc::clone(&closure_captured);

    // setup requestAnimationFrame Loop
    {
        closure_cloned.replace(Some(Closure::wrap(Box::new(move |time: f64| {
            my_app_mut().on_animation_frame(time);
            request_animation_frame(closure_captured.borrow().as_ref().unwrap());
        }) as Box<dyn FnMut(f64)>)));
        request_animation_frame(closure_cloned.borrow().as_ref().unwrap());
    }

    // setup onClick
    {
        let c = Closure::wrap(Box::new(move |e| {
            my_app_mut().on_click(e);
        }) as Box<dyn FnMut(JsValue)>);
        document()
            .add_event_listener_with_callback("click", c.as_ref().unchecked_ref())
            .unwrap();
        c.forget(); // c を Rustのメモリ管理から外して JSのGCにわたす
    }
}

impl MyApp {
    pub fn on_click(&mut self, event: JsValue) {
        self.clicks += 1;
        spawn_local(my_async_process(self.clicks));
    }
}

pub async fn my_async_process(param: u32) {
    // 何か await とかしたりできる(fetchとか)
    // spawn_local() で使えるのは static な 参照しかないが、 my_app_mut() はそれを満たしている。
    my_app_mut().async_count += param;
}

さいごに

コード全体はこちらです。
こういうのはパンドラの箱感がありますね。