1. Introduction
L'interface de fonction étrangère (FFI, Foreign Function Interface) de Dart permet aux applications Flutter d'utiliser les bibliothèques natives qui divulguent une API C. Dart accepte les FFI sous Android, iOS, Windows, macOS et Linux. Pour le Web, Dart accepte l'interopérabilité JavaScript, même si le sujet n'est pas abordé dans cet atelier de programmation.
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez développer un plug-in pour mobiles et ordinateurs qui utilise une bibliothèque C. Dans cette API, vous allez écrire une application exemple simple qui utilise le plug-in. Le plug-in et l'application vont permettre :
- d'importer le code source de la bibliothèque C dans le nouveau plug-in Flutter ;
- de personnaliser le plug-in pour l'intégrer à Windows, macOS, Linux, Android et iOS ;
- de développer une application qui utilise le plug-in avec une boucle de lecture-évaluation-impression (REPL, Read Reveal Print Loop) JavaScript.
Points abordés
Dans cet atelier de programmation, vous allez apprendre à développer un plug-in Flutter avec FFI sur les plates-formes pour mobiles et ordinateurs, y compris à :
- générer un modèle de plug-in Flutter avec FFI Dart ;
- utiliser le package
ffigen
pour générer un code de liaison pour une bibliothèque C ; - utiliser CMake pour développer un plug-in Flutter avec FFI pour Android, Windows et Linux ;
- utiliser CocoaPods pour développer un plug-in Flutter avec FFI pour iOS et macOS
Ce dont vous aurez besoin
- Android Studio 4.1 ou version ultérieure pour le développement sous Android
- Xcode 13 ou version ultérieure pour le développement sous iOS et macOS
- Visual Studio 2022 ou Visual Studio Build Tools 2022 pour le développement sur ordinateur avec charge de travail C++ pour le développement sous Windows
- Le SDK Flutter
- Les outils de création nécessaires pour les plates-formes visées par le développement (par exemple, CMake, CocoaPods, etc.)
- LLVM pour les plates-formes visées par le développement.
ffigen
utilise la suite de compilateurs LLVM pour analyser le fichier d'en-tête C et créer la liaison FFI divulguée dans Dart. - Un éditeur de code, comme Visual Studio Code.
2. Commencer
L'outil ffigen
est un ajout récent de Flutter. Pour vérifier que votre installation Flutter utilise la version stable actuelle, exécutez la commande suivante :
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.3.9, on macOS 13.1 22C65 darwin-arm, locale en) [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 14.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.2) [✓] IntelliJ IDEA Community Edition (version 2022.2.2) [✓] VS Code (version 1.74.0) [✓] Connected device (2 available) [✓] HTTP Host Availability • No issues found!
Vérifiez que le résultat de flutter doctor
indique que vous utilisez un canal stable et qu'il n'existe aucune version stable plus récente de Flutter. Si le canal n'est pas stable ou qu'il existe une version plus récente de Flutter, exécutez les deux commandes suivantes pour mettre l'outil à jour.
$ flutter channel stable $ flutter upgrade
Dans cet atelier de programmation, vous pouvez exécuter le code avec l'un des appareils suivants :
- Votre ordinateur de développement (pour créer le plug-in et l'application exemple)
- Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur
- Le simulateur iOS (outils Xcode à installer)
- Android Emulator (à configurer dans Android Studio)
3. Générer le modèle de plug-in
Premiers pas avec le développement de plug-ins Flutter
Flutter est fourni avec des modèles de plug-ins qui permettent de commencer facilement. Lorsque vous générez le modèle de plug-in, vous pouvez spécifier le langage à utiliser.
Exécutez la commande suivante dans le répertoire de travail pour créer votre projet avec le modèle de plug-in :
$ flutter create --template=plugin_ffi \ --platforms=android,ios,linux,macos,windows ffigen_app
Le paramètre --platforms
définit les plates-formes compatibles avec votre plug-in.
Vous pouvez contrôler la mise en page du projet généré avec la commande tree
ou l'explorateur de fichiers de votre système d'exploitation.
$ tree -L 2 ffigen_app ffigen_app ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android │ ├── build.gradle │ ├── ffigen_app_android.iml │ ├── local.properties │ ├── settings.gradle │ └── src ├── example │ ├── README.md │ ├── analysis_options.yaml │ ├── android │ ├── ffigen_app_example.iml │ ├── ios │ ├── lib │ ├── linux │ ├── macos │ ├── pubspec.lock │ ├── pubspec.yaml │ └── windows ├── ffigen.yaml ├── ffigen_app.iml ├── ios │ ├── Classes │ └── ffigen_app.podspec ├── lib │ ├── ffigen_app.dart │ └── ffigen_app_bindings_generated.dart ├── linux │ └── CMakeLists.txt ├── macos │ ├── Classes │ └── ffigen_app.podspec ├── pubspec.lock ├── pubspec.yaml ├── src │ ├── CMakeLists.txt │ ├── ffigen_app.c │ └── ffigen_app.h └── windows └── CMakeLists.txt 17 directories, 26 files
Nous vous invitons à examiner la structure du répertoire pour avoir une idée des éléments créés et de leur emplacement. Le modèle plugin_ffi
place le code Dart associé au plug-in sous lib
. Il comporte les répertoires spécifiques aux plates-formes (appelés android
, ios
, linux
, macos
et windows
) et surtout, un répertoire example
.
Cette structure peut paraître étrange aux développeurs habitués à un développement Flutter normal, car aucun exécutable n'est défini au niveau supérieur. Même si le plug-in est destiné à intégrer d'autres projets Flutter, vous allez compléter le code dans le répertoire example
pour vous assurer qu'il fonctionne.
C'est parti !
4. Créer et exécuter l'exemple
Pour vous assurer que le système de compilation et les éléments prérequis sont correctement installés et opérationnels sur chaque plate-forme compatible, compilez et exécutez L'application exemple générée pour chaque cible.
Windows
Assurez-vous d'utiliser une version de Windows compatible. Cet atelier de programmation est réputé fonctionner sous Windows 10 et Windows 11.
Vous pouvez développer l'application depuis votre éditeur de code ou dans la ligne de commande.
PS C:\Users\brett\Documents> cd .\ffigen_app\example\ PS C:\Users\brett\Documents\ffigen_app\example> flutter run -d windows Launching lib\main.dart on Windows in debug mode...Building Windows application... Syncing files to device Windows... 160ms Flutter run key commands. r Hot reload. R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). Running with sound null safety An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:53317/OiKWpyHXxHI=/ The Flutter DevTools debugger and profiler on Windows is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:53317/OiKWpyHXxHI=/
La fenêtre de l'application en cours doit ressembler à ceci :
Linux
Assurez-vous d'utiliser une version de Linux compatible. Cet atelier de programmation utilise Ubuntu 22.04.1
.
Une fois que vous avez installé les éléments prérequis listés à l'étape 2, exécutez la commande suivante dans un terminal :
$ cd ffigen_app/example $ flutter run -d linux Launching lib/main.dart on Linux in debug mode... Building Linux application... Syncing files to device Linux... 504ms Flutter run key commands. r Hot reload. 🔥🔥🔥 R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). 💪 Running with sound null safety 💪 An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:36653/Wgek1JGag48=/ The Flutter DevTools debugger and profiler on Linux is available at: http://127.0.0.1:9103?uri=http://127.0.0.1:36653/Wgek1JGag48=/
La fenêtre de l'application en cours doit ressembler à ceci :
Android
Pour Android, vous pouvez utiliser Windows, macOS ou Linux pour la compilation. Tout d'abord, vérifiez que vous disposez d'un appareil Android qui est connecté à l'ordinateur de développement ou exécute une instance Android Emulator (AVD). Assurez-vous que Flutter peut se connecter à l'appareil Android ou à l'émulateur en exécutant la commande suivante :
$ flutter devices 3 connected devices: sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 12 (API 32) (emulator) macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
macOS et iOS
Avec le développement Flutter pour macOS et iOS, vous devez utiliser un ordinateur macOS.
Commencez par exécuter l'application exemple sous macOS. Vérifiez de nouveau les appareils auxquels Flutter peut se connecter :
$ flutter devices 2 connected devices: macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
Exécutez l'application exemple avec le projet de plug-in généré :
$ cd ffigen_app/example $ flutter run -d macos
La fenêtre de l'application en cours doit ressembler à ceci :
Pour iOS, vous pouvez utiliser le simulateur ou un véritable appareil physique. Pour utiliser le simulateur, vous devez d'abord le lancer. La commande flutter devices
liste maintenant le simulateur comme l'un des appareils disponibles.
$ flutter devices 3 connected devices: iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator) macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
Une fois que le simulateur a démarré, exécutez : flutter run
.
$ cd ffigen_app/example $ flutter run -d iphone
Le simulateur iOS prévaut sur la cible macOS. Vous pouvez donc ignorer l'étape de spécifications d'un appareil avec le paramètre -d
.
Félicitations, vous avez réussi à compiler et à créer une application sur cinq systèmes d'exploitation différents. Vous allez maintenant développer le plug-in natif et interagir avec lui dans Dart avec la FFI.
5. Utiliser Duktape sous Windows, Linux et Android
Dans cet atelier de programmation, vous allez utiliser la bibliothèque C Duktape. Duktape est un moteur JavaScript intégrable qui privilégie la portabilité et le format compact. Dans cette étape, vous allez configurer le plug-in pour compiler la bibliothèque Duktape, associer celle-ci au plug-in et y accéder via la FFI Dart.
Cette étape permet de configurer l'intégration pour travailler sous Windows, Linux et Android. L'intégration iOS et macOS exige une configuration supplémentaire (comparé à la procédure détaillée ici) pour inclure la bibliothèque compilée dans l'exécutable Flutter final. La prochaine étape décrit cette configuration supplémentaire.
Récupérer Duktape
Tout d'abord, récupérez une copie du code source de duktape
en le téléchargeant sur le site Web duktape.org.
Pour Windows, vous pouvez utiliser PowerShell avec Invoke-WebRequest
:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
Pour Linux, wget
constitue un choix adapté.
$ wget https://duktape.org/duktape-2.7.0.tar.xz --2022-12-22 16:21:39-- https://duktape.org/duktape-2.7.0.tar.xz Resolving duktape.org (duktape.org)... 104.198.14.52 Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1026524 (1002K) [application/x-xz] Saving to: ‘duktape-2.7.0.tar.xz' duktape-2.7.0.tar.x 100%[===================>] 1002K 1.01MB/s in 1.0s 2022-12-22 16:21:41 (1.01 MB/s) - ‘duktape-2.7.0.tar.xz' saved [1026524/1026524]
Le fichier est une archive tar.xz
. Sous Windows, vous avez la possibilité de télécharger l'outil 7Zip pour l'utiliser comme suit :
PS> 7z x .\duktape-2.7.0.tar.xz 7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15 Scanning the drive for archives: 1 file, 1026524 bytes (1003 KiB) Extracting archive: .\duktape-2.7.0.tar.xz -- Path = .\duktape-2.7.0.tar.xz Type = xz Physical Size = 1026524 Method = LZMA2:26 CRC64 Streams = 1 Blocks = 1 Everything is Ok Size: 19087360 Compressed: 1026524
Vous devez exécuter 7z à deux reprises, pour décompresser l'archive xz, puis pour développer l'archive tar.
PS> 7z x .\duktape-2.7.0.tar 7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15 Scanning the drive for archives: 1 file, 19087360 bytes (19 MiB) Extracting archive: .\duktape-2.7.0.tar -- Path = .\duktape-2.7.0.tar Type = tar Physical Size = 19087360 Headers Size = 543232 Code Page = UTF-8 Characteristics = GNU ASCII Everything is Ok Folders: 46 Files: 1004 Size: 18281564 Compressed: 19087360
Sur les environnements Linux modernes, tar
extrait les contenus en une seule fois, comme ceci :
$ tar xvf duktape-2.7.0.tar.xz x duktape-2.7.0/ x duktape-2.7.0/README.rst x duktape-2.7.0/Makefile.sharedlibrary x duktape-2.7.0/Makefile.coffee x duktape-2.7.0/extras/ x duktape-2.7.0/extras/README.rst x duktape-2.7.0/extras/module-node/ x duktape-2.7.0/extras/module-node/README.rst x duktape-2.7.0/extras/module-node/duk_module_node.h x duktape-2.7.0/extras/module-node/Makefile [... and many more files]
Installer LLVM
Pour utiliser ffigen
, vous devez installer LLVM que ffigen
utilise pour analyser les en-têtes C. Sous Windows, exécutez la commande suivante :
PS> winget install -e --id LLVM.LLVM Found LLVM [LLVM.LLVM] Version 15.0.5 This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages. Downloading https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.5/LLVM-15.0.5-win64.exe ██████████████████████████████ 277 MB / 277 MB Successfully verified installer hash Starting package install... Successfully installed
Configurez les chemins d'accès système pour ajouter C:\Program Files\LLVM\bin
au chemin de recherche binaire et finaliser l'installation de LLVM sur votre machine Windows. Vérifiez que l'installation s'est bien déroulée comme ceci :
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
Sous Ubuntu, la dépendance LLVM être installé comme suit. Les autres distributions Linux disposent de dépendances semblables pour LLVM et Clang.
$ sudo apt install libclang-dev [sudo] password for brett: Reading package lists... Done Building dependency tree... Done Reading state information... Done The following additional packages will be installed: libclang-15-dev The following NEW packages will be installed: libclang-15-dev libclang-dev 0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded. Need to get 26.1 MB of archives. After this operation, 260 MB of additional disk space will be used. Do you want to continue? [Y/n] y Get:1 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-15-dev amd64 1:15.0.2-1 [26.1 MB] Get:2 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-dev amd64 1:15.0-55.1ubuntu1 [2962 B] Fetched 26.1 MB in 7s (3748 kB/s) Selecting previously unselected package libclang-15-dev. (Reading database ... 85898 files and directories currently installed.) Preparing to unpack .../libclang-15-dev_1%3a15.0.2-1_amd64.deb ... Unpacking libclang-15-dev (1:15.0.2-1) ... Selecting previously unselected package libclang-dev. Preparing to unpack .../libclang-dev_1%3a15.0-55.1ubuntu1_amd64.deb ... Unpacking libclang-dev (1:15.0-55.1ubuntu1) ... Setting up libclang-15-dev (1:15.0.2-1) ... Setting up libclang-dev (1:15.0-55.1ubuntu1) ...
Comme précédemment, vous pouvez tester l'installation de LLVM sous Linux comme ceci :
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
Configurer ffigen
Le pubpsec.yaml
de niveau supérieur généré par le modèle peut comporter une version obsolète du package ffigen
. Exécutez la commande suivante pour mettre à jour les dépendances Dart dans le projet de plug-in :
$ flutter pub upgrade --major-versions
Le package ffigen
est désormais à jour. vous devez maintenant configurer les fichiers que ffigen
va utiliser pour générer les fichiers de liaison. Modifiez le contenu du fichier ffigen.yaml
du projet pour qu'il corresponde à ce qui suit.
ffigen.yaml
# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
Bindings for `src/duktape.h`.
Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
output: 'lib/duktape_bindings_generated.dart'
headers:
entry-points:
- 'src/duktape.h'
include-directives:
- 'src/duktape.h'
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full
Cette configuration comprend le fichier d'en-tête C à transmettre à LLVM, le fichier de sortie à générer, la description à insérer en haut du fichier, ainsi que le préambule à utiliser pour ajouter un avertissement d'analyse lint. Consultez la documentation de ffigen
pour en savoir plus sur les clés et les valeurs.
Vous devez copier les fichiers Duktape spécifiques de la distribution Duktape dans l'emplacement indiqué à cet effet dans la configuration de ffigen
.
$ cp duktape-2.7.0/src/duktape.c src/ $ cp duktape-2.7.0/src/duktape.h src/ $ cp duktape-2.7.0/src/duk_config.h src/
En pratique, la copie ne concerne que duktape.h
pour ffigen
. Cependant, vous allez configurer CMake pour créer une bibliothèque qui utilise les trois. Exécutez ffigen
pour générer la nouvelle liaison :
$ flutter pub run ffigen --config ffigen.yaml Running in Directory: '/home/brett/GitHub/codelabs/ffigen_codelab/step_05' Input Headers: [./src/duktape.h] [WARNING]: No definition found for declaration - (Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread [WARNING]: No definition found for declaration - (Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread [WARNING]: Generated declaration '__va_list_tag' start's with '_' and therefore will be private. Finished, Bindings generated in /home/brett/GitHub/codelabs/ffigen_codelab/step_05/./lib/duktape_bindings_generated.dart
Différents avertissements s'affichent pour chaque système d'exploitation. Vous pouvez les ignorer pour le moment (Duktape 2.7.0 est connu pour effectuer une compilation avec clang
sous Windows, Linux et macOS).
Configurer CMake
CMake est un outil de génération de systèmes de compilation. Le plug-in utilise CMake pour créer le système de compilation pour Android, Windows et Linux, et intégrer Duktape dans le fichier binaire Flutter généré. Vous devez modifier le fichier de configuration CMake généré par le modèle comme ceci :
src/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ffigen_app_library VERSION 0.0.1 LANGUAGES C)
add_library(ffigen_app SHARED
duktape.c # Modify
)
set_target_properties(ffigen_app PROPERTIES
PUBLIC_HEADER duktape.h # Modify
PRIVATE_HEADER duk_config.h # Add
OUTPUT_NAME "ffigen_app" # Add
)
# Add from here...
if (WIN32)
set_target_properties(ffigen_app PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif (WIN32)
# ... to here.
target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)
La configuration CMake ajoute les fichiers sources, mais modifie surtout le comportement par défaut du fichier binaire généré sous Windows pour exporter l'ensemble des symboles C par défaut. Cette technique CMake permet de transposer les bibliothèques de type Unix (comme Duktape) dans l'univers Windows.
Remplacez le contenu de lib/ffigen_app.dart
par le code suivant :
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart' as ffi;
import 'duktape_bindings_generated.dart';
const String _libName = 'ffigen_app';
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final DuktapeBindings _bindings = DuktapeBindings(_dylib);
class Duktape {
Duktape() {
ctx =
_bindings.duk_create_heap(nullptr, nullptr, nullptr, nullptr, nullptr);
}
void evalString(String jsCode) {
var nativeUtf8 = jsCode.toNativeUtf8();
_bindings.duk_eval_raw(
ctx,
nativeUtf8.cast<Char>(),
0,
0 |
DUK_COMPILE_EVAL |
DUK_COMPILE_SAFE |
DUK_COMPILE_NOSOURCE |
DUK_COMPILE_STRLEN |
DUK_COMPILE_NOFILENAME);
ffi.malloc.free(nativeUtf8);
}
int getInt(int index) {
return _bindings.duk_get_int(ctx, index);
}
void dispose() {
_bindings.duk_destroy_heap(ctx);
ctx = nullptr;
}
late Pointer<duk_hthread> ctx;
}
Ce fichier est responsable de charger le fichier bibliothèque de liens dynamiques (.so
pour Linux et Android, .dll
pour Windows) et de fournir un wrapper qui divulgue une interface idiomatique Dart renforcée au code C sous-jacent.
Remplacez le contenu de l'exemple de main.dart
par ceci :
example/lib/main.dart
import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
const String jsCode = '1+2';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Duktape duktape;
String output = '';
@override
void initState() {
super.initState();
duktape = Duktape();
setState(() {
output = 'Initialized Duktape';
});
}
@override
void dispose() {
duktape.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 25);
const spacerSmall = SizedBox(height: 10);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Duktape Test'),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
output,
style: textStyle,
textAlign: TextAlign.center,
),
spacerSmall,
ElevatedButton(
child: const Text('Run JavaScript'),
onPressed: () {
duktape.evalString(jsCode);
setState(() {
output = '$jsCode => ${duktape.getInt(-1)}';
});
},
),
],
),
),
),
),
);
}
}
Vous pouvez de nouveau exécuter l'application exemple avec ce qui suit :
$ cd example $ flutter run
L'application doit s'exécuter comme ceci :
Ces deux captures d'écran représentent l'application avant et après avoir appuyé sur le bouton Run JavaScript (Exécuter JavaScript). Elles représentent l'exécution du code JavaScript depuis Dart, ainsi que les résultats à l'écran.
Android
Android est un OS avec noyau Linux qui s'apparente, d'une certaine façon, aux distributions Linux pour ordinateurs. Le système de compilation CMake peut masquer la plupart des différences entre les deux plates-formes. Pour compiler et exécuter sous Android, assurez-vous qu'Android Emulator fonctionne (ou que l'appareil Android est connecté). Exécutez l'application. Par exemple :
$ cd example $ flutter run -d emulator-5554
L'application doit maintenant s'exécuter sous Android comme ceci :
6. Utiliser Duktape sous macOS et iOS
Vous allez maintenant rendre le plug-in opérationnel sous macOS et iOS, deux systèmes d'exploitation intimement liés. Commencez avec macOS. Même si CMake est compatible avec macOS et iOS, vous n'allez pas réutiliser le travail réalisé pour Linux et Android. En effet, sous macOS et iOS, Flutter utilisent CocoaPods pour importer les bibliothèques.
Nettoyage
À l'étape précédente, vous avez créé une application opérationnelle sous Android, Windows et Linux. Toutefois, certains fichiers ont été laissés par le modèle d'origine. Vous allez maintenant les nettoyer. Pour les supprimer, procédez comme suit :
$ rm src/ffigen_app.c $ rm src/ffigen_app.h $ rm ios/Classes/ffigen_app.c $ rm macos/Classes/ffigen_app.c
macOS
Sur la plate-forme macOS, Flutter utilise CocoaPods pour importer les codes C et C++. Vous devez donc intégrer ce package à l'infrastructure de compilation CocoaPods. Pour réutiliser le code C que vous avez configuré pour compiler avec CMake à l'étape précédente, vous devez ajouter un fichier de transfert simple à l'exécuteur de la plate-forme macOS.
macos/Classes/duktape.c
#include "../../src/duktape.c"
Le fichier utilise la puissance du préprocesseur C pour intégrer le code source à partir du code source natif configuré à l'étape précédente. Pour en savoir plus sur ce processus, consultez macos/ffigen_app.podspec.
L'exécution de l'application utilise désormais le même schéma que celui employé pour Windows et Linux.
$ cd example $ flutter run -d macos
iOS
Comme pour la configuration macOS, vous devez ajouter un fichier de transfert simple C avec iOS.
ios/Classes/duktape.c
#include "../../src/duktape.c"
Avec ce fichier simple, le plug-in est maintenant configuré pour s'exécuter sous iOS. Exécutez-le comme d'habitude.
$ flutter run -d iPhone
Félicitations ! Vous avez intégré un code natif sur cinq plates-formes. Vous pouvez vous réjouir et envisager de créer une interface utilisateur plus fonctionnelle à l'étape suivante.
7. Implémenter une boucle de lecture-évaluation-impression
Interagir avec un langage de programmation se révèle bien plus amusant dans un environnement réactif. À l'origine, l'implémentation d'un environnement de ce type consistait en une boucle de lecture-évaluation-impression (REPL, Read Reveal Print Loop) LISP. Pour cette étape, vous allez implémenter une solution semblable avec Duktape.
Se préparer pour la production
Le code actuel qui interagit avec la bibliothèque C de Duktape C suppose qu'aucune erreur ne peut se produire. Par ailleurs, il ne charge pas les bibliothèques de liens dynamiques Duktape pendant le test. Pour que l'intégration soit prête pour la production, vous devez apporter plusieurs modifications à lib/ffigen_app.dart
.
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p; // Add this import
import 'duktape_bindings_generated.dart';
const String _libName = 'ffigen_app';
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open('build/macos/Build/Products/Debug'
'/$_libName/$_libName.framework/$_libName');
}
// ...to here.
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(
'build/linux/x64/debug/bundle/lib/lib$_libName.so');
}
// ...to here.
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(p.canonicalize(
p.join(r'build\windows\runner\Debug', '$_libName.dll')));
}
// ...to here.
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final DuktapeBindings _bindings = DuktapeBindings(_dylib);
class Duktape {
Duktape() {
ctx =
_bindings.duk_create_heap(nullptr, nullptr, nullptr, nullptr, nullptr);
}
// Modify this function
String evalString(String jsCode) {
var nativeUtf8 = jsCode.toNativeUtf8();
final evalResult = _bindings.duk_eval_raw(
ctx,
nativeUtf8.cast<Char>(),
0,
0 |
DUK_COMPILE_EVAL |
DUK_COMPILE_SAFE |
DUK_COMPILE_NOSOURCE |
DUK_COMPILE_STRLEN |
DUK_COMPILE_NOFILENAME);
ffi.malloc.free(nativeUtf8);
if (evalResult != 0) {
throw _retrieveTopOfStackAsString();
}
return _retrieveTopOfStackAsString();
}
// Add this function
String _retrieveTopOfStackAsString() {
Pointer<Size> outLengthPtr = ffi.calloc<Size>();
final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
final returnVal =
errorStrPtr.cast<ffi.Utf8>().toDartString(length: outLengthPtr.value);
ffi.calloc.free(outLengthPtr);
return returnVal;
}
void dispose() {
_bindings.duk_destroy_heap(ctx);
ctx = nullptr;
}
late Pointer<duk_hthread> ctx;
}
Vous avez étendu le code pour charger une bibliothèque de liens dynamiques de sorte à gérer les cas où le plug-in est utilisé dans un exécuteur de test. Cela permet d'écrire un test d'intégration qui essaie l'API sous la forme d'un test Flutter. Vous avez étendu la chaîne de code JavaScript de sorte à gérer correctement les conditions d'erreur (lorsque le code est incomplet ou incorrect, par exemple). Ce code supplémentaire indique comment gérer les cas où les chaînes sont renvoyées sous la forme de tableaux d'octets et doivent être converties en chaînes Dart.
Ajouter des packages
Lorsque vous créez une REPL, une interaction entre l'utilisateur et le moteur JavaScript de Duktape apparaît. L'utilisateur saisit des lignes de code et Duktape répond en générant les résultats du calcul ou une exception. Vous allez utiliser freezed
pour réduire le volume de code récurrent à écrire. Vous allez également utiliser google_fonts
pour thématiser davantage le contenu affiché et flutter_riverpod
pour gérer les états.
Ajoutez les dépendances nécessaires à l'application exemple :
$ cd example $ flutter pub add flutter_riverpod freezed_annotation google_fonts $ flutter pub add -d build_runner freezed
Créez ensuite un fichier pour enregistrer les interactions de la REPL :
example/lib/duktape_message.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'duktape_message.freezed.dart';
@freezed
class DuktapeMessage with _$DuktapeMessage {
factory DuktapeMessage.evaluate(String code) = DuktapeMessageCode;
factory DuktapeMessage.response(String result) = DuktapeMessageResponse;
factory DuktapeMessage.error(String log) = DuktapeMessageError;
}
La classe utilise les types d'union de freezed
pour exprimer facilement la forme de chaque ligne affichée dans la REPL comme appartenant à l'un de ces trois types. À cette étape, le code affiche probablement une erreur, car un code supplémentaire doit encore être généré. Pour ce faire, procédez comme suit :
$ flutter pub run build_runner build
Cela permet de générer le fichier example/lib/duktape_message.freezed.dart
qui repose sur le code que vous venez de saisir.
Vous devez ensuite apporter deux modifications aux fichiers de configuration macOS afin que google_fonts
puisse effectuer des requêtes réseau pour les données de police.
example/macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- ...to here -->
</dict>
</plist>
example/macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- ...to here -->
</dict>
</plist>
Compiler la REPL
Vous avez mis à jour la couche d'intégration pour gérer les erreurs et développé une représentation des données de l'interaction. Vous allez maintenant créer l'interface utilisateur de l'application exemple.
example/lib/main.dart
import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'duktape_message.dart';
void main() {
runApp(const ProviderScope(child: DuktapeApp()));
}
final duktapeMessagesProvider =
StateNotifierProvider<DuktapeMessageNotifier, List<DuktapeMessage>>((ref) {
return DuktapeMessageNotifier(messages: <DuktapeMessage>[]);
});
class DuktapeMessageNotifier extends StateNotifier<List<DuktapeMessage>> {
DuktapeMessageNotifier({required List<DuktapeMessage> messages})
: duktape = Duktape(),
super(messages);
final Duktape duktape;
void eval(String code) {
state = [
DuktapeMessage.evaluate(code),
...state,
];
try {
final response = duktape.evalString(code);
state = [
DuktapeMessage.response(response),
...state,
];
} catch (e) {
state = [
DuktapeMessage.error('$e'),
...state,
];
}
}
}
class DuktapeApp extends StatelessWidget {
const DuktapeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Duktape App',
home: DuktapeRepl(),
);
}
}
class DuktapeRepl extends ConsumerStatefulWidget {
const DuktapeRepl({
super.key,
});
@override
ConsumerState<DuktapeRepl> createState() => _DuktapeReplState();
}
class _DuktapeReplState extends ConsumerState<DuktapeRepl> {
final _controller = TextEditingController();
final _focusNode = FocusNode();
var _isComposing = false;
void _handleSubmitted(String text) {
_controller.clear();
setState(() {
_isComposing = false;
});
setState(() {
ref.read(duktapeMessagesProvider.notifier).eval(text);
});
_focusNode.requestFocus();
}
@override
Widget build(BuildContext context) {
final messages = ref.watch(duktapeMessagesProvider);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Duktape REPL'),
elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
),
body: Column(
children: [
Flexible(
child: Ink(
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
bottom: false,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (context, idx) => messages[idx].when(
evaluate: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'> $str',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
),
),
),
response: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'= $str',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
color: Colors.blue[800],
),
),
),
error: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
str,
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleSmall,
color: Colors.red[800],
fontWeight: FontWeight.bold,
),
),
),
),
itemCount: messages.length,
),
),
),
),
const Divider(height: 1.0),
SafeArea(
top: false,
child: Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
),
],
),
);
}
Widget _buildTextComposer() {
return IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text('>', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(width: 4),
Flexible(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
border: InputBorder.none,
),
onChanged: (text) {
setState(() {
_isComposing = text.isNotEmpty;
});
},
onSubmitted: _isComposing ? _handleSubmitted : null,
focusNode: _focusNode,
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: _isComposing
? () => _handleSubmitted(_controller.text)
: null,
),
),
],
),
),
);
}
}
Ce code offre de nombreuses possibilités dont l'explication ne relève toutefois pas de cet atelier de programmation. Nous vous invitons à exécuter le code et à le modifier après avoir consulté la documentation correspondante.
$ cd example $ flutter run
8. Félicitations
Félicitations ! Vous avez créé un plug-in Flutter avec FFI pour Windows, macOS, Linux, Android et iOS.
Après avoir créé votre plug-in, vous pouvez le partager en ligne afin que d'autres personnes puissent l'utiliser. Vous trouverez la documentation complète sur la procédure de publication de votre plug-in sur pub.dev dans la section Developing plugin packages (Développer des packages de plug-ins).