FFI (Foreign Function Interface) — is een mechanisme van de Dart-taal, geleverd door het dart:ffi-pakket, waarmee functies uit native C-bibliotheken direct kunnen worden aangeroepen, zonder tussenliggende lagen in Kotlin, Swift of Java. De ontwikkelaar laadt een dynamische bibliotheek (.so op Android, .dylib op iOS, .dll op Windows), declareert de handtekeningen van C-functies en roept ze aan als gewone Dart-functies. Volgens Dart API Reference (2025) vermindert FFI de overhead van inter-language calls tot 0.1 μs, wat tientallen keren sneller is dan via Method Channel.
Belangrijkste punten
FFI (Foreign Function Interface) — is een mechanisme waarmee een programmeertaal functies kan aanroepen die in andere talen zijn geschreven. In de context van Dart en Flutter betekent FFI de mogelijkheid om functies uit C/C++-bibliotheken rechtstreeks vanuit Dart-code aan te roepen, zonder platformcode te hoeven schrijven in Java (Android) of Swift/Objective-C (iOS).
Het dart:ffi-pakket verscheen in Dart 2.12 (2021) en is sindsdien een belangrijk hulpmiddel geworden voor integratie van Flutter met native code. Vóór dart:ffi was de enige manier om een C-functie vanuit Dart aan te roepen Method Channel — een asynchroon mechanisme dat berichten via JSON-serialisatie tussen Dart en de native kant verzond. FFI werkt anders: Dart-code heeft rechtstreeks toegang tot het geheugen van de C-bibliotheek en roept functies aan via native ABI (Application Binary Interface) zonder serialisatie en zonder contextwisseling.
FFI is vooral gewild in scenario's waar prestaties kritisch zijn: beeldverwerking (OpenCV), audio (FFmpeg), cryptografie (OpenSSL), machine learning (TensorFlow Lite) en databases (SQLite). In al deze gevallen creëert Method Channel onaanvaardbare vertragingen, terwijl FFI prestaties biedt die vergelijkbaar zijn met native C/C++-code. De bibliotheek dart:ffi ondersteunt ook het werken met geheugen: toewijzing, vrijgave en beheer van pointers.
Method Channel werkt asynchroon: Dart stuurt een bericht naar de native code, de native code verwerkt het en stuurt het resultaat terug. Elke aanroep vereist serialisatie van argumenten naar Map, overdracht via een wachtrij en deserialisatie. Dit duurt 0.5–5 ms per aanroep. FFI werkt synchroon en zonder serialisatie — een C-functieaanroep duurt 0.01–0.1 μs. Verschil van 50–500 keer, wat cruciaal is voor hoogfrequente bewerkingen.
Werken met dart:ffi bestaat uit drie fasen: laden van de bibliotheek, declareren van handtekeningen en aanroepen van functies. Elke fase gebruikt de strikte typering van Dart, wat fouten tijdens runtime minimaliseert.
In de eerste fase wordt de dynamische bibliotheek geladen via de klasse DynamicLibrary. De bibliotheek kan op naam worden geladen (libxyz.so, libxyz.dylib, xyz.dll) of via een volledig pad. Dart zoekt automatisch naar de bibliotheek in de standaard systeempaden. DynamicLibrary biedt de methode lookupFunction, die een Dart-functie koppelt aan een C-functie op basis van de symboolnaam.
In de tweede fase wordt een Dart-functie gedeclareerd met typeannotaties die overeenkomen met de C-handtekening. Hiervoor worden speciale typen uit dart:ffi gebruikt: Int32, Float, Double, Pointer, NativeFunction, Handle en andere. De annotatie lookupFunction accepteert twee generieke parameters: het type van de Dart-functie (hoe deze er in Dart uitziet) en het type van de native C-functie (zoals gedeclareerd in C).
In de derde fase wordt de gegenereerde Dart-functie aangeroepen als een gewone functie. Argumenten worden rechtstreeks doorgegeven, het resultaat wordt onmiddellijk geretourneerd. Als de C-functie het geheugen via pointers wijzigt, kan Dart deze wijzigingen lezen via de klasse Pointer. Geheugenbeheer aan de C-kant blijft de verantwoordelijkheid van de ontwikkelaar — dart:ffi beheert geen geheugen dat is toegewezen door malloc in C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C-functiedeclaratie: 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
}
In dit voorbeeld is add een C-functie die twee int ontvangt en een int retourneert. typedef AddNative beschrijft de C-handtekening met dart:ffi-typen, en AddDart — hoe deze functie er in Dart uitziet. lookupFunction koppelt ze en retourneert een Dart-functie die als een gewone kan worden aangeroepen.
dart:ffi biedt een reeks typen die overeenkomen met C-typen. Elk type heeft een vaste grootte en conversieregels tussen Dart en C. Inzicht in de typeovereenkomst is cruciaal voor correcte werking van FFI — een fout in de grootte of het teken van een type kan leiden tot een crash van de applicatie.
| C-type | dart:ffi-type | Dart-type | Grootte (bytes) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (pointer) |
| void* | Pointer<Void> | Pointer | 8 (pointer) |
| struct | Pointer<T> (Struct) | Pointer | afhankelijk van velden |
Voor het werken met C-strings (char*) gebruikt dart:ffi Pointer<Int8>. Conversie van Dart String naar C char* en omgekeerd wordt uitgevoerd via de methoden toNativeUtf8 (uit het ffi-pakket) en fromUtf8. Het is belangrijk om C-strings na gebruik vrij te geven via calloc.free om geheugenlekken te voorkomen.
dart:ffi ondersteunt het declareren van C-structuren als Dart-klassen die overerven van Struct. Velden van de structuur worden gedeclareerd met annotaties @Int32(), @Float(), @Array() en andere. De grootte en offset van velden worden automatisch berekend volgens de ABI van het platform. Pointer<Point> kan worden verkregen uit een C-functie die een pointer naar een structuur retourneert of worden toegewezen in Dart via calloc.
// C-structuur: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// C-functie aanroepen die Point* retourneert
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) // geheugen vrijmaken
De klasse Point erft van Struct en declareert de velden x en y met annotaties @Int32(). De gegenereerde C-code heeft exact dezelfde veldindeling in het geheugen. Pointer.ref biedt toegang tot de velden van de structuur via getters en setters.
Laten we een complexer voorbeeld bekijken — integratie met een C-bibliotheek voor het berekenen van SHA256-hash. Dit is een typische taak waarbij FFI een aanzienlijk prestatievoordeel biedt ten opzichte van Method Channel.
De OpenSSL-bibliotheek biedt de functie SHA256 die de hash van een string berekent. Via dart:ffi kunnen we deze direct aanroepen, zonder Java- of Swift-wrappers te schrijven. Dit is een voorbeeld van hoe FFI het mogelijk maakt om bestaande C-bibliotheken in Flutter te hergebruiken.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Handtekening: 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 bytes
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
}
In dit voorbeeld laadt de functie sha256 de bibliotheek libcrypto.so, vindt het symbool SHA256 en roept het aan met pointers naar invoer- en uitvoergegevens. toNativeUtf8 converteert Dart String naar C-string (wijst geheugen toe), en asTypedList maakt het mogelijk de byte-array van het resultaat te lezen. Geheugen wordt na gebruik vrijgegeven — dit is een verplichte stap om lekken te voorkomen.
Het ffi-pakket biedt de functie calloc voor het toewijzen van C-compatibel geheugen. Toegewezen geheugen moet worden vrijgegeven via calloc.free, anders treedt er een lek op. Voor automatisch geheugenbeheer kan de klasse Arena uit het ffi-pakket worden gebruikt, die al het daarin toegewezen geheugen vrijgeeft bij aanroep van arena.release(). Dit is vooral handig bij een groot aantal tijdelijke toewijzingen.
Ondanks de kracht van FFI heeft het beperkingen waarmee rekening moet worden gehouden bij het ontwerpen van de architectuur van een Flutter-applicatie. De belangrijkste beperkingen hebben betrekking op typeveiligheid, geheugenbeheer en platformcompatibiliteit.
FFI controleert typen niet tijdens runtime. Als een C-functie een pointer verwacht en er wordt een getal doorgegeven, crasht de applicatie met een segmentation fault. Het wordt aanbevolen om FFIgen te gebruiken — een tool die typeveilige Dart-wrappers genereert op basis van C-headers (.h-bestanden). FFIgen analyseert declaraties van C-functies en maakt Dart-code met correcte typen, waardoor fouten in de schrijffase worden geëlimineerd.
Namen en paden naar dynamische bibliotheken verschillen per platform: libxyz.so op Android/Linux, libxyz.dylib op iOS/macOS, xyz.dll op Windows. Voor cross-platform bibliotheken wordt conditionele compilatie gebruikt via dart:io (Platform.isAndroid, Platform.isIOS) of abstracties zoals package:ffi. Het wordt aanbevolen om een factory-methode te maken die de juiste bibliotheek voor het huidige platform retourneert.
FFI beheert geen geheugen aan de C-kant. Als een C-functie geheugen toewijst via malloc, moet dit worden vrijgegeven via free, anders treedt een lek op. In Dart is er geen garbage collector voor C-geheugen. Aanbeveling: geef geheugen altijd vrij in dezelfde methode waar het is toegewezen, of gebruik Arena voor groepsgewijze vrijgave.
FFI-aanroepen worden uitgevoerd in dezelfde thread als de Dart-code. Langdurige synchrone bewerkingen (meer dan 10 ms) blokkeren de UI-thread en veroorzaken framedrops. Voor langdurige bewerkingen moet de C-functie in een isolaat (Isolate) worden aangeroepen of moet ervoor worden gezorgd dat de C-functie werk in een achtergrondthread start en Dart via een Port of callback op de hoogte stelt.
// FFI in isolaat voor lange bewerkingen
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI-aanroep
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Het verplaatsen van een FFI-aanroep naar een isolaat garandeert dat de UI-thread niet wordt geblokkeerd. Het overbrengen van grote hoeveelheden gegevens tussen isolaten vereist echter het kopiëren van geheugen. Voor grote buffers (>10 MB) heeft het gebruik van ShareMemory of memory-mapped files de voorkeur.
Veelgestelde vragen
FFI roept C-functies direct, synchroon en zonder serialisatie aan — vertraging 0.01–0.1 μs. Method Channel werkt asynchroon via JSON-serialisatie met een vertraging van 0.5–5 ms. FFI is geschikt voor hoogwaardige bewerkingen, Method Channel voor eenvoudige platform-API-aanroepen.
Direct — nee, dart:ffi ondersteunt alleen C-functies. Om C++ aan te roepen moet een C-wrapper met extern "C" worden gemaakt (entry points die als C-symbolen worden geëxporteerd). C++-klassen vereisen een extra laag die methode-aanroepen converteert naar C-functies.
FFI ondersteunt geen uitzonderingen — als een C-functie een foutcode retourneert, moet deze handmatig worden gecontroleerd. Het wordt aanbevolen FFI-aanroepen in try-catch in Dart te wrappen en de retourcodes van C-functies te controleren. Kritieke fouten (segfault) kunnen niet worden opgevangen.
FFI werkt niet met bibliotheken die complexe initialisatie van Java (JNI) of Objective-C (Message Dispatch) vereisen. Bijvoorbeeld UIKit en Android Views zijn niet toegankelijk via FFI. Beperking houdt verband met het feit dat FFI werkt op C ABI-niveau, terwijl deze API's specifieke runtimes vereisen.
Ja, C-bibliotheken worden voor elk doelplatform afzonderlijk gecompileerd. Voor Android wordt .so gebouwd voor verschillende ABI's (armeabi-v7a, arm64-v8a, x86_64). Voor iOS — een universele .dylib (arm64). Voor Windows — .dll. Flutter verpakt automatisch de juiste versie van de bibliotheek tijdens het bouwen.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook