FFI(Foreign Function Interface)は、dart:ffiパッケージによって提供されるDart言語のメカニズムであり、Kotlin、Swift、Javaの中間層を介さずに、ネイティブCライブラリの関数を直接呼び出すことを可能にします。開発者は動的ライブラリ(Androidでは.so、iOSでは.dylib、Windowsでは.dll)をロードし、C関数のシグネチャを宣言して、通常のDart関数のように呼び出します。Dart API Reference(2025)によると、FFIは言語間呼び出しのオーバーヘッドを0.1 µsに削減し、Method Channelよりも数十倍高速です。
重要ポイント
FFI(Foreign Function Interface)は、プログラミング言語が他の言語で書かれた関数を呼び出すことを可能にするメカニズムです。DartとFlutterのコンテキストでは、FFIはJava(Android)やSwift/Objective-C(iOS)でプラットフォーム固有のコードを書くことなく、Dartコードから直接C/C++ライブラリの関数を呼び出す機能を意味します。
dart:ffiパッケージはDart 2.12(2021)で登場し、それ以来Flutterをネイティブコードと統合するための重要なツールとなっています。dart:ffi以前は、DartからC関数を呼び出す唯一の方法はMethod Channelでした。これはDartとネイティブ側の間でJSONシリアライゼーションを介してメッセージを渡す非同期メカニズムです。FFIは異なる方法で動作します:DartコードはCライブラリのメモリに直接アクセスし、シリアライゼーションやコンテキストスイッチなしでネイティブABI(アプリケーションバイナリインターフェース)を介して関数を呼び出します。
FFIは特にパフォーマンスが重要なシナリオで需要があります:画像処理(OpenCV)、オーディオ(FFmpeg)、暗号化(OpenSSL)、機械学習(TensorFlow Lite)、データベース(SQLite)。これらすべてのケースで、Method Channelは許容できない遅延を生み出す一方、FFIはネイティブC/C++コードに匹敵するパフォーマンスを提供します。dart:ffiライブラリはメモリ管理(割り当て、解放、ポインタ操作)もサポートしています。
Method Channelは非同期で動作します:Dartがネイティブコードにメッセージを送信し、ネイティブコードがそれを処理して結果を返送します。各呼び出しでは、Mapへの引数のシリアライズ、キューを介した受け渡し、デシリアライズが必要です。これには呼び出しあたり0.5~5 msかかります。FFIは同期的かつシリアライゼーションなしで動作します — C関数の呼び出しには0.01~0.1 µsかかります。50~500倍の差であり、高頻度の操作には重要です。
dart:ffiの操作は3つの段階で構成されます:ライブラリのロード、シグネチャの宣言、関数の呼び出し。各段階でDartの厳格な型付けが使用され、ランタイムエラーを最小限に抑えます。
最初の段階では、DynamicLibraryクラスを介して動的ライブラリがロードされます。ライブラリは名前(libxyz.so、libxyz.dylib、xyz.dll)またはフルパスでロードできます。Dartは自動的にシステムの標準パスでライブラリを検索します。DynamicLibraryはlookupFunctionメソッドを提供し、シンボル名でDart関数をC関数にバインドします。
2番目の段階では、Cシグネチャに対応する型アノテーションを持つDart関数が宣言されます。dart:ffiの特別な型(Int32、Float、Double、Pointer、NativeFunction、Handleなど)が使用されます。lookupFunctionアノテーションは2つのジェネリックパラメータを受け取ります:Dart関数の型(Dartでの見え方)とネイティブC関数の型(Cでの宣言方法)。
3番目の段階では、生成されたDart関数が通常の関数のように呼び出されます。引数は直接渡され、結果は即座に返されます。C関数がポインタを介してメモリを変更する場合、DartはPointerクラスを介してこれらの変更を読み取ることができます。C側のメモリ管理は開発者の責任です — dart:ffiはCでmallocによって割り当てられたメモリを管理しません。
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C関数宣言: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)
void main() {
final lib = DynamicLibrary.open('libcalculator.so')
final AddDart add = lib
.lookupFunction<AddNative, AddDart>('add')
print(add(5, 3)) // 8
}
この例では、addは2つのintを受け取りintを返すC関数です。AddNative typedefはdart:ffi型でCシグネチャを記述し、AddDartはこの関数がDartでどのように見えるかを記述します。lookupFunctionがそれらをバインドし、通常のように呼び出せるDart関数を返します。
dart:ffiはC型に対応する一連の型を提供します。各型には固定サイズとDartとC間の変換ルールがあります。FFIの正しい動作には型マッピングの理解が極めて重要です — 型のサイズや符号の誤りはアプリケーションのクラッシュを引き起こす可能性があります。
| C型 | dart:ffi型 | Dart型 | サイズ(バイト) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8(ポインタ) |
| void* | Pointer<Void> | Pointer | 8(ポインタ) |
| struct | Pointer<T>(Struct) | Pointer | フィールドによる |
C文字列(char*)を扱うには、dart:ffiはPointer<Int8>を使用します。Dart StringからC char*への変換および逆変換は、toNativeUtf8(ffiパッケージから)とfromUtf8を介して行われます。メモリリークを防ぐために、使用後はcalloc.freeを介してC文字列を解放することが重要です。
dart:ffiはStructを拡張するDartクラスとしてC構造体の宣言をサポートしています。構造体のフィールドは@Int32()、@Float()、@Array()などのアノテーションで宣言されます。フィールドのサイズとオフセットはプラットフォームのABIに従って自動的に計算されます。Pointer<Point>は構造体へのポインタを返すC関数から取得するか、Dartでcallocを介して割り当てることができます。
// C構造体: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Point*を返すC関数を呼び出し
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)
final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // メモリ解放
PointクラスはStructを拡張し、@Int32()アノテーションでxとyフィールドを宣言します。生成されたCコードはまったく同じメモリレイアウトになります。Pointer.refはゲッターとセッターを介して構造体フィールドへのアクセスを提供します。
より複雑な例を見てみましょう — SHA256ハッシュ計算のためのCライブラリとの統合です。これはFFIがMethod Channelと比較して大幅なパフォーマンス向上を提供する典型的なタスクです。
OpenSSLライブラリはSHA256関数を提供し、文字列のハッシュを計算します。dart:ffiを介して、JavaやSwiftのラッパーを書くことなく直接呼び出すことができます。これはFFIがFlutterで既存のCライブラリを再利用する方法を示す例です。
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// シグネチャ: unsigned char* SHA256(
// const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
Pointer<Uint8>, int, Pointer<Uint8>)
String sha256(String input) {
final lib = DynamicLibrary.open('libcrypto.so')
final Sha256Dart sha256Fn = lib
.lookupFunction<Sha256Native, Sha256Dart>('SHA256')
final inputPtr = input.toNativeUtf8()
final outputPtr = calloc(Uint8)(32) // SHA256 = 32バイト
sha256Fn(inputPtr, input.length, outputPtr)
final digest = outputPtr.asTypedList(32)
final hex = digest.map((b) => b.toRadixString(16)
.padLeft(2, '0')).join()
calloc.free(inputPtr)
calloc.free(outputPtr)
return hex
}
この例では、sha256関数がlibcrypto.soライブラリをロードし、SHA256シンボルを見つけて、入力データと出力データへのポインタで呼び出します。toNativeUtf8はDart StringをC文字列に変換し(メモリを割り当て)、asTypedListは結果のバイト配列を読み取ることができます。使用後はメモリが解放されます — これはリークを防ぐための必須の手順です。
ffiパッケージはC互換メモリを割り当てるためのcalloc関数を提供します。割り当てられたメモリはcalloc.freeを介して解放する必要があります。そうしないとリークが発生します。自動メモリ管理には、ffiパッケージのArenaクラスを使用できます。これはarena.release()が呼び出されると、その中で割り当てられたすべてのメモリを解放します。これは多数の一時的な割り当てに特に便利です。
FFIのパワーにもかかわらず、Flutterアプリケーションのアーキテクチャを設計する際には考慮すべき制限があります。主な制限は型安全性、メモリ管理、プラットフォーム互換性に関連しています。
FFIは実行時に型チェックを行いません。C関数がポインタを期待しているのに数値を受け取った場合、アプリケーションはセグメンテーションフォールトでクラッシュします。FFIgen — Cヘッダーファイル(.h)に基づいて型安全なDartラッパーを生成するツール — を使用することをお勧めします。FFIgenはC関数の宣言を分析し、正しい型でDartコードを作成し、コード作成段階でのエラーを排除します。
動的ライブラリの名前とパスはプラットフォームによって異なります:Android/Linuxではlibxyz.so、iOS/macOSではlibxyz.dylib、Windowsではxyz.dll。クロスプラットフォームライブラリには、dart:io(Platform.isAndroid、Platform.isIOS)やpackage:ffiなどの抽象化を介した条件付きコンパイルが使用されます。現在のプラットフォームに適したライブラリを返すファクトリメソッドを作成することをお勧めします。
FFIはC側のメモリを管理しません。C関数がmallocを介してメモリを割り当てた場合、freeを介して解放する必要があります。そうしないとリークが発生します。DartにはCメモリ用のガベージコレクタはありません。推奨事項:常にメモリを割り当てた同じメソッドで解放するか、グループ解放のためにArenaを使用してください。
FFI呼び出しはDartコードと同じスレッドで実行されます。長時間の同期操作(10 ms以上)はUIスレッドをブロックし、フレームドロップを引き起こします。長時間の操作では、IsolateでC関数を呼び出すか、C関数がバックグラウンドスレッドで作業を実行し、Portまたはコールバックを介してDartに通知することを確認する必要があります。
// 長時間操作のためのIsolate内のFFI
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI呼び出し
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
FFI呼び出しをIsolateに移動することで、UIスレッドがブロックされないことが保証されます。ただし、Isolate間での大量のデータ転送にはメモリコピーが必要になることに注意してください。大きなバッファ(>10 MB)には、SharedMemoryまたはメモリマップトファイルを使用することをお勧めします。
よくある質問
FFIはC関数を直接、同期的に、シリアライゼーションなしで呼び出します — 遅延0.01~0.1 µs。Method ChannelはJSONシリアライゼーションを介して非同期的に動作し、遅延は0.5~5 msです。FFIは高性能な操作に適しており、Method Channelは単純なプラットフォームAPI呼び出しに適しています。
直接は — いいえ、dart:ffiはC関数のみをサポートしています。C++を呼び出すには、extern “C”(Cシンボルとしてエクスポートされるエントリポイント)を持つCラッパーを作成する必要があります。C++クラスは、メソッド呼び出しをC関数に変換する追加のレイヤーを必要とします。
FFIは例外をサポートしていません — C関数がエラーコードを返す場合、手動でチェックする必要があります。DartでFFI呼び出しをtry-catchでラップし、C関数の戻りコードをチェックすることをお勧めします。重大なエラー(segfault)はキャッチできません。
FFIは複雑なJava(JNI)やObjective-C(Message Dispatch)の初期化を必要とするライブラリでは動作しません。例えば、UIKitやAndroid ViewsはFFIを介してアクセスできません。制限は、FFIがC ABIレベルで動作するのに対し、これらのAPIは特定のランタイムを必要とするためです。
はい、Cライブラリはターゲットプラットフォームごとに個別にコンパイルされます。Androidでは異なるABI(armeabi-v7a、arm64-v8a、x86_64)用に.soがビルドされます。iOS用にはユニバーサル.dylib(arm64)、Windows用には.dllがビルドされます。Flutterはビルド時に自動的に正しいバージョンのライブラリをパッケージ化します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。