dart-setup-ffi-assets

Par flutter · skills

Guide les agents dans la compilation et l'empaquetage de code source C/C++ en bibliothèques dynamiques ou statiques (Code Assets) à l'aide du système de hooks Native Assets de Dart (via `hook/build.dart` et `hook/link.dart` utilisant `package:hooks` et `package:native_toolchain_c`). À utiliser lorsqu'un utilisateur demande de : « configurer des native assets », « compiler du code source C/C++ », « bundler des bibliothèques dynamiques », « builder du code natif C », « linker des native assets », « implémenter des hooks build.dart ou link.dart », ou « intégrer de l'interop C/C++ dans Dart/Flutter ». Aide les agents à éviter l'orchestration manuelle de la toolchain et configure des téléchargements de binaires validés par hash sécurisé ou un tree-shaking avancé du linker avec le mapping `package:record_use`.

npx skills add https://github.com/flutter/skills --skill dart-setup-ffi-assets

Compilation du code C en Code Assets avec Native Assets Hooks

Intégrez et automatisez la compilation et l'empaquetage du code source natif C/C++ dans des Code Assets sous la fonctionnalité Native Assets de Dart en utilisant les hooks de construction et de liaison.

Sommaire


Introduction

Sous la fonctionnalité Native Assets de Dart, les paquets peuvent empaqueter du code natif (comme les bibliothèques C/C++) en tant que Code Assets et les regrouper automatiquement lors des cycles de développement standard (par exemple, dart run, dart test, dart build et flutter run). L'empaquetage des Code Assets est piloté par deux scripts de hook programmatiques placés dans le dossier hook/ d'un paquet :

  1. hook/build.dart : Compile les sources C locales en code machine ou regroupe les binaires natifs précompilés en tant que code assets pour une architecture hôte/cible spécifique.
  2. hook/link.dart : Lie les code assets construits, en appliquant des optimisations avancées d'arborescence pour éliminer les symboles natifs inutilisés et compresser la taille du binaire d'exécution.

Contraintes

[!IMPORTANT] Gardez tous les chemins de fichiers indépendants de la plateforme. Ne codifiez jamais en dur les chemins cibles absolus, les scripts shell ou les variables de commande système. Utilisez toujours Platform.script.resolve() ou la résolution basée sur Uri pour garantir que les scripts sont entièrement portables.

  • Emplacements des hooks : Les hooks de compilation et d'empaquetage doivent résider strictement dans le répertoire hook/ à la racine du paquet :
    • hook/build.dart (Phase d'exécution de la construction)
    • hook/link.dart (Phase optionnelle d'empaquetage/liaison/arborescence)
  • Norme de chaîne d'outils de compilation : Utilisez les APIs programmatiques de package:native_toolchain_c (par exemple CBuilder et CLibrary) pour exécuter les chaînes d'outils de compilation. N'invoquez jamais les commandes gcc, clang ou msvc via des commandes shell.
  • En-têtes de préambule et de licence : Chaque fichier source rédigé à la main et généré (y compris les bindings, les assistants et les hooks) doit contenir strictement l'en-tête de copyright et de licence du paquet cible.
  • Mappage d'arborescence : Si vous utilisez l'arborescence du compilateur, vous devez mapper les noms de méthodes Dart cibles (par exemple Method.name) vers leurs noms de symboles C natifs bruts en utilisant un mappage d'utilisation de record généré par FFIgen. Le fichier de mappage doit résider sous lib/src/third_party/ et utiliser strictement l'extension .g.dart (par exemple, sqlite3.record_use_mapping.g.dart).
  • Garanties d'intégrité pour les bibliothèques précompilées : Si vous adoptez le modèle de téléchargement dynamique :
    • Vérification cryptographique : Les binaires précompilés téléchargés doivent être vérifiés par rapport à des tables de consultation préconfigurées contenant des hachages MD5 ou SHA-256 pour garantir l'intégrité binaire et prévenir la falsification.
    • Récupération progressive : Supportez les développeurs hors ligne en fournissant des solutions de secours (comme l'exécution du compilateur local via des indicateurs comme local_build).

Paquets Native Interop

Les hooks de construction et de liaison programmatiques pour les Code Assets exploitent trois paquets natifs d'interopérabilité spécialisés :

Dépendance Objectif Abstractions clés
package:hooks Orchestrateur principal définissant les limites d'exécution. build(args, callback), link(args, callback)
package:native_toolchain_c Détecte les compilateurs locaux (MSVC, Xcode/Clang, GCC) et exécute les chaînes d'outils de construction. CLibrary, CBuilder, LinkerOptions.treeshake
package:code_assets Modélise les enregistrements de métadonnées de code transmis aux chargeurs dynamiques. CodeAsset, DynamicLoadingBundled

Flux de travail étape par étape

Étape 1 : Ajouter les dépendances

Ajoutez les dépendances de hook Code Assets et de chaîne d'outils à votre paquet. Vous devez récupérer ces dépendances directement à partir de pub.dev.

Vous pouvez l'ajouter automatiquement en utilisant la CLI :

dart pub add code_assets hooks native_toolchain_c record_use dev:ffigen

Ou déclarez-les manuellement dans le pubspec.yaml de votre paquet cible :

dependencies:
  code_assets: ^1.0.0
  hooks: ^0.1.0
  native_toolchain_c: ^0.1.0
  record_use: ^0.6.0

dev_dependencies:
  ffigen: ^20.1.1

Étape 2 : Définir les spécifications C

Définissez les métadonnées de compilation de votre bibliothèque C cible dans lib/src/c_library.dart. Cela permet aux hooks de construction et de liaison de partager une seule source de vérité pour les assets, les noms et les sources.

Étape 3 : Implémenter les scripts de hook de construction et de liaison

Écrivez le script d'orchestration de la compilation dans hook/build.dart et la logique d'élimination du code mort dans hook/link.dart.

Étape 4 : Exécuter le cycle de hook

L'exécution de suites de tests standard lance dynamiquement le cycle de vie du hook de construction et de liaison en arrière-plan :

dart test

Choix d'une approche d'intégration

Il y a deux méthodes principales pour intégrer et livrer des assets natifs C/C++ dans Dart. Sélectionnez celle qui correspond à vos exigences de projet :

Aspect Méthode 1 : Compilation locale et arborescence optimisée Méthode 2 : Téléchargements précompilés
Cas d'utilisation principal Quand le code source C/C++ est inclus directement dans le paquet et que vous voulez une optimisation de taille maximale. Quand la compilation locale est lente/complexe, ou quand vous évitez les exigences de chaîne d'outils hôte pour développeurs.
Exigences de chaîne d'outils hôte Nécessite un compilateur C de plateforme préinstallé (outils Xcode, MSVC, GCC). Aucune configuration de compilateur requise sur les machines hôte de développeur/utilisateur.
Optimisation binaire Premium. Les symboles inutilisés sont complètement éliminés de l'arborescence, réduisant la taille de la bibliothèque. Standard. Les binaires compilés standard sont livrés tels quels.
Installation hors ligne Entièrement conforme. Fonctionne complètement hors ligne. Nécessite un accès réseau pour télécharger les bibliothèques, avec secours hors ligne.

Méthode 1 : Compilation locale avec arborescence de liaison optimisée (Recommandée)

Dans cette approche, le hook de construction appelle les chaînes d'outils locales (GCC, Clang, MSVC) pour compiler les fichiers source directement. Le hook de liaison filtre ensuite les symboles de résultat en utilisant les options de compilateur, en conservant uniquement les méthodes cibles invoquées dans le code utilisateur. Cela représente le modèle SQLite standard et robuste sous pkgs/code_assets/example/sqlite.

Chaînes d'outils compilateur hôte requises

Puisque package:native_toolchain_c délègue la compilation dynamique réelle à la chaîne d'outils par défaut du système d'exploitation hôte, la machine de développement doit avoir l'un des paquets compilateur suivants préinstallés :

  • macOS : Xcode Command Line Tools. Installez via :
    xcode-select --install
  • Linux : GCC ou Clang. Installez via :
    sudo apt install build-essential
  • Windows : MSVC (Microsoft Visual C++). Installez Visual Studio Installer et sélectionnez la charge de travail Desktop development with C++.

Remarque : Si aucune chaîne d'outils compatible n'est découverte dans le chemin hôte, le script du hook de construction lèvera une exception d'exécution de compilation. Assurez-vous de spécifier les contraintes du compilateur ou adoptez la méthode 2 si les chaînes d'outils ne peuvent pas être garanties.

Installation des sources C et des bindings

Supposons une source C définissant des fonctions mathématiques simples à third_party/sqlite/sqlite3.c avec son en-tête de point d'entrée à third_party/sqlite/sqlite3.h :

#ifndef SQLITE3_H_
#define SQLITE3_H_

const char *sqlite3_libversion(void);

#endif // SQLITE3_H_

Nous utilisons un script FFIgen programmatique (tool/ffigen.dart) pour créer des bindings FFI dans lib/src/third_party/sqlite3.g.dart, en activant le suivi d'utilisation enregistrée et en produisant la carte de métadonnées de recherche dans lib/src/third_party/sqlite3.record_use_mapping.g.dart :

// AUTO-GENERATED FILE - DO NOT MODIFY.
// Generated via ffigen.

const recordUseMapping = {
  'sqlite3_libversion': 'sqlite3_libversion',
};

Définition de la spécification de construction de la bibliothèque C

Définissez la spécification centralisée de la bibliothèque dans lib/src/c_library.dart :

import 'package:native_toolchain_c/native_toolchain_c.dart';

/// The C build specification for the sqlite library.
final cLibrary = CLibrary(
  name: 'sqlite3',
  assetName: 'src/third_party/sqlite3.g.dart',
  sources: ['third_party/sqlite/sqlite3.c'],
);

Implémentation de hook/build.dart

Implémentez hook/build.dart en utilisant CLibrary.build. Cela construit la bibliothèque en une bibliothèque dynamique (par exemple .so, .dylib ou .dll) dans le répertoire cible du hook :

import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:sqlite/src/c_library.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    if (input.config.buildCodeAssets) {
      await cLibrary.build(
        input: input,
        output: output,
        defines: {
          if (input.config.code.targetOS == OS.windows)
            // Ensure C functions are explicitly exported in the Windows DLL
            'SQLITE_API': '__declspec(dllexport)',
        },
      );
    }
  });
}

Implémentation de hook/link.dart

Implémentez la phase d'optimisation de liaison dans hook/link.dart. Cela utilise les options d'arborescence du compilateur (LinkerOptions.treeshake) pour compiler un binaire minimisé, sans code mort, basé sur les enregistrements d'utilisation de symboles :

import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:record_use/record_use.dart';
import 'package:sqlite/src/c_library.dart';
import 'package:sqlite/src/third_party/sqlite3.record_use_mapping.g.dart';

void main(List<String> arguments) async {
  await link(arguments, (input, output) async {
    await cLibrary.link(
      input: input,
      output: output,
      linkerOptions: LinkerOptions.treeshake(
        // Map Dart Method references back to raw C symbol names
        symbolsToKeep: input.recordedUses?.calls.keys.cast<Method>().map(
          (e) => recordUseMapping[e.name]!,
        ),
      ),
    );
  });
}

Méthode 2 : Téléchargement de bibliothèques dynamiques précompilées

Une approche alternative compile les binaires au préalable sur une machine de construction centrale, les archive, et télécharge le binaire cible pendant l'exécution du hook de construction. Cela correspond au paradigme démontré dans le paquet du hook download_asset.

Pourquoi télécharger des binaires précompilés ?

  • Contraintes hôte : La compilation de grandes bibliothèques C/C++ localement nécessite une configuration de compilateur complète (GCC, Xcode/SDKs, Visual Studio) que la machine hôte du développeur final ne possède peut-être pas.
  • Vitesse de compilation : Les téléchargements précompilés s'exécutent en millisecondes par rapport à des processus de compilation potentiellement longs de plusieurs minutes.
  • Passerelle entre plateformes : Permet d'éviter les contraintes de compilation croisée si les architectures hôte sont limitées.

Implémentation des téléchargements dynamiques précompilés

Nous configurons notre hook de construction pour détecter les indicateurs de compilateur local (par exemple local_build). S'il n'est pas spécifié, le hook utilise HttpClient pour extraire les bibliothèques spécifiques à la plateforme, calcule le hachage MD5 pour confirmer la sécurité du téléchargement par rapport à une table de consultation de hachages configurée, et enregistre le fichier binaire en tant que CodeAsset :

1. Définition des hachages cibles (lib/src/hook_helpers/hashes.dart)

Définissez les vérifications de hachage MD5 cibles par fichier de plateforme dans vos sources de paquet :

const assetHashes = {
  'libnative_add_macos_arm64.dylib': '4a88f50438a98402db2dbd47b59eb412',
  'libnative_add_linux_x64.so': '9f5e15043aa98402dcdbbd47b59ea520',
  'native_add_windows_x64.dll': 'a881e5043ba98402acdebd47b59fa321',
};

2. Assistante de téléchargement de hook (lib/src/hook_helpers/download.dart)

Implémentez la logique de téléchargement et de vérification d'intégrité en utilisant la correspondance dynamique de nom de fichier cible :

import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart';

const version = '1.0.0';

Uri downloadUri(String target) => Uri.parse(
  'https://github.com/my-org/my-native-repo/releases/download/$version/$target',
);

Future<File> downloadAsset(
  OS targetOS,
  Architecture targetArchitecture,
  Directory outputDir,
) async {
  final fileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArchitecture.name}');
  final uri = downloadUri(fileName);

  final client = HttpClient()..findProxy = HttpClient.findProxyFromEnvironment;
  final request = await client.getUrl(uri);
  final response = await request.close();

  if (response.statusCode != 200) {
    throw ArgumentError('Download target $uri failed: Code ${response.statusCode}');
  }

  final targetFile = File.fromUri(outputDir.uri.resolve(fileName));
  await targetFile.create(recursive: true);
  await response.pipe(targetFile.openWrite());

  return targetFile;
}

Future<String> hashAsset(File file) async {
  return md5.convert(await file.readAsBytes()).toString();
}

3. Implémentation de hook/build.dart

Écrivez le hook de construction de téléchargement final intégrant la solution de secours de compilation locale :

import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:my_download_package/src/hook_helpers/hashes.dart';
import 'package:my_download_package/src/hook_helpers/download.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    final localBuild = input.userDefines['local_build'] as bool? ?? false;

    if (localBuild) {
      final name = 'native_add_${input.config.code.targetOS.name}_${input.config.code.targetArchitecture.name}';
      final builder = CBuilder.library(
        name: name,
        assetName: 'native_add.dart',
        sources: ['src/native_add.c'],
      );
      await builder.run(input: input, output: output);
    } else {
      final targetOS = input.config.code.targetOS;
      final targetArch = input.config.code.targetArchitecture;
      final outputDir = Directory.fromUri(input.outputDirectory);

      final file = await downloadAsset(targetOS, targetArch, outputDir);

      final fileHash = await hashAsset(file);
      final expectedFileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArch.name}');
      final expectedHash = assetHashes[expectedFileName];

      if (fileHash != expectedHash) {
        throw Exception(
          'Security Mismatch: File $expectedFileName hash verification failed! '
          'Found hash: $fileHash, expected: $expectedHash.'
        );
      }

      output.assets.code.add(
        CodeAsset(
          package: input.packageName,
          name: 'native_add.dart',
          linkMode: DynamicLoadingBundled(),
          file: file.uri,
        ),
      );
    }
  });
}

Liste de vérification

Avant de déclarer complète une implémentation de hook de construction ou de liaison, effectuez toujours les vérifications suivantes :

1. Bac à sable d'exécution local

Exécutez les tests unitaires et confirmez que le processus de compilation/liaison d'assets natifs se termine sans exception d'exécution ou d'outil de construction :

dart test

2. Vérifier les résultats cibles

Naviguez vers le répertoire cible de votre paquet et vérifiez que les assets binaires dynamiques sont créés pour le système hôte :

  • macOS : Vérifiez que .dart_tool/resources/ ou les répertoires cibles contiennent des fichiers .dylib.
  • Linux : Vérifiez que .dart_tool/resources/ ou les répertoires cibles contiennent des fichiers .so.
  • Windows : Vérifiez que .dart_tool/resources/ ou les répertoires cibles contiennent des fichiers .dll.

3. Vérifier l'élimination de l'arborescence

Pour garantir que le hook de liaison élimine effectivement les symboles natifs inutilisés et compresse l'empaquetage binaire, effectuez la validation suivante :

  1. Compilez un ensemble de production de la CLI/app :
    dart build cli bin/main.dart
  2. Naviguez vers le répertoire de compilation contenant la bibliothèque dynamique.
  3. Interrogez les tables de symboles dynamiques exportées :
    • macOS :
      nm -gU build/cli/lib/libsqlite3.dylib
    • Linux :
      nm -D build/cli/lib/libsqlite3.so
    • Windows (en utilisant l'invite de commande de développeur MSVC) :
      dumpbin /EXPORTS build\cli\lib\sqlite3.dll
  4. Confirmer les exports cibles : Vérifiez que la commande génère uniquement les fonctions de point d'entrée explicitement conservées (par exemple sqlite3_libversion) et ne génère aucun symbole non référencé/éliminé.
  5. Scénario sans ensemble : Si l'application n'importe ni n'invoque aucune méthode de la bibliothèque native :
    • Vérifiez que le hook de liaison enregistre : Skipping linking as no symbols are to be kept.
    • Vérifiez qu'aucune bibliothèque n'a été construite/placée dans l'ensemble de production (le fichier .dylib/.so/.dll n'est pas généré, économisant la taille de l'ensemble).

4. Vérifier la conformité hors ligne (Définitions utilisateur)

Confirmez que la conformité hors ligne est entièrement active et que la solution de secours de téléchargement s'exécute parfaitement hors ligne :

  1. Configurez la définition local_build: true pour votre paquet dans le pubspec.yaml du paquet (ou le pubspec.yaml racine de l'espace de travail) :
    hooks:
      user_defines:
        <your_package_name>:
          local_build: true
  2. Désactivez l'adaptateur réseau de la machine ou exécutez dans un shell bac à sable hors ligne.
  3. Lancez les tests unitaires :
    dart test
  4. Vérifiez que la suite de tests compile avec succès les fichiers source locaux en utilisant les compilateurs hôte, n'a aucune erreur de compilation et ne tente jamais de demandes de téléchargement réseau.

Skills similaires