FFI (Foreign Function Interface) — je mechanismus jazyka Dart, poskytovaný balíčkem dart:ffi, který umožňuje přímé volání funkcí z nativních C knihoven bez mezivrstev v Kotlin, Swift nebo Javě. Vývojář načte dynamickou knihovnu (.so na Androidu, .dylib na iOS, .dll na Windows), deklaruje signatury C funkcí a volá je jako běžné Dart funkce. Podle Dart API Reference (2025) snižuje FFI režii mezijazykových volání na 0.1 μs, což je desítkykrát rychlejší než přes Method Channel.
Hlavní body
FFI (Foreign Function Interface) — je mechanismus, který umožňuje programovacímu jazyku volat funkce napsané v jiných jazycích. V kontextu Dartu a Flutteru FFI znamená možnost volat funkce z C/C++ knihoven přímo z Dart kódu, bez nutnosti psát platformní kód v Javě (Android) nebo Swift/Objective-C (iOS).
Balíček dart:ffi se objevil v Dart 2.12 (2021) a od té doby se stal klíčovým nástrojem pro integraci Flutteru s nativním kódem. Před příchodem dart:ffi byl jediným způsobem, jak zavolat C funkci z Dartu, Method Channel — asynchronní mechanismus, který přenášel zprávy přes JSON serializaci mezi Dartem a nativní stranou. FFI funguje jinak: Dart kód přímo přistupuje k paměti C knihovny a volá funkce prostřednictvím nativního ABI (Application Binary Interface) bez serializace a bez přepínání kontextu.
FFI je obzvláště žádané ve scénářích, kde je výkon kritický: zpracování obrazu (OpenCV), audio (FFmpeg), kryptografie (OpenSSL), strojové učení (TensorFlow Lite) a databáze (SQLite). Ve všech těchto případech Method Channel vytváří nepřijatelná zpoždění, zatímco FFI poskytuje výkon srovnatelný s nativním C/C++ kódem. Knihovna dart:ffi také podporuje práci s pamětí: alokaci, uvolňování a správu ukazatelů.
Method Channel funguje asynchronně: Dart odešle zprávu nativnímu kódu, nativní kód ji zpracuje a odešle výsledek zpět. Každé volání vyžaduje serializaci argumentů do Map, přenos přes frontu a deserializaci. To trvá 0.5–5 ms na volání. FFI funguje synchronně a bez serializace — volání C funkce trvá 0.01–0.1 μs. Rozdíl 50–500krát, což je kritické pro vysocefrekvenční operace.
Práce s dart:ffi se skládá ze tří fází: načtení knihovny, deklarování signatur a volání funkcí. Každá fáze využívá striktní typování Dartu, což minimalizuje chyby za běhu.
V první fázi se dynamická knihovna načítá prostřednictvím třídy DynamicLibrary. Knihovnu lze načíst podle názvu (libxyz.so, libxyz.dylib, xyz.dll) nebo podle úplné cesty. Dart automaticky hledá knihovnu ve standardních systémových cestách. DynamicLibrary poskytuje metodu lookupFunction, která propojuje Dart funkci s C funkcí podle názvu symbolu.
Ve druhé fázi se deklaruje Dart funkce s typovými anotacemi odpovídajícími C signatuře. K tomu se používají speciální typy z dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle a další. Anotace lookupFunction přijímá dva generické parametry: typ Dart funkce (jak bude vypadat v Dartu) a typ nativní C funkce (jak je deklarována v C).
Ve třetí fázi se vygenerovaná Dart funkce volá jako běžná funkce. Argumenty se předávají přímo, výsledek se vrací okamžitě. Pokud C funkce mění paměť prostřednictvím ukazatelů, Dart může tyto změny číst pomocí třídy Pointer. Správa paměti na straně C zůstává odpovědností vývojáře — dart:ffi nespravuje paměť alokovanou malloc v C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Deklarace C funkce: 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
}
V tomto příkladu je add C funkce, která přijímá dva int a vrací int. typedef AddNative popisuje C signaturu s typy dart:ffi, a AddDart — jak bude tato funkce vypadat v Dartu. lookupFunction je propojuje a vrací Dart funkci, kterou lze volat jako běžnou.
dart:ffi poskytuje sadu typů odpovídajících typům C. Každý typ má pevnou velikost a pravidla převodu mezi Dartem a C. Pochopení shody typů je kritické pro správnou funkci FFI — chyba ve velikosti nebo znaménku typu může vést k pádu aplikace.
| C typ | dart:ffi typ | Dart typ | Velikost (bajty) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (ukazatel) |
| void* | Pointer<Void> | Pointer | 8 (ukazatel) |
| struct | Pointer<T> (Struct) | Pointer | záleží na polích |
Pro práci s C řetězci (char*) používá dart:ffi Pointer<Int8>. Převod z Dart String na C char* a naopak se provádí pomocí metod toNativeUtf8 (z balíčku ffi) a fromUtf8. Je důležité uvolňovat C řetězce po použití pomocí calloc.free, aby se předešlo únikům paměti.
dart:ffi podporuje deklarování C struktur jako Dart tříd dědících ze Struct. Pole struktury se deklarují s anotacemi @Int32(), @Float(), @Array() a dalšími. Velikost a posun polí se počítají automaticky podle ABI platformy. Pointer<Point> lze získat z C funkce vracející ukazatel na strukturu nebo alokovat v Dartu pomocí calloc.
// C struktura: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Volání C funkce vracející 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) // uvolnit paměť
Třída Point dědí ze Struct a deklaruje pole x a y s anotacemi @Int32(). Generovaný C kód bude mít přesně stejné rozložení polí v paměti. Pointer.ref poskytuje přístup k polím struktury pomocí getterů a setterů.
Podívejme se na složitější příklad — integraci s C knihovnou pro výpočet hash SHA256. To je typický úkol, kde FFI poskytuje významnou výkonnostní výhodu oproti Method Channelu.
Knihovna OpenSSL poskytuje funkci SHA256, která vypočítá hash řetězce. Přes dart:ffi ji můžeme zavolat přímo, bez psaní Java nebo Swift wrapperů. To je příklad, jak FFI umožňuje znovupoužití stávajících C knihoven ve Flutteru.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Signatura: 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 bajtů
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
}
V tomto příkladu funkce sha256 načte knihovnu libcrypto.so, najde symbol SHA256 a zavolá jej s ukazateli na vstupní a výstupní data. toNativeUtf8 převede Dart String na C řetězec (alokuje paměť) a asTypedList umožňuje číst pole bajtů výsledku. Paměť se po použití uvolní — to je povinný krok k zabránění únikům.
Balíček ffi poskytuje funkci calloc pro alokaci paměti kompatibilní s C. Alokovaná paměť musí být uvolněna pomocí calloc.free, jinak dojde k úniku. Pro automatickou správu paměti lze použít třídu Arena z balíčku ffi, která uvolní veškerou v ní alokovanou paměť při volání arena.release(). To je obzvláště výhodné při velkém počtu dočasných alokací.
Navzdory síle FFI má omezení, která je třeba zvážit při navrhování architektury Flutter aplikace. Hlavní omezení se týkají typové bezpečnosti, správy paměti a kompatibility platforem.
FFI nekontroluje typy za běhu. Pokud C funkce očekává ukazatel, ale předá se číslo, aplikace spadne s segmentation fault. Doporučuje se používat FFIgen — nástroj, který generuje typově bezpečné Dart wravery na základě C hlaviček (.h souborů). FFIgen analyzuje deklarace C funkcí a vytváří Dart kód se správnými typy, což eliminuje chyby ve fázi psaní kódu.
Názvy a cesty k dynamickým knihovnám se liší na různých platformách: libxyz.so na Android/Linux, libxyz.dylib na iOS/macOS, xyz.dll na Windows. Pro multiplatformní knihovny se používá podmíněný překlad přes dart:io (Platform.isAndroid, Platform.isIOS) nebo abstrakce jako package:ffi. Doporučuje se vytvořit tovární metodu, která vrací správnou knihovnu pro aktuální platformu.
FFI nespravuje paměť na straně C. Pokud C funkce alokuje paměť přes malloc, musí být uvolněna přes free, jinak dojde k úniku. V Dartu neexistuje garbage collector pro C paměť. Doporučení: vždy uvolňujte paměť ve stejné metodě, kde byla alokována, nebo použijte Arena pro skupinové uvolnění.
Volání FFI se provádějí ve stejném vlákně jako Dart kód. Dlouhé synchronní operace (více než 10 ms) blokují UI vlákno a způsobují vypadávání snímků. Pro dlouhé operace je třeba zavolat C funkci v izolátu (Isolate) nebo zajistit, že C funkce spustí práci na pozadí a upozorní Dart přes Port nebo callback.
// FFI v izolátu pro dlouhé operace
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // Volání FFI
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Přesun volání FFI do izolátu zaručuje, že UI vlákno není blokováno. Přenos velkých objemů dat mezi izoláty však vyžaduje kopírování paměti. Pro velké buffery (>10 MB) je vhodnější použít ShareMemory nebo soubory mapované do paměti.
Často kladené otázky
FFI volá C funkce přímo, synchronně a bez serializace — zpoždění 0.01–0.1 μs. Method Channel funguje asynchronně přes JSON serializaci se zpožděním 0.5–5 ms. FFI je vhodné pro vysoce výkonné operace, Method Channel pro jednoduchá volání platformních API.
Přímo — ne, dart:ffi podporuje pouze C funkce. Pro volání C++ je třeba vytvořit C wrapper s extern "C" (vstupní body exportované jako C symboly). C++ třídy vyžadují další vrstvu, která převádí volání metod na C funkce.
FFI nepodporuje výjimky — pokud C funkce vrací chybový kód, je třeba jej zkontrolovat ručně. Doporučuje se obalovat FFI volání do try-catch v Dartu a kontrolovat návratové kódy C funkcí. Kritické chyby (segfault) nelze zachytit.
FFI nefunguje s knihovnami vyžadujícími složitou inicializaci Java (JNI) nebo Objective-C (Message Dispatch). Například UIKit a Android Views nejsou přes FFI dostupné. Omezení souvisí s tím, že FFI pracuje na úrovni C ABI, zatímco tato API vyžadují specifická běhová prostředí.
Ano, C knihovny se kompilují zvlášť pro každou cílovou platformu. Pro Android se .sestavuje .so pro různé ABI (armeabi-v7a, arm64-v8a, x86_64). Pro iOS — univerzální .dylib (arm64). Pro Windows — .dll. Flutter automaticky zabalí správnou verzi knihovny při sestavování.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.