dart-use-ffigen

Par flutter · skills

Guide les agents à utiliser `package:ffigen` pour générer automatiquement des bindings FFI plutôt que de les écrire manuellement. Utilisez cette skill lorsqu'une tâche implique l'écriture de nouveaux bindings FFI, l'extension d'intégrations C/Objective-C/Swift, ou le remplacement de configurations `dart:ffi` faites à la main.

npx skills add https://github.com/flutter/skills --skill dart-use-ffigen

Génération de liaisons FFI avec package:ffigen

Contenu

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 manuellement DynamicLibrary.lookup, les fonctions externes @Native, ou les classes struct brutes. Utilisez toujours FfiGenerator pour 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épertoire src/ à 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.includeSet ou en filtrant les correspondances dans les closures include).
  • 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 preamble premium dans la classe Output pour 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: (_) => true sous Functions.
    • Spécifiez la cible recordUseMapping dans Output (qui doit strictement être un fichier .g.dart sous lib/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.

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ées Uri d'en-tête cible.
  • include : une fonction de filtre bool 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 sur true pour 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) ou Functions.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 dans dart-lang/native). Définez sur (_) => true.

4. Output

Configure les fichiers générés cibles.

  • dartFile : la Uri cible où les liaisons FFI principales seront écrites.
  • recordUseMapping : la Uri cible 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 sur true pour 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.

  1. Exécutez l'analyseur statique dans le package cible :
    dart analyze
  2. Traiter les avertissements/lints : si dart analyze signale des avertissements de style ou de lint dans le fichier généré, ajoutez les codes d'avertissement correspondants à la liste ignore_for_file: dans la configuration preamble de votre script du générateur (par ex. en ajoutant camel_case_types, non_constant_identifier_names, etc.). Ne modifiez pas les règles globales du package.
  3. Traiter les erreurs de compilation : si dart analyze signale 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 :

  1. lib/src/third_party/sqlite3.g.dart
  2. lib/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 :

  1. 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.
  2. Analyse statique : exécutez dart analyze et 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 configuration preamble du 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.

Skills similaires