WebAssembly自体はプログラミング言語ではありません。対応した各種言語から変換します。そのため、採用する言語によって利用できる機能や開発効率が異なるでしょう。
今回は生成されるバイナリをプログラミング言語ごとに見てみます。
GoとRust
利用したのはGoとRustです。RustはWebAssemblyで最初に採用されているプログラミング言語であり、サイズも最適化されているのではないかと予想されます。対してGoはWebAssemblyを目的としてはいませんし、標準ライブラリなども多数入りそうな予感がします。
どちらもHello Worldを出力するだけのWebAssemblyファイルを作った場合のサイズです。
- Go : 1,301,957 byte
- Rust : 1,981,246 byte
となり、意外にもGoのが小さくなります。
Rustのサイズを軽減する
Rustはデフォルトのままでは余計なコードがたくさん入っています。そこで wasm-gc を使ってサイズを軽減するのが基本です。このコマンドは下記コマンドでインストールできます。
| cargo install --git https://github.com/alexcrichton/wasm-gc |
そしてコマンドを実行します。
| wasm-gc target/wasm32-unknown-unknown/debug/hello.wasm hello.min.wasm |
この時生成される hello.min.wasm は207 byteとなっています。大幅に軽減されるのが分かります。
まとめ
Goはまだベータ実装なので、今後のバージョンアップによってサイズが最適化されていくのではないでしょうか。数MBあると、Webアプリケーションとしてはキャッシュの利用を考えたくなるでしょう。
Rustを使うことで小さなWebAssemblyファイルは作りやすくなります(もちろんコード量によりますが)。開発効率性をとるか、サイズをとるかは実行環境によるでしょう。よりシビアに動かす際にはプログラミング言語の選定にも注意が必要です。
WebAssemblyを使う場合、wasmという拡張子のファイルをサーバ上に配置して、それを読み込むのが一般的です。しかし何もファイルが必ず必要な訳ではありません。
そんな試みで作られているのがinline-wastです。WebAssemblyのテキスト版であるWASTを使ってインラインでWebAssemblyを使えるようにするソフトウェアです。
inline-wastの使い方
コード例です。i32.constなどがWASTにあたるコードです。
| const {wastInstructions} = require('inline-wast/lib/interpreter'); | |
| function add(a, b) { | |
| const fn = wastInstructions` | |
| (i32.const ${a}) | |
| (i32.const ${b}) | |
| (i32.add) | |
| `; | |
| return fn(); | |
| } | |
| console.log(add(3, 1)); // 4 |
ここで定義されるwastInstructionsの実行結果として返ってくる関数はWebAssemblyなので高速に処理されるものです。興味深いのは文字列で関数を生成しているので、動的にその内容を変更できるということです。処理が長くなりそうな場合、動的にWebAssembly化することで高速化させられる可能性があります。
WebAssemblyはバイナリとテキスト版があります。テキスト版は慣れれば読めないことはない文字列です。面白い仕組みです。
WebAssemblyはプログラミング言語ではなく、様々なプログラミング言語から生成されるWebブラウザ上で実行できるバイナリフォーマットになります。基本はRustやC、C++であり、他にもGo、Java、Kotlin、Swiftなど様々なプログラミング言語が対応しています。
しかし複数のプログラミング言語を混ぜて開発するのは要員確保も大変です。そこで注目したいのがTypeScriptからWebAssemblyファイルを生成するAssemblyScriptです。今回はその特徴を紹介します。
JavaScriptの関数を呼び出せる
AssemblyScriptではdeclareを使って関数を定義することで、JavaScript側の関数を呼び出せます。例えば以下はsayHello関数を呼び出しています。
| declare function sayHello(): void; | |
| sayHello(); |
そしてJavaScript側では次のように記述します。mainの中でsayHelloという関数を定義することで、AssemblyScriptからJavaScriptの関数呼び出しを可能にしています。
| WebAssembly.instantiateStreaming(fetch("../out/main.wasm"), { | |
| main: { | |
| sayHello() { | |
| console.log("Hello from WebAssembly!"); | |
| } | |
| }, | |
| env: { | |
| abort(msg, file, line, column) { | |
| console.error("abort called at main.ts:" + line + ":" + column); | |
| } | |
| }, | |
| }) |
JavaScriptからAssemblyScriptの関数を呼び出す
逆にJavaScriptからAssemblyScriptの関数を呼び出す場合はexportを使って関数を公開します。型を指定しなければなりません。
| export function add(x: i32, y: i32): i32 { | |
| return x + y; | |
| } | |
| export function minus(x: i32, y: i32): i32 { | |
| return x - y; | |
| } |
これで、WebAssemblyファイル読み込み後の result.instance.exports の中に関数が入ります。
| exports.minus(23, 20); | |
| // -> 3 |
文字列を返す場合
文字列の場合は多少注意が必要です。まず関数の定義の時点で文字列の長さも返すように定義します。
| declare function sayHello(msg: string, len: usize): void; | |
| export function say(): void { | |
| const str: string = "hifive"; | |
| sayHello(str, str.length); | |
| } |
次に受け取った側ではメモリ中の値を読み取り、デコードしなければなりません。
| sayHello(index) { | |
| const length = mem.getUint32(index,true); | |
| const array = new Uint16Array(mem.buffer,index + 4,length); | |
| const str = new TextDecoder('utf-16').decode(array); | |
| console.log(`Hello ${str}, from WebAssembly!`); | |
| } |
memはWebAssemblyファイルを読み込んだ後 result.instance.exports.memory.buffer で取得できます。
| mem = new DataView(result.instance.exports.memory.buffer); |
まとめ
WebAssemblyがTypeScriptで書けるようになれば、 TypeScriptだけでフロントエンドの開発が完結できます。もちろんサブセットなので全ての機能が使える訳ではありません。しかし高速な処理が必要になった際に、AssemblyScriptであれば書き慣れた構文でWebAssembly化できるメリットが大きいでしょう。
AssemblyScript/assemblyscript: A TypeScript to WebAssembly compiler 🚀
WebAssemblyはあらかじめコンパイルされているのでJavaScriptのように実行時にコードをパースする必要がなく、高速に動作します。WebAssemblyのコードはバイナリデータになっているので処理を隠蔽できているように見えますが、デコンパイルを行うことで人間が読める形に変換できます。
今回はwabtを使ってWebAssemblyのデコンパイルを行ってみます。
元になるコード
今回はGoを使っています。一番簡単なコードです。
| package main | |
| func main() { | |
| println("Hello, wasm!") | |
| } |
WebAssemblyにする
このコードをコンパイルしてWebAssemblyにします。
| $ GOOS=js GOARCH=wasm go build -o test.wasm main.go |
そしてできあがるのが test.wasm です。このファイルは1.3MBありました。
デコンパイルする
ではここからデコンパイルします。その際に使うのがwabtです。WebAssembly用のツールが各種含まれています。ビルド時にはcmakeが必要です。
| $ git clone --recursive https://github.com/WebAssembly/wabt | |
| $ cd wabt | |
| $ make |
そして生成されるwasm2watコマンドを使ってデコンパイルします。
| bin/wasm2wat test.wasm > test.wat |
デコンパイルすると25.8MBになりました。かなり肥大化しているのが分かります。
コードを見る
デコンパイルされたコードは、WebAssemblyのテキスト版です。
| (module | |
| (type (;0;) (func (result i32))) | |
| (type (;1;) (func (param i32))) | |
| (type (;2;) (func (param i64 i64 i64 i64) (result i64))) | |
| (type (;3;) (func (param i32 i32 i32) (result i32))) | |
| (type (;4;) (func (param i64 i64 i64) (result i64))) | |
| (type (;5;) (func (param i64 i64))) | |
| (type (;6;) (func (param i32 i32))) | |
| (type (;7;) (func (param i32 i32 i32))) | |
| (type (;8;) (func (param i64 i64) (result i64))) | |
| (type (;9;) (func (param f64) (result i64))) | |
| (import "go" "debug" (func (;0;) (type 1))) | |
| (import "go" "runtime.wasmExit" (func (;1;) (type 1))) | |
| : |
その中に Hello, wasm という文字列も入っています。
| byte block (GC sweep waitHello, wasm!\0abad map statefatal error: |
つまりWebAssemblyでバイナリ化していたとしても、任意の処理がどこで、どのように行われているか、探そうと思えば探し出せると言うことです。
WebAssemblyでコードをコンパイルしたとしても、100%安全という訳ではありません。Javaと同レベルくらいに考える方が良いかもしれません。隠し方は幾つかありますので、なるべく分かりづらくなる方法を選ぶべきで、安直にキーなどをコードに書かない方が良さそうです。
Go1.11からサポートされたWebAssemblyにおいて、ネットワークを利用する方法を紹介します。今回はJSONファイルの取得方法です。
注意点
実際のネットワークアクセスはWebブラウザの開発者ツール、ネットワークタブ内で確認できます。ネットワークアクセスを匿名化したり、CORS制限も適用されますので注意してください。
利用法
今回はCORS制限がなく、APIキーも不要なMicrolinkのコンテンツを取得します。 "net/http" パッケージを使ってネットワークアクセスをします。そして ioutil.ReadAll を使ってコンテンツを取得します。これはバイト文字列になっていますので、 string 関数を使って文字列化します。
| package main | |
| import ( | |
| "fmt" | |
| "syscall/js" | |
| "net/http" | |
| "strconv" | |
| "io/ioutil" | |
| ) | |
| func main() { | |
| resp, err := http.Get("https://api.microlink.io/?url=https://twitter.com/futurism/status/882987478541533189&video&palette") | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| defer resp.Body.Close() | |
| b, err := ioutil.ReadAll(resp.Body) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| js.Global().Get("document"). | |
| Call("querySelector", "#html"). | |
| Set("value", string(b)) | |
| } |
これまでのWebAssemblyはDOMやFetch APIなどが使えないのが難点でした。しかしGo版のWebAssemblyによって、その問題はなくなっています。 WebAssemblyの可能性を飛躍的に高めてくれるのではないでしょうか。
Go1.11からWebAssemblyが公式サポートされました。これまでWebAssemblyの開発はRustがメインに使われてきましたが、Goは構文も分かりやすく、非常に良い選択肢になると思われます。
特に面白いのはDOM連携がとても容易な点ではないかと思います。今回はその使い方を紹介します。
まず基本から
Go1.11以降のインストールが終わっている前提とします。例えばコンソールに出力するためには以下のようなコードを書きます。
| package main | |
| import ( | |
| "fmt" | |
| ) | |
| func main() { | |
| fmt.Println("Hello World") | |
| } |
これをコンパイルします。test.wasmというファイルが作成されます。
| GOOS=js GOARCH=wasm go build -o test.wasm main.go |
Webブラウザで実行する際には、公式に用意されているHTML/JavaScriptファイルを使うと簡単です。
| curl -sO https://raw.githubusercontent.com/golang/go/master/misc/wasm/wasm_exec.html | |
| curl -sO https://raw.githubusercontent.com/golang/go/master/misc/wasm/wasm_exec.js |
そしてWebサーバを立ち上げて wasm_exec.html を読み込みます。runというボタンがあるので、それを押すと処理が実行されます。
DOM連携
Goの場合、main関数を必ず使うようです。そしてmain関数は引数が使えません。そこで syscall/js パッケージを読み込みます。これを読み込むと、例えば #num1 の値を次のようにして取得できます。
| js.Global().Get("document"). | |
| Call("querySelector", "#num1"). | |
| Get("value"). | |
| String() |
逆にセットする場合は下記のようになります。
| js.Global().Get("document"). | |
| Call("querySelector", "#res"). | |
| Set("value", "Hello") |
Callはメソッドの呼び出し、値の取得はGet、設定はSetと非常にシンプルな形です。例えば足し算の処理は下記のように記述できます。
| package main | |
| import ( | |
| "fmt" | |
| "syscall/js" | |
| "strconv" | |
| ) | |
| func main() { | |
| num1 := js.Global().Get("document"). | |
| Call("querySelector", "#num1"). | |
| Get("value"). | |
| String() | |
| i, _ := strconv.Atoi(num1) | |
| fmt.Println(i) | |
| num2 := js.Global().Get("document"). | |
| Call("querySelector", "#num2"). | |
| Get("value"). | |
| String() | |
| j, _ := strconv.Atoi(num2) | |
| js.Global().Get("document"). | |
| Call("querySelector", "#res"). | |
| Set("value", i + j) | |
| } |
これで足し算ができます。デモをこちらに置いてありますので体験してみてください。
なお、GoのWASMはサイズが若干大きめです(上記の足し算を行うもので2.5MBあります)。また、関数が使えない(分かっていないだけかも知れません)ので、ファイル数が増えたり、main関数での分岐が必要になるかも知れません。
とは言え、Goの文法でWASMが書けるメリットは非常に大きく、Webアプリケーションの高速化が容易に実現できたり、全体をGoで開発することも夢ではなさそうです。
WebAssemblyはWebアプリケーションを高速に処理できる技術ですが、幾つかの欠点もありました。その一つがDOMを処理できないこと、さらにFetch APIやWebAudioなどのAPIが使えない点です。
しかし先日Announcing the web-sys crate! | Rust and WebAssemblyがアナウンスされ、WebAssemblyでもDOMや各種APIの利用が可能となっています。
実際にはラッピング?
今回発表されたのはweb-sysというクレート(パッケージ)です。すでにWebAssemblyに対応しているWebブラウザであれば利用可能となっていることから、WebAssemblyエンジン自体のバージョンアップは不要なようです。そのため、DOMやAPI操作はメインスレッド側のJavaScriptで行っているかと思われます。ただし、Fetch APIで使ったURLなどはWebブラウザ側のソースにはありませんでした(ネットワークログを見ると、どこにアクセスしたかは分かります)。
Hello World
Hello Worldのアラートを出す場合のコードです。
| extern crate wasm_bindgen; | |
| use wasm_bindgen::prelude::*; | |
| #[wasm_bindgen] | |
| extern "C" { | |
| fn alert(s: &str); | |
| } | |
| #[wasm_bindgen] | |
| pub fn greet(name: &str) { | |
| alert(&format!("Hello, {}!", name)); | |
| } |
DOM操作
DOMに記述する際のコードです。 web_sys クレートを介してWindowやDocumentにアクセスできます。
| extern crate wasm_bindgen; | |
| extern crate web_sys; | |
| use wasm_bindgen::prelude::*; | |
| #[wasm_bindgen] | |
| pub fn run() -> Result<(), JsValue> { | |
| let window = web_sys::window().expect("no global `window` exists"); | |
| let document = window.document().expect("should have a document on window"); | |
| let body = document.body().expect("document should have a body"); | |
| let val = document.create_element("p")?; | |
| val.set_inner_html("Hello from Rust!"); | |
| AsRef::<web_sys::Node>::as_ref(&body).append_child(val.as_ref())?; | |
| Ok(()) | |
| } |
Fetch API
Fetch APIを使って外部リソースを取得するコードです。Promiseを使います。
| extern crate futures; | |
| extern crate js_sys; | |
| extern crate wasm_bindgen; | |
| extern crate wasm_bindgen_futures; | |
| extern crate web_sys; | |
| #[macro_use] | |
| extern crate serde_derive; | |
| use futures::{future, Future}; | |
| use js_sys::Promise; | |
| use wasm_bindgen::prelude::*; | |
| use wasm_bindgen::JsCast; | |
| use wasm_bindgen_futures::future_to_promise; | |
| use wasm_bindgen_futures::JsFuture; | |
| use web_sys::{Request, RequestInit, RequestMode, Response}; | |
| #[derive(Debug, Serialize, Deserialize)] | |
| pub struct Branch { | |
| pub name: String, | |
| pub commit: Commit, | |
| } | |
| #[derive(Debug, Serialize, Deserialize)] | |
| pub struct Commit { | |
| pub sha: String, | |
| pub commit: CommitDetails, | |
| } | |
| #[derive(Debug, Serialize, Deserialize)] | |
| pub struct CommitDetails { | |
| pub author: Signature, | |
| pub committer: Signature, | |
| } | |
| #[derive(Debug, Serialize, Deserialize)] | |
| pub struct Signature { | |
| pub name: String, | |
| pub email: String, | |
| } | |
| #[wasm_bindgen] | |
| pub fn run() -> Promise { | |
| let mut opts = RequestInit::new(); | |
| opts.method("GET"); | |
| opts.mode(RequestMode::Cors); | |
| let request = Request::new_with_str_and_init( | |
| "https://api.github.com/repos/rustwasm/wasm-bindgen/branches/master", | |
| &opts, | |
| ).unwrap(); | |
| request | |
| .headers() | |
| .set("Accept", "application/vnd.github.v3+json") | |
| .unwrap(); | |
| let window = web_sys::window().unwrap(); | |
| let request_promise = window.fetch_with_request(&request); | |
| let future = JsFuture::from(request_promise) | |
| .and_then(|resp_value| { | |
| assert!(resp_value.is_instance_of::<Response>()); | |
| let resp: Response = resp_value.dyn_into().unwrap(); | |
| resp.json() | |
| }).and_then(|json_value: Promise| { | |
| JsFuture::from(json_value) | |
| }).and_then(|json| { | |
| let branch_info: Branch = json.into_serde().unwrap(); | |
| future::ok(JsValue::from_serde(&branch_info).unwrap()) | |
| }); | |
| future_to_promise(future) | |
| } |
rustwasm/wasm-bindgen: Facilitating high-level interactions between wasm modules and JavaScriptに多数のサンプルが掲載されていますので、ぜひコードを見てください。動いているコードはExamples – The wasm-bindgen Guideにて確認できます。WebGLやCanvas、WebAudioなどのサンプルが掲載されています。
WebAssembly(WASM)はWebブラウザ上で動くプログラムです。JavaScriptとの違いはコンパイルされた実行ファイルであることで、JavaScriptのようにパースする必要がないので高速に処理されます。
高速とは言っても、どれくらい速いのか実行してみないと分からないでしょう。そこで試した結果を紹介します。
フィボナッチ関数を実行する
フィボナッチ関数はJavaScriptで書くと次のように表現されます。
| function f(n) { | |
| if (n <= 1) return 1; | |
| return f(n - 1) + f(n - 2); | |
| } |
例えば5のフィボナッチ数は8、6のフィボナッチ数は13になります。小さな数であれば一瞬で求められますが、40くらいの数字になると時間がかかるようになります。同じ関数をRustで書くと次のようになります。
| pub fn f(n:i32) -> i32 { | |
| if n <= 1 { | |
| 1 | |
| } else { | |
| f(n - 1) + f(n - 2) | |
| } | |
| } |
計算する
では実際に計算を行ってみた結果です(macOS Safariでの実行結果。ハードウェアの性能によって数値は異なります)。単位はmsです。
| JavaScript | WASM | |
|---|---|---|
| 20 | 2 | 1 |
| 30 | 69 | 4 |
| 40 | 6,733 | 423 |
| 42 | 17,356 | 1,061 |
| 44 | 46,195 | 2,839 |
この結果から分かるのは、少ない数であればJavaScriptもWebAssemblyも殆ど変わりませんが、40以上になるとWebAssemblyが圧倒的に高速になります。50になるとJavaScript側で処理するのは現実的ではないでしょう。フィボナッチ関数に限らず、リストのソート処理であったり、ループ処理などはWebAssemblyの方が効率的なはずです。
WebAssemblyではネットワーク関数が使えない、DOMが扱えないと言った制限がありますが(現在解消されようとしています)、計算処理の速度においては使わない手はありません。開発できる言語はRustに限らず、様々な言語があります。ぜひWebAssemblyの活用を検討してください。
PWA(Progressive Web Apps)というキーワードに注目が集まっていますが、実際に取り組む機会がないという方は多いようです。そこでhifiveのTodoアプリをベースに、PWAを体験してもらうハンズオンを開催しました。
コンテンツはGitHubにあります
体験してもらったハンズオンコンテンツはhifivemania/pwahandsondocsに置いてあります。内容としてはこれに合わせて進めたので、オンラインで体験してもらうことも可能です。ハンズオンはこの資料を基に各自のペースで進めてもらうのですが、何か分からないところがあればすぐにフォローしてもらえるのが参加メリットになります。
hifiveとは
ハンズオンの最初にhifiveとは何かを紹介しました。今回のハンズオンではTodo投稿機能などにおいてhifiveを使っていますが、PWAを扱う上では必須ではありません。しかしビジネスとしてWebシステムを開発する上で問題になりがちな要員確保や中長期的なメンテナンスという問題においてhifiveは最適な解決策になるべく開発を進めています。これはPWAにおいて役立つ場面があるはずです。
hifive – HTML5企業Webシステムのための開発プラットフォーム – hifive
ハンズオン開始
ハンズオンは前述の通り、各自のペースで行ってもらいます。そのため、ハンズオン中は基本的に静かで黙々と作業されています。進めていく中でつまずくポイントは幾つも、人によっても異なるのでそれらを随時フォローアップしていきました。

今回体験してもらったもの
今回のハンズオンではPWAにおいて以下の要素を体験してもらいました。
- オンライン/オフライン対応
- アプリ化
- Webプッシュ通知
PWAでは他にも要素技術があります。今回のを基礎として、自社やクライアント向けの開発に役立てて欲しいと思います。
次回開催について
PWAハンズオンは好評いただいており、すでに2回目が設定済みです。会場は今回と同じく渋谷のhoops link tokyoさんになります。日時は4月12日(金)19時からです。PWAを体験してみたい方はぜひご参加ください!
PWAにおいて肝となるのがキャッシュです。あるURLにアクセスした時にコンテンツを返却するのがキャッシュですが、HTML5におけるCACHE APIはすべてプログラムから操作しなければならず、実装が大変です。
そこで使ってみたいのがWorkboxになります。Google製のPWAにおけるキャッシュコントロールを便利にしてくれるライブラリになります。今回はWorkboxに対応しているプラグインと、その機能を紹介します。
workbox.backgroundSync.Plugin
ネットワークがオフラインだった場合、オンラインになったタイミングで同期してくれます。queueArgsとしてキューに送信する引数を指定します。
workbox.broadcastUpdate.Plugin
キャッシュを更新した時に、それをメッセージングしてくれます。メインスレッドのJavaScriptで受け取ったならば、表示を更新したりユーザに再読込を促したりできるでしょう。チャンネル名を指定します。
オプションは以下の通りです。
- headersToCheck
チェックするヘッダーを指定。 - source
データ元のソースを指定。
workbox.cacheableResponse.Plugin
キャッシュする際のヘッダーレスポンスを指定。
オプションは以下の通りです。
- statuses
キャッシュするHTTPレスポンスコードを配列で指定。 - headers
キャッシュするHTTPレスポンスヘッダーをオブジェクトで指定。
workbox.expiration.Plugin
キャッシュに有効期限を付けられるプラグイン。
オプションは以下の通りです。
- maxEntries
最大の登録数。 - maxAgeSeconds
有効期限。秒で指定。 - purgeOnQuotaError
容量制限に引っかかった場合はエラーにするかどうか。
workbox.rangeRequests.Plugin
HTTPのRangeへのアクセス対応用です。分割ファイルをリクエスト、キャッシュするのに使います。
自作プラグインの作り方
プラグインは以下の5つのメソッドを持ったオブジェクトになります。
- cacheWillUpdate
- cacheDidUpdate
- cachedResponseWillBeUsed
- requestWillFetch
- fetchDidFail
Workboxを使えばキャッシュのコントロールが柔軟になります。プラグインを使ったり、自作することでより簡単に実装できるようになるでしょう。ぜひ使い方を覚えましょう。