Dart で他の言語を利用するのは Go で試みたことがあります。
- gomobile で作ったライブラリ + Flutter の Platform Channel
- Go で作ったライブラリ + Dart FFI
今回は Zig です。
いつかあるかもしれない実用の機会を視野に入れつつも、ほとんど興味本位です。
いくつかの言語の Pros / Cons
Dart で組み合わせて使うときの観点で挙げます。
Go
- Pros
- クロスコンパイルできる
- 標準パッケージが充実していてそれらを活用できる
- Cons
- ライブラリのサイズが小さくても数 MB になる
- モバイルアプリではサイズが大きく増えるのは避けたい
- ライブラリのサイズが小さくても数 MB になる
Rust
- Pros
- Go より速い、小さい
- 堅牢
- 利用の実例がある
- Cons
- クロスコンパイルが簡単ではなさそう
- 学習コストが高くて手をつけにくい
Zig
参考: ざっくりとしたZigの紹介
- Pros
- Go より速い、小さい
- クロスコンパイルできるし、C のコンパイラとしても使える
- 覚えることが少ない
- Cons
- 標準ライブラリが貧弱
- 言語仕様も貧弱と捉える人がいそう
- 私は好感を持ったが、機能が充実した言語から始めた人にはたぶん辛い
Dart での利用において、Zig の Pros として挙げた点が有利になると思いました。
Zig でビルド
ziglearn.org のフィボナッチ数の関数に export
を付けて使います。
u16
はまずいので後で変えます。
export fn fibonacci(n: u16) u16 {
if (n == 0 or n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
ソースコードは src/main.zig
というパスになっているとします。
ライブラリを生成
https://ziglang.org/documentation/master/#Exporting-a-C-Library
zig build-lib src/main.zig -dynamic --name fibonacci -O ReleaseSmall
これだけでライブラリが生成されます。
Windows では DLL です。
これを後で Dart FFI で利用します。
ファイルサイズ
出来上がった DLL ファイルは 4.5 KB でした。
-O
の最適化のフラグを指定しないと数百 KB になり、動作速度も遅くなります。
クロスコンパイル
ターゲットを -target
で指定するだけでできます。
https://ziglang.org/learn/overview/#cross-compiling-is-a-first-class-use-case
試しに Windows 上で -target x86_64-linux-gnu
を付けてビルドすると libfibonacci.so
が作られ、ファイルサイズは DLL より大きめの 47.9 KB になりました。
Wasm を生成
zig build-lib src/main.zig -dynamic --name fibonacci -O ReleaseSmall -target wasm32-freestanding
クロスコンパイルと同じ要領で -target wasm32-freestanding
を指定するだけです。
https://ziglang.org/documentation/master/#Freestanding
ファイルサイズは DLL とほぼ同じでした。
Dart で利用
ライブラリと Wasm をそれぞれ Dart で使ってみます。
作った順序と逆に Wasm のほうから。
Wasm
wasm ファイルはプラットフォームが違っても共通なのでクロスコンパイルが不要で、Dart で使うときに分岐しなくていいので扱いやすいです。
Dart で利用するには package:wasm を用います。
experimental ですが、興味深くて期待できるので試すことにしました。
パッケージは最初のバージョンである 0.1.0+1 が一年ほど前に出てから更新されていません。
GitHub のそのリポジトリでは多少の動きはあるようです。
準備
ツール
package:wasm では WebAssembly のランタイムである Wasmer が用いられます。
そのために Rust が使われるので、開発マシンに入っていなければインストールが必要です。
Windows の場合
Windows では link.exe
が使えるように Visual Studio の Build Tools で 「C++ によるデスクトップ開発」のインストールも必要とのことです。
Flutter のデスクトップアプリを Windows 上で開発している人はインストール済みだと思います。
私の場合、それはインストール済みでしたが LLVM / Clang がなかったので、Visual Studio Installer の「個別のコンポーネント」のところで「C++ Clang-cl」と「Windows 用 C++ Clang コンパイラ」をインストールし、さらに Path を設定しました。
プロジェクト
ドキュメントに従い、pubspec.yaml への wasm パッケージの追加などを行います。
dart run wasm:setup
ではプロジェクト内の .dart_tools/wasm/
に Wasmer のライブラリが生成されます。
読み込み
wasm ファイルをソースコードと同じ場所に置いている場合は次のようになります。
windows
の引数は Windows 以外では指定しないか false
で。
import 'dart:io';
import 'package:wasm/wasm.dart';
void main() {
final path = Platform.script.resolve('fibonacci.wasm').toFilePath(windows: true);
final data = File(path).readAsBytesSync();
final mod = WasmModule(data);
print(mod.describe());
}
WasmModule() では、コンパイル済みの Wasm のバイナリを使って module のオブジェクトが組み立てられるそうです。
そのオブジェクトの import / export の情報を describe() で得ることができるようになっています。
上のコードではそれを print するようにしているので、実行すると次のように出力されます。
export memory: memory
export function: int32 fibonacci(int32)
Zig のソースコードでは u16
だったのに、この情報では int32
になっています。
このままフィボナッチ数を求めたところ間違った結果が返ったので、桁が溢れたと思われます。
int32
に見えているだけで実際には uint16
のようです。
Zig で u64
か i64
に直して wasm ファイルとライブラリの両方を生成し直しましょう。
利用
Wasm を Dart で利用するのは、もうここまで準備ができていると簡単です。
型の定義を自分で用意しないといけない FFI より扱いやすいです。
// この関数を使って実行とかかった時間の出力を行うことにします
void run(int Function() func) {
final sw = Stopwatch()..start();
final number = func();
final elapsed = (sw..stop()).elapsedMilliseconds;
print('[$elapsed ms] $number');
}
final mod = WasmModule(data);
final inst = mod.builder().build();
final fibonacci = inst.lookupFunction('fibonacci');
run(() => fibonacci(45)); // [8111 ms] 113490317
FFI
以前には別の PC で ffigen を使ったのですが、今の自分の PC では何かが不足していて利用できなかったので、型の定義は手動で行いました。
import 'dart:ffi' as ffi;
import 'dart:io';
typedef Fibonacci = int Function(int);
typedef FibonacciFunc = ffi.Int Function(ffi.Int);
void main() {
final path = Platform.script.resolve('fibonacci.dll').toFilePath(windows: true);
final lib = ffi.DynamicLibrary.open(path);
final fibonacci = lib
.lookup<ffi.NativeFunction<FibonacciFunc>>('fibonacci')
.asFunction<Fibonacci>();
run(() => fibonacci(45)); // [3944 ms] 1134903170
}
Dart
Dart 単体での速度も比較のために確認します。
int fibonacci(int n) {
return n == 0 || n == 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
void main() {
run(() => fibonacci(45)); // [10138 ms] 1134903170
}
結果
Wasm | FFI | Dart |
---|---|---|
8111 ms | 3944 ms | 10138 ms |
Zig + Dart FFI は使えそうですね。
Wasm はわざわざ利用する意味がありそうな圧倒的な速さではありませんでした。
Wasm か Wasmer の限界なのでしょうか。
扱いやすさでは勝っていただけに残念に思います。
Dart が苦手とする処理が何かあるなら、そこでは Wasm でも効果的かもしれません。