FFI : fondamentaux, Foreign Function Interface et intégration avec C

Auteur : IT Sectr Publié le : 2026-06-05 Temps de lecture : 10 min

FFI (Foreign Function Interface) est un mécanisme du langage Dart, fourni par le paquet dart:ffi, qui permet d’appeler des fonctions de bibliothèques natives C directement, sans couches intermédiaires en Kotlin, Swift ou Java. Le développeur charge une bibliothèque dynamique (.so sur Android, .dylib sur iOS, .dll sur Windows), déclare les signatures des fonctions C et les appelle comme des fonctions Dart ordinaires. Selon la Référence Dart API (2025), FFI réduit la surcharge des appels inter-langages à 0,1 µs, soit des dizaines de fois plus rapide que via Method Channel.

Points clés à retenir

  • FFI (Foreign Function Interface) — un mécanisme pour appeler des fonctions C directement depuis Dart
  • dart:ffi fournit une API pour charger des bibliothèques et déclarer des signatures
  • Performances des appels FFI sont 50–100 fois supérieures à celles du Method Channel
  • Typage FFI prend en charge les types primitifs, les structures et les pointeurs C
  • Flutter utilise FFI pour l’intégration avec les bibliothèques natives : OpenCV, SQLite, FFmpeg

Qu’est-ce que FFI ?

FFI (Foreign Function Interface) est un mécanisme qui permet à un langage de programmation d’appeler des fonctions écrites dans d’autres langages. Dans le contexte de Dart et Flutter, FFI signifie la capacité d’appeler des fonctions de bibliothèques C/C++ directement depuis du code Dart, sans avoir à écrire de code spécifique à la plateforme en Java (Android) ou Swift/Objective-C (iOS).

Le paquet dart:ffi est apparu dans Dart 2.12 (2021) et est depuis devenu un outil clé pour intégrer Flutter avec du code natif. Avant dart:ffi, la seule façon d’appeler une fonction C depuis Dart était via Method Channel — un mécanisme asynchrone qui transmettait des messages par sérialisation JSON entre Dart et le côté natif. FFI fonctionne différemment : le code Dart accède directement à la mémoire de la bibliothèque C, appelant des fonctions via l’ABI native (Application Binary Interface) sans sérialisation ni changement de contexte.

FFI est particulièrement demandé dans les scénarios où les performances sont critiques : traitement d’images (OpenCV), audio (FFmpeg), cryptographie (OpenSSL), apprentissage automatique (TensorFlow Lite) et bases de données (SQLite). Dans tous ces cas, Method Channel crée des latences inacceptables, tandis que FFI offre des performances comparables au code C/C++ natif. La bibliothèque dart:ffi prend également en charge la gestion de la mémoire : allocation, libération et manipulation de pointeurs.

FFI vs Method Channel : différence fondamentale

Method Channel fonctionne de manière asynchrone : Dart envoie un message au code natif, le code natif le traite et renvoie le résultat. Chaque appel nécessite la sérialisation des arguments dans une Map, le passage par une file d’attente et la désérialisation. Cela prend 0,5–5 ms par appel. FFI fonctionne de manière synchrone et sans sérialisation — un appel de fonction C prend 0,01–0,1 µs. Une différence de 50–500 fois, ce qui est critique pour les opérations à haute fréquence.

Comment fonctionne dart:ffi ?

Travailler avec dart:ffi comprend trois étapes : charger la bibliothèque, déclarer les signatures et appeler les fonctions. Chaque étape utilise le typage strict de Dart, minimisant les erreurs d’exécution.

Dans la première étape, la bibliothèque dynamique est chargée via la classe DynamicLibrary. La bibliothèque peut être chargée par nom (libxyz.so, libxyz.dylib, xyz.dll) ou par chemin complet. Dart recherche automatiquement la bibliothèque dans les chemins standard du système. DynamicLibrary fournit la méthode lookupFunction, qui lie une fonction Dart à une fonction C par nom de symbole.

Dans la deuxième étape, une fonction Dart est déclarée avec des annotations de type correspondant à la signature C. Des types spéciaux de dart:ffi sont utilisés : Int32, Float, Double, Pointer, NativeFunction, Handle et autres. L’annotation lookupFunction prend deux paramètres génériques : le type de la fonction Dart (comment elle apparaîtra dans Dart) et le type de la fonction C native (comment elle est déclarée en C).

Dans la troisième étape, la fonction Dart générée est appelée comme une fonction ordinaire. Les arguments sont passés directement, le résultat est retourné immédiatement. Si la fonction C modifie la mémoire via des pointeurs, Dart peut lire ces modifications via la classe Pointer. La gestion de la mémoire du côté C reste la responsabilité du développeur — dart:ffi ne gère pas la mémoire allouée par malloc en C.

Exemple de base : appeler une fonction C depuis Dart

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

// Déclaration de fonction 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
}

Dans cet exemple, add est une fonction C qui prend deux int et retourne un int. Le typedef AddNative décrit la signature C avec les types dart:ffi, tandis que AddDart décrit comment cette fonction apparaîtra dans Dart. lookupFunction les lie et retourne une fonction Dart qui peut être appelée comme une fonction ordinaire.

Types de données dans FFI

dart:ffi fournit un ensemble de types correspondant aux types C. Chaque type a une taille fixe et des règles de conversion entre Dart et C. Comprendre la correspondance des types est crucial pour le bon fonctionnement de FFI — une erreur de taille ou de signe d’un type peut entraîner le plantage de l’application.

Type CType dart:ffiType DartTaille (octets)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (pointeur)
void*Pointer<Void>Pointer8 (pointeur)
structPointer<T> (Struct)Pointerdépend des champs

Pour travailler avec les chaînes C (char*), dart:ffi utilise Pointer<Int8>. La conversion de Dart String en char* C et vice versa s’effectue via toNativeUtf8 (du paquet ffi) et fromUtf8. Il est important de libérer les chaînes C après utilisation via calloc.free pour éviter les fuites mémoire.

Structures (Struct)

dart:ffi prend en charge la déclaration de structures C comme classes Dart qui héritent de Struct. Les champs de la structure sont déclarés avec des annotations @Int32(), @Float(), @Array() et autres. La taille et le décalage des champs sont calculés automatiquement selon l’ABI de la plateforme. Pointer<Point> peut être obtenu à partir d’une fonction C retournant un pointeur vers une structure, ou alloué dans Dart via calloc.

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

    @Int32()
    external int y
}

// Appel de fonction C retournant 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) // libérer la mémoire

La classe Point étend Struct et déclare les champs x et y avec des annotations @Int32(). Le code C généré aura exactement la même disposition mémoire. Pointer.ref fournit un accès aux champs de la structure via des getters et setters.

Exemples pratiques de FFI

Examinons un exemple plus complexe — l’intégration avec une bibliothèque C pour calculer le hachage SHA256. C’est une tâche typique où FFI offre un gain de performance significatif par rapport à Method Channel.

Intégration avec OpenSSL via FFI

La bibliothèque OpenSSL fournit la fonction SHA256, qui calcule le hachage d’une chaîne. Grâce à dart:ffi, nous pouvons l’appeler directement, sans écrire de wrappers Java ou Swift. C’est un exemple de la façon dont FFI permet de réutiliser des bibliothèques C existantes dans Flutter.

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

// Signature : 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 octets

    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
}

Dans cet exemple, la fonction sha256 charge la bibliothèque libcrypto.so, trouve le symbole SHA256 et l’appelle avec des pointeurs vers les données d’entrée et de sortie. toNativeUtf8 convertit une Dart String en chaîne C (alloue de la mémoire), et asTypedList permet de lire le tableau d’octets du résultat. La mémoire est libérée après utilisation — c’est une étape obligatoire pour prévenir les fuites.

Allocation et libération de mémoire

Le paquet ffi fournit la fonction calloc pour allouer de la mémoire compatible C. La mémoire allouée doit être libérée via calloc.free, sinon une fuite se produira. Pour la gestion automatique de la mémoire, vous pouvez utiliser la classe Arena du paquet ffi, qui libère toute la mémoire allouée en son sein lorsque arena.release() est appelé. C’est particulièrement pratique pour un grand nombre d’allocations temporaires.

Limitations et bonnes pratiques

Malgré la puissance de FFI, il a des limitations qui doivent être prises en compte lors de la conception de l’architecture d’une application Flutter. Les principales limitations concernent la sécurité des types, la gestion de la mémoire et la compatibilité des plateformes.

Sécurité

FFI n’effectue pas de vérification de type à l’exécution. Si une fonction C attend un pointeur mais reçoit un nombre, l’application plantera avec une erreur de segmentation. Il est recommandé d’utiliser FFIgen, un outil qui génère des wrappers Dart type-sécurisés basés sur des fichiers d’en-tête C (.h). FFIgen analyse les déclarations de fonctions C et crée du code Dart avec des types corrects, éliminant les erreurs au stade de l’écriture du code.

Compatibilité des plateformes

Les noms et chemins des bibliothèques dynamiques diffèrent selon les plateformes : libxyz.so sur Android/Linux, libxyz.dylib sur iOS/macOS, xyz.dll sur Windows. Pour les bibliothèques multiplateformes, la compilation conditionnelle est utilisée via dart:io (Platform.isAndroid, Platform.isIOS) ou des abstractions comme package:ffi. Il est recommandé de créer une méthode fabrique qui retourne la bibliothèque correcte pour la plateforme actuelle.

Gestion de la mémoire

FFI ne gère pas la mémoire du côté C. Si une fonction C alloue de la mémoire via malloc, elle doit être libérée via free, sinon une fuite se produira. Dart n’a pas de ramasse-miettes pour la mémoire C. Recommandation : toujours libérer la mémoire dans la même méthode où elle a été allouée, ou utiliser Arena pour une libération groupée.

Performances et threads

Les appels FFI s’exécutent dans le même thread que le code Dart. Les opérations synchrones longues (plus de 10 ms) bloquent le thread de l’interface utilisateur et provoquent des chutes d’images. Pour les opérations longues, il faut appeler la fonction C dans un Isolate ou s’assurer que la fonction C exécute le travail dans un thread d’arrière-plan et notifie Dart via Port ou callback.

dart
// FFI dans Isolate pour les opérations longues
import 'dart:isolate'

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

    return await port.first as String
}

Déplacer les appels FFI dans un isolate garantit que le thread de l’interface utilisateur n’est pas bloqué. Cependant, il faut noter que le transfert de grandes quantités de données entre isolates nécessite une copie mémoire. Pour les grands tampons (>10 Mo), il est préférable d’utiliser SharedMemory ou des fichiers mappés en mémoire.

Questions fréquentes

En quoi FFI diffère-t-il de Method Channel ?

FFI appelle les fonctions C directement, de manière synchrone et sans sérialisation — latence de 0,01–0,1 µs. Method Channel fonctionne de manière asynchrone via sérialisation JSON avec une latence de 0,5–5 ms. FFI convient aux opérations hautes performances, Method Channel aux appels simples d’API de plateforme.

Peut-on appeler des fonctions C++ via FFI ?

Directement — non, dart:ffi ne prend en charge que les fonctions C. Pour appeler du C++, il faut créer un wrapper C avec extern « C » (points d’entrée qui sont exportés comme symboles C). Les classes C++ nécessitent une couche supplémentaire qui traduit les appels de méthodes en fonctions C.

Comment gérer les erreurs dans les fonctions C ?

FFI ne prend pas en charge les exceptions — si une fonction C retourne un code d’erreur, il doit être vérifié manuellement. Il est recommandé d’encapsuler les appels FFI dans try-catch dans Dart et de vérifier les codes de retour des fonctions C. Les erreurs critiques (segfault) ne peuvent pas être interceptées.

Quelles bibliothèques ne peuvent pas être utilisées via FFI ?

FFI ne fonctionne pas avec les bibliothèques nécessitant une initialisation complexe de Java (JNI) ou Objective-C (Message Dispatch). Par exemple, UIKit et Android Views ne sont pas accessibles via FFI. La limitation est due au fait que FFI fonctionne au niveau de l’ABI C, tandis que ces APIs nécessitent des environnements d’exécution spécifiques.

Faut-il compiler les bibliothèques C pour chaque plateforme ?

Oui, les bibliothèques C sont compilées séparément pour chaque plateforme cible. Pour Android, le .so est construit pour différentes ABI (armeabi-v7a, arm64-v8a, x86_64). Pour iOS — un .dylib universel (arm64). Pour Windows — .dll. Flutter empaquette automatiquement la version correcte de la bibliothèque lors de la compilation.

Résumé

  • FFI (Foreign Function Interface) — mécanisme pour appeler des fonctions C directement depuis Dart via dart:ffi
  • Performances des appels FFI sont 50–500 fois supérieures à celles du Method Channel
  • Architecture comprend le chargement de bibliothèque, la déclaration de signatures et l’appel de fonctions
  • Types de données dart:ffi prend en charge Int32, Float, Double, Pointer, Struct et autres types C
  • Mémoire du côté C est gérée manuellement via calloc/free ou Arena
  • Limitations de FFI : pas de vérification de type à l’exécution, pas de support direct de C++, bloque le thread UI
  • Utilisez FFI pour une intégration haute performance avec les bibliothèques natives dans Flutter

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi