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
- Contraintes
- Paquets Native Interop
- Flux de travail étape par étape
- Choix d'une approche d'intégration
- Méthode 1 : Compilation locale avec arborescence de liaison optimisée (Recommandée)
- Méthode 2 : Téléchargement de bibliothèques dynamiques précompilées
- Liste de vérification
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 :
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.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 surUripour 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 exempleCBuilderetCLibrary) pour exécuter les chaînes d'outils de compilation. N'invoquez jamais les commandesgcc,clangoumsvcvia 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 souslib/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 :
- Compilez un ensemble de production de la CLI/app :
dart build cli bin/main.dart - Naviguez vers le répertoire de compilation contenant la bibliothèque dynamique.
- 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
- macOS :
- 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é. - 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/.dlln'est pas généré, économisant la taille de l'ensemble).
- Vérifiez que le hook de liaison enregistre :
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 :
- Configurez la définition
local_build: truepour votre paquet dans lepubspec.yamldu paquet (ou lepubspec.yamlracine de l'espace de travail) :hooks: user_defines: <your_package_name>: local_build: true - Désactivez l'adaptateur réseau de la machine ou exécutez dans un shell bac à sable hors ligne.
- Lancez les tests unitaires :
dart test - 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.