FFI: bazele, Foreign Function Interface și integrarea cu C

Autor: IT Sectr Publicat: 2026-06-05 Timp de citire: 10 min

FFI (Foreign Function Interface) — este mecanismul limbajului Dart, furnizat de pachetul dart:ffi, care permite apelarea directă a funcțiilor din bibliotecile C native, fără straturi intermediare în Kotlin, Swift sau Java. Dezvoltatorul încarcă o bibliotecă dinamică (.so pe Android, .dylib pe iOS, .dll pe Windows), declară semnăturile funcțiilor C și le apelează ca pe funcții obișnuite Dart. Conform Dart API Reference (2025), FFI reduce costul apelurilor inter-lingvistice la 0.1 μs, ceea ce este de zeci de ori mai rapid decât prin Method Channel.

Principalele puncte

  • FFI (Foreign Function Interface) — mecanism de apelare directă a funcțiilor C din Dart
  • dart:ffi oferă API pentru încărcarea bibliotecilor și declararea semnăturilor
  • Performanța apelurilor prin FFI este de 50–100 de ori mai mare decât prin Method Channel
  • Tipizarea FFI suportă tipuri primitive, structuri și pointeri C
  • Flutter folosește FFI pentru integrarea cu biblioteci native: OpenCV, SQLite, FFmpeg

Ce este FFI?

FFI (Foreign Function Interface) — este un mecanism care permite unui limbaj de programare să apeleze funcții scrise în alte limbaje. În contextul Dart și Flutter, FFI înseamnă posibilitatea de a apela funcții din bibliotecile C/C++ direct din codul Dart, fără a fi nevoie să scrieți cod de platformă în Java (Android) sau Swift/Objective-C (iOS).

Pachetul dart:ffi a apărut în Dart 2.12 (2021) și de atunci a devenit un instrument cheie pentru integrarea Flutter cu codul nativ. Înainte de apariția dart:ffi, singura modalitate de a apela o funcție C din Dart era Method Channel — un mecanism asincron care transmitea mesaje prin serializare JSON între Dart și partea nativă. FFI funcționează diferit: codul Dart accesează direct memoria bibliotecii C, apelând funcții prin ABI nativ (Application Binary Interface) fără serializare și fără comutare de context.

FFI este deosebit de căutat în scenariile în care performanța este critică: procesarea imaginilor (OpenCV), audio (FFmpeg), criptografia (OpenSSL), învățarea automată (TensorFlow Lite) și bazele de date (SQLite). În toate aceste cazuri, Method Channel creează întârzieri inacceptabile, iar FFI oferă performanță comparabilă cu codul C/C++ nativ. Biblioteca dart:ffi suportă, de asemenea, lucrul cu memoria: alocare, eliberare și gestionarea pointerilor.

FFI vs Method Channel: diferența fundamentală

Method Channel funcționează asincron: Dart trimite un mesaj către codul nativ, codul nativ îl procesează și trimite rezultatul înapoi. Fiecare apel necesită serializarea argumentelor în Map, transmiterea prin coadă și deserializarea. Aceasta durează 0.5–5 ms per apel. FFI funcționează sincron și fără serializare — apelul unei funcții C durează 0.01–0.1 μs. Diferență de 50–500 de ori, ceea ce este critic pentru operațiile de înaltă frecvență.

Cum funcționează dart:ffi?

Lucrul cu dart:ffi constă în trei etape: încărcarea bibliotecii, declararea semnăturilor și apelarea funcțiilor. Fiecare etapă folosește tipizarea strictă a Dart, ceea ce minimizează erorile în timpul execuției.

În prima etapă, biblioteca dinamică este încărcată prin clasa DynamicLibrary. Biblioteca poate fi încărcată după nume (libxyz.so, libxyz.dylib, xyz.dll) sau după calea completă. Dart caută automat biblioteca în căile standard ale sistemului. DynamicLibrary oferă metoda lookupFunction, care leagă o funcție Dart de o funcție C după numele simbolului.

În a doua etapă, se declară o funcție Dart cu adnotări de tip corespunzătoare semnăturii C. Pentru aceasta se folosesc tipuri speciale din dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle și altele. Adnotarea lookupFunction primește doi parametri generici: tipul funcției Dart (cum va arăta în Dart) și tipul funcției C native (cum este declarată în C).

În a treia etapă, funcția Dart generată este apelată ca o funcție obișnuită. Argumentele sunt transmise direct, rezultatul este returnat imediat. Dacă funcția C modifică memoria prin pointeri, Dart poate citi aceste modificări prin clasa Pointer. Gestionarea memoriei din partea C rămâne responsabilitatea dezvoltatorului — dart:ffi nu gestionează memoria alocată prin malloc în C.

Exemplu de bază: apelarea unei funcții C din Dart

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Declarația funcției 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
}

În acest exemplu, add este o funcție C care primește doi int și returnează int. typedef AddNative descrie semnătura C cu tipuri dart:ffi, iar AddDart — cum va arăta această funcție în Dart. lookupFunction le leagă și returnează o funcție Dart care poate fi apelată ca una obișnuită.

Tipuri de date în FFI

dart:ffi oferă un set de tipuri corespunzătoare tipurilor C. Fiecare tip are o dimensiune fixă și reguli de conversie între Dart și C. Înțelegerea corespondenței tipurilor este critică pentru funcționarea corectă a FFI — o eroare în dimensiunea sau semnul tipului poate duce la prăbușirea aplicației.

Tip CTip dart:ffiTip DartDimensiune (octeți)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (pointer)
void*Pointer<Void>Pointer8 (pointer)
structPointer<T> (Struct)Pointerdepinde de câmpuri

Pentru lucrul cu șiruri C (char*) dart:ffi folosește Pointer<Int8>. Conversia din Dart String în char* C și invers se realizează prin metodele toNativeUtf8 (din pachetul ffi) și fromUtf8. Este important să eliberați șirurile C după utilizare prin calloc.free pentru a evita scurgerile de memorie.

Structuri (Struct)

dart:ffi suportă declararea structurilor C ca clase Dart care moștenesc Struct. Câmpurile structurii se declară cu adnotări @Int32(), @Float(), @Array() și altele. Dimensiunea și deplasarea câmpurilor sunt calculate automat conform ABI al platformei. Pointer<Point> poate fi obținut dintr-o funcție C care returnează un pointer la structură sau alocat în Dart prin calloc.

dart
// Structura C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// Apelarea funcției C care returnează 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) // eliberează memoria

Clasa Point moștenește Struct și declară câmpurile x și y cu adnotări @Int32(). Codul C generat va avea exact aceeași dispunere a câmpurilor în memorie. Pointer.ref oferă acces la câmpurile structurii prin gettere și settere.

Exemple practice FFI

Să analizăm un exemplu mai complex — integrarea cu o bibliotecă C pentru calculul hash-ului SHA256. Aceasta este o sarcină tipică în care FFI oferă un avantaj semnificativ de performanță față de Method Channel.

Integrarea cu OpenSSL prin FFI

Biblioteca OpenSSL oferă funcția SHA256 care calculează hash-ul unui șir. Prin dart:ffi o putem apela direct, fără a scrie wrapper-e Java sau Swift. Acesta este un exemplu al modului în care FFI permite reutilizarea bibliotecilor C existente în Flutter.

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Semnătura: 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 octeți

    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
}

În acest exemplu, funcția sha256 încarcă biblioteca libcrypto.so, găsește simbolul SHA256 și îl apelează cu pointeri către datele de intrare și ieșire. toNativeUtf8 convertește Dart String în șir C (alocă memorie), iar asTypedList permite citirea tabloului de octeți al rezultatului. Memoria este eliberată după utilizare — acesta este un pas obligatoriu pentru prevenirea scurgerilor.

Alocarea și eliberarea memoriei

Pachetul ffi oferă funcția calloc pentru alocarea memoriei compatibile cu C. Memoria alocată trebuie eliberată prin calloc.free, altfel va apărea o scurgere. Pentru gestionarea automată a memoriei se poate folosi clasa Arena din pachetul ffi, care eliberează toată memoria alocată în ea la apelul arena.release(). Acest lucru este deosebit de convenabil atunci când există un număr mare de alocări temporare.

Limitări și bune practici

În ciuda puterii FFI, acesta are limitări care trebuie luate în considerare la proiectarea arhitecturii aplicației Flutter. Principalele limitări sunt legate de siguranța tipurilor, gestionarea memoriei și compatibilitatea platformelor.

Siguranță

FFI nu verifică tipurile în timpul execuției. Dacă o funcție C așteaptă un pointer, iar se transmite un număr, aplicația se va prăbuși cu segmentation fault. Se recomandă utilizarea FFIgen — un instrument care generează wrapper-e Dart sigure din punct de vedere al tipurilor pe baza header-elor C (fișiere .h). FFIgen analizează declarațiile funcțiilor C și creează cod Dart cu tipuri corecte, ceea ce elimină erorile în etapa de scriere a codului.

Compatibilitatea platformelor

Numele și căile către bibliotecile dinamice diferă pe diferite platforme: libxyz.so pe Android/Linux, libxyz.dylib pe iOS/macOS, xyz.dll pe Windows. Pentru bibliotecile cross-platform se folosește compilarea condiționată prin dart:io (Platform.isAndroid, Platform.isIOS) sau abstraciții precum package:ffi. Se recomandă crearea unei metode factory care returnează biblioteca corectă pentru platforma curentă.

Gestionarea memoriei

FFI nu gestionează memoria din partea C. Dacă o funcție C alocă memorie prin malloc, aceasta trebuie eliberată prin free, altfel va apărea o scurgere. În Dart nu există un colector de gunoi pentru memoria C. Recomandare: eliberați întotdeauna memoria în aceeași metodă în care a fost alocată sau folosiți Arena pentru eliberarea în grup.

Performanță și fire de execuție

Apelurile FFI se execută în același fir de execuție ca și codul Dart. Operațiile sincrone lungi (peste 10 ms) blochează firul UI și provoacă omiterea cadrelor. Pentru operațiile lungi, trebuie să apelați funcția C într-un izolat (Isolate) sau să vă asigurați că funcția C rulează într-un fir de fundal și notifică Dart prin Port sau callback.

dart
// FFI în izolat pentru operații lungi
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // Apel FFI
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

Mutarea apelului FFI într-un izolat garantează că firul UI nu este blocat. Cu toate acestea, transmiterea unor volume mari de date între izolate necesită copierea memoriei. Pentru buffere mari (>10 MB) este preferabil să se utilizeze ShareMemory sau fișiere mapate în memorie.

Întrebări frecvente

Cu ce se deosebește FFI de Method Channel?

FFI apelează funcțiile C direct, sincron și fără serializare — întârziere 0.01–0.1 μs. Method Channel funcționează asincron prin serializare JSON cu întârziere 0.5–5 ms. FFI este potrivit pentru operații de înaltă performanță, Method Channel — pentru apeluri simple ale API-urilor de platformă.

Se pot apela funcții C++ prin FFI?

Direct — nu, dart:ffi suportă doar funcții C. Pentru a apela C++ trebuie creat un wrapper C cu extern "C" (puncte de intrare care sunt exportate ca simboluri C). Clasele C++ necesită un strat suplimentar care convertește apelurile de metode în funcții C.

Cum se gestionează erorile în funcțiile C?

FFI nu suportă excepții — dacă o funcție C returnează un cod de eroare, acesta trebuie verificat manual. Se recomandă înfășurarea apelurilor FFI în try-catch în Dart și verificarea codurilor returnate de funcțiile C. Erorile critice (segfault) nu pot fi prinse.

Ce biblioteci nu pot fi utilizate prin FFI?

FFI nu funcționează cu bibliotecile care necesită inițializare complexă Java (JNI) sau Objective-C (Message Dispatch). De exemplu, UIKit și Android Views nu sunt accesibile prin FFI. Limitarea se datorează faptului că FFI funcționează la nivelul C ABI, iar aceste API-uri necesită runtime-uri specifice.

Trebuie să compilez bibliotecile C pentru fiecare platformă separat?

Da, bibliotecile C se compilează separat pentru fiecare platformă țintă. Pentru Android se construiește .so pentru diferite ABI-uri (armeabi-v7a, arm64-v8a, x86_64). Pentru iOS — un .dylib universal (arm64). Pentru Windows — .dll. Flutter împachetează automat versiunea corectă a bibliotecii la compilare.

Concluzii

  • FFI (Foreign Function Interface) — mecanism de apelare directă a funcțiilor C din Dart prin dart:ffi
  • Performanța apelurilor FFI este de 50–500 de ori mai mare decât prin Method Channel
  • Arhitectura include încărcarea bibliotecii, declararea semnăturilor și apelarea funcțiilor
  • Tipurile de date dart:ffi suportă Int32, Float, Double, Pointer, Struct și alte tipuri C
  • Memoria din partea C este gestionată manual prin calloc/free sau Arena
  • Limitări FFI: fără verificare tipurilor la runtime, fără suport direct pentru C++, blochează firul UI
  • Folosiți FFI pentru integrarea de înaltă performanță cu biblioteci native în Flutter

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și