はじめに
この記事は 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.
web_sysを読んでいくと以上のように書かれている。ブラウザがWebで提供するすべてのAPIへのバインディングを提供していて、図のようにwasm-bindgenとグルーコードのWebIDLを使用してweb_sysを提供している。
web_sysを使ったtutorialで実際に実装しながら学べるのでオススメです。
コードを見る
今回は実装されたものは wasm-bindgenのtutorialにもある、簡単なお絵かきができるwebアプリですhttps://paint-wasm-sample.netlify.com/ に飛んで実際に試してみてください!
また、paint-wasm にソースコードをおいているので見たいかたはリンクを飛んで見てみてください。
見てみる
実際のコードをみると
<!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をください!)
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さん の[記事] (https://qiita.com/wada314/items/24249418983312795c08)を読むことをオススメします!
2 Closure::wrap
コードに書いてあるように、JSでのaddEvenntListener
の実装のようにクロージャ(Event発火時の処理)を実装していきます。
ここでは、rustのクロージャをjsのクロージャにするために、Closure::wrap
を使用します。
Closure.html#method.wrapに書いてある通り、wrap
methodを使用するには注意点があります。
-
Fn
orFnMut
のtraitの実装が必要 : この記事が詳細です -
no stack references
=>move
を使う: これはクロージャに環境の所有権を取得することを強制する - 最大で7つの引数を使用可能
- 引数と戻り値はすべてJSで共有できるものとする
3 forget()
これが今回自分が面白いなと思ったポイントで
rustの関数にはstd::mem::forgetっていうものがあって端的にいうと意図的にメモリリークをさせる関数らしい。
— soe (@poccariswet) December 18, 2019
wasmとかでClosureを使う処理の時に、スコープを抜けるとDropするというrustの機能を無視して使うっぽい
forgetしないとDropするので画像のようにすでに破棄されてしまう pic.twitter.com/7MTBt0YW3b
といったように、ここではあえてメモリリーク(スコープ抜けてもDropしない)するような実装になっています。
理由としては、ここにも書いてある通り、「このクロージャをリークして、プログラム全体の期間中有効にする」というようにJSのイベントリスナーのように常にイベントの発火を受け付けるようにするためです。
メモリ安全が担保されているrustであえてメモリリークされるような実装があるのは面白いですよね。
でも、メモリリークされていることが明示的にわかることで結局、メモリ安全が担保されているような気がします。
まとめ
今回は、単純なお絵かきアプリを元に、wasmでのEventListener
の実装を見ていきました。
コードとともに解説した3つのことを覚えれば誰でも簡単にEventListener
の実装が可能だと思いますので、みなさんもぜひ rustでwasmをしましょう!