Génération de liaisons FFI avec package:ffigen
Contenu
- Introduction
- Contraintes
- Aperçu de FFIgen
- Flux de travail étape par étape
- Exemple concret : lier une bibliothèque C
- Liste de vérification
Introduction
Automatisez et standardisez la génération de liaisons FFI avec package:ffigen (FfiGenerator). Écrire les liaisons FFI à la main est sujet aux erreurs, fragile et fortement déconseillé.
Contraintes
- Pas de liaisons FFI écrites à la main : si des en-têtes natifs (fichiers
.h) existent ou sont générés lors d'une étape de compilation, ne rédigez jamais manuellementDynamicLibrary.lookup, les fonctions externes@Native, ou les classes struct brutes. Utilisez toujoursFfiGeneratorpour les générer. - Emplacement du générateur : le script du générateur doit être situé à
tool/ffigen.dartà la racine du package cible. - Emplacement de l'en-tête : si les fichiers d'en-tête natifs sont tiers, ils doivent être situés dans
third_party/à l'intérieur du package cible (sinon les placer dans un répertoiresrc/à la racine du package est également acceptable). Si les en-têtes ne se trouvent pas dans l'un de ces emplacements standard, informez l'utilisateur qu'il serait plus propre de déplacer les fichiers d'en-tête à l'emplacement standard (par ex.third_party/). - Filtres d'inclusion ciblés : évitez d'importer une bibliothèque native entière sauf si c'est spécifiquement nécessaire. Appliquez toujours des listes d'inclusion précises en utilisant des correspondances positives pour minimiser la taille et la charge cognitive du code généré (par ex. en utilisant
Functions.includeSetou en filtrant les correspondances dans les closuresinclude). - Configuration de sortie : si les liaisons FFI générées interagissent avec une bibliothèque tierce (ou référencent des en-têtes tiers), les fichiers générés doivent toujours être placés sous
lib/src/third_party/. Le fichier de liaisons FFI généré principal doit strictement utiliser l'extension.g.dart(par ex.sqlite3.g.dart). - Préambule et en-têtes de licence : fournissez toujours un
preamblepremium dans la classeOutputpour spécifier la licence. Celui-ci doit correspondre à la licence de la bibliothèque native tierce, inclure explicitement l'en-tête de droit d'auteur du fichier d'en-tête natif cible, et contenir un avertissement de génération automatique (par ex.// Generated by package:ffigen. Do not edit manually.). - Pas de commits inutiles de liaisons obsolètes : assurez-vous d'exécuter le script du générateur et vérifiez que les fichiers générés ont changé avant de terminer votre tâche. Vérifiez toujours le package en exécutant
dart analyze. - Enregistrement d'utilisation et tree shaking : si le package est intégré à l'exécution standard du runtime ou compile des actifs natifs via des hooks natifs :
- Activez l'utilisation enregistrée sur toutes les fonctions en définissant
recordUse: (_) => truesousFunctions. - Spécifiez la cible
recordUseMappingdansOutput(qui doit strictement être un fichier.g.dartsouslib/src/third_party/, par ex.lib/src/third_party/sqlite3.record_use_mapping.g.dart) pour enregistrer les liaisons pour le tree shaking des symboles.
- Activez l'utilisation enregistrée sur toutes les fonctions en définissant
Aperçu de FFIgen
Pour construire le générateur programmatique, utilisez les objets de configuration centraux importés de package:ffigen/ffigen.dart :
1. FfiGenerator
La classe parent qui orchestre la configuration, l'analyse et la génération de code.
FfiGenerator({
Headers headers = const Headers(),
Enums enums = Enums.excludeAll,
Functions functions = Functions.excludeAll,
Globals globals = Globals.excludeAll,
Integers integers = const Integers(),
Macros macros = Macros.excludeAll,
Structs structs = Structs.excludeAll,
Typedefs typedefs = Typedefs.excludeAll,
Unions unions = Unions.excludeAll,
UnnamedEnums unnamedEnums = UnnamedEnums.excludeAll,
ObjectiveC? objectiveC,
required Output output,
}).generate();
2. Headers
Configure les cibles d'analyse d'en-tête Clang et les drapeaux du compilateur.
entryPoints: une liste d'entréesUrid'en-tête cible.include: une fonction de filtrebool Function(Uri header)qui gère les importations d'en-têtes transitifs.compilerOptions: les drapeaux du compilateur de préprocesseur/inclusion personnalisés à passer directement à libclang.ignoreSourceErrors: définez surtruepour supprimer les erreurs survenant à l'intérieur des en-têtes tiers lors de l'analyse.
3. Functions
Spécifie quelles fonctions C/C++ natives exposer dans Dart.
include: une fonction de correspondance (par ex.(decl) => {'my_func'}.contains(decl.originalName)ouFunctions.includeSet({'my_func'})).isLeaf: déclare les fonctions comme fonctions feuille ((decl) => true) si elles ne rappellent pas dans Dart ou bloquent l'exécution des threads.recordUse: active la génération de métadonnées pour le tree shaking des actifs natifs (essentiel dansdart-lang/native). Définez sur(_) => true.
4. Output
Configure les fichiers générés cibles.
dartFile: laUricible où les liaisons FFI principales seront écrites.recordUseMapping: laUricible pour les cartes de métadonnées d'utilisation enregistrée (cruciale pour le tree shaking au moment de la liaison).preamble: du texte inséré au début du fichier généré (licences, annotations).format: définez surtruepour exécuter automatiquement le formateur Dart.
Flux de travail étape par étape
Étape 1 : vérifier/ajouter les dépendances
Ouvrez pubspec.yaml du package et vérifiez que dev_dependencies contient ffigen. Utilisez le serveur MCP Dart ou consultez la dernière version sur pub.dev (par ex. ^20.1.1).
Vous pouvez l'ajouter automatiquement via la CLI :
dart pub add dev:ffigen
Étape 2 : formuler les chemins dynamiquement
Créez un script de générateur programmatique dans le répertoire tool/ du package (par ex. tool/ffigen.dart).
Résolvez les chemins relatifs à Platform.script pour vous assurer qu'il s'exécute correctement depuis n'importe quel répertoire de travail :
final packageRoot = Platform.script.resolve('../');
final headerFile = packageRoot.resolve('third_party/library.h');
final targetBindings = packageRoot.resolve('lib/src/third_party/bindings.g.dart');
Étape 3 : écrire le script (tool/ffigen.dart)
Définissez void main() et exécutez FfiGenerator avec des options dynamiques (voir l'exemple complet ci-dessous).
Étape 4 : exécuter la génération de code
Exécutez le script depuis le terminal dans le dossier du package cible :
dart run tool/ffigen.dart
Étape 5 : analyse statique
Vérifiez que les liaisons générées sont correctes et résolvez tous les problèmes d'analyse. FFIgen exécute automatiquement le formateur Dart sur le fichier de sortie (via la configuration format: true), donc aucune mise en forme manuelle n'est requise.
- Exécutez l'analyseur statique dans le package cible :
dart analyze - Traiter les avertissements/lints : si
dart analyzesignale des avertissements de style ou de lint dans le fichier généré, ajoutez les codes d'avertissement correspondants à la listeignore_for_file:dans la configurationpreamblede votre script du générateur (par ex. en ajoutantcamel_case_types,non_constant_identifier_names, etc.). Ne modifiez pas les règles globales du package. - Traiter les erreurs de compilation : si
dart analyzesignale des erreurs réelles du compilateur ou d'analyse (pas des avertissements) dans le fichier généré, ne tentez pas de modifier manuellement le fichier généré. Signalez les détails de ces erreurs directement à l'utilisateur afin qu'il puisse déposer un problème sur le dépôt à github.com/dart-lang/native.
Exemple concret : lier une bibliothèque C
Supposons que nous travaillons avec le package SQLite dans pkgs/code_assets/example/sqlite, qui intègre les sources de la bibliothèque C SQLite à l'intérieur de third_party/sqlite/ et y accède via FFI.
Le fichier d'en-tête C (third_party/sqlite/sqlite3.h)
// The author disclaims copyright to this source code.
#ifndef SQLITE3_H_
#define SQLITE3_H_
const char *sqlite3_libversion(void);
#endif // SQLITE3_H_
AVANT : liaison FFI manuelle (l'anti-motif)
Un développeur pourrait tenter de créer manuellement cette intégration. C'est fragile, bloque les métadonnées de tree shaking, et est très sujet aux problèmes de mappage ABI et structurel :
// lib/src/sqlite3_manual.dart
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';
// Défaut 1 : la recherche DynamicLibrary codée en dur bloque l'intégration avec la compilation moderne des actifs natifs.
final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open('libsqlite3.so');
// Défaut 2 : la correspondance de type de fonction manuelle nécessite d'écrire du code passe-partout de recherche dynamique redondant et ne dispose pas de métadonnées de tree shaking.
typedef _sqlite3_libversion_C = ffi.Pointer<ffi.Char> Function();
typedef _sqlite3_libversion_Dart = ffi.Pointer<ffi.Char> Function();
final _sqlite3_libversion_Dart sqlite3LibVersion = _dylib
.lookup<ffi.NativeFunction<_sqlite3_libversion_C>>('sqlite3_libversion')
.asFunction();
APRÈS : générer via FFIgen (le motif correct)
Créez un script programmatique à tool/ffigen.dart :
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:ffigen/ffigen.dart';
void main() {
// Resolve paths dynamically relative to Platform.script
final packageRoot = Platform.script.resolve('../');
final entryHeader = packageRoot.resolve('third_party/sqlite/sqlite3.h');
final bindingsOutput = packageRoot.resolve('lib/src/third_party/sqlite3.g.dart');
final treeShakeMapping = packageRoot.resolve('lib/src/third_party/sqlite3.record_use_mapping.g.dart');
FfiGenerator(
headers: Headers(
entryPoints: [entryHeader],
),
functions: Functions(
include: (decl) => {'sqlite3_libversion'}.contains(decl.originalName),
// Essential for package optimization and tree-shaking
recordUse: (_) => true,
),
output: Output(
dartFile: bindingsOutput,
recordUseMapping: treeShakeMapping,
format: true,
preamble: '''
// AUTO-GENERATED FILE - DO NOT MODIFY.
// Generated via ffigen.
// To regenerate: dart run tool/ffigen.dart
// ignore_for_file: type=lint, unused_import, unused_element, deprecated_member_use_from_same_package, experimental_member_use
''',
),
).generate();
print('Successfully generated sqlite3 FFI bindings.');
}
Exécution du script du générateur
Exécutez ceci dans le répertoire racine du package :
dart run tool/ffigen.dart
Cela créera automatiquement :
lib/src/third_party/sqlite3.g.dartlib/src/third_party/sqlite3.record_use_mapping.g.dart
Liste de vérification
Effectuez toujours la vérification suivante avant de terminer une tâche de génération de liaison :
- Configuration correcte : vérifiez que les fichiers générés cibles se trouvent dans
lib/src/third_party/(requis pour le code tiers sous licence) et que le fichier de liaisons FFI principal utilise strictement l'extension.g.dart. - Analyse statique : exécutez
dart analyzeet assurez-vous qu'il n'y a aucune erreur ou avertissement du compilateur/analyseur dans le package.- Si des avertissements statiques sont signalés dans les liaisons générées, supprimez-les en ajoutant des règles
ignore_for_fileà la configurationpreambledu générateur (ne modifiez pas les règles globales du package). - Si des erreurs réelles du compilateur ou de l'analyseur sont signalées dans les liaisons générées, ne modifiez pas manuellement le fichier généré. Signalez les détails à l'utilisateur et dirigez-le pour déposer un problème à github.com/dart-lang/native.
- Si des avertissements statiques sont signalés dans les liaisons générées, supprimez-les en ajoutant des règles