FFI: fondamenti, Foreign Function Interface e integrazione con C

Autore: IT Sectr Pubblicato: 2026-06-05 Tempo di lettura: 10 min

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) — meccanismo per chiamare funzioni C direttamente da Dart
  • dart:ffi fornisce un'API per caricare librerie e dichiarare firme
  • Prestazioni delle chiamate FFI sono 50–100 volte superiori rispetto a Method Channel
  • Tipizzazione FFI supporta tipi primitivi, strutture e puntatori C
  • Flutter usa FFI per l'integrazione con librerie native: OpenCV, SQLite, FFmpeg

Cos'è FFI?

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.

FFI vs Method Channel: differenza fondamentale

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.

Come funziona dart:ffi?

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.

Esempio base: chiamare una funzione C da Dart

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

Tipi di dati in FFI

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 CTipo dart:ffiTipo DartDimensione (byte)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (puntatore)
void*Pointer<Void>Pointer8 (puntatore)
structPointer<T> (Struct)Pointerdipende 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.

Strutture (Struct)

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.

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

Esempi pratici di FFI

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.

Integrazione con OpenSSL tramite FFI

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.

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

Allocazione e deallocazione della memoria

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.

Limitazioni e best practices

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.

Sicurezza

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.

Compatibilità delle piattaforme

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.

Gestione della memoria

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.

Prestazioni e thread

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.

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

In cosa FFI si differenzia da Method Channel?

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.

Si possono chiamare funzioni C++ tramite FFI?

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.

Come gestire gli errori nelle 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.

Quali librerie non possono essere utilizzate tramite FFI?

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.

Le librerie C devono essere compilate per ogni piattaforma?

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

  • FFI (Foreign Function Interface) — meccanismo per chiamare funzioni C direttamente da Dart tramite dart:ffi
  • Prestazioni delle chiamate FFI sono 50–500 volte superiori rispetto a Method Channel
  • Architettura include caricamento libreria, dichiarazione firme e chiamata funzioni
  • Tipi di dati dart:ffi supporta Int32, Float, Double, Pointer, Struct e altri tipi C
  • Memoria lato C è gestita manualmente tramite calloc/free o Arena
  • Limitazioni di FFI: nessun controllo dei tipi a runtime, nessun supporto diretto per C++, blocca il thread UI
  • Utilizza FFI per l'integrazione ad alte prestazioni con librerie native in Flutter

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.

Discuti il progetto

Leggi anche