Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
TypeScript で WebAssembly 処理系を書いた話
Search
ryohey
April 19, 2019
Programming
1.7k
11
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
TypeScript で WebAssembly 処理系を書いた話
ryohey
April 19, 2019
Other Decks in Programming
See All in Programming
Creating Composable Callables in Contemporary C++
rollbear
0
170
AIキャラアプリkaiwaの低遅延音声通話基盤をどう作ったか - AWS Gravitonで支える低遅延・低コストAI Agent基盤
mogamit
0
110
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
そのテスト、説明できますか?~LWテスト戦略FW~のご紹介
nakahara
0
170
AI 輔助遺留系統現代化的經驗分享
jame2408
1
1k
OSもどきOS
arkw
0
590
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
8.4k
どこまでゆるくて許されるのか
tk3fftk
0
260
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
310
才能?センス?知らん、 続けたもん勝ちだ。-- 結婚・出産・癌を越えてなお、私がプロダクトを創り続ける理由
16bitidol
1
470
「なぜそう決めたのか」を残し続ける仕組み ― Notion AI カスタムエージェント × Slack連携による設計判断の自動記録 - NIKKEI Tech Talk #47
niftycorp
PRO
0
230
Oxlintのカスタムルールの現況
syumai
6
1.2k
Featured
See All Featured
Exploring anti-patterns in Rails
aemeredith
3
430
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
2
250
Side Projects
sachag
455
43k
The untapped power of vector embeddings
frankvandijk
2
1.8k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.4k
Large-scale JavaScript Application Architecture
addyosmani
515
110k
The Mindset for Success: Future Career Progression
greggifford
PRO
0
370
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
0
440
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
210
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
420
Building AI with AI
inesmontani
PRO
1
1.1k
The Cult of Friendly URLs
andyhume
79
6.9k
Transcript
TypeScript で TypeScript で WebAssembly 処理系を WebAssembly 処理系を 書いた話 書いた話
亀山龍平 (合同会社コベリン) 1
自己紹介 自己紹介 合同会社コベリン代表 普段はモバイル、Web、AR をやっています Swift+ARKit, TypeScript+React, HoloLens 等 趣味で
WebAssembly をやっています github.com/ryohey twitter.com/ryoheyc 2
話す内容 話す内容 なにを作ったか なぜ作ったか デモ どう作ったか 3
何を作ったか 何を作ったか TypeScript の WebAssembly 処理系 1から実装 ライブラリ使ってません ts‑wasm‑runtime 4
デモ デモ 5
できること できること テキスト形式のパース バイナリ形式のパース ブラウザ上での実行環境 6
せっかく JavaScript を通さず実行できる WASM を JavaScript で実行する つまり意味なし! 7
なぜ作ったか なぜ作ったか WebVR, WebAR に注目している WASM すごいらしい 言語を超えたモジュールが作れる? WASM に
VR, ARでの未来を感じた WASMとは何なのか深く知りたい 8
そうだ処理系を書こう そうだ処理系を書こう 9
なぜ TypeScript か なぜ TypeScript か 型が高機能で楽しい 型が高機能で楽しい あと書きなれてる 10
タイムライン タイムライン 2018年後半 WebAssembly 知りたくなる 2018年11月 WebAssembly 処理系の実装に着手 2018年12月 S式パーサ、テキスト形式パーサを実装
2019年1月 インタプリタを実装 フィボナッチ数を計算するモジュールが動く 2019年2月 バイナリ形式パーサを実装 ブラウザ環境を実装 2019年3月 足りてない命令の実装など (現在も進行中) 11
何を書いたか 何を書いたか パーサコンビネータ fn‑parser S 式パーサ s‑parser 命令セットの型定義 wasm‑ast wat
パーサ wat‑parser wasm パーサ wasm‑parser インタプリタ wasm‑vm ブラウザ環境 playground それぞれ npm のモジュールとして実装 (publish してないので npm install できないかも) 12
まずテキスト形式のパース まずテキスト形式のパース WebAssembly の人が読める形式 S 式になっている 13
S 式パーサ S 式パーサ これを こうしたい "(foo bar (1 22
(3)))" ["foo", "bar", ["1", "22", ["3"]]] 14
パーサコンビネータ パーサコンビネータ パーサの関数を生成する関数 部分をパースする関数を合成してでっかいパーサを作るこ とができる 関数型プログラミング的に書けるので気持ちがいい 15
fnparse.js fnparse.js パーサってどう書けばいいんだろうと思って見つけた anatoo さんの解説記事 ここからすべてが始まった anatoo/fnparse.js を TypeScript に移植した
fn‑parser を実装 テキスト形式もバイナリ形式も fn‑parser で実装 http://blog.anatoo.jp/entry/2015/04/26/220026 16
簡単なパーサ 簡単なパーサ より引用 function parseHoge(target, position) { if (target.substr(position, 4)
=== 'hoge') { // 成功 return [true, 'hoge', position + 4]; } else { // 失敗 return [false, null, position]; } } parseHoge('hoge', 0) // => [true, 'hoge', 4] を返す parseHoge('ahoge', 1) // => [true, 'hoge', 5] を返す parseHoge('aaa', 0) // => [false, null, 0] を返す http://blog.anatoo.jp/entry/2015/04/26/220026 17
簡単なパーサコンビネータ 簡単なパーサコンビネータ // 渡された複数のパーサのどれかが成功したら成功するパーサを生成する関数 const or = (...parsers) => (target,
position) => { for (let parser of parsers) { const parsed = parser(target, position) // どれか一つにマッチしたら成功 if (parsed[0]) { return parsed } } return [false, null, position] } // どちらかの文字列にマッチするパーサ const animal = or( token("Cat"), token("Dog") ) animal("Cat", 0) // => [true, "Cat", 3] animal("Dog", 0) // => [true, "Dog", 3] animal("iPhone", 0) // => [false, null, 0] 18
例: 数式のパース 例: 数式のパース // regexp は正規表現でマッチするパーサ // map は第一引数でマッチした結果を第二引数の関数で加工するパーサを生成する関数
const num = map(regexp(/^([0-9]+)/), parseInt) const operator = or(token("+"), token("-"), token("*"), token("/")) // lazy はパース時にパーサを生成する関数 // seq は特定の並びにマッチするパーサコンビネータ const mathExpression = lazy(() => or( num, seq(mathExpression, operator, mathExpression) )) mathExpression(“42”) == 42 mathExpression("15/3*9-3", 0) == [15, "/" , [3, "*", [9, "-", 3]]] 19
S 式パーサ S 式パーサ const string = regexp(/^([^)^(^ ^\n]+)/) const
comment = regexp(/^;;.*\n/) const separator = many(or(regexp(/^\s+/), comment)) const list = lazy(() => map( seq(opt(separator), map(seq( token("("), opt(separator), expression, opt(many(map(seq(separator, expression), r => r[1]))), opt(separator), token(")") ), r => (r[3] !== null ? [r[2], ...r[3]] : [r[2]]) )), r => r[1] )) 20
型安全なパーサコンビネータ 型安全なパーサコンビネータ 21
パーサの定義 パーサの定義 export type Parser<T, S> = (target: T, position:
number) => [boolean, S, number] 22
型情報を維持したパーサの合 型情報を維持したパーサの合 成 成 Parser<string, (string|number)[]> ではないところに注目 複雑なパーサを書くとき型を間違えない const fooParser
= seq(token("foo"), num, token("bar")) // Parser<string, ["foo", number, "bar"]> 23
TypeScript のジェネリクスパラメータは可変長にできないの で地道に定義 24
書いてみて 書いてみて パーサコンビネータすごい fnparse.js ありがとう テストしやすい デバッグが難しい 手続き的だとデバッガで止めてどこがおかしいかわかる が、関数合成されているのでいまどこにいるのか分かり づらい
最初は S 式パーサで数値もパースしていたが、テキスト形 式の Float の仕様が複雑なので文字列だけにした 25
テキスト形式パーサ テキスト形式パーサ S式パーサの結果を入力してインタプリタが読める形に出 力するパーサ バイナリ形式パーサと共通化したいのでパース結果の型だ け別モジュール wasm‑ast に定義 26
リファレンス リファレンス で wat を書いてみる Empty Wat Project を選ぶと wat
を書ける おかしいところではエラーがちゃんと出るので嬉しい Mozilla の解説記事 公式ドキュメント WebAssembly Studio 27
公式ドキュメント 公式ドキュメント const exportSection = seq(keyword("export"), string, exportDesc) const exportDesc
= or( array(seq(keyword("func"), identifier)), array(seq(keyword("memory"), identifier)) ) 28
命令の型定義 命令の型定義 ひたすら書く 29
命令のパーサ 命令のパーサ ひたすら書く 30
型の集合をいい感じに扱う 型の集合をいい感じに扱う 31
Before Before interface BaseOperation { opType: string } interface Nop
extends BaseOperation { opType: "nop" } interface Local_Get extends BaseOperation { opType: "local.get" parameter: number } // ベースの interface で受け取る関数 function runOperation(op: BaseOperation) { switch(op.opType) { case "nop": ... case "local.get": const op2 = op as Local_Get // キャストが必要 op2.parameter ... default: // 網羅してることが保証されないので default が必要 } } 32
After After // ジェネリクスパラメータに文字列そのものを渡す interface BaseOperation<T extends string> { opType:
T } interface Nop extends BaseOperation<"nop"> {} interface Local_Get extends BaseOperation<"local.get"> { parameter: number } // | で繋いだ型の集合を作る const AnyOperation = Nop | Return // AnyOperation で受け取る関数 function runOperation(op: AnyOperation) { switch(op.opType) { case "nop": ... case "local.get": op.parameter // キャストせずに Local_Get 型として扱える ... // AnyOperation の全てを網羅しているので default を書かないでよい } } 33
fold 表記 fold 表記 分かりやすく書ける表記法 (i32.add (i32.const 3) (i32.const 14))
i32.const 3 i32.const 14 i32.add 34
fold 表記の対応 fold 表記の対応 入れ子をフラットに展開するパーサを追加して対応 ↓ const foldedInstructions = map(
array( seq( plainInstructions, opt(map(many(lazy(() => operations)), r => flatten(r))) ) ), r => [...(r[1] ? r[1] : []), r[0]] ) ["i32.add", ["i32.const", "3"], ["i32.const", "14"]] [ { opType: "i32.const", parameter: { i32: 3 } }, { opType: "i32.const", parameter: { i32: 14 } }, { opType: "i32.add" } ] 35
ラベル ラベル $ で始まる部分 変数や関数に名前をつけて分かりやすく書ける表記法 バイナリ形式には無い export とは関係ない (func $fib
(export "fib") (param $p0 i32) (result i32) (local $l0 i32)) 36
ラベルの処理 ラベルの処理 パーサで対応するにはパースしながらラベルの宣言を保持 していく必要がある パーサに状態を持たせたくないので、そのまま文字列とし て保持 特別なオペレータとして型を付ける インタプリタの前処理で対応 interface TextGlobalGet
{ opType: "text.global.get" parameter: string } interface GlobalGet { opType: "global.get" parameter: number } 37
感想 感想 命令がなんだかんだ多い 思ったより仕様変更が頻繁で驚いた 命令の名前の変更がちょうどあった (get_local ‑> local.get 等) wasm
にはバージョンがあるけどテキスト形式にはバー ジョンがない 今後もたくさん機能追加があるようで仕様を追い続けるの は大変そう 38
WASM パーサーを書いた話 WASM パーサーを書いた話 テキスト形式より後に作ったけどこっちの方が仕様が小さ くて簡単だった 先にやればよかった セクションの順番が決まっててパースしやすい テキスト形式のパーサと同じ出力に揃える (wasm‑ast)
39
開発方法 開発方法 を読む でバイナリを眺める バイナリのところをマウスオーバーするとどのような値 にパースすべきか分かる 公式ドキュメント WebAssembly Code Explorer
40
可変長数値表現の取扱い 可変長数値表現の取扱い 固定長の整数ではなく LEB128 という可変長整数 MSB が継続するかフラグ、残りの 7 bit がデータ
プログラムに大きな数字はあまり出てこないので効率的 MIDI の VLQ を思い出す データの内容によって挙動を変えるパーサが必要 41
セクションのパース セクションのパース 最初にセクションの id とサイズが入っている 画像ファイルの chunk みたいなもの custom section
は読み飛ばす必要がある 動的に指定サイズ分を読み込むパーサが必要 42
seqMap seqMap 第1引数でパースした内容を使って第2引数の関数でパーサ を生成して返すパーサ 名前は適当 例: サイズの 32bit 整数を読み込んでそのサイズ分の変数を読 み込むパーサ
seqMap(u32, size => variable(size)) // variable は指定したサイズを読み込むパーサ 43
インタプリタを書いた話 インタプリタを書いた話 分かりやすさ重視、パフォーマンスは二の次というコンセ プト なるべくクラスではなく関数として実装 44
最初の実装方針 最初の実装方針 最初は仮想機械にこだわってエミュレータみたいな実装に していた ALU やメモリなど装置に対応するコードを書いていた グローバルな状態が多く綺麗ではなかった 45
今の実装方針 今の実装方針 AST をいかに JavaScript の関数にマッピングするかという 問題として捉えた block のスコープを関数スコープに合わせる メモリを引数にとる関数をコードから生成する構造として
実装 46
抽象的なインタプリタを実装 抽象的なインタプリタを実装 命令を読み込んでメモリを操作するもの プログラムカウンタを増やして繰り返す 中断をサポート 命令はメモリに対する操作として一般化 メモリにプログラムカウンタも入れ、break などもメモ リの操作として表現できる 47
export type InstructionSet<Code, Memory> = (code: Code) => Instruction<Memory> //
mutates register and memory export type Instruction<Memory> = (memory: Memory) => void export interface VMMemory { programCounter: number programTerminated: boolean } /** * 特定の命令セットやプログラムに依存しない VM の抽象的な実装 * 利用側が命令セットとプログラムの組み合わせを用意する */ export const virtualMachine = <Code, Memory extends VMMemory>( instructionSet: InstructionSet<Code, Memory>, ) => (program: Code[], memory: Memory) => { while (memory.programCounter < program.length && !memory.programTerminated) { const code = program[memory.programCounter++] t i t i t ti S t( d ) 48
WebAssembly インタプリタ WebAssembly インタプリタ の実装 の実装 49
WASMMemory WASMMemory export interface WASMMemory extends VMMemory { readonly values:
Stack<WASMMemoryValue> readonly memory: DataView readonly global: WASMMemoryValue[] readonly local: WASMMemoryValue[] readonly functions: WASMFunction[] readonly table: WASMTable readonly programCounter: number programTerminated: boolean } export type WASMMemoryValue = Int32 | Int64 | Float32 | Float64 export type WASMTable = { // index: funcId [key: number]: number } 50
命令の実装 命令の実装 opType で判別して memory を操作する関数を返す const variableInstructionSet = (code:
WASMCode): ((memory: WASMMemory) => void) => switch (code.opType) { case "drop": return ({ values }) => { values.pop() } ... 51
block の実装 block の実装 AST の Block には中身の命令が入っている func と同様に、JavaScript
の関数に変換 block, call それぞれがインタプリタを生成する たくさんのインタプリタを使い捨てる interface BlockBase<T extends string> extends Base<T> { results: ValType[]; body: AnyOperation[]; } interface Block extends BlockBase<"block"> { } interface Loop extends BlockBase<"loop"> { } 52
br と return br と return br は loop と
block で挙動が違う ジャンプ先のプログラムカウンタの計算に末尾、先頭を 指定できるようにする block を関数にマッピングしているので、return がいわゆる javascript の return に対応しない 戻り値に break する回数を返して対処 53
数値型の取扱い 数値型の取扱い i32, f32, f64: number i64: BigInt 最初はバイナリを保持して全部自前で加算や乗算をビット 演算で実装してた
あまりにも膨大で大変なので普通に number 型を使うよう にした 仮想機械といっても実行環境でサポートされた型じゃな いととても大変 i64 は BigInt を使う。Chrome でも node でも実行可能 54
数値型への変換 数値型への変換 パースする段階で数値型に変換 インタプリタでやっても良かったが、数値がどのような表 現で入っているかは各フォーマットの仕様なのでパーサで 対応 parseInt, parseFloat, new BigInt(number)
を使う 55
DataView DataView Memory 周りの命令で使用 数値をバイト列に変換して書き込む バイト列から読み込む i32.load、i32.store8_s など 最初はメモリを Uint8Array
として持ち、各数値型ごとに頑 張ってバイナリ表現を実装していた DataView.setUInt8 などがあることに気づいて DataView に 変えた BigInt も対応していてうってつけだった 56
wast について wast について に仕様のテストコードがある 各命令について正しく動作しているか確認できる テストコードはテキスト形式の拡張仕様 assert_return などテストコード用の命令がある 1ファイルに複数の
S式が並んでいる形式なので専用のパー サを用意 例外チェックは面倒なので値のテストだけ動かしている テストがあって安心だった WebAssembly のリポジトリ 57
playground を書いた話 playground を書いた話 9割くらいjestでテスト駆動開発してきた 対話的に触ってみたくなった いままで書きっぱなしで使える状態になってないプログ ラムを多く書いた反省からいつでも Web で動くものを
用意している 普段なら React を使う所だが JSX を使いたくないので lit‑ html を使った Qiita に紹介記事を書いたのでよければ見てね 58
monorepo monorepo 最初は一つの module にディレクトリで分けて書いていた 後から分けた 効率おちるので構成が固まるまで分けないか、最初から 分けるかが良さそう 完全に分離される気持ち良さがある Lerna
は必要なかった Typescript と相性悪そうだった npm install でローカルのモジュールに依存できる シンボリックリンクがはられるのでいちいち install し直 さなくても最新になる playground は全てのモジュールに依存している 59
終わりに 終わりに
処理系を書いて WebAssembly を (やや偏った方向に) 深く 知ることができた TypeScript の型に助けられた 言語を超えたモジュールづくりはできそうか? 仕様を見る限り難しそう
型が落ちるので wasm バイナリだけでなくメタデータ的 なものが必要 WebIDL をブラウザのAPIだけでなく wasm モジュールの メタデータとしてバイナリと一緒に配る方法があったら 良いかも?詳しくないので教えてほしい JS を挟むので現状あまり意味ない 現状ではアプリケーション全体を特定の言語で書いて wasmに変換するのが実用的と感じた Unity とか Rust とか Go とかで 60
今後について 今後について スペックテストの網羅 勉強用に作ったけど用途があったら教えて下さい PR, Fork 大歓迎 JavaScript API を標準仕様に寄せる
AssemblyScript に移植して WASM で動かす バイナリ形式とテキスト形式の相互変換 61
小ネタ 小ネタ テキスト形式の名称は wat か wast か local 変数があるからスタックマシンじゃない WAST
vs WAT · webassemblyjs WebAssembly Is Not a Stack Machine 62