概要
年内にリリース予定の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ファイルも出力します。必要なので取っておいてください。
長いので、バッチファイルを作っておくといいかもしれません。自分はこんな感じのバッチファイルを作りました。
@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ファイル)を出力します。
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/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仕様上まだ試験的な機能に相当依存しているのでそれを有効にするオプションを付ける必要があります。
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ファイル]
ものすごく長いのでこれもバッチファイルを作っておいたほうがいいです。自分はこんな感じにしました。
@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
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で試験的機能の設定は必要です)
<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>
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で失敗する。
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
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 Collection
、Experimental WebAssembly
、Experimental WebAssembly JavaScript Promise Integration (JSPI)
(Experimental WebAssembly Stack Switchingから名前が変わった模様)です。
解決も時間の問題だといいんですが…
感想
現状ではまだWebAssemblyの試験的機能に相当依存しているので、Dart側の対応よりwasm実行環境側の対応が追いつくのかどうか心配になりました。