飴屋

WebAssembly/web-sysとjs-sys

WebAssemblyの学習にあたって、JavaScriptでは低速すぎる処理をWebAssemblyで置き換えて高速化を図ろうというのを念頭においていましたが、WebAssemblyで数値計算だけ行い、求めた数値をJavaScriptに渡して利用するとなるとステップが増えてまどろっこしいかもなぁ、とは思っていましたが、どうやらJavaScript相当のことができるようなクレートが開発されているみたいです。

web-sys
Web APIの機能
js-sys
ECMAScript標準の機能

この二つのクレートを使うと#[wasm_bindgen]アトリビュートの指定なしでJavaScriptでやっていた実装相当のことができるようになるみたいです。最初はWebAssemblyでわざわざ書くようなことではないんじゃないかと思ったんですが、いざ実装を考えてみるといちいちJavaScriptと協調させながら処理を進めていくように書くのはやはり億劫になりそうですね。自分はCanvasに何か描画する処理を書きたいので、web-sysクレートを使ってCanvas APIを叩いてみようと思います。

Cargo.toml

[dependencies]
js-sys = "0.3.46"
wasm-bindgen = "0.2.63"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'CanvasRenderingContext2d',
  'Document',
  'Element',
  'HtmlCanvasElement',
  'Window',
]

dependencies.web-sysという項目を追加してバージョンと使用する構造体を書きだします。列挙した構造はJavaScriptの同名クラスと互換性があるみたいでわかりやすい!
dependencies項にはjs-sysも書き足しました。

実装

事前に動作確認用の環境で、HTMLファイルにcanvas#canvas要素を追加し、wasmファイルのdrawメソッドを呼び出すようにしておきました。その上でlib.rsに以下のようにdrawメソッドを書き足しました。

use std::f64;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[wasm_bindgen]
pub fn draw() {
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document.get_element_by_id("canvas").unwrap();
    let canvas: web_sys::HtmlCanvasElement = canvas
        .dyn_into::<web_sys::HtmlCanvasElement>()
        .map_err(|_| ())
        .unwrap();

    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()
        .unwrap();

    context.begin_path();

    // Draw the outer circle.
    context
        .arc(75.0, 75.0, 50.0, 0.0, f64::consts::PI * 2.0)
        .unwrap();

    // Draw the mouth.
    context.move_to(110.0, 75.0);
    context.arc(75.0, 75.0, 35.0, 0.0, f64::consts::PI).unwrap();

    // Draw the left eye.
    context.move_to(65.0, 65.0);
    context
        .arc(60.0, 65.0, 5.0, 0.0, f64::consts::PI * 2.0)
        .unwrap();

    // Draw the right eye.
    context.move_to(95.0, 65.0);
    context
        .arc(90.0, 65.0, 5.0, 0.0, f64::consts::PI * 2.0)
        .unwrap();

    context.stroke();
}

中身はこちらのスマイルマークを描画するものです。Canvasのコンテキスト(CanvasRenderingContext2d)を取得して、描画命令を行っていくのはJavaScriptのまんまですね。unwrapというRustならではの操作がついていますが大きく違うのはそれくらいかも。JavaScrptのdocument.getElementByIdはget_element_by_idという表記になるんですね。

これを自分に必要な形に落とし込んでいこうと思います。web-sysについて提供されている構造体について調べたかったら、この辺が参考になりそうです。メソッド(関数?)の返り値にunwrapが必要なのかどうかとかもここをみたらわかりそうです。WebAudioやらWebGLやらいろんなAPIにも対応してそうなので、使用用途も広がりますね。

DOMの操作

web-sysを使うと、当然のごとくDOMの操作もできるようになるわけですね。そのスピードがどんなものかわかりませんが、今ちょうどcanvas要素のサイズをプログラムから動的に変更したかったところなので、試しに自分でwidth,heightアトリビュートを書き換えてみます。

use web_sys::HtmlCanvasElement;
const CANVAS_WIDTH:i32 = 600;
const CANVAS_HEIGHT:i32 = 600;

pub fn draw() {
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document.get_element_by_id("canvas").unwrap();
    let canvas: HtmlCanvasElement = canvas
        .dyn_into::<HtmlCanvasElement>()
        .map_err(|_| ())
        .unwrap();
    adjust_canvas_size(&canvas);
}
fn adjust_canvas_size(_canvas:&HtmlCanvasElement) {
    _canvas.set_attribute("width",&CANVAS_WIDTH.to_string()).unwrap();
    _canvas.set_attribute("height",&CANVAS_HEIGHT.to_string()).unwrap();
}

web_sys::window()でブラウザ窓のWindowを取得したところから始まって、Documentを取得してget_element_by_idメソッドからcanvasのElementを取得し、dyn_intoメソッドでweb_sys::HtmlCanvasElementに動的にキャストしたところまではさっきと一緒です。HtmlCanvasElementを引数に新たに作ったadjust_canvas_size関数の中でElement.set_attributeメソッドでアトリビュートを書き換えました。実に明快です。動作確認してもcanvasのサイズが変更されていることを確認できました。

今回は重たい処理ではなかったので、処理スピードについて言及できませんが、別のプロダクトでお世話になるかもしれないのでいつかもう少し深入りすることになりそうな予感はあります。