FFI (Foreign Function Interface) — a Dart nyelv egy mechanizmusa, amelyet a dart:ffi csomag biztosít, és lehetővé teszi a natív C könyvtárakból származó függvények közvetlen meghívását, Kotlin, Swift vagy Java köztes rétegek nélkül. A fejlesztő betölt egy dinamikus könyvtárat (.so Androidon, .dylib iOS-en, .dll Windows-on), deklarálja a C függvények aláírásait, és meghívja azokat mint közönséges Dart függvényeket. A Dart API Reference (2025) szerint az FFI 0.1 μs-ra csökkenti a nyelvek közötti hívások többletterhelését, ami tízszer gyorsabb, mint a Method Channelen keresztül.
Főbb pontok
FFI (Foreign Function Interface) — egy mechanizmus, amely lehetővé teszi egy programozási nyelv számára, hogy más nyelveken írt függvényeket hívjon meg. A Dart és Flutter kontextusában az FFI azt jelenti, hogy képesek vagyunk C/C++ könyvtárakból függvényeket meghívni közvetlenül Dart kódból, anélkül hogy platformkódot kellene írni Java (Android) vagy Swift/Objective-C (iOS) nyelven.
A dart:ffi csomag a Dart 2.12-ben (2021) jelent meg, és azóta kulcsfontosságú eszközzé vált a Flutter natív kóddal való integrációjában. A dart:ffi megjelenése előtt az egyetlen módja a C függvény Dartból való meghívásának a Method Channel volt — egy aszinkron mechanizmus, amely JSON-szerializáción keresztül továbbított üzeneteket a Dart és a natív oldal között. Az FFI másképp működik: a Dart kód közvetlenül hozzáfér a C könyvtár memóriájához, függvényeket hívva a natív ABI-n (Application Binary Interface) keresztül, szerializáció és kontextusváltás nélkül.
Az FFI különösen olyan forgatókönyvekben keresett, ahol a teljesítmény kritikus: képfeldolgozás (OpenCV), hang (FFmpeg), kriptográfia (OpenSSL), gépi tanulás (TensorFlow Lite) és adatbázisok (SQLite). Mindezekben az esetekben a Method Channel elfogadhatatlan késleltetést okoz, míg az FFI a natív C/C++ kóddal összehasonlítható teljesítményt nyújt. A dart:ffi könyvtár támogatja a memóriával való munkát is: foglalás, felszabadítás és mutatók kezelése.
A Method Channel aszinkron módon működik: Dart üzenetet küld a natív kódnak, a natív kód feldolgozza azt, és visszaküldi az eredményt. Minden hívás megköveteli az argumentumok Map-pé szerializálását, soron keresztüli továbbítását és deszerializálását. Ez 0.5–5 ms-ot vesz igénybe hívásonként. Az FFI szinkron és szerializáció nélkül működik — egy C függvény hívása 0.01–0.1 μs-ot vesz igénybe. 50–500-szoros különbség, ami kritikus a nagy frekvenciájú műveleteknél.
A dart:ffi-vel való munka három szakaszból áll: a könyvtár betöltése, az aláírások deklarálása és a függvények meghívása. Minden szakasz a Dart szigorú típusosságát használja, ami minimalizálja a futásidőben jelentkező hibákat.
Az első szakaszban a dinamikus könyvtár a DynamicLibrary osztályon keresztül töltődik be. A könyvtár betölthető név szerint (libxyz.so, libxyz.dylib, xyz.dll) vagy teljes elérési úttal. A Dart automatikusan keresi a könyvtárat a rendszer szabványos útvonalain. DynamicLibrary biztosítja a lookupFunction metódust, amely egy Dart függvényt egy C függvényhez kapcsol a szimbólum neve alapján.
A második szakaszban egy Dart függvény kerül deklarálásra a C aláírásnak megfelelő típusannotációkkal. Ehhez speciális típusokat használnak a dart:ffi-ből: Int32, Float, Double, Pointer, NativeFunction, Handle és mások. A lookupFunction annotáció két generikus paramétert fogad: a Dart függvény típusát (hogyan fog kinézni Dartban) és a natív C függvény típusát (hogyan van deklarálva C-ben).
A harmadik szakaszban a létrehozott Dart függvény úgy hívható meg, mint egy közönséges függvény. Az argumentumok közvetlenül átadásra kerülnek, az eredmény azonnal visszatér. Ha a C függvény mutatókon keresztül módosítja a memóriát, a Dart képes olvasni ezeket a változtatásokat a Pointer osztályon keresztül. A memória kezelése a C oldalon a fejlesztő felelőssége marad — a dart:ffi nem kezeli a C-ben malloc által lefoglalt memóriát.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C függvény deklaráció: 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
}
Ebben a példában az add egy C függvény, amely két int-et fogad és int-et ad vissza. A typedef AddNative a C aláírást írja le dart:ffi típusokkal, az AddDart pedig azt, hogy ez a függvény hogyan fog kinézni Dartban. A lookupFunction összekapcsolja őket, és visszaad egy Dart függvényt, amely közönségesként hívható meg.
A dart:ffi a C típusoknak megfelelő típuskészletet biztosít. Minden típusnak rögzített mérete és konverziós szabályai vannak a Dart és C között. A típusok megfelelésének megértése kritikus az FFI helyes működéséhez — hiba a típus méretében vagy előjelében az alkalmazás összeomlásához vezethet.
| C típus | dart:ffi típus | Dart típus | Méret (bájt) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (mutató) |
| void* | Pointer<Void> | Pointer | 8 (mutató) |
| struct | Pointer<T> (Struct) | Pointer | mezőktől függ |
A C karakterláncokkal (char*) való munkához a dart:ffi a Pointer<Int8> típust használja. A Dart String C char*-rá és fordítva történő konvertálása a toNativeUtf8 (az ffi csomagból) és fromUtf8 metódusokon keresztül történik. Fontos a C karakterláncok felszabadítása használat után a calloc.free segítségével a memóriaszivárgás elkerülése érdekében.
A dart:ffi támogatja a C struktúrák deklarálását Dart osztályokként, amelyek a Struct-ból származnak. A struktúra mezői @Int32(), @Float(), @Array() és más annotációkkal vannak deklarálva. A mezők mérete és eltolása automatikusan kiszámításra kerül a platform ABI-jának megfelelően. Pointer<Point> beszerezhető egy C függvényből, amely mutatót ad vissza struktúrára, vagy lefoglalható Dartban a calloc segítségével.
// C struktúra: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Point*-ot visszaadó C függvény hívása
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) // memória felszabadítása
A Point osztály a Struct-ból származik, és az x és y mezőket @Int32() annotációkkal deklarálja. A generált C kód pontosan ugyanolyan mezőelrendezéssel rendelkezik a memóriában. Pointer.ref hozzáférést biztosít a struktúra mezőihez getterek és setterek segítségével.
Nézzünk egy összetettebb példát — integráció egy C könyvtárral a SHA256 hash kiszámításához. Ez egy tipikus feladat, ahol az FFI jelentős teljesítményelőnyt biztosít a Method Channel-hez képest.
Az OpenSSL könyvtár biztosítja a SHA256 függvényt, amely kiszámítja egy karakterlánc hash-ét. A dart:ffi segítségével közvetlenül meghívhatjuk, Java vagy Swift wrapper-ek írása nélkül. Ez egy példa arra, hogy az FFI hogyan teszi lehetővé meglévő C könyvtárak újrafelhasználását Flutterben.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Aláírás: 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 bájt
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
}
Ebben a példában a sha256 függvény betölti a libcrypto.so könyvtárat, megtalálja a SHA256 szimbólumot, és meghívja azt a bemeneti és kimeneti adatokra mutató pointerekkel. A toNativeUtf8 konvertálja a Dart String-et C karakterlánccá (memóriát foglal), az asTypedList pedig lehetővé teszi az eredmény bájt tömbjének olvasását. A memória használat után felszabadításra kerül — ez kötelező lépés a szivárgás megelőzésére.
Az ffi csomag biztosítja a calloc függvényt C-kompatibilis memória foglalásához. A lefoglalt memóriát a calloc.free segítségével kell felszabadítani, ellenkező esetben szivárgás lép fel. Automatikus memóriakezeléshez használható az Arena osztály az ffi csomagból, amely az arena.release() hívásakor felszabadítja az összes benne lefoglalt memóriát. Ez különösen kényelmes nagyszámú ideiglenes foglalás esetén.
Az FFI ereje ellenére vannak korlátozásai, amelyeket figyelembe kell venni a Flutter alkalmazás architektúrájának tervezésekor. A fő korlátozások a típusbiztonsággal, memóriakezeléssel és platformkompatibilitással kapcsolatosak.
Az FFI nem ellenőrzi a típusokat futásidőben. Ha egy C függvény mutatót vár, de egy szám kerül átadásra, az alkalmazás segmentation fault-tal összeomlik. Ajánlott az FFIgen használata — egy eszköz, amely típusbiztos Dart wrapper-eket generál C fejlécfájlok (.h fájlok) alapján. FFIgen elemzi a C függvények deklarációit, és helyes típusokkal hozza létre a Dart kódot, ami kiküszöböli a hibákat a kódírás szakaszában.
A dinamikus könyvtárak nevei és elérési útjai különböznek a különböző platformokon: libxyz.so Android/Linux rendszeren, libxyz.dylib iOS/macOS rendszeren, xyz.dll Windows rendszeren. Többplatformos könyvtárakhoz feltételes fordítás használható a dart:io-n (Platform.isAndroid, Platform.isIOS) vagy absztrakciók, mint a package:ffi keresztül. Ajánlott egy gyári metódus létrehozása, amely visszaadja a megfelelő könyvtárat az aktuális platformhoz.
Az FFI nem kezeli a memóriát a C oldalon. Ha egy C függvény malloc segítségével foglal memóriát, azt free segítségével kell felszabadítani, ellenkező esetben szivárgás lép fel. Dartban nincs szemétgyűjtő a C memóriához. Javaslat: mindig ugyanabban a metódusban szabadítsa fel a memóriát, ahol lefoglalta, vagy használja az Arena-t csoportos felszabadításhoz.
Az FFI hívások ugyanabban a szálban hajtódnak végre, mint a Dart kód. Hosszú szinkron műveletek (több mint 10 ms) blokkolják az UI szálat, és képkivágásokat okoznak. Hosszú műveletek esetén a C függvényt egy izolátumban (Isolate) kell meghívni, vagy biztosítani kell, hogy a C függvény háttérszálon indítsa el a munkát, és Port vagy callback segítségével értesítse a Dart-ot.
// FFI izolátumban hosszú műveletekhez
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI hívás
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Az FFI hívás izolátumba helyezése garantálja, hogy az UI szál nem blokkolódik. Azonban nagy mennyiségű adat átvitele az izolátumok között memória másolását igényli. Nagy pufferek (>10 MB) esetén előnyösebb a ShareMemory vagy memóriába leképezett fájlok használata.
Gyakran ismételt kérdések
Az FFI közvetlenül, szinkron módon és szerializáció nélkül hívja a C függvényeket — késleltetés 0.01–0.1 μs. A Method Channel aszinkron módon működik JSON-szerializáción keresztül, 0.5–5 ms késleltetéssel. Az FFI nagy teljesítményű műveletekhez alkalmas, a Method Channel egyszerű platform API hívásokhoz.
Közvetlenül — nem, a dart:ffi csak C függvényeket támogat. A C++ hívásához létre kell hozni egy C wrapper-t extern "C"-vel (belépési pontok, amelyek C szimbólumokként kerülnek exportálásra). A C++ osztályok további réteget igényelnek, amely a metódushívásokat C függvényekké alakítja.
Az FFI nem támogatja a kivételeket — ha egy C függvény hibakódot ad vissza, azt manuálisan kell ellenőrizni. Ajánlott az FFI hívásokat try-catch blokkba csomagolni Dartban, és ellenőrizni a C függvények által visszaadott kódokat. Kritikus hibák (segfault) nem foghatók el.
Az FFI nem működik olyan könyvtárakkal, amelyek összetett Java (JNI) vagy Objective-C (Message Dispatch) inicializálást igényelnek. Például a UIKit és Android Views nem érhetők el FFI-n keresztül. Korlátozás összefügg azzal, hogy az FFI a C ABI szintjén működik, míg ezek az API-k speciális futási környezeteket igényelnek.
Igen, a C könyvtárakat minden célplatformhoz külön kell fordítani. Android esetén a .so különböző ABI-khoz készül (armeabi-v7a, arm64-v8a, x86_64). iOS esetén — egy univerzális .dylib (arm64). Windows esetén — .dll. Flutter automatikusan becsomagolja a könyvtár megfelelő verzióját az építés során.
Összegzés
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.