Try   HackMD

技術に広く浅く触れてみる〜WASM編〜

はじめに

今回は概要の概要みたいな部分しか扱いません.歴史や詳しい話などを知りたい方は 参考文献 に示したページを参照してください(特に MDN は何もかもが載っているのでおすすめです.)

(1) WASM ってなんだ

WebAssembly はモダンなウェブブラウザーで実行できる新しいタイプのコードです。ネイティブに近いパフォーマンスで動作するコンパクトなバイナリー形式の低レベルなアセンブリ風言語です。(WebAssembly | MDN より引用)

従来では,ブラウザ上でプログラムを動作させるためには JavaScript を記述するしかありませんでした.JavaScript には長い歴史があり,今でこそ柔軟性や表現量に優れている高水準な言語となりました.しかし,C/C++ や Rust といったコンパイル言語と比較してパフォーマンスが悪いという問題点はおそらくこれからも解決しないでしょう.それを解決するのが WASM です.

WASM はプログラミング言語とかライブラリとかではなく,ブラウザにおけるバイナリコードの新しいフォーマット です.以下のような特徴を持ちます.

  • C/C++ や Rust といったコンパイル言語から生成することができる(でかい)
  • JavaScript に置き換わるのではなく,JavaScript と WebAssembly が共存する形になる.つまり,互いに呼び出し合うことができる.

これにより,以下のことが可能になります.

  • C/C++ で記述されたアプリケーションを Web 上に移植する
  • ロジック的な部分は WASM で全て実装し,UI といった部分は JavaScript で実装する

これらの点から,近年では WASM が注目され初めています.

(2) WASM の体験

作ってみるもの

今回は単純に WASM のパフォーマンスの高さを知ろうということで,入力した整数以下の素数を全列挙する Web アプリケーションを制作してみます.

Requirements

  • Rust
  • Go

ウォーミングアップ

まずは肩慣らしとして JS から Rust で実装した fn add(a: i32, b: i32) -> i32; を呼び出してみましょう.

準備

適当なディレクトリで以下のコマンドを実行し,warming-up に移動してエディタを開いてください.

$ rustup target add wasm32-unknown-unknown
$ cargo new --lib warming-up

コード

src/lib.rs

// rustc はコンパイル時にデフォルトで名前修飾(mangle)をするので, // attribute を指定することによって mangle しないようにコンパイラに伝える #[no_mangle] pub fn add(a: i32, b: i32) -> i32 { a + b }

index.html

<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>Hello, WebAssembly!</title> <script> const wasm = "./target/wasm32-unknown-unknown/release/warming_up.wasm"; fetch(wasm) // wasm ファイルを fetch(読み込み) する .then((response) => response.arrayBuffer()) // 読み込んだ結果をバイト列に変換 .then((bytes) => WebAssembly.instantiate(bytes, {})) // wasm のバイト列から wasm のインスタンスを生成 .then((results) => { console.log(results.instance.exports.add(1, 2)); // exports の中に rust で定義した関数が入っているので,そこから add を呼び出し }); </script> </head> </html>

Cargo.toml

[lib]
crate-type = ["cdylib"]

実行

# rust を wasm にビルド
$ cargo build --target=wasm32-unknown-unknown --release

# 簡単な http サーバーを建てる
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

http://localhost:8000 にアクセスし,開発者ツールを開いて console を除くと 3 と表示されていると思います.これが wasm の add() を呼び出した結果となります.

add 関数の引数を可変にしてみる

現状,add の引数がハードコーディングされているので少し味気ないですね.そこで,ブラウザ上から整数を2つ入力し,それを add 関数に渡すようにしてみましょう.

コード

index.html

<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>Hello, WebAssembly!</title> <script> const wasm = "./target/wasm32-unknown-unknown/release/warming_up.wasm"; function calcbtn_handle() { const x = document.getElementById("input_x").value; const y = document.getElementById("input_y").value; const res = add(parseInt(x, 10), parseInt(y, 10)).then((x) => { document.getElementById("result").textContent = x; }); } function add(x, y) { return fetch(wasm) // wasm ファイルを fetch(読み込み) する .then((response) => response.arrayBuffer()) // 読み込んだ結果をバイト列に変換 .then((bytes) => WebAssembly.instantiate(bytes, {})) // wasm のバイト列から wasm のインスタンスを生成 .then( (results) => results.instance.exports.add(x, y) // exports の中に rust で定義した関数が入っているので,そこから add を呼び出し ); } </script> </head> <body> <label>数値を入力してください.</label> <div> <label>x: </label> <input type="number" id="input_x" /> </div> <div> <label>y: </label> <input type="number" id="input_y" /> </div> <div> <button onclick="calcbtn_handle();">calc</button> </div> <div id="result"></div> </body> </html>

実行

以下のようにして整数を2つ入力し,calc ボタンを押すこと色々な計算ができるようになったと思います.

N
以下の素数を全列挙してくれるしずっぴーを実装

ということでウォーミングアップが終わったので本題の方を進めていきましょう.

とはいえ,ウォーミングアップで大体やりたいことはやったので,後は add 関数を sieve 関数にするだけです.

準備

また適当なディレクトリに移動して以下のコマンドを実行してください.
今回は npm を用います.

$ npm init rust-webpack sieve-wasm
$ cd sieve-wasm
$ npm install react react-dom  # 私は素の js が書けません

コード

src/lib.rs

use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { fn alert(s: &str); } /// \[1, n\] 内の素数を列挙して alert に渡す \ #[wasm_bindgen] pub fn sieve(n: usize) { let mut prime_table = vec![] for i in 3..=((n as f64).sqrt() as usize) { if !prime_table[i] { continue; } let mut j = 2 * i; while j <= n { prime_table[j] = false; j += i; } } let mut prime_list = Vec::new(); for (i, is_prime) in prime_table.iter().enumerate() { if *is_prime { prime_list.push(i); } } let primes_str = prime_list .iter() .map(|x| x.to_string()) .collect::<Vec<String>>() .join(" "); alert(&primes_str); }

js/index.js

import("../pkg/index.js").then((module) => { module.sieve(10000); }) function sieve(n) { let prime_table = new Array(n + 1); prime_table.fill(true); prime_table[0] = false; prime_table[1] = false; console.log(Math.sqrt(n)); for (let i = 2; i <= Math.sqrt(n); i++) { if (!prime_table[i]) continue; let j = i * 2; while (j <= n) { prime_table[j] = false; j += i; } } let primes = new Array(); for (let i = 2; i <= n; i++) { if (prime_table[i]) { primes.push(i); } } alert(primes.join(" ")); }

今日のまとめ

WASM は JS よりも速い!

参考文献

  1. WebAssembly とは - Qiita
  2. WebAssembly - MDN
  3. WebAssembly 開発環境構築の本