読んだので簡単なまとめを。
この本について
前半は WebAssembly の基本的なしくみや Node.js・ブラウザで動かす方法、後半はパフォーマンス測定やデバッグ方法について学べる本。
また、特定の言語やツールチェイン(たとえば Rust だったら wasm-pack とか)に依存した内容ではなく、WAT と呼ばれる WebAssembly のテキスト表現を書きながら実装する。
構成
ざっくり Chapter ごとにやってることを書く。
- Chapter 1: Introduction to WebAssembly
- WebAssemblyとは何か、その特徴は、なぜ WebAssembly を使うかみたいな話
- WebAssembly Text Format (WAT) の紹介もここで
- Chapter 2: WAT Basics
- WAT の基本構文:関数の import、data、変数、if 文や loop など
- Chapter 3: Functions and Tables
- WebAssembly で定義した関数を JS から呼び出す
- Table (function table) について。Table を使った場合と関数を直接呼び出した場合のパフォーマンス比較
- Chapter 4: Low Level Bit Manipulation
- ビット演算について。 読んでない
- Chapter 5: String Manipulation
- 文字列操作
- Null-Terminated Strings と Length-Prefixed Strings という 2 つのポピュラーな方法
- Chapter 6: Linear Memory
- linear memory の使い方
- オブジェクトのような複雑なデータ構造を linear memory でどう管理するか
- Chapter 7: Web Applications
- ブラウザで WebAssembly を実行する方法
- DOM 操作をどう実現するか
- Chapter 8: Working with the Canvas
- Canvas を使ったアニメーション
- Chapter 9: Optimizing Performance
- ブラウザの DevTools を使ったパフォーマンス計測と改善
- パフォーマンス改善につながるテクニック
- Chapter 10: Debugging WebAssembly
- Chapter 11: AssemblyScript
Chapter 8 の Canvas アプリケーションは↓のような、物体の衝突判定アプリ。
正方形の形をした大量の物体が一定速度で動いており、他と衝突してるときだけ色が変わるというもの。
フレームごとの物体の座標計算と衝突判定を WebAssembly 側でやっている。
これは https://rustwasm.github.io/book/ で作ったものと似てるなーと思った。
全体的な感想
C や Rust からのコンパイルではなく素の WAT をひたすら書くので実用的な知識が得られたとは思わないけど、知らなかったことも多かったので学びはあった。
特に
- WebAssembly では文字列扱うの難しいのは知っていたが、Chapter 5 で具体的なアプローチとともに文字列操作の方法を学べた
- Chapter 6 の linear memory の章で、やや複雑な(
i32
みたいなプリミティブな値でない)データ構造を扱うために linear memory 上にデータをどう格納するのかがわかった - Chapter 9 で DevTools でのパフォーマンスの見方がわかった(これは若干 WebAssembly 関係なくなるけど)
- ところどころでパフォーマンス比較の話が登場しており、WebAssembly をどういうとき/どういうふうに使うべき/べきでないみたいなのが腹落ちした。たとえば:
- 純粋な WebAssembly 関数と、WebAssembly 関数の内部で JS から import した関数を呼び出した場合
- Table を使って間接的に関数を呼び出した場合と直接呼び出した場合
- 同じ処理内容を WebAssembly で実装した場合と JS で実装した場合
というあたりはよかった。
反面、WAT の構文の説明とか動作原理についてきちんと説明されてないと感じる部分もいくつかあった。たとえば i32.store
/ i32.load
とか data
とか。
local variable や global variable ってデータ上はどういうふうに格納されてるの?とか。
WAT の構文だけで言えば、 MDN のこのページを読むのとそんなに大差ないのでは、と感じた。
本自体の構成については、300 ページ弱あるもののサンプルコードは基本的に省略せず全部載せているためコードが大半を占めていて、ページ数から受ける印象ほど読むのが辛くなかった。
またコードの説明はかなり丁寧に書かれていた。
なんだかんだ英語の本を読むのが初めての挑戦だったけど挫折せずに完走できてよかった。
良かったところ、勉強になったところ
Chapter 2: WAT Basics
WAT の記法は S式 (S-Expression) 以外に Linear Instruction List Style というのもあり、両者が混在している。
S 式で表現した
(i32.add (i32.const 3) (i32.const 2))
は、Linear Instruction List Style で書いた以下と等価。
i32.const 3 i32.const 2 i32.add
後者は値が上から順にスタックされてく様子を脳内で覚えておかないといけないので、S 式の方が読みやすいと個人的には思う。
Chapter 5: String Manipulation
文字列を linear memory を介して JS とやり取りする場合、文字列の開始位置とともに文字列がどこで終わるのかという情報も JS 側がわかる必要がある。
そのための2つのポピュラーな方法として Null-Terminated Strings と Length-Prefixed Strings という方法がある。
1) Null-Terminated Strings
文字列の終わりに null 終端文字列を挿入する。
;; wat 側 (data (i32.const 0) "null-terminating string\00")
// js 側 const bytes = new Uint8Array(memory.buffer, str_pos, max_mem - str_pos); let log_string = new TextDecoder("utf8").decode(bytes); log_string = log_string.split("\0")[0];
2) Length-Prefixed Strings
文字列の最初に文字列自体の長さを埋め込んでおく。
;; wat 側 ;; 22 文字で、16 進数にすると 16 (data (i32.const 512) "\16length-prefixed string")
// js 側 const str_len = new Uint8Array(memory.buffer, str_pos, 1)[0]; // 文字列自体の長さが先頭に1バイト埋め込まれているので + 1 const bytes = new Uint8Array(memory.buffer, str_pos + 1, str_len); const log_string = new TextDecoder("utf8").decode(bytes);
Chapter 6: Linear Memory
Linear Memory は「ページ」という単位で確保する。1ページ 64 KB、最大ページ数は 32,767。
WAT 側では
(memory 1)
JS 側では
const memory = new WebAssembly.Memory({ initial: 1 });
というように初期化時にページ数を指定する。
(p125 「Collision Detection」)
あるデータ構造の配列を linear memory に格納する場合、以下の情報が必要。
- starting address: linear memory 上でそのデータ構造の配列が始まる位置
- stride: データ構造1つあたりに必要なバイト数 = distance in bytes between each structure
- offset of any structure's attributes: データ構造の各属性へのオフセット
- たとえば x, y という属性があり、それぞれ 4 バイトだとすると、そのデータ構造の属性 y のオフセットは 4
学習メモ
読みながら取ったメモは
https://github.com/zaki-yama/learn-rust-and-webassembly/tree/main/the-art-of-webassembly
の章ごとのディレクトリに置いてある。