FFI (Foreign Function Interface) — to mechanizm języka Dart, dostarczany przez pakiet dart:ffi, który umożliwia bezpośrednie wywoływanie funkcji z natywnych bibliotek C, bez pośrednich warstw w Kotlin, Swift czy Java. Deweloper ładuje bibliotekę dynamiczną (.so na Android, .dylib na iOS, .dll na Windows), deklaruje sygnatury funkcji C i wywołuje je jak zwykłe funkcje Dart. Według Dart API Reference (2025), FFI redukuje narzut na wywołania międzyjęzykowe do 0.1 µs, co jest dziesiątki razy szybsze niż przez Method Channel.
Najważniejsze
FFI (Foreign Function Interface) — to mechanizm, który pozwala językowi programowania wywoływać funkcje napisane w innych językach. W kontekście Dart i Flutter, FFI oznacza możliwość wywoływania funkcji z bibliotek C/C++ bezpośrednio z kodu Dart, bez konieczności pisania kodu platformowego w Java (Android) czy Swift/Objective-C (iOS).
Pakiet dart:ffi pojawił się w Dart 2.12 (2021) i od tego czasu stał się kluczowym narzędziem do integracji Flutter z kodem natywnym. Przed pojawieniem się dart:ffi jedynym sposobem na wywołanie funkcji C z Dart był Method Channel — mechanizm asynchroniczny, który przesyłał komunikaty przez serializację JSON między Dart a stroną natywną. FFI działa inaczej: kod Dart bezpośrednio uzyskuje dostęp do pamięci biblioteki C, wywołując funkcje przez natywny ABI (Application Binary Interface) bez serializacji i bez przełączania kontekstu.
FFI jest szczególnie poszukiwany w scenariuszach, gdzie wydajność jest krytyczna: przetwarzanie obrazów (OpenCV), audio (FFmpeg), kryptografia (OpenSSL), uczenie maszynowe (TensorFlow Lite) i bazy danych (SQLite). We wszystkich tych przypadkach Method Channel tworzy niedopuszczalne opóźnienia, a FFI zapewnia wydajność porównywalną z natywnym kodem C/C++. Biblioteka dart:ffi obsługuje również pracę z pamięcią: alokację, zwalnianie i zarządzanie wskaźnikami.
Method Channel działa asynchronicznie: Dart wysyła komunikat do kodu natywnego, kod natywny przetwarza go i odsyła wynik z powrotem. Każde wywołanie wymaga serializacji argumentów do Map, przesłania przez kolejkę i deserializacji. Zajmuje to 0.5–5 ms na wywołanie. FFI działa synchronicznie i bez serializacji — wywołanie funkcji C zajmuje 0.01–0.1 µs. Różnica 50–500 razy, co jest krytyczne dla operacji o wysokiej częstotliwości.
Praca z dart:ffi składa się z trzech etapów: załadowanie biblioteki, zadeklarowanie sygnatur i wywołanie funkcji. Każdy etap wykorzystuje ścisłe typowanie Dart, co minimalizuje błędy w czasie wykonania.
Na pierwszym etapie ładowana jest biblioteka dynamiczna przez klasę DynamicLibrary. Biblioteka może być załadowana po nazwie (libxyz.so, libxyz.dylib, xyz.dll) lub po pełnej ścieżce. Dart automatycznie szuka biblioteki w standardowych ścieżkach systemu. DynamicLibrary dostarcza metodę lookupFunction, która wiąże funkcję Dart z funkcją C po nazwie symbolu.
Na drugim etapie deklarowana jest funkcja Dart z adnotacjami typów odpowiadającymi sygnaturze C. W tym celu używane są specjalne typy z dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle i inne. Adnotacja lookupFunction przyjmuje dwa parametry generyczne: typ funkcji Dart (jak będzie wyglądać w Dart) i typ natywnej funkcji C (jak jest zadeklarowana w C).
Na trzecim etapie wygenerowana funkcja Dart jest wywoływana jak zwykła funkcja. Argumenty są przekazywane bezpośrednio, wynik zwracany natychmiastowo. Jeśli funkcja C zmienia pamięć przez wskaźniki, Dart może odczytać te zmiany przez klasę Pointer. Zarządzanie pamięcią po stronie C pozostaje odpowiedzialnością dewelopera — dart:ffi nie zarządza pamięcią przydzieloną przez malloc w C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Deklaracja funkcji 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
}
W tym przykładzie add — funkcja C przyjmująca dwa int i zwracająca int. typedef AddNative opisuje sygnaturę C z typami dart:ffi, a AddDart — jak ta funkcja będzie wyglądać w Dart. lookupFunction wiąże je i zwraca funkcję Dart, którą można wywołać jak zwykłą.
dart:ffi dostarcza zestaw typów odpowiadających typom C. Każdy typ ma stały rozmiar i zasady konwersji między Dart a C. Zrozumienie zgodności typów jest krytycznie ważne dla poprawnego działania FFI — błąd w rozmiarze lub znaku typu może doprowadzić do awarii aplikacji.
| Typ C | Typ dart:ffi | Typ Dart | Rozmiar (bajty) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (wskaźnik) |
| void* | Pointer<Void> | Pointer | 8 (wskaźnik) |
| struct | Pointer<T> (Struct) | Pointer | zależy od pól |
Do pracy z łańcuchami C (char*) dart:ffi używa Pointer<Int8>. Konwersja z Dart String na C char* i odwrotnie wykonywana jest przez metody toNativeUtf8 (z pakietu ffi) i fromUtf8. Ważne jest zwalnianie łańcuchów C po użyciu przez calloc.free, aby uniknąć wycieków pamięci.
dart:ffi obsługuje deklarowanie struktur C jako klas Dart dziedziczących po Struct. Pola struktury deklarowane są z adnotacjami @Int32(), @Float(), @Array() i innymi. Rozmiar i przesunięcie pól są obliczane automatycznie zgodnie z ABI platformy. Pointer<Point> może być uzyskany z funkcji C zwracającej wskaźnik na strukturę lub przydzielony w Dart przez calloc.
// Struktura C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Wywołanie funkcji C zwracającej 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) // zwolnij pamięć
Klasa Point dziedziczy po Struct i deklaruje pola x i y z adnotacjami @Int32(). Wygenerowany kod C będzie miał dokładnie takie samo rozmieszczenie pól w pamięci. Pointer.ref zapewnia dostęp do pól struktury przez gettery i settery.
Rozważmy bardziej złożony przykład — integrację z biblioteką C do obliczania skrótu SHA256. To typowe zadanie, w którym FFI daje znaczną przewagę wydajnościową w porównaniu z Method Channel.
Biblioteka OpenSSL dostarcza funkcję SHA256, która oblicza skrót łańcucha. Przez dart:ffi możemy wywołać ją bezpośrednio, bez pisania wrapperów w Java czy Swift. To przykład jak FFI pozwala ponownie wykorzystać istniejące biblioteki C we Flutter.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Sygnatura: 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 bajty
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
}
W tym przykładzie funkcja sha256 ładuje bibliotekę libcrypto.so, znajduje symbol SHA256 i wywołuje go ze wskaźnikami na dane wejściowe i wyjściowe. toNativeUtf8 konwertuje Dart String na C-string (alokuje pamięć), a asTypedList pozwala odczytać tablicę bajtów wyniku. Pamięć jest zwalniana po użyciu — to obowiązkowy krok zapobiegający wyciekom.
Pakiet ffi dostarcza funkcję calloc do alokacji pamięci kompatybilnej z C. Przydzielona pamięć musi być zwolniona przez calloc.free, w przeciwnym razie nastąpi wyciek. Do automatycznego zarządzania pamięcią można użyć klasy Arena z pakietu ffi, która zwalnia całą przydzieloną w niej pamięć przy wywołaniu arena.release(). Jest to szczególnie wygodne przy dużej liczbie tymczasowych alokacji.
Pomimo mocy FFI, ma on ograniczenia, które należy uwzględnić przy projektowaniu architektury aplikacji Flutter. Główne ograniczenia dotyczą bezpieczeństwa typów, zarządzania pamięcią i zgodności platform.
FFI nie sprawdza typów w czasie wykonania. Jeśli funkcja C oczekuje wskaźnika, a przekazana zostanie liczba, aplikacja ulegnie awarii z segmentation fault. Zaleca się używanie FFIgen — narzędzia, które generuje bezpieczne typowo wrappery Dart na podstawie nagłówków C (plików .h). FFIgen analizuje deklaracje funkcji C i tworzy kod Dart z poprawnymi typami, co eliminuje błędy na etapie pisania kodu.
Nazwy i ścieżki do bibliotek dynamicznych różnią się na różnych platformach: libxyz.so na Android/Linux, libxyz.dylib na iOS/macOS, xyz.dll na Windows. Dla bibliotek wieloplatformowych stosuje się kompilację warunkową przez dart:io (Platform.isAndroid, Platform.isIOS) lub abstrakcje takie jak package:ffi. Zaleca się tworzenie metody fabrycznej, która zwraca właściwą bibliotekę dla bieżącej platformy.
FFI nie zarządza pamięcią po stronie C. Jeśli funkcja C alokuje pamięć przez malloc, musi być zwolniona przez free, w przeciwnym razie nastąpi wyciek. W Dart nie ma garbage collectora dla pamięci C. Zalecenie: zawsze zwalniać pamięć w tej samej metodzie, w której została przydzielona, lub używać Arena do grupowego zwalniania.
Wywołania FFI są wykonywane w tym samym wątku co kod Dart. Długotrwałe operacje synchroniczne (powyżej 10 ms) blokują wątek UI i powodują pomijanie klatek. W przypadku długotrwałych operacji należy wywoływać funkcję C w izolacie (Isolate) lub upewnić się, że funkcja C uruchamia pracę w tle i powiadamia Dart przez Port lub callback.
// FFI w izolacie dla długich operacji
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // Wywołanie FFI
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Przeniesienie wywołania FFI do izolatu gwarantuje, że wątek UI nie jest blokowany. Należy jednak pamiętać, że przesyłanie dużych ilości danych między izolatami wymaga kopiowania pamięci. Dla dużych buforów (>10 MB) preferowane jest użycie ShareMemory lub plików mapowanych w pamięci.
Często zadawane pytania
FFI wywołuje funkcje C bezpośrednio, synchronicznie i bez serializacji — opóźnienie 0.01–0.1 µs. Method Channel działa asynchronicznie przez serializację JSON z opóźnieniem 0.5–5 ms. FFI nadaje się do operacji wymagających wysokiej wydajności, Method Channel — do prostych wywołań platformowych API.
Bezpośrednio — nie, dart:ffi obsługuje tylko funkcje C. Do wywołania C++ należy utworzyć wrapper C z extern "C" (punkty wejścia eksportowane jako symbole C). Klasy C++ wymagają dodatkowej warstwy, która konwertuje wywołania metod na funkcje C.
FFI nie obsługuje wyjątków — jeśli funkcja C zwraca kod błędu, należy go sprawdzić ręcznie. Zaleca się owijanie wywołań FFI w try-catch w Dart i sprawdzanie zwracanych kodów funkcji C. Krytycznych błędów (segfault) nie da się przechwycić.
FFI nie działa z bibliotekami wymagającymi złożonej inicjalizacji Java (JNI) lub Objective-C (Message Dispatch). Na przykład UIKit i Android Views są niedostępne przez FFI. Ograniczenie wynika z tego, że FFI działa na poziomie C ABI, a te API wymagają specyficznych środowisk uruchomieniowych.
Tak, biblioteki C są kompilowane osobno dla każdej platformy docelowej. Dla Android buduje się .so dla różnych ABI (armeabi-v7a, arm64-v8a, x86_64). Dla iOS — uniwersalny .dylib (arm64). Dla Windows — .dll. Flutter automatycznie pakuje odpowiednią wersję biblioteki podczas budowania.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również