FFI: alapok, Foreign Function Interface és integráció C-vel

Szerző: IT Sectr Megjelenés: 2026-06-05 Olvasási idő: 10 perc

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) — C függvények közvetlen meghívásának mechanizmusa Dartból
  • dart:ffi API-t biztosít könyvtárak betöltéséhez és aláírások deklarálásához
  • Teljesítmény az FFI-n keresztüli hívások 50–100-szor magasabbak, mint a Method Channelen keresztül
  • Típusosság az FFI támogatja a primitív típusokat, struktúrákat és C mutatókat
  • Flutter az FFI-t használja natív könyvtárakkal való integrációhoz: OpenCV, SQLite, FFmpeg

Mi az FFI?

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.

FFI vs Method Channel: alapvető különbség

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.

Hogyan működik a dart:ffi?

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.

Alapvető példa: C függvény meghívása Dartból

dart
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.

Adattípusok az FFI-ban

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ípusdart:ffi típusDart típusMéret (bájt)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (mutató)
void*Pointer<Void>Pointer8 (mutató)
structPointer<T> (Struct)Pointermező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.

Struktúrák (Struct)

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.

dart
// 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.

FFI gyakorlati példák

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.

Integráció az OpenSSL-lel FFI-n keresztül

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.

dart
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.

Memória foglalása és felszabadítása

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.

Korlátozások és legjobb gyakorlatok

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.

Biztonság

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.

Platformkompatibilitás

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.

Memóriakezelés

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.

Teljesítmény és szálak

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.

dart
// 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

Miben különbözik az FFI a Method Channel-től?

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.

Lehet C++ függvényeket hívni FFI-n keresztül?

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.

Hogyan kezeljük a hibákat C függvényekben?

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.

Milyen könyvtárak nem használhatók FFI-n keresztül?

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.

Le kell fordítani a C könyvtárakat minden platformhoz külön?

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

  • FFI (Foreign Function Interface) — C függvények közvetlen meghívásának mechanizmusa Dartból dart:ffi-n keresztül
  • Teljesítmény az FFI hívások 50–500-szor gyorsabbak, mint a Method Channel-en keresztül
  • Architektúra tartalmazza a könyvtár betöltését, aláírások deklarálását és függvények hívását
  • Adattípusok a dart:ffi támogatja az Int32, Float, Double, Pointer, Struct és más C típusokat
  • Memória a C oldalon manuálisan kezelendő calloc/free vagy Arena segítségével
  • Korlátozások FFI: nincs futásidőbeni típusellenőrzés, nincs közvetlen C++ támogatás, blokkolja az UI szálat
  • Használja az FFI-t nagy teljesítményű natív könyvtár-integrációhoz Flutterben

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.

Projekt megbeszélése

Olvassa el is