WebAssemblyでもブラウザでステップ実行ができるようになってたので寄り道しながら頑張った話

# はじめに

D言語でもWebAssembly開発ができることが知られて久しいですが、今回はD言語でWebAssemblyに親しむためのサンプルを触りつつ、巨人の肩に乗ることで最先端？のステップ実行までこぎつけた記録をまとめたいと思います。

本記事のトピックは以下3つです。

- WebAssemblyに移植されたゲームの簡単な紹介（寄り道）
- WebAssemblyで書かれたTodoMVCをのサンプルを実行するまでの流れ
- WebAssemblyにsourcemapを埋め込んでChromeの開発者ツールでステップ実行する方法


ちなみにD言語でWebAssemblyを作る記事は昨年もWebAssemblyアドベントカレンダーに投稿されているので、細かいところはこのあたり参照ください。

- D言語で始めるWebAssembly
    - https://qiita.com/ueshita/items/d1431b23c87acdb7c3cc

# 結論

巨人の肩に乗れば、最終的にこんな画面を出してブレークポイントを置きつつステップ実行ができます。
もちろんソースに表示されているのはD言語です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11612/3f03f026-748b-220c-e48e-09b108a5847a.png)

# 今回遊ぶプロジェクト

今回遊んだのは `spasm` という、D言語でシングルページなサイトをWebAssemblyで一通り作るためのフレームワークです。

- spasm
    - https://github.com/skoppe/spasm

`spasm` にはサンプルとして4つのプロジェクトが同梱されています。
`fetch` 、 `dom` といった基本機能のサンプル、フロントでよくある`TodoMVC`の実装例、 `underrun` というjsからDへ移植したゲームです。


# やったこと

## サンプルのゲームで遊ぶ（寄り道）

まずはサンプルにあるゲームを遊ぶことでテンションを上げていきます。何事も勢いです。

興味のある方は以下のリンクから遊んでみてください！

リンクを踏むとプロローグが始まり、内容は音ありの3D風TPS？で、操作的にはWASDで移動、マウス移動で照準を合わせてクリックで攻撃という感じです。

- underrun
    - https://skoppe.github.io/spasm/examples/underrun/


これはJavaScript製のゲームをDに移植したもので、元となったのは、js13kGamesというJavaScript製ゲームのコンペに出展された作品です。

- js13kGames
    - https://js13kgames.com/


結構作りこまれている感はあり、ビビりプレイでしたがクリアまで10分くらいだったと思います。（うろ覚え）
しかしちゃんとエンディングまであるのが素晴らしいですね！

## TodoMVCを動かす

日頃フロントエンジニャーをしがちなので、サンプルとしてTodoMVCは触り慣れていました。
というわけで今回もこれをビルドして動かしていきます。

### 開発環境

D言語のビルド環境が必要になりますが、D言語アドベントカレンダーでVSCodeのRemote Developmentを使った環境構築手順がありますので、サッと作るならこちらが簡単だと思います。

- 指先一つで立ち上げるD言語開発環境
    - https://qiita.com/tm_tn/items/7f966c38f6f9a3820565

私もこの記事に従ってそのまま環境を作りましたので、以下それを前提として進めます。

まずは `git clone` して開発用のリポジトリを用意し、ここにVSCodeでつないで作業していきます。

```console:
git clone https://github.com/lempiji/dlang-remote.git
```

VSCodeでコンテナにつなぐと裏でDockerが立ち上がるのですが、初回はUbuntuイメージのダウンロード等で数分かかります。


### ソースの取得

上記の開発用リポジトリをcloneした先に、サンプルが入っている `spasm` のリポジトリもcloneします。

引き取れたら適当にサンプルフォルダに移動しましょう。

```console:
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`のディレクトリで以下のコマンドを実行するとビルドできます。

```console:Dのビルド
dub build --compiler=ldc2 --build=release --build-mode=allAtOnce
```

```console:フロントのビルド
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` すれば実行できます。

```console:
npm start
```

あとはブラウザで `localhost:3000` にアクセス！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11612/b02c1a1d-b422-353f-ac92-6ee655f83ea2.png)

やったぜ！（いつものやつ～）

### ソースを眺めてみる

一番ロジックを持ってるメインのところだけ抜粋します。

アプリを生成する部分がDのUDA（User Defined Attributes、 `@style` や `@connect`）を使って色々自動バインドするフレームワークになっています。

私も読んでて気づいたんですがコードの見た目が結構衝撃的で、 UDA を JavaScript の [Decorator](https://github.com/tc39/proposal-decorators) だと思うとほぼそのまま読めるんですね。面白い！

```d:
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 対応が入るというアナウンスがありました。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Chrome DevTools is getting better at debugging WebAssembly! By using DWARF debugging metadata, we now support:<br><br>➡️ native source mapping<br>➡️ debug breakpoints<br>➡️ useful stack traces<br><br>This is just a first step towards a good Wasm debugging experience.<a href="https://t.co/lcbauSC7Wf">https://t.co/lcbauSC7Wf</a></p>&mdash; Chrome DevTools (@ChromeDevTools) <a href="https://twitter.com/ChromeDevTools/status/1202614858308603904?ref_src=twsrc%5Etfw">December 5, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


ブレークポイントを置いてステップ実行ができるということでさっそく試していきたいと思います。


### 概要

技術的には、wasmファイルに埋め込まれたDWARF形式のデバッグ情報をsourcemapに変換してwasmファイルに再埋め込みする、という流れです。

Windows民なのでDWARFとか滅多に聞かないのですが、Linux系だと結構主流？のデバッグ情報形式の名前のようです。

- https://ja.wikipedia.org/wiki/DWARF


というわけで、sourcemapの埋め込みにあたりやることは大きく2つあります。

- wasmのデバッグビルド（wasmファイルにデバッグ情報を埋め込む）
    - ビルド設定の書き換え
- デバッグ情報からsourcemapを生成してwasmに埋め込む
    - wasm-sourcemapsというツールを使う

### デバッグビルドする

サンプルプロジェクトはrelease形式でビルドするようになっているため、各所のフラグを削ってデバッグモードでビルドしてやる必要があります。

変えるファイルは以下の2つです。

```d:spasm/examples/todo-mvc/dub.sdl
// 最適化フラグを消します（境界チェックも消しましたがどちらでも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"
```

```console:ビルドコマンド
dub build --compiler=ldc2 --build=debug --build-mode=allAtOnce
```


### sourcemapを埋め込む

D言語でWebAssemblyにsourcemapを埋め込むには、上記のサンプルと同じ作者の方が作った `wasm-sourcemaps` というツールを使います。D言語製です。

- wasm-sourcemaps
    - https://github.com/skoppe/wasm-sourcemaps

普通にCLIツールとして利用でき、mapファイルを生成したり、インラインで埋め込んだり、いくつか動作を切り替えられるようになっているようです。

dubが入っていれば、wasmのファイルがあるディレクトリで直接埋め込むコマンドが実行できます。

```console:sourcemapを埋め込む
dub run wasm-sourcemaps -- --include-sources=true todo-mvc-spasm-example
```


### ステップ実行する

忘れずChrome 80以降で実行しましょう！私は面倒なのでちゃちゃっとCanaryを入れてバージョン81で試しました。

再度 `localhost:3000` にアクセスし、DevToolsを開けば以下の通りステップ実行ができます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11612/3f03f026-748b-220c-e48e-09b108a5847a.png)


やったぜ！


#### 補足

ステップ実行は無事にできたのですが、まだ完ぺきというわけではなく以下のような課題がありました。

- CallStackがマングリングされている
    - 慣れれば読めなくはないんですが、もう少し何とかなると嬉しい（なるのかな？）
- ローカル変数の確認は厳しくほぼ無理
    - Scope変数のlocalsを展開すると連番になっていてほとんど何もわからない
    - 変数などにマウスを載せても全部 `undefined` と出る

というわけで、「ステップ実行しながら画面と比較して、何がどう動いているのか確認する」というのが現状の使い方になりそうです。

しかしログ地獄から解放されるだけでもありがたいですね！
きっと今後数バージョンで更なる改善があると思います！


#### メモ：デバッグ情報が埋め込まれず苦労した

デバッグのために削る `lflag "-strip-all"` を消し損ねており、延々空のsourcemapと戦っていました（数時間？）

wasmファイルにデバッグ情報が埋め込まれているかどうかは、githubのWebAssembly Organizationで公開されている `wabt` というツールキットの中にある `wasm-objdump` というコマンドを使えば確認できます。

https://github.com/WebAssembly/wabt

上記のコンテナ環境に `cmake` と `clang` をインストールしてやればビルドして使いました。
使ってみると以下のような感じです。

```console:確認コマンド
wasm-objdump -h todo-mvc-spasm-example
```

以下のように `.debug` で始まるセクションがいくつか埋め込まれていればOKです。

```console:実行結果
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 さんには大変感謝しています :pray:

というわけでWebAssemblyもD言語も面白いのでがんばってやっていきたいと思います！

あと [D言語アドベントカレンダー](https://qiita.com/advent-calendar/2019/dlang) もやってるのでよろしくお願いします！