FFI (Foreign Function Interface) è un meccanismo del linguaggio Dart, fornito dal pacchetto dart:ffi, che consente di chiamare funzioni da librerie native C direttamente, senza livelli intermedi in Kotlin, Swift o Java. Lo sviluppatore carica una libreria dinamica (.so su Android, .dylib su iOS, .dll su Windows), dichiara le firme delle funzioni C e le chiama come normali funzioni Dart. Secondo il Riferimento API Dart (2025), FFI riduce l'overhead delle chiamate tra linguaggi a 0,1 µs, decine di volte più veloce rispetto a Method Channel.
Punti chiave
FFI (Foreign Function Interface) è un meccanismo che consente a un linguaggio di programmazione di chiamare funzioni scritte in altri linguaggi. Nel contesto di Dart e Flutter, FFI significa la capacità di chiamare funzioni da librerie C/C++ direttamente dal codice Dart, senza dover scrivere codice specifico della piattaforma in Java (Android) o Swift/Objective-C (iOS).
Il pacchetto dart:ffi è apparso in Dart 2.12 (2021) e da allora è diventato uno strumento chiave per integrare Flutter con codice nativo. Prima di dart:ffi, l'unico modo per chiamare una funzione C da Dart era tramite Method Channel — un meccanismo asincrono che passava messaggi tramite serializzazione JSON tra Dart e il lato nativo. FFI funziona diversamente: il codice Dart accede direttamente alla memoria della libreria C, chiamando funzioni tramite l'ABI nativa (Application Binary Interface) senza serializzazione o cambio di contesto.
FFI è particolarmente richiesto in scenari dove le prestazioni sono critiche: elaborazione immagini (OpenCV), audio (FFmpeg), crittografia (OpenSSL), machine learning (TensorFlow Lite) e database (SQLite). In tutti questi casi, Method Channel crea ritardi inaccettabili, mentre FFI offre prestazioni paragonabili al codice C/C++ nativo. La libreria dart:ffi supporta anche la gestione della memoria: allocazione, deallocazione e manipolazione dei puntatori.
Method Channel funziona in modo asincrono: Dart invia un messaggio al codice nativo, il codice nativo lo elabora e rimanda il risultato. Ogni chiamata richiede la serializzazione degli argomenti in una Map, il passaggio attraverso una coda e la deserializzazione. Ciò richiede 0,5–5 ms per chiamata. FFI funziona in modo sincrono e senza serializzazione — una chiamata a funzione C richiede 0,01–0,1 µs. Una differenza di 50–500 volte, che è critica per operazioni ad alta frequenza.
Lavorare con dart:ffi consiste in tre fasi: caricare la libreria, dichiarare le firme e chiamare le funzioni. Ogni fase utilizza la tipizzazione rigorosa di Dart, minimizzando gli errori a runtime.
Nella prima fase, la libreria dinamica viene caricata tramite la classe DynamicLibrary. La libreria può essere caricata per nome (libxyz.so, libxyz.dylib, xyz.dll) o per percorso completo. Dart cerca automaticamente la libreria nei percorsi standard del sistema. DynamicLibrary fornisce il metodo lookupFunction, che lega una funzione Dart a una funzione C per nome del simbolo.
Nella seconda fase, viene dichiarata una funzione Dart con annotazioni di tipo corrispondenti alla firma C. Vengono utilizzati tipi speciali di dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle e altri. L'annotazione lookupFunction accetta due parametri generici: il tipo della funzione Dart (come apparirà in Dart) e il tipo della funzione C nativa (come è dichiarata in C).
Nella terza fase, la funzione Dart generata viene chiamata come una funzione normale. Gli argomenti vengono passati direttamente, il risultato viene restituito immediatamente. Se la funzione C modifica la memoria tramite puntatori, Dart può leggere queste modifiche tramite la classe Pointer. La gestione della memoria lato C rimane responsabilità dello sviluppatore — dart:ffi non gestisce la memoria allocata da malloc in C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Dichiarazione funzione 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
}
In questo esempio, add è una funzione C che prende due int e restituisce un int. Il typedef AddNative descrive la firma C con i tipi dart:ffi, mentre AddDart descrive come questa funzione apparirà in Dart. lookupFunction le lega e restituisce una funzione Dart che può essere chiamata come una normale.
dart:ffi fornisce un insieme di tipi corrispondenti ai tipi C. Ogni tipo ha una dimensione fissa e regole di conversione tra Dart e C. Comprendere la mappatura dei tipi è fondamentale per il corretto funzionamento di FFI — un errore nella dimensione o nel segno di un tipo può causare l'arresto anomalo dell'applicazione.
| Tipo C | Tipo dart:ffi | Tipo Dart | Dimensione (byte) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (puntatore) |
| void* | Pointer<Void> | Pointer | 8 (puntatore) |
| struct | Pointer<T> (Struct) | Pointer | dipende dai campi |
Per lavorare con le stringhe C (char*), dart:ffi usa Pointer<Int8>. La conversione da Dart String a C char* e viceversa viene eseguita tramite toNativeUtf8 (dal pacchetto ffi) e fromUtf8. È importante liberare le stringhe C dopo l'uso tramite calloc.free per evitare perdite di memoria.
dart:ffi supporta la dichiarazione di strutture C come classi Dart che estendono Struct. I campi della struttura vengono dichiarati con annotazioni @Int32(), @Float(), @Array() e altre. La dimensione e l'offset dei campi vengono calcolati automaticamente secondo l'ABI della piattaforma. Pointer<Point> può essere ottenuto da una funzione C che restituisce un puntatore a una struttura, o allocato in Dart tramite calloc.
// Struct C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Chiamata a funzione C che restituisce 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) // liberare memoria
La classe Point estende Struct e dichiara i campi x e y con annotazioni @Int32(). Il codice C generato avrà esattamente lo stesso layout di memoria. Pointer.ref fornisce l'accesso ai campi della struttura tramite getter e setter.
Consideriamo un esempio più complesso — l'integrazione con una libreria C per il calcolo dell'hash SHA256. Questo è un compito tipico dove FFI offre un significativo guadagno in termini di prestazioni rispetto a Method Channel.
La libreria OpenSSL fornisce la funzione SHA256, che calcola l'hash di una stringa. Tramite dart:ffi, possiamo chiamarla direttamente, senza scrivere wrapper Java o Swift. Questo è un esempio di come FFI consente di riutilizzare librerie C esistenti in Flutter.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Firma: 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 byte
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
}
In questo esempio, la funzione sha256 carica la libreria libcrypto.so, trova il simbolo SHA256 e lo chiama con puntatori ai dati di input e output. toNativeUtf8 converte una Dart String in una stringa C (alloca memoria), e asTypedList consente di leggere l'array di byte del risultato. La memoria viene liberata dopo l'uso — questo è un passaggio obbligatorio per prevenire perdite.
Il pacchetto ffi fornisce la funzione calloc per allocare memoria compatibile con C. La memoria allocata deve essere liberata tramite calloc.free, altrimenti si verificherà una perdita. Per la gestione automatica della memoria, è possibile utilizzare la classe Arena del pacchetto ffi, che libera tutta la memoria allocata al suo interno quando viene chiamato arena.release(). Ciò è particolarmente comodo per un gran numero di allocazioni temporanee.
Nonostante la potenza di FFI, ci sono limitazioni che devono essere considerate durante la progettazione dell'architettura di un'applicazione Flutter. Le principali limitazioni riguardano la sicurezza dei tipi, la gestione della memoria e la compatibilità delle piattaforme.
FFI non esegue il controllo dei tipi a runtime. Se una funzione C si aspetta un puntatore ma riceve un numero, l'applicazione si arresterà con un errore di segmentazione. Si consiglia di utilizzare FFIgen, uno strumento che genera wrapper Dart type-safe basati su file di intestazione C (.h). FFIgen analizza le dichiarazioni delle funzioni C e crea codice Dart con tipi corretti, eliminando gli errori nella fase di scrittura del codice.
I nomi e i percorsi delle librerie dinamiche differiscono tra le piattaforme: libxyz.so su Android/Linux, libxyz.dylib su iOS/macOS, xyz.dll su Windows. Per le librerie multipiattaforma, viene utilizzata la compilazione condizionale tramite dart:io (Platform.isAndroid, Platform.isIOS) o astrazioni come package:ffi. Si consiglia di creare un metodo factory che restituisca la libreria corretta per la piattaforma corrente.
FFI non gestisce la memoria lato C. Se una funzione C alloca memoria tramite malloc, deve essere liberata tramite free, altrimenti si verificherà una perdita. Dart non ha un garbage collector per la memoria C. Raccomandazione: liberare sempre la memoria nello stesso metodo in cui è stata allocata, o utilizzare Arena per la deallocazione di gruppo.
Le chiamate FFI vengono eseguite nello stesso thread del codice Dart. Le operazioni sincrone lunghe (più di 10 ms) bloccano il thread dell'interfaccia utente e causano cadute di fotogrammi. Per operazioni lunghe, è necessario chiamare la funzione C in un Isolate o assicurarsi che la funzione C esegua il lavoro in un thread in background e notifichi Dart tramite Port o callback.
// FFI in isolate per operazioni lunghe
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // Chiamata FFI
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Spostare le chiamate FFI in un isolate garantisce che il thread dell'interfaccia utente non venga bloccato. Tuttavia, va notato che il trasferimento di grandi quantità di dati tra isolate richiede la copia della memoria. Per buffer grandi (>10 MB), è preferibile utilizzare SharedMemory o file mappati in memoria.
Domande frequenti
FFI chiama funzioni C direttamente, in modo sincrono e senza serializzazione — latenza 0,01–0,1 µs. Method Channel funziona in modo asincrono tramite serializzazione JSON con una latenza di 0,5–5 ms. FFI è adatto per operazioni ad alte prestazioni, Method Channel per semplici chiamate API di piattaforma.
Direttamente — no, dart:ffi supporta solo funzioni C. Per chiamare C++, è necessario creare un wrapper C con extern “C” (punti di ingresso esportati come simboli C). Le classi C++ richiedono un livello aggiuntivo che traduce le chiamate ai metodi in funzioni C.
FFI non supporta le eccezioni — se una funzione C restituisce un codice di errore, deve essere verificato manualmente. Si consiglia di avvolgere le chiamate FFI in try-catch in Dart e verificare i codici di ritorno delle funzioni C. Gli errori critici (segfault) non possono essere catturati.
FFI non funziona con librerie che richiedono un'inizializzazione complessa di Java (JNI) o Objective-C (Message Dispatch). Ad esempio, UIKit e Android Views non sono accessibili tramite FFI. La limitazione è dovuta al fatto che FFI opera a livello di ABI C, mentre queste API richiedono runtime specifici.
Sì, le librerie C vengono compilate separatamente per ogni piattaforma target. Per Android, il .so viene costruito per diverse ABI (armeabi-v7a, arm64-v8a, x86_64). Per iOS — un .dylib universale (arm64). Per Windows — .dll. Flutter impacchetta automaticamente la versione corretta della libreria durante la compilazione.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.