FFI: podstawy, Foreign Function Interface i integracja z C

Autor: IT Sectr Opublikowano: 2026-06-05 Czas czytania: 10 min

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) — mechanizm bezpośredniego wywoływania funkcji C z Dart
  • dart:ffi dostarcza API do ładowania bibliotek i deklarowania sygnatur
  • Wydajność wywołań przez FFI jest 50–100 razy wyższa niż przez Method Channel
  • Typowanie FFI obsługuje typy prymitywne, struktury i wskaźniki C
  • Flutter używa FFI do integracji z natywnymi bibliotekami: OpenCV, SQLite, FFmpeg

Co to jest FFI?

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.

FFI vs Method Channel: zasadnicza różnica

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.

Jak działa dart:ffi?

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.

Podstawowy przykład: wywołanie funkcji C z Dart

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

Typy danych w FFI

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 CTyp dart:ffiTyp DartRozmiar (bajty)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (wskaźnik)
void*Pointer<Void>Pointer8 (wskaźnik)
structPointer<T> (Struct)Pointerzależ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.

Struktury (Struct)

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.

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

Praktyczne przykłady FFI

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.

Integracja z OpenSSL przez FFI

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.

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

Alokacja i zwalnianie pamięci

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.

Ograniczenia i najlepsze praktyki

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.

Bezpieczeństwo

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.

Zgodność platformowa

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.

Zarządzanie pamięcią

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.

Wydajność i wątki

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.

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

Czym FFI różni się od Method Channel?

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.

Czy można wywoływać funkcje C++ przez FFI?

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.

Jak obsługiwać błędy w funkcjach 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ć.

Jakich bibliotek nie można używać przez FFI?

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.

Czy trzeba kompilować biblioteki C dla każdej platformy?

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

  • FFI (Foreign Function Interface) — mechanizm bezpośredniego wywoływania funkcji C z Dart przez dart:ffi
  • Wydajność wywołań FFI jest 50–500 razy wyższa niż przez Method Channel
  • Architektura obejmuje ładowanie biblioteki, deklarowanie sygnatur i wywoływanie funkcji
  • Typy danych dart:ffi obsługują Int32, Float, Double, Pointer, Struct i inne typy C
  • Pamięć po stronie C jest zarządzana ręcznie przez calloc/free lub Arena
  • Ograniczenia FFI: brak sprawdzania typów w runtime, brak bezpośredniego wsparcia C++, blokuje wątek UI
  • Używaj FFI do wysokowydajnej integracji z natywnymi bibliotekami we Flutter

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.

Omów projekt

Przeczytaj również