DartをWebAssemblyにコンパイルする(2023年3月版)

# 概要

年内にリリース予定のDart3からWebAssemblyへのコンパイルが正式にサポートされるようです。

https://www.publickey1.jp/blog/23/flutter_webassemblywebflutter_foward_2023.html

JSへの変換は正直品質に難があると思っているので、WebAssembly対応によってFlutter以外の特にバックエンドにDartを使うことが現実的になるかもしれません。少なくとも遅々として進んでいないように見えるJS対応よりはマシかな。

現在でもWebAssemblyへのコンパイルは正式サポートではありませんが、一応そのためのツールがあるようなのでどのようなものなのかインストールして使ってみたいと思います。

# インストール

Dart2用のWebAssemblyコンパイラはdart2wasmとしてgithubにDartSDKの一部として公開されています。
ただし、2023年3月現在配布されているDartSDKには含まれていないようなのでレポジトリからチェックアウトする必要があります、dartのビルドまではする必要はありません。またflutterをインストールしているならflutterに入っているdartでも動くので別にDartSDKをインストールする必要はありません。

https://github.com/dart-lang/sdk/blob/main/pkg/dart2wasm/README.md

単純にgitでクローンするだけではうまくいかないので`depot_tools`というChromiumやChromium OSのソースコードのダウンロードやレビューの管理などを行うためのスクリプト群を利用してチェックアウトする必要があるようです。

https://github.com/rust-skia/depot_tools

Windowsの場合は以下のページの「Download the depot_tools bundle and extract it somewhere.」と言う文のbundle部分にリンクが張られておりそれをクリックするとzipファイルがダウンロードできるので、それを解凍してパスを通すだけでdepot_toolsが使えるようになります。それ以外のOSではgitでcloneしてパスを通せばいけるようです、以下のページを参照してください。

内部でpythonやgitを使っているようですが、Windowsの場合は必要ならチェックアウト中に内部でインストールするようなので個別に用意する必要はありません、むしろ別にインストールされていてパスが通っていると上手くいかないことがあるようです。

https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up

dart(flutter)とdepot_toolsのインストールが終わったらdartSDKをチェックアウトします。
Windowsの場合は「**管理者権限で**」コマンドプロンプトを起動し、DartSDKをチェックアウトしたいディレクトリに移動して`fetch`コマンドを実行します。
※終了までに結構時間がかかるので注意してください

```batch
fetch dart
```

# コンパイル

チェックアウトしたDartSDKの`sdk/pkg/dart2wasm/bin`にdart2wasmコンパイラのdartコードがありますので、dartコマンドで実行することでコンパイルすることができます。

```batch
dart --enable-asserts [DartSDKのパス]\sdk\pkg\dart2wasm\bin\dart2wasm.dart [コンパイルするdartコード] [出力ファイル名]
```

コンパイルに成功するとwasmと同時にwasmと同名のmjsファイルも出力します。必要なので取っておいてください。

長いので、バッチファイルを作っておくといいかもしれません。自分はこんな感じのバッチファイルを作りました。

```dart2wasm.bat
@echo off
set DARTSDK=C:\flutter\dart\dart-sdk
dart --enable-asserts %DARTSDK%\sdk\pkg\dart2wasm\bin\dart2wasm.dart %1.dart %1.wasm
```

カレントディレクトリのdartファイルを読み込んで同名のwasmファイル(とmjsファイル)を出力します。

```test.dart
import 'dart:wasm';

void main() {
  print('Hello World!');
}
```


```batch
#カレントディレクトリのtest.dartをコンパイルしてtest.wasm(とtest.mjs)を出力
dart2wasm.bat test
```

# 実行

コンパイルしたwasmファイルはV8で実行する事ができます。

※ただし、残念ながら2023年3月現在では試した限りnode.jsで~~もChromeでもDenoでも~~動かすことはできませんでした…

:::note info
2023/3/26現在、ChromeとDenoでは最新バージョンで動くようになりました。
:::

チェックアウトしたDartSDKの`/sdk/third_party/d8`に`D8`というV8用のデバッグシェルが入っているのでそこからwasmを実行できます。

Windowsの場合はチェックアウトした段階で`/sdk/third_party/d8/windows/d8.exe`にビルド済みのexeファイルが存在するのでD8実行のために特に何もする必要はありません。

なぜかパスを通してd8.exeを実行するとd8実行に必要な`snapshot_blob.bin`(d8.exeと同じディレクトリにある)を**なぜかカレントディレクトリから読みに行こうとして実行に失敗する**ので、パスを通さずにフルパスでd8.exeを実行します。

DartSDKの`sdk/pkg/dart2wasm/bin`に`run_wasm.js`がありますが、フルパスで参照するとrun_wasm.jsのあるディレクトリからmjsやwasmファイルを参照しようとするようなので、wasmのあるディレクトリにコピーしておきます。

また2023年3月現在ではwasm仕様上まだ試験的な機能に相当依存しているのでそれを有効にするオプションを付ける必要があります。

```batch
COPY /Y [DartSDKのパス]\sdk\pkg\dart2wasm\bin\run_wasm.js .
[DartSDKのパス]\sdk\third_party\d8 --experimental-wasm-gc --experimental-wasm-stack-switching --experimental-wasm-type-reflection run_wasm.js -- [コンパイル時に一緒に出力されたmjsファイル] [wasmファイル]
```

ものすごく長いのでこれもバッチファイルを作っておいたほうがいいです。自分はこんな感じにしました。

```wasm.bat
@echo off
set DARTSDK=C:\flutter\dart\dart-sdk
set D8PATH=%DARTSDK%\sdk\third_party\d8\windows
set D8OPT=--experimental-wasm-gc --experimental-wasm-stack-switching --experimental-wasm-type-reflection
COPY /Y %DARTSDK%\sdk\pkg\dart2wasm\bin\run_wasm.js . > NUL
%D8PATH%\d8.exe %D8OPT% run_wasm.js -- %1.mjs %1.wasm
```

```batch
#test.wasm(test.mjs)をd8で実行する
wasm.bat test
```

```
>wasm.bat test
Hello World!
```

# Node.jsやChromeやDenoでやってみて失敗した記録

### Node.js

```javascript:testnode.mjs
import fs from 'fs';
import { instantiate, invoke } from './test.mjs';

let file = fs.readFileSync('test.wasm');
let bytes = new Uint8Array(file);
let module = new WebAssembly.Module(bytes);
let importObject = {};

instantiate(Promise.resolve(module), Promise.resolve(importObject)).then((instance) => {
    invoke(instance);
});
```

run_wasm.js相当のコードを作成して実行してみた。

```batch
>node --experimental-wasm-gc --experimental-wasm-stack-switching --experimental-wasm-type-reflection testnode.mjs
file:///C:/**********/flutter/wasmtest/testnode.mjs:6
let module = new WebAssembly.Module(bytes);
             ^

CompileError: WebAssembly.Module(): Compiling function #58:"_asyncBridge" failed: invalid gc opcode: fb41 @+87865
    at file:///C:/**********/flutter/wasmtest/testnode.mjs:6:14
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)

Node.js v19.7.0
```

一部オペコードに対応していないっぽい。これ以上あがいても無駄そうなので断念。

### Chrome

:::note info
3/8リリースのChrome 111からは動作するようになりました。(chrome://flagsで[試験的機能の設定](https://qiita.com/riafeed/items/6b32354a2b68647457bd#chromebeta%E3%81%A7%E3%81%AF%E5%8B%95%E3%81%84%E3%81%9F)は必要です)
:::

```test.html
<html>

<head>
  <title>wasmのテスト</title>
</head>

<body>
  <script type="module">
    import { instantiate, invoke } from './test.mjs';
    const testWorker = new Worker('testworker.js');
    let importObject = {};

    fetch("test.wasm")
      .then((response) => response.arrayBuffer())
      .then((bytes) => {
        testWorker.postMessage({ code: bytes });
      });

    testWorker.onmessage = (e) => {
      instantiate(e.data.module).then(results => {
        invoke(results);
      });
    }
  </script>
</body>

</html>
```

```testworker.js
onmessage = function(e) {
  const mod = new WebAssembly.Module(e.data.code);
  postMessage({module: mod});
}
```

Chromeでは`new WebAssembly.Module`をメインスレッドで実行するとエラーになるのでワーカースレッドで実行する。

また2023年3月記事執筆時点の最新安定版110.0.5481.178では試験的機能を`chrome://flags`で有効にする必要がある。ここでは`WebAssembly Garbage Collection`、`Experimental WebAssembly`、
`Experimental WebAssembly Stack Switching`の3つを有効にした。

wasmのロードまではうまく行ったが、関数を呼び出そうとするとTypeErrorで失敗する。

:::note alert
test.mjs:172 Uncaught (in promise) TypeError: type incompatibility when transforming from/to JS
    at invoke (test.mjs:172:63)
    at test.html:25:9
:::

いろいろ試したもののエラーの原因がさっぱりわからないので断念。

### Deno

:::note info
3/22リリースのDeno1.32.0からv8のバージョンが11.2になり動作するようになりました。
```batch
>deno --version
deno 1.32.1 (release, x86_64-pc-windows-msvc)
v8 11.2.214.9
typescript 5.0.2
```
:::

Denoでもやってみた
```batch
>deno --version
deno 1.31.1 (release, x86_64-pc-windows-msvc)
v8 11.0.226.13
typescript 4.9.4
```

```testdeno.js
import { instantiate, invoke } from './test.mjs';

let file = await Deno.readFile("test.wasm");
let bytes = new Uint8Array(file);
let module = new WebAssembly.Module(bytes);
let importObject = {};

instantiate(Promise.resolve(module), Promise.resolve(importObject)).then((instance) => {
  invoke(instance);
});
```

```batch
>deno run --allow-read=. --v8-flags=--experimental-wasm-gc,--experimental-wasm-stack-switching,--experimental-wasm-type-reflection testdeno.js
error: Uncaught (in promise) TypeError: type incompatibility when transforming from/to JS
    moduleInstance.exports.$invokeMain(moduleInstance.exports.$getMain());
                                                              ^
    at invoke (file:///C:/**********/flutter/wasmtest/test.mjs:172:63)
    at file:///C:/**********/flutter/wasmtest/testdeno.js:9:3

```

Chrome(110)と同じ結果になりました…V8のバージョンがChromeと同じなのかな？

ちなみに

```batch
>C:\flutter\dart\dart-sdk\sdk\third_party\d8\windows\d8 -v
V8 version 11.1.193
```

でしたので、nodeやchromeやdenoにv8のver11.1が採用されるのを待つしかないのかなぁ…？？？

### ChromeBetaでは動いた

:::note info
Chrome 111は2023/3/8にStableとなっています。
:::

2023/3/7時点のBeta版111.0.5563.64で動かしてみたところchrome://flagsで試験的機能を有効にする必要はありますが動きました。
やっぱりV8のバージョンの問題のようです。
有効にした試験的機能は`WebAssembly Garbage Collection`、`Experimental WebAssembly`、`Experimental WebAssembly JavaScript Promise Integration (JSPI)`(Experimental WebAssembly Stack Switchingから名前が変わった模様)です。
解決も時間の問題だといいんですが…

# 感想

現状ではまだWebAssemblyの試験的機能に相当依存しているので、Dart側の対応よりwasm実行環境側の対応が追いつくのかどうか心配になりました。