14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WebAssemblyAdvent Calendar 2019

Day 19

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

Last updated at Posted at 2019-12-18

はじめに

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

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

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

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

結論

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

image.png

今回遊ぶプロジェクト

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

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

やったこと

サンプルのゲームで遊ぶ(寄り道)

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

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

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

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

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

TodoMVCを動かす

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

開発環境

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

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

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

Dのビルド
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=allAtOnceldc2 のバージョンが 1.18.0 以降で必要になるようで、1.17.0以前の ldc2 であれば不要です。

Dのビルドでエラーが出たとき、メッセージに -no-as-needed という文言があったらこのフラグが足りていないサインになります。

実行

開発用の簡単なWebサーバーが同梱されているので、 todo-mvc のディレクトリで npm start すれば実行できます。

npm start

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

image.png

やったぜ!(いつものやつ~)

ソースを眺めてみる

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

アプリを生成する部分が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 対応が入るというアナウンスがありました。

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

概要

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

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

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

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

デバッグビルドする

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

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

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"
ビルドコマンド
dub build --compiler=ldc2 --build=debug --build-mode=allAtOnce

sourcemapを埋め込む

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

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

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

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

ステップ実行する

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

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

image.png

やったぜ!

補足

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

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

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

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

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

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

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

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

確認コマンド
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 さんには大変感謝しています :pray:

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

あと D言語アドベントカレンダー もやってるのでよろしくお願いします!

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?