KabologHomeXGitHub

Zig + Dart(FFI / Wasm)の可能性

Dart で他の言語を利用するのは Go で試みたことがあります。

今回は Zig です。
いつかあるかもしれない実用の機会を視野に入れつつも、ほとんど興味本位です。

いくつかの言語の Pros / Cons

Dart で組み合わせて使うときの観点で挙げます。

Go

  • Pros
    • クロスコンパイルできる
    • 標準パッケージが充実していてそれらを活用できる
  • Cons
    • ライブラリのサイズが小さくても数 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 で u64i64 に直して 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 でも効果的かもしれません。

Xに投稿する