2
1

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 1 year has passed since last update.

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

Last updated at Posted at 2023-03-05

概要

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

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

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

インストール

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

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

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

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

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

fetch dart

コンパイル

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

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!');
}
#カレントディレクトリのtest.dartをコンパイルしてtest.wasm(とtest.mjs)を出力
dart2wasm.bat test

実行

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

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

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

チェックアウトしたDartSDKの/sdk/third_party/d8D8という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/binrun_wasm.jsがありますが、フルパスで参照するとrun_wasm.jsのあるディレクトリからmjsやwasmファイルを参照しようとするようなので、wasmのあるディレクトリにコピーしておきます。

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

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
#test.wasm(test.mjs)をd8で実行する
wasm.bat test
>wasm.bat test
Hello World!

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

Node.js

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相当のコードを作成して実行してみた。

>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

3/8リリースのChrome 111からは動作するようになりました。(chrome://flagsで試験的機能の設定は必要です)

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 CollectionExperimental WebAssembly
Experimental WebAssembly Stack Switchingの3つを有効にした。

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

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

3/22リリースのDeno1.32.0からv8のバージョンが11.2になり動作するようになりました。

>deno --version
deno 1.32.1 (release, x86_64-pc-windows-msvc)
v8 11.2.214.9
typescript 5.0.2

Denoでもやってみた

>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);
});
>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と同じなのかな?

ちなみに

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

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

ChromeBetaでは動いた

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

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

感想

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

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?