Utiliser une FFI dans un plug-in Flutter

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 :

76b496eb58ef120a.png

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 :

3e0aca5027bf9ee5.png

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 :

d2298ee958814232.png

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

5616e9d659614460.png

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 :

808f738662f4a43.png

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

d39c62d1959718cd.png

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).