wasmのEventListenerの実装をコードとともに見てみる

## はじめに

この記事は [Rustその2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rust2)の18日目の記事です。


自己紹介などについては、以前に書いた[記事](https://poccariswet.hatenablog.com/entry/2019/12/11/000037)を読んでみてください

## 今回のゴール

本来、jsで実装されている<b>addEventListener</b>を<span style="font-size: 150%">wasmで登録して使えるようにする</span>ところまでを簡単に説明できればと思います。

## web_sys の ライブラリを見ていく

https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Node.html#method.add_event_listener_with_callback

rustのwasm crateである [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) には <span style="font-size: 150%">web_sys</span>というものがある。これはRustからDOM APIを触るためのラッパー(ライブラリ)を担っている

> This is a procedurally generated crate from browser WebIDL which provides a binding to all APIs that browser provide on the web.

![new-wasm-bindgen-architecture.png](https://rustwasm.github.io/images/new-wasm-bindgen-architecture.png)

web_sysを読んでいくと以上のように書かれている。ブラウザがWebで提供するすべてのAPIへのバインディングを提供していて、図のようにwasm-bindgenとグルーコードのWebIDLを使用して<b>web_sys</b>を提供している。

web_sysを使った[tutorial](https://rustwasm.github.io/wasm-bindgen/examples/dom.html)で実際に実装しながら学べるのでオススメです。


## コードを見る

![painting-sample (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192279/b8f19ed3-fe18-e78e-9b84-454e7c96e24a.gif)


今回は実装されたものは wasm-bindgenのtutorialにもある、簡単なお絵かきができるwebアプリですhttps://paint-wasm-sample.netlify.com/ に飛んで実際に試してみてください！

また、[paint-wasm](https://github.com/poccariswet/paint-wasm) にソースコードをおいているので見たいかたはリンクを飛んで見てみてください。


### 見てみる
実際のコードをみると

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>paint-wasm</title>
    <style>
      html, body {height: 100%; width:100%; margin: 0;}
    </style>
  </head>
  <body>
    <script type="module">
      import { start_app, default as init } from './pkg/paint_wasm.js';
        async function run() {
          await init('./pkg/paint_wasm_bg.wasm');
          start_app();
        }
        run();
    </script>
    <canvas id="draw"></canvas>
  </body>
</html>
```
今回は、そこまで大きな開発でもないため、bundlerはなしで実装しています。そのため、index.html内では `script type="module"` を設定して wasm-pack のbuildも `wasm-pack build --target web` でしています。

ちなみに、bundlerなしでwasm開発するためのtemplateを作ったのでよければどうぞ
https://github.com/poccariswet/rust-wasm-template-without-bundler
(⚠︎ 開発仕立てのため、バグがあるかもしれないのでよろしければissueをください！)

```rs:lib.rs
fn get_body_dimensions(body: &HtmlElement) -> (u32, u32) {
    let width = body.client_width() as u32;
    let height = body.client_height() as u32;

    (width, height)
}

#[wasm_bindgen]
pub fn start_app() -> Result<(), JsValue> {
    let document = window()
        .unwrap()
        .document()
        .expect("Could not find `document`");

    let body = document.body().expect("Could not find `body` element");

    let canvas = document.get_element_by_id("draw").unwrap();
    let canvas: HtmlCanvasElement = canvas
        .dyn_into::<HtmlCanvasElement>()
        .map_err(|_| ())
        .unwrap();

    let (w, h) = get_body_dimensions(&body);
    // offset solid space
    canvas.set_width(w - 10);
    canvas.set_height(h - 10);
    canvas.style().set_property("border", "5px solid")?;

    let context = canvas
        .get_context("2d")
        .expect("Could not get context")
        .unwrap()
        .dyn_into::<CanvasRenderingContext2d>()
        .unwrap();

    let drawing_ok = Rc::new(Cell::new(false));
    {
        let context = context.clone();
        let drawing_ok = drawing_ok.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            drawing_ok.set(true);
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }
    {
        let context = context.clone();
        let drawing_ok = drawing_ok.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            if drawing_ok.get() {
                context.line_to(event.offset_x() as f64, event.offset_y() as f64);
                context.stroke();
                context.begin_path();
                context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            }
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }
    {
        let context = context.clone();
        let drawing_ok = drawing_ok.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            drawing_ok.set(false);
            context.line_to(event.offset_x() as f64, event.offset_y() as f64);
            context.stroke();
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }

    Ok(())
}
```

余談はさておき、`lib.rs` 内のコードを見ていきましょう

```rs
// 1
let drawing_ok = Rc::new(Cell::new(false));
    {
        let context = context.clone();
        let drawing_ok = drawing_ok.clone();
        // 2 Closure::wrapでRustのクロージャをJSのクロージャにする
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            drawing_ok.set(true); // drawing_okの値をset
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
        // 3
        closure.forget();
    }
    ...
```
### 1 Rc::new(Cell::new())
まず、`drawing_ok`の初期化です。今回は `MouseEvent` の状態によって挙動が変わるものなのでシングルスレッド間で状態を共有できる[Rc](https://doc.rust-lang.org/std/rc/struct.Rc.html)(参照カウントポインター)を使用します。また、`MouseEvent` には、*up*, *down* があるように可変な状態を持ちます。その際に使うのが内部可変性コンテナである [Cell](https://doc.rust-lang.org/std/cell/struct.Cell.html) (ただCellにはmemcpyでコピーできなければならないといったような制約がある)
シングルスレッドかつ複数のクロージャ間内での状態共有をしたいためにRcまたCellでget, setの恩恵を受けています。

この辺について深く知りたいかたは　@qnighyさん の[記事](https://qiita.com/qnighy/items/4bbbb20e71cf4ae527b9) と @wada314さん の[記事] (https://qiita.com/wada314/items/24249418983312795c08)を読むことをオススメします！


### 2 Closure::wrap

コードに書いてあるように、JSでの`addEvenntListener`の実装のようにクロージャ(Event発火時の処理)を実装していきます。
ここでは、rustのクロージャをjsのクロージャにするために、`Closure::wrap` を使用します。

[Closure.html#method.wrap](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#method.wrap)に書いてある通り、`wrap` methodを使用するには注意点があります。

-  `Fn` or `FnMut` のtraitの実装が必要 : この[記事](https://qiita.com/shortheron/items/c1735dc4c7c78b0b55e9)が詳細です
-  `no stack references` => `move` を使う: これはクロージャに環境の所有権を取得することを強制する
- 最大で7つの引数を使用可能
- 引数と戻り値はすべてJSで共有できるものとする

### 3 forget()
これが今回自分が面白いなと思ったポイントで

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">rustの関数にはstd::mem::forgetっていうものがあって端的にいうと意図的にメモリリークをさせる関数らしい。<br>wasmとかでClosureを使う処理の時に、スコープを抜けるとDropするというrustの機能を無視して使うっぽい<br>forgetしないとDropするので画像のようにすでに破棄されてしまう <a href="https://t.co/7MTBt0YW3b">pic.twitter.com/7MTBt0YW3b</a></p>&mdash; soe (@poccariswet) <a href="https://twitter.com/poccariswet/status/1207237989996232704?ref_src=twsrc%5Etfw">December 18, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


といったように、ここではあえてメモリリーク(スコープ抜けてもDropしない)するような実装になっています。
理由としては、[ここ](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#method.forget)にも書いてある通り、「このクロージャをリークして、プログラム全体の期間中有効にする」というようにJSのイベントリスナーのように常にイベントの発火を受け付けるようにするためです。

メモリ安全が担保されているrustであえてメモリリークされるような実装があるのは面白いですよね。
でも、メモリリークされていることが明示的にわかることで結局、メモリ安全が担保されているような気がします。

## まとめ

今回は、単純なお絵かきアプリを元に、wasmでの`EventListener`の実装を見ていきました。
コードとともに解説した3つのことを覚えれば誰でも簡単に`EventListener`の実装が可能だと思いますので、みなさんもぜひ rustでwasmをしましょう！