FFI (Foreign Function Interface) — är en mekanism i språket Dart, som tillhandahålls av paketet dart:ffi, som möjliggör direktanrop av funktioner från nativa C-bibliotek utan mellanliggande lager i Kotlin, Swift eller Java. Utvecklaren laddar ett dynamiskt bibliotek (.so på Android, .dylib på iOS, .dll på Windows), deklarerar signaturer för C-funktioner och anropar dem som vanliga Dart-funktioner. Enligt Dart API Reference (2025) minskar FFI overhead för språköverskridande anrop till 0.1 μs, vilket är tiotals gånger snabbare än via Method Channel.
Huvudpunkter
FFI (Foreign Function Interface) — är en mekanism som gör att ett programmeringsspråk kan anropa funktioner skrivna på andra språk. I sammanhanget Dart och Flutter innebär FFI möjligheten att anropa funktioner från C/C++-bibliotek direkt från Dart-kod, utan att behöva skriva plattformskod i Java (Android) eller Swift/Objective-C (iOS).
Paketet dart:ffi dök upp i Dart 2.12 (2021) och har sedan dess blivit ett nyckelverktyg för att integrera Flutter med natív kod. Före dart:ffi var det enda sättet att anropa en C-funktion från Dart Method Channel — en asynkron mekanism som överförde meddelanden via JSON-serialisering mellan Dart och den natíva sidan. FFI fungerar annorlunda: Dart-kod får direkt åtkomst till C-bibliotekets minne och anropar funktioner via nativt ABI (Application Binary Interface) utan serialisering och utan kontextbyte.
FFI är särskilt efterfrågat i scenarier där prestanda är kritisk: bildbehandling (OpenCV), ljud (FFmpeg), kryptering (OpenSSL), maskininlärning (TensorFlow Lite) och databaser (SQLite). I alla dessa fall skapar Method Channel oacceptabla fördröjningar, medan FFI ger prestanda jämförbar med nativ C/C++-kod. Biblioteket dart:ffi stöder också arbete med minne: allokering, frigöring och hantering av pekare.
Method Channel fungerar asynkront: Dart skickar ett meddelande till den natíva koden, den natíva koden bearbetar det och skickar tillbaka resultatet. Varje anrop kräver serialisering av argument till Map, överföring via kö och deserialisering. Detta tar 0.5–5 ms per anrop. FFI fungerar synkront och utan serialisering — ett C-funktionsanrop tar 0.01–0.1 μs. Skillnad på 50–500 gånger, vilket är kritiskt för högfrekventa operationer.
Arbete med dart:ffi består av tre steg: ladda biblioteket, deklarera signaturer och anropa funktioner. Varje steg använder Darts strikta typning, vilket minimerar fel under körning.
I det första steget laddas det dynamiska biblioteket via klassen DynamicLibrary. Biblioteket kan laddas efter namn (libxyz.so, libxyz.dylib, xyz.dll) eller efter fullständig sökväg. Dart söker automatiskt efter biblioteket på systemets standardsökvägar. DynamicLibrary tillhandahåller metoden lookupFunction, som kopplar en Dart-funktion till en C-funktion baserat på symbolnamnet.
I det andra steget deklareras en Dart-funktion med typannoteringar som motsvarar C-signaturen. För detta används speciella typer från dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle och andra. Annoteringen lookupFunction tar två generiska parametrar: typen av Dart-funktionen (hur den kommer att se ut i Dart) och typen av den natíva C-funktionen (hur den är deklarerad i C).
I det tredje steget anropas den genererade Dart-funktionen som en vanlig funktion. Argument skickas direkt, resultatet returneras omedelbart. Om C-funktionen ändrar minne via pekare kan Dart läsa dessa ändringar via klassen Pointer. Minnesshantering på C-sidan förblir utvecklarens ansvar — dart:ffi hanterar inte minne som allokerats av malloc i C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C-funktionsdeklaration: 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
}
I detta exempel är add en C-funktion som tar två int och returnerar int. typedef AddNative beskriver C-signaturen med dart:ffi-typer, och AddDart — hur denna funktion kommer att se ut i Dart. lookupFunction kopplar dem och returnerar en Dart-funktion som kan anropas som en vanlig.
dart:ffi tillhandahåller en uppsättning typer som motsvarar C-typer. Varje typ har en fast storlek och konverteringsregler mellan Dart och C. Att förstå typernas överensstämmelse är avgörande för korrekt funktion av FFI — ett fel i typens storlek eller tecken kan leda till att applikationen kraschar.
| C-typ | dart:ffi-typ | Dart-typ | Storlek (byte) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (pekare) |
| void* | Pointer<Void> | Pointer | 8 (pekare) |
| struct | Pointer<T> (Struct) | Pointer | beror på fält |
För arbete med C-strängar (char*) använder dart:ffi Pointer<Int8>. Konvertering från Dart String till C char* och vice versa utförs via metoderna toNativeUtf8 (från paketet ffi) och fromUtf8. Det är viktigt att frigöra C-strängar efter användning via calloc.free för att undvika minnesläckor.
dart:ffi stöder deklaration av C-strukturer som Dart-klasser som ärver Struct. Strukturens fält deklareras med annoteringar @Int32(), @Float(), @Array() och andra. Storlek och förskjutning av fält beräknas automatiskt enligt plattformens ABI. Pointer<Point> kan erhållas från en C-funktion som returnerar en pekare till en struktur eller allokeras i Dart via calloc.
// C-struktur: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Anropa C-funktion som returnerar Point*
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) // frigör minne
Klassen Point ärver Struct och deklarerar fälten x och y med annoteringar @Int32(). Den genererade C-koden kommer att ha exakt samma fältlayout i minnet. Pointer.ref ger åtkomst till strukturens fält via getters och setters.
Låt oss titta på ett mer komplext exempel — integrering med ett C-bibliotek för att beräkna SHA256-hash. Detta är en typisk uppgift där FFI ger en betydande prestandafördel jämfört med Method Channel.
OpenSSL-biblioteket tillhandahåller funktionen SHA256 som beräknar hash av en sträng. Via dart:ffi kan vi anropa den direkt, utan att skriva Java- eller Swift-wrapper. Detta är ett exempel på hur FFI möjliggör återanvändning av befintliga C-bibliotek i Flutter.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Signatur: 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 byte
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
}
I detta exempel laddar funktionen sha256 biblioteket libcrypto.so, hittar symbolen SHA256 och anropar den med pekare till indata och utdata. toNativeUtf8 konverterar Dart String till C-sträng (allokerar minne), och asTypedList möjliggör läsning av resultatets byte-array. Minne frigörs efter användning — detta är ett obligatoriskt steg för att förhindra läckor.
Paketet ffi tillhandahåller funktionen calloc för allokering av C-kompatibelt minne. Allokerat minne måste frigöras via calloc.free, annars uppstår en läcka. För automatisk minneshantering kan klassen Arena från paketet ffi användas, som frigör allt minne som allokerats i den vid anrop av arena.release(). Detta är särskilt praktiskt vid ett stort antal temporära allokeringar.
Trots FFI:s styrka har det begränsningar som måste beaktas vid utformning av arkitekturen för en Flutter-applikation. De främsta begränsningarna rör typsäkerhet, minneshantering och plattformskompatibilitet.
FFI kontrollerar inte typer under körning. Om en C-funktion förväntar sig en pekare men ett tal skickas, kraschar applikationen med segmentation fault. Det rekommenderas att använda FFIgen — ett verktyg som genererar typsäkra Dart-wrapper baserat på C-huvudfiler (.h-filer). FFIgen analyserar deklarationer av C-funktioner och skapar Dart-kod med korrekta typer, vilket eliminerar fel i kodningsfasen.
Namn och sökvägar till dynamiska bibliotek skiljer sig på olika plattformar: libxyz.so på Android/Linux, libxyz.dylib på iOS/macOS, xyz.dll på Windows. För plattformsoberoende bibliotek används villkorlig kompilering via dart:io (Platform.isAndroid, Platform.isIOS) eller abstraktioner som package:ffi. Det rekommenderas att skapa en fabriksmetod som returnerar rätt bibliotek för den aktuella plattformen.
FFI hanterar inte minne på C-sidan. Om en C-funktion allokerar minne via malloc måste det frigöras via free, annars uppstår en läcka. I Dart finns ingen sophämtare för C-minne. Rekommendation: frigör alltid minne i samma metod där det allokerades eller använd Arena för gruppfrigöring.
FFI-anrop körs i samma tråd som Dart-koden. Långa synkrona operationer (mer än 10 ms) blockerar UI-tråden och orsakar bildrutebortfall. För långa operationer bör C-funktionen anropas i en isolat (Isolate) eller säkerställas att C-funktionen startar arbete i en bakgrundstråd och meddelar Dart via en Port eller callback.
// FFI i isolat för långa operationer
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI-anrop
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Att flytta ett FFI-anrop till en isolat garanterar att UI-tråden inte blockeras. Överföring av stora datamängder mellan isolat kräver dock kopiering av minne. För stora buffertar (>10 MB) är det bättre att använda ShareMemory eller minnesmappade filer.
Vanliga frågor
FFI anropar C-funktioner direkt, synkront och utan serialisering — fördröjning 0.01–0.1 μs. Method Channel fungerar asynkront via JSON-serialisering med en fördröjning på 0.5–5 ms. FFI är lämpligt för högpresterande operationer, Method Channel för enkla plattforms-API-anrop.
Direkt — nej, dart:ffi stöder endast C-funktioner. För att anropa C++ måste en C-wrapper skapas med extern "C" (inkopplingspunkter som exporteras som C-symboler). C++-klasser kräver ett extra lager som konverterar metodanrop till C-funktioner.
FFI stöder inte undantag — om en C-funktion returnerar en felkod måste den kontrolleras manuellt. Det rekommenderas att omsluta FFI-anrop i try-catch i Dart och kontrollera returkoder från C-funktioner. Kritiska fel (segfault) kan inte fångas.
FFI fungerar inte med bibliotek som kräver komplex initiering av Java (JNI) eller Objective-C (Message Dispatch). Till exempel är UIKit och Android Views inte tillgängliga via FFI. Begränsningen beror på att FFI arbetar på C ABI-nivå, medan dessa API:er kräver specifika körningar.
Ja, C-bibliotek kompileras separat för varje målplattform. För Android byggs .so för olika ABI (armeabi-v7a, arm64-v8a, x86_64). För iOS — en universell .dylib (arm64). För Windows — .dll. Flutter paketerar automatiskt rätt version av biblioteket vid bygge.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.