はじめに
D言語でもWebAssembly開発ができることが知られて久しいですが、今回はD言語でWebAssemblyに親しむためのサンプルを触りつつ、巨人の肩に乗ることで最先端?のステップ実行までこぎつけた記録をまとめたいと思います。
本記事のトピックは以下3つです。
- WebAssemblyに移植されたゲームの簡単な紹介(寄り道)
- WebAssemblyで書かれたTodoMVCをのサンプルを実行するまでの流れ
- WebAssemblyにsourcemapを埋め込んでChromeの開発者ツールでステップ実行する方法
ちなみにD言語でWebAssemblyを作る記事は昨年もWebAssemblyアドベントカレンダーに投稿されているので、細かいところはこのあたり参照ください。
- D言語で始めるWebAssembly
結論
巨人の肩に乗れば、最終的にこんな画面を出してブレークポイントを置きつつステップ実行ができます。
もちろんソースに表示されているのはD言語です!
今回遊ぶプロジェクト
今回遊んだのは spasm
という、D言語でシングルページなサイトをWebAssemblyで一通り作るためのフレームワークです。
spasm
にはサンプルとして4つのプロジェクトが同梱されています。
fetch
、 dom
といった基本機能のサンプル、フロントでよくあるTodoMVC
の実装例、 underrun
というjsからDへ移植したゲームです。
やったこと
サンプルのゲームで遊ぶ(寄り道)
まずはサンプルにあるゲームを遊ぶことでテンションを上げていきます。何事も勢いです。
興味のある方は以下のリンクから遊んでみてください!
リンクを踏むとプロローグが始まり、内容は音ありの3D風TPS?で、操作的にはWASDで移動、マウス移動で照準を合わせてクリックで攻撃という感じです。
これはJavaScript製のゲームをDに移植したもので、元となったのは、js13kGamesというJavaScript製ゲームのコンペに出展された作品です。
- js13kGames
結構作りこまれている感はあり、ビビりプレイでしたがクリアまで10分くらいだったと思います。(うろ覚え)
しかしちゃんとエンディングまであるのが素晴らしいですね!
TodoMVCを動かす
日頃フロントエンジニャーをしがちなので、サンプルとしてTodoMVCは触り慣れていました。
というわけで今回もこれをビルドして動かしていきます。
開発環境
D言語のビルド環境が必要になりますが、D言語アドベントカレンダーでVSCodeのRemote Developmentを使った環境構築手順がありますので、サッと作るならこちらが簡単だと思います。
- 指先一つで立ち上げるD言語開発環境
私もこの記事に従ってそのまま環境を作りましたので、以下それを前提として進めます。
まずは git clone
して開発用のリポジトリを用意し、ここにVSCodeでつないで作業していきます。
git clone https://github.com/lempiji/dlang-remote.git
VSCodeでコンテナにつなぐと裏でDockerが立ち上がるのですが、初回はUbuntuイメージのダウンロード等で数分かかります。
ソースの取得
上記の開発用リポジトリをcloneした先に、サンプルが入っている spasm
のリポジトリもcloneします。
引き取れたら適当にサンプルフォルダに移動しましょう。
git clone https://github.com/skoppe/spasm.git
cd spasm/examples/todo-mvc
ちなみにこの時点で以下のようなディレクトリ構成です。
dlang-remote
spasm
examples
todo-mvc
source
app.d // todo-mvcのソース(1ファイルのみ)
dub.sdl // todo-mvcのプロジェクト設定ファイル
todo-mvc-spasm-example // ビルドするとできるwasmファイル
dub.sdl // spasmのプロジェクト設定ファイル
ビルド
Dの部分は dub
を使ってビルドし、Web部分は npm
でビルドします。
todo-mvc
のディレクトリで以下のコマンドを実行するとビルドできます。
dub build --compiler=ldc2 --build=release --build-mode=allAtOnce
npm install
npx webpack
ちなみに dub
コマンドは上記のコンテナで開発していれば最初から使えますので、npm
を適当にインストールして試してみてください。
ちなみに私はホストにインストール済みの npm
でビルドしていました。
これはローカルで構築したRemote-Dev環境の場合、「元になったリポジトリのフォルダとコンテナのディレクトリが共有フォルダ構成になる」という特徴を使ったものです。リアルタイムに相互の変更が反映されるので非常に強い感じです。
メモ:ビルドが lld: error: unknown argument: -no-as-needed
で失敗する
Dのビルドにつけている --build=mode=allAtOnce
は ldc2
のバージョンが 1.18.0
以降で必要になるようで、1.17.0
以前の ldc2
であれば不要です。
Dのビルドでエラーが出たとき、メッセージに -no-as-needed
という文言があったらこのフラグが足りていないサインになります。
実行
開発用の簡単なWebサーバーが同梱されているので、 todo-mvc
のディレクトリで npm start
すれば実行できます。
npm start
あとはブラウザで localhost:3000
にアクセス!
やったぜ!(いつものやつ~)
ソースを眺めてみる
一番ロジックを持ってるメインのところだけ抜粋します。
アプリを生成する部分がDのUDA(User Defined Attributes、 @style
や @connect
)を使って色々自動バインドするフレームワークになっています。
私も読んでて気づいたんですがコードの見た目が結構衝撃的で、 UDA を JavaScript の Decorator だと思うとほぼそのまま読めるんですね。面白い!
mixin Spa!App; // フレームワークで必要な初期化コードを生成する部分
enum FilterStyle
{
All,
Active,
Completed
}
struct App
{
nothrow:
@style!"todoapp" mixin Node!"section";
@child Header header;
@child Main main;
@child Footer footer;
int count = 0;
int completed = 0;
int size = 0;
FilterStyle filter = FilterStyle.All;
@visible!"footer"
bool showFooter(int size) {
return size > 0;
}
@visible!"main"
bool showMain(int size) {
return size > 0;
}
void updateItems() {
import std.algorithm : count;
main.update!(main.items);
this.update.size = main.items.length;
this.update.count = main.items[].count!(i=>!i.checked);
this.update.completed = main.items.length - this.count;
}
@connect!"main.toggleAll.input.toggle"
void toggle() {
bool checked = main.toggleAll.input.node.checked;
main.toggleEach(checked);
updateItems();
}
@trusted @connect!"header.field.enter"
void enter() {
import spasm.rt.memory;
Item* item = allocator.make!Item;
item.textContent = header.field.value;
(*item).setPointers();
header.field.update.value = "";
main.items.put(item);
updateItems();
}
@connect!("main.list.items", "view.button.click")
void removeItem(size_t idx) {
main.items.removeItem(idx);
updateItems();
}
@connect!("main.list.items", "view.checkbox.toggle")
void toggleItem(size_t idx) {
main.list.items[idx].checked = !main.list.items[idx].checked;
// TODO: here we need to update data[idx] again, else state in dom is wrong
updateItems();
}
@connect!"footer.filters.all.link.click"
void allClick() {
this.update.filter = FilterStyle.All;
}
@connect!"footer.filters.active.link.click"
void activeClick() {
this.update.filter = FilterStyle.Active;
}
@connect!"footer.filters.completed.link.click"
void completedClick() {
this.update.filter = FilterStyle.Completed;
}
@connect!"footer.clear.click"
void clearCompleted() {
main.items.removePred!(i => i.checked);
updateItems();
}
}
WebAssemblyのステップ実行
今年の11月上旬、Chrome 80 の DevTools で WebAssembly の soucemap 対応が入るというアナウンスがありました。
Chrome DevTools is getting better at debugging WebAssembly! By using DWARF debugging metadata, we now support:
— Chrome DevTools (@ChromeDevTools) December 5, 2019
➡️ native source mapping
➡️ debug breakpoints
➡️ useful stack traces
This is just a first step towards a good Wasm debugging experience.https://t.co/lcbauSC7Wf
ブレークポイントを置いてステップ実行ができるということでさっそく試していきたいと思います。
概要
技術的には、wasmファイルに埋め込まれたDWARF形式のデバッグ情報をsourcemapに変換してwasmファイルに再埋め込みする、という流れです。
Windows民なのでDWARFとか滅多に聞かないのですが、Linux系だと結構主流?のデバッグ情報形式の名前のようです。
というわけで、sourcemapの埋め込みにあたりやることは大きく2つあります。
- wasmのデバッグビルド(wasmファイルにデバッグ情報を埋め込む)
- ビルド設定の書き換え
- デバッグ情報からsourcemapを生成してwasmに埋め込む
- wasm-sourcemapsというツールを使う
デバッグビルドする
サンプルプロジェクトはrelease形式でビルドするようになっているため、各所のフラグを削ってデバッグモードでビルドしてやる必要があります。
変えるファイルは以下の2つです。
// 最適化フラグを消します(境界チェックも消しましたがどちらでもOKです)
//dflags "-mtriple=wasm32-unknown-unknown-wasm" "-Oz" "-betterC" "-fvisibility=hidden" "-boundscheck=off"
dflags "-mtriple=wasm32-unknown-unknown-wasm" "-betterC" "-fvisibility=hidden"
// デバッグ情報を落とすリンカフラグを消します
//lflags "-strip-all"
dub build --compiler=ldc2 --build=debug --build-mode=allAtOnce
sourcemapを埋め込む
D言語でWebAssemblyにsourcemapを埋め込むには、上記のサンプルと同じ作者の方が作った wasm-sourcemaps
というツールを使います。D言語製です。
- wasm-sourcemaps
普通にCLIツールとして利用でき、mapファイルを生成したり、インラインで埋め込んだり、いくつか動作を切り替えられるようになっているようです。
dubが入っていれば、wasmのファイルがあるディレクトリで直接埋め込むコマンドが実行できます。
dub run wasm-sourcemaps -- --include-sources=true todo-mvc-spasm-example
ステップ実行する
忘れずChrome 80以降で実行しましょう!私は面倒なのでちゃちゃっとCanaryを入れてバージョン81で試しました。
再度 localhost:3000
にアクセスし、DevToolsを開けば以下の通りステップ実行ができます!
やったぜ!
補足
ステップ実行は無事にできたのですが、まだ完ぺきというわけではなく以下のような課題がありました。
- CallStackがマングリングされている
- 慣れれば読めなくはないんですが、もう少し何とかなると嬉しい(なるのかな?)
- ローカル変数の確認は厳しくほぼ無理
- Scope変数のlocalsを展開すると連番になっていてほとんど何もわからない
- 変数などにマウスを載せても全部
undefined
と出る
というわけで、「ステップ実行しながら画面と比較して、何がどう動いているのか確認する」というのが現状の使い方になりそうです。
しかしログ地獄から解放されるだけでもありがたいですね!
きっと今後数バージョンで更なる改善があると思います!
メモ:デバッグ情報が埋め込まれず苦労した
デバッグのために削る lflag "-strip-all"
を消し損ねており、延々空のsourcemapと戦っていました(数時間?)
wasmファイルにデバッグ情報が埋め込まれているかどうかは、githubのWebAssembly Organizationで公開されている wabt
というツールキットの中にある wasm-objdump
というコマンドを使えば確認できます。
上記のコンテナ環境に cmake
と clang
をインストールしてやればビルドして使いました。
使ってみると以下のような感じです。
wasm-objdump -h todo-mvc-spasm-example
以下のように .debug
で始まるセクションがいくつか埋め込まれていればOKです。
todo-mvc-spasm-example: file format wasm 0x1
Sections:
Type start=0x0000000b end=0x000000fc (size=0x000000f1) count: 30
Import start=0x000000ff end=0x00000327 (size=0x00000228) count: 25
Function start=0x0000032a end=0x000006c9 (size=0x0000039f) count: 925
Table start=0x000006cb end=0x000006d0 (size=0x00000005) count: 1
Memory start=0x000006d2 end=0x000006d5 (size=0x00000003) count: 1
Global start=0x000006d7 end=0x000006df (size=0x00000008) count: 1
Export start=0x000006e2 end=0x0000076d (size=0x0000008b) count: 9
Elem start=0x0000076f end=0x000007a8 (size=0x00000039) count: 1
Code start=0x000007ac end=0x00023ded (size=0x00023641) count: 925
Data start=0x00023df0 end=0x00025742 (size=0x00001952) count: 3
Custom start=0x00025746 end=0x0009e9b3 (size=0x0007926d) ".debug_info"
Custom start=0x0009e9b5 end=0x0009e9ed (size=0x00000038) ".debug_macinfo"
Custom start=0x0009e9f0 end=0x000a24b3 (size=0x00003ac3) ".debug_pubtypes"
Custom start=0x000a24b7 end=0x000b39a5 (size=0x000114ee) ".debug_ranges"
Custom start=0x000b39a9 end=0x000b7a1c (size=0x00004073) ".debug_abbrev"
Custom start=0x000b7a20 end=0x0010daf4 (size=0x000560d4) ".debug_line"
Custom start=0x0010daf9 end=0x0035ca31 (size=0x0024ef38) ".debug_str"
Custom start=0x0035ca35 end=0x0037178a (size=0x00014d55) ".debug_pubnames"
Custom start=0x0037178e end=0x003960f1 (size=0x00024963) "name"
Custom start=0x003960f3 end=0x00396124 (size=0x00000031) "producers"
Custom start=0x00396126 end=0x00396154 (size=0x0000002e) "sourceMappingURL"
さいごに
TwitterでChromeからアナウンスが出る前に、サンプル作者の方がDevToolsにD言語を表示しているのを見て衝撃を受けてチャレンジを決めていました。
そこから約1か月、結局ギリギリで突貫工事することになりましたが無事に目標が達成できて非常に安堵しています。
リンカフラグを落とすあたりに気づけずちょっと手間取りましたが、慣れてしまえば流れ作業でした。
Twitterで色々教えていただいた @kubo39 さんには大変感謝しています
というわけでWebAssemblyもD言語も面白いのでがんばってやっていきたいと思います!
あと D言語アドベントカレンダー もやってるのでよろしくお願いします!