FFI: základy, Foreign Function Interface a integrace s C

Autor: IT Sectr Publikováno: 2026-06-05 Doba čtení: 10 min

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) — mechanismus přímého volání C funkcí z Dartu
  • dart:ffi poskytuje API pro načítání knihoven a deklarování signatur
  • Výkon volání přes FFI je 50–100krát vyšší než přes Method Channel
  • Typování FFI podporuje primitivní typy, struktury a ukazatele C
  • Flutter používá FFI pro integraci s nativními knihovnami: OpenCV, SQLite, FFmpeg

Co je FFI?

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

FFI vs Method Channel: zásadní rozdíl

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.

Jak funguje dart:ffi?

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.

Základní příklad: volání C funkce z Dartu

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

Datové typy v FFI

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 typdart:ffi typDart typVelikost (bajty)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (ukazatel)
void*Pointer<Void>Pointer8 (ukazatel)
structPointer<T> (Struct)Pointerzá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.

Struktury (Struct)

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.

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

Praktické příklady FFI

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.

Integrace s OpenSSL přes FFI

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.

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

Alokace a uvolňování paměti

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

Omezení a osvědčené postupy

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.

Bezpečnost

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.

Kompatibilita platforem

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.

Správa paměti

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

Výkon a vlákna

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.

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

Čím se FFI liší od Method Channelu?

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.

Lze volat C++ funkce přes FFI?

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.

Jak zpracovávat chyby v C funkcích?

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.

Jaké knihovny nelze použít přes FFI?

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

Je třeba kompilovat C knihovny pro každou platformu zvlášť?

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í

  • FFI (Foreign Function Interface) — mechanismus přímého volání C funkcí z Dartu přes dart:ffi
  • Výkon volání FFI je 50–500krát vyšší než přes Method Channel
  • Architektura zahrnuje načtení knihovny, deklarování signatur a volání funkcí
  • Datové typy dart:ffi podporuje Int32, Float, Double, Pointer, Struct a další C typy
  • Paměť na straně C je spravována ručně pomocí calloc/free nebo Arena
  • Omezení FFI: žádná runtime kontrola typů, žádná přímá podpora C++, blokuje UI vlákno
  • Použijte FFI pro vysoce výkonnou integraci s nativními knihovnami ve Flutteru

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

Prodiskutovat projekt

Přečtěte si také