LoginSignup
22
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

はじめに

この記事は Rustその2 Advent Calendar 2019の18日目の記事です。

自己紹介などについては、以前に書いた記事を読んでみてください

今回のゴール

本来、jsで実装されているaddEventListenerをwasmで登録して使えるようにするところまでを簡単に説明できればと思います。

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

rustのwasm crateである wasm-bindgen には web_sysというものがある。これは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

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

web_sysを使ったtutorialで実際に実装しながら学べるのでオススメです。

コードを見る

painting-sample (1).gif

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

また、paint-wasm にソースコードをおいているので見たいかたはリンクを飛んで見てみてください。

見てみる

実際のコードをみると

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をください!)

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 内のコードを見ていきましょう

// 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(参照カウントポインター)を使用します。また、MouseEvent には、up, down があるように可変な状態を持ちます。その際に使うのが内部可変性コンテナである Cell (ただCellにはmemcpyでコピーできなければならないといったような制約がある)
シングルスレッドかつ複数のクロージャ間内での状態共有をしたいためにRcまたCellでget, setの恩恵を受けています。

この辺について深く知りたいかたは @qnighyさん の記事@wada314さん の記事を読むことをオススメします!

2 Closure::wrap

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

Closure.html#method.wrapに書いてある通り、wrap methodを使用するには注意点があります。

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

3 forget()

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

といったように、ここではあえてメモリリーク(スコープ抜けてもDropしない)するような実装になっています。
理由としては、ここにも書いてある通り、「このクロージャをリークして、プログラム全体の期間中有効にする」というようにJSのイベントリスナーのように常にイベントの発火を受け付けるようにするためです。

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

まとめ

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

22
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
10