はじめに
DartからC関数を呼び出すdart:ffiの使い方は以下のページにドキュメントがあります。
ただ、いずれもシンプルな同期関数呼び出しの例でしたので、
callback関数呼び出しの実装方法を備忘のため書いておきます。
誤りやよりよい方法などありましたら、ぜひご指摘ください。
なお、ドキュメントは見つかりませんでしたが、
dart-lang/sdkにサンプルは見つかったのでそちらを参考にさせていただきました。
動作確認環境は以下のとおりです。
また、FlutterのplatformとしてはLinux Desktopで動作を確認しています。
$ uname -a
Linux chama 5.5.8 #1 SMP Sat Mar 7 22:29:22 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -d
Description: Ubuntu 18.04.5 LTS
$ flutter --version
Flutter 1.24.0-8.0.pre.117 • channel master • https://github.com/flutter/flutter
Framework • revision 8cb2665118 (8 hours ago) • 2020-11-06 16:02:19 +0800
Engine • revision 0693ee04d1
Tools • Dart 2.12.0 (build 2.12.0-21.0.dev)
今回実装するcallback関数呼び出し
どこでも使える有名なcallback関数を呼び出す関数として、今回はqsort(3)
を選びました。
実際にはqsort_r(3)
を使うと思いますが、今回の本筋ではないのでコードを簡潔にするためqsort(3)
にしています。
qsort(3)
のプロトタイプと説明をmanから引用しておきます。
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
[man qsort(3)]
qsort() 関数は、 nmemb 個の大きさ size の要素をもつ配列を並べ変える。 base 引き数は配列の先頭へのポインターである。
compar をポインターとする比較関数によって、 配列の中身は昇順 (値の大きいものほど後に並ぶ順番) に並べられる。 比較関数の引き数は比較されるふたつのオブジェクトのポインターである。比較関数は、第一引き数が第二引き数に対して、 1) 小さい、2) 等しい、3) 大きいのそれぞれに応じて、 1) ゼロより小さい整数、2) ゼロ、3) ゼロより大きい整数の いずれかを返さなければならない。 二つの要素の比較結果が等しいとき、 並べ変えた後の配列では、これら二つの順序は規定されていない。
このqsort(3)
を使ってintの配列を昇順に並べます。
C言語で実装したら以下です。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
// callback関数
static int compar(const void* rhs_ptr, const void* lhs_ptr) {
int32_t rhs = *(int32_t*)rhs_ptr;
int32_t lhs = *(int32_t*)lhs_ptr;
if (rhs > lhs) {
return 1;
} else if (rhs < lhs) {
return -1;
} else {
return 0;
}
}
int main(void) {
int32_t array[] = {1, 5, -10, 3, 9, 8, 7, 13};
qsort(array, sizeof(array) / sizeof(array[0]), sizeof(array[0]), compar);
for (uint32_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {
printf("%d\n", array[i]);
}
return 0;
}
今回はこれと同じ処理をdart:ffi経由で呼び出します。
Dartからqsort(3)
を呼び出す
では早速Dartでの実装を載せます。
// 型定義
typedef NativeCompar = Int32 Function(Pointer<Int32>, Pointer<Int32>);
typedef NativeQsort = Void Function(
Pointer<Int32>, Uint64, Uint64, Pointer<NativeFunction<NativeCompar>>);
typedef Qsort = void Function(
Pointer<Int32>, int, int, Pointer<NativeFunction<NativeCompar>>);
// callback関数
int compar(Pointer<Int32> rhsPtr, Pointer<Int32> lhsPtr) {
final rhs = rhsPtr.value;
final lhs = lhsPtr.value;
if (rhs > lhs) {
return 1;
} else if (rhs < lhs) {
return -1;
} else {
return 0;
}
}
List<int> qsort(final List<int> data, _compar) {
// DartのListをCの配列に変換
final dataPtr = intListToArray(data);
// qsort(3)を定義しているlibcのハンドラを取得
final libc = DynamicLibrary.open('libc.so.6');
// libcの中からqsortというシンボルのアドレスを取得しDart関数に変換
final qsort =
libc.lookup<NativeFunction<NativeQsort>>('qsort').asFunction<Qsort>();
// Dartの関数comparからC言語の関数ポインタに変換
Pointer<NativeFunction<NativeCompar>> pointer =
Pointer.fromFunction(compar, 0);
// libcのqsort(3)に配列、要素数、要素サイズ、関数ポインタを渡して呼び出し
qsort(dataPtr, data.length, sizeOf<Int32>(), pointer);
// Cの配列をDartのListに変換
return arrayToIntList(dataPtr, data.length);
}
処理の流れはコメントとして記載しました。
Dart関数をコールバック関数として渡すときのポイントは、Pointer#fromFunctionです。
このメソッドでDart関数からCの関数ポインタに変換できます。
引数に関しても自明なものについては自動的にmarshallingしてくれます。
上記サンプルのFlutterアプリとして動作するコードは、以下においています。
おわりに
他言語バインドは型変換が多くどうしても表記上は長くなりますが、
落ち着いて読めば本質的にはC言語の関数ポインタの取扱と変わっていません。
もちろん実際に開発で使う際にはエラー処理やリソース管理、微妙な型間違い防止、
コピー回数を減らす工夫、ライブラリハンドラ・シンボルのキャッシングなど
考えないといけないことは他にたくさんありますが、
型変換に惑わされなければそのあたりもNativeの開発と同じように考えられるのではないかと思います。
参考
-
sdk/sample_ffi_functions_callbacks.dart at 2.12.0-21.0.dev · dart-lang/sdk
- dart-lang/sdkのdart:ffiでcallback呼び出しをするサンプル
- native側のコードはruntime側のffi_test_functions
-
samples/ffi at master · dart-lang/samples
- dart-lang/samplesのffiサンプル
-
- ffi呼び出しのコードを自動生成するためのパッケージ
-
- callbackに関する言及はありませんが、FFIの歴史をわかりやすくまとめてくださっていてとても参考になりました。