Utilizzo di FFI in un plug-in Flutter

1. Introduzione

L'FFI (Foreign Functions Interface) di Dart consente alle app Flutter di utilizzare librerie native esistenti che espongono un'API C. Dart supporta FFI su Android, iOS, Windows, macOS e Linux. Per il web, Dart supporta l'interoperabilità JavaScript, ma questo argomento non è trattato in questo codelab.

Cosa creerai

In questo codelab, creerai un plug-in mobile e desktop che utilizza una libreria C. Con questa API, scriverai una semplice app di esempio che utilizza il plug-in. Il plug-in e l'app:

  • Importa il codice sorgente della libreria C nel nuovo plug-in Flutter
  • Personalizza il plug-in per consentirne la creazione su Windows, macOS, Linux, Android e iOS
  • Creare un'applicazione che utilizzi il plug-in per un REPL (read Detect Loop di stampa) JavaScript.

Duktape REPL in esecuzione come applicazione macOS

Cosa imparerai a fare

In questo codelab apprenderai le conoscenze pratiche necessarie per creare un plug-in Flutter basato su FFI su piattaforme desktop e mobili, tra cui:

  • Generazione di un modello di plug-in Flutter basato su Dart FFI
  • Utilizzo del pacchetto ffigen per generare un codice di associazione per una libreria C
  • Utilizzo di CMake per creare un plug-in FFI Flutter per Android, Windows e Linux
  • Utilizzo di CocoaPods per creare un plug-in FFI Flutter per iOS e macOS

Che cosa ti serve

  • Android Studio 4.1 o versioni successive per sviluppo Android
  • Xcode 13 o versioni successive per sviluppo iOS e macOS
  • Visual Studio 2022 o Visual Studio Build Tools 2022 con "Sviluppo desktop con C++" carico di lavoro per lo sviluppo desktop di Windows
  • SDK Flutter
  • Eventuali strumenti di creazione necessari per le piattaforme su cui intendi sviluppare (ad esempio, CMake, CocoaPods e così via).
  • LLVM per le piattaforme su cui realizzerai lo sviluppo. La suite di strumenti di compilazione LLVM viene utilizzata da ffigen per analizzare il file di intestazione C e creare l'associazione FFI esposta in Dart.
  • Un editor di codice, ad esempio Visual Studio Code.

2. Per iniziare

Gli strumenti di ffigen sono stati aggiunti di recente a Flutter. Puoi confermare che la tua installazione di Flutter stia eseguendo la release stabile corrente eseguendo questo comando.

$ 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!

Verifica che l'output flutter doctor indichi che ti trovi sul canale stabile e che non siano disponibili release di Flutter stabili più recenti. Se non stai utilizzando la versione stabile o sono disponibili release più recenti, esegui i due comandi seguenti per ottimizzare gli strumenti di Flutter.

$ flutter channel stable
$ flutter upgrade

Puoi eseguire il codice in questo codelab utilizzando uno di questi dispositivi:

  • Il tuo computer di sviluppo (per le build desktop del tuo plug-in e dell'app di esempio)
  • Un dispositivo fisico Android o iOS collegato al computer e impostato sulla modalità sviluppatore
  • Il simulatore iOS (richiede l'installazione degli strumenti Xcode)
  • L'emulatore Android (richiede la configurazione in Android Studio)

3. Genera il modello di plug-in

Introduzione allo sviluppo del plug-in Flutter

Flutter include modelli per i plug-in che semplificano l'avvio. Quando generi il modello di plug-in, puoi specificare il linguaggio che desideri utilizzare.

Esegui il comando seguente nella directory di lavoro per creare il progetto utilizzando il modello di plug-in:

$ flutter create --template=plugin_ffi \
  --platforms=android,ios,linux,macos,windows ffigen_app

Il parametro --platforms specifica le piattaforme supportate dal plug-in.

Puoi esaminare il layout del progetto generato utilizzando il comando tree o lo strumento Esplora file del tuo sistema operativo.

$ 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

Vale la pena dedicare un momento a esaminare la struttura della directory per avere un'idea di cosa è stato creato e di dove si trova. Il modello plugin_ffi inserisce il codice Dart per il plug-in in lib, directory specifiche della piattaforma denominate android, ios, linux, macos e windows e, soprattutto, in una directory example.

Per uno sviluppatore abituato al normale sviluppo di Flutter, questa struttura potrebbe sembrare strana, in quanto non esiste un eseguibile definito al livello più alto. Un plug-in è pensato per essere incluso in altri progetti Flutter, ma darai dettagli al codice nella directory example per assicurarti che il codice del plug-in funzioni.

È ora di iniziare.

4. Crea ed esegui l'esempio

Per assicurarti che il sistema di compilazione e i prerequisiti siano installati correttamente e funzionino per ogni piattaforma supportata, crea ed esegui l'app di esempio generata per ogni destinazione.

Windows

Assicurati di utilizzare una versione supportata di Windows. È noto che questo codelab funziona su Windows 10 e Windows 11.

Puoi creare l'applicazione dall'editor di codice o dalla riga di comando.

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=/

Dovresti vedere una finestra dell'app in esecuzione come la seguente:

App FFI generata da modello eseguita come app Windows

Linux

Assicurati di utilizzare una versione supportata di Linux. Questo codelab utilizza Ubuntu 22.04.1.

Dopo aver installato tutti i prerequisiti elencati nel passaggio 2, esegui i comandi seguenti in un terminale:

$ 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=/

Dovresti vedere una finestra dell'app in esecuzione come la seguente:

App FFI generata da modello eseguita come applicazione Linux

Android

Per Android puoi utilizzare Windows, macOS o Linux per la compilazione. Innanzitutto, assicurati di avere un dispositivo Android collegato al tuo computer di sviluppo o di eseguire un'istanza di Android Emulator (AVD). Verifica che Flutter sia in grado di connettersi all'emulatore o al dispositivo Android eseguendo questo comando:

$ 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

App FFI generata da modello in esecuzione in un emulatore Android

macOS e iOS

Per lo sviluppo di macOS e iOS Flutter, è necessario utilizzare un computer macOS.

Inizia eseguendo l'app di esempio su macOS. Conferma di nuovo i dispositivi visualizzati da Flutter:

$ 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

Esegui l'app di esempio utilizzando il progetto plug-in generato:

$ cd ffigen_app/example
$ flutter run -d macos

Dovresti vedere una finestra dell'app in esecuzione come la seguente:

App FFI generata da modello eseguita come applicazione Linux

Per iOS, puoi utilizzare il simulatore o un dispositivo hardware reale. Se usi il simulatore, prima avvialo. Il comando flutter devices ora elenca il simulatore tra i dispositivi disponibili.

$ 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

Una volta avviato il simulatore, esegui: flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

App FFI generata da modello in esecuzione in un simulatore iOS

Il simulatore iOS ha la precedenza sulla destinazione macOS, quindi puoi saltare la specifica di un dispositivo con il parametro -d.

Congratulazioni. Hai creato ed eseguito un'applicazione su cinque diversi sistemi operativi. Ora passiamo alla creazione del plug-in nativo e all'interfacciamento con Dart tramite FFI.

5. Utilizzo di Duktape su Windows, Linux e Android

La libreria C che utilizzerai in questo codelab è Duktape. Duktape è un motore JavaScript incorporabile, con particolare attenzione alla portabilità e alle dimensioni compatte. In questo passaggio devi configurare il plug-in per compilare la libreria Duktape, collegarlo al tuo plug-in e quindi accedervi utilizzando FFI di Dart.

Questo passaggio consente di configurare l'integrazione in modo che funzioni su Windows, Linux e Android. L'integrazione tra iOS e macOS richiede una configurazione aggiuntiva (oltre a quella descritta in questo passaggio) per includere la libreria compilata nell'eseguibile finale di Flutter. La configurazione aggiuntiva richiesta è trattata nel passaggio successivo.

Recupero di Duktape

Innanzitutto, ottieni una copia del codice sorgente di duktape scaricandolo dal sito web duktape.org.

Per Windows puoi utilizzare PowerShell con Invoke-WebRequest:

PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz

Per Linux, wget è una buona scelta.

$ 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]

Il file è un archivio di tar.xz. Su Windows, un'opzione è scaricare gli strumenti 7Zip e utilizzarlo come segue.

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

Devi eseguire 7z due volte, prima per annullare l'archiviazione della compressione xz e per espandere l'archivio 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

Negli ambienti Linux moderni, tar estrae i contenuti in un passaggio come segue.

$ 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]

Installazione della LLVM

Per utilizzare ffigen, devi installare una LLVM, che ffigen utilizza per analizzare le intestazioni C. Su Windows, esegui questo comando.

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

Configura i percorsi di sistema per aggiungere C:\Program Files\LLVM\bin al tuo percorso di ricerca binario per completare l'installazione della VM LLVM sul tuo computer Windows. Per verificare se è stata installata correttamente, procedi nel seguente modo.

PS> clang --version
clang version 15.0.5
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin

Per Ubuntu, la dipendenza LLVM può essere installata come segue. Altre distribuzioni Linux hanno dipendenze simili per LLVM e 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) ...

Come illustrato in precedenza, puoi testare l'installazione della tua LLVM su Linux come descritto di seguito.

$ clang --version
Ubuntu clang version 15.0.2-1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Configurazione di ffigen

L'elemento pubpsec.yaml di primo livello generato dal modello potrebbe avere versioni obsolete del pacchetto ffigen. Esegui questo comando per aggiornare le dipendenze Dart nel progetto del plug-in:

$ flutter pub upgrade --major-versions

Ora che il pacchetto ffigen è aggiornato, configura i file che ffigen utilizzerà per generare i file di associazione. Modifica i contenuti del file ffigen.yaml del progetto in modo che corrispondano a quanto segue.

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
ignore-source-errors: true

Questa configurazione include il file di intestazione C da passare a LLVM, il file di output da generare, la descrizione da inserire all'inizio del file e una sezione di preambolo utilizzata per aggiungere un avviso di lint.

C'è un elemento di configurazione alla fine del file che merita ulteriori spiegazioni. A partire dalla versione 11.0.0 di ffigen, per impostazione predefinita il generatore di associazioni non genererà associazioni se vengono generati avvisi o errori da clang durante l'analisi dei file di intestazione.

I file di intestazione Duktape, così come sono scritti, attivano clang su macOS per generare avvisi a causa della mancanza di identificatori dei tipi di valori null nei puntatori di Duktape. Per supportare completamente macOS e iOS, Duktape richiede l'aggiunta di questi indicatori di tipo al codebase Duktape. Nel frattempo, stiamo decidendo di ignorare questi avvisi impostando il flag ignore-source-errors su true.

In un'applicazione di produzione, è necessario eliminare tutti gli avvisi del compilatore prima di spedire l'applicazione. Tuttavia, questa operazione per Duktape non rientra nell'ambito di questo codelab.

Per ulteriori dettagli sugli altri valori e chiavi, consulta la documentazione di ffigen.

Devi copiare file Duktape specifici dalla distribuzione Duktape nella posizione in cui ffigen è configurato per trovarli.

$ 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/

Tecnicamente, devi solo copiare in duktape.h per ffigen, ma stai per configurare CMake per creare la libreria che richiede tutti e tre. Esegui ffigen per generare la nuova associazione:

$ 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

Gli avvisi visualizzati sono diversi a seconda del sistema operativo. Puoi ignorarli per ora, poiché è noto che Duktape 2.7.0 esegue la compilazione con clang su Windows, Linux e macOS.

Configurare CMake

CMake è un sistema di generazione di sistemi di compilazione. Questo plug-in utilizza CMake per generare il sistema di compilazione per Android, Windows e Linux in modo da includere Duktape nel file binario di Flutter generato. Devi modificare il file di configurazione CMake generato dal modello come segue.

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 configurazione di CMake aggiunge i file di origine e, soprattutto, modifica il comportamento predefinito del file di libreria generato su Windows per esportare tutti i simboli C per impostazione predefinita. Questa è una soluzione CMake per facilitare la porta delle librerie in stile Unix, che è Duktape, nel mondo di Windows.

Sostituisci i contenuti di lib/ffigen_app.dart con i seguenti.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
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;
}

Questo file è responsabile del caricamento del file della libreria di link dinamici (.so per Linux e Android, .dll per Windows) e della fornitura di un wrapper che espone un'interfaccia più idiomatica Dart al codice C sottostante.

Dato che questo file importa direttamente il pacchetto ffi, devi spostare il pacchetto da dev_dependencies a dependencies. Un modo semplice per farlo è eseguire questo comando:

$ dart pub add ffi

Sostituisci i contenuti del valore main.dart dell'esempio con il seguente.

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)}';
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Ora puoi eseguire di nuovo l'app di esempio utilizzando:

$ cd example
$ flutter run

Dovresti vedere che l'app funziona nel seguente modo:

Visualizzazione di Duktape inizializzata in un&#39;applicazione Windows

Visualizzazione dell&#39;output JavaScript di Duktape in un&#39;applicazione Windows

Questi due screenshot mostrano il prima e il dopo della pressione del pulsante Esegui JavaScript. Questo dimostra l'esecuzione del codice JavaScript da Dart e la visualizzazione del risultato sullo schermo.

Android

Android è un sistema operativo Linux basato su kernel ed è in qualche modo simile alle distribuzioni desktop Linux. Il sistema di compilazione CMake può nascondere la maggior parte delle differenze tra le due piattaforme. Per sviluppare ed eseguire su Android, assicurati che l'emulatore Android sia in esecuzione (o che il dispositivo Android sia connesso). Esegui l'app. Ad esempio:

$ cd example
$ flutter run -d emulator-5554

A questo punto dovresti vedere l'app di esempio in esecuzione su Android:

Visualizzazione di Duktape inizializzata in un emulatore Android

Visualizzazione dell&#39;output JavaScript di Duktape in un emulatore Android

6. Utilizzo di Duktape su macOS e iOS

È arrivato il momento di far funzionare il plug-in su macOS e iOS, due sistemi operativi strettamente correlati. Inizia con macOS. Anche se CMake supporta macOS e iOS, non riutilizzerai il lavoro fatto per Linux e Android, poiché Flutter su macOS e iOS utilizza CocoaPods per importare le librerie,

Pulizia

Nel passaggio precedente hai creato un'applicazione funzionante per Android, Windows e Linux. Tuttavia, sono rimasti un paio di file dal modello originale che ora devi pulire. Rimuovili ora come descritto di seguito.

$ rm src/ffigen_app.c
$ rm src/ffigen_app.h
$ rm ios/Classes/ffigen_app.c
$ rm macos/Classes/ffigen_app.c

macOS

Flutter sulla piattaforma macOS utilizza CocoaPods per importare il codice C e C++. Ciò significa che questo pacchetto deve essere integrato nell'infrastruttura di build di CocoaPods. Per consentire il riutilizzo del codice C che hai già configurato per la creazione con CMake nel passaggio precedente, devi aggiungere un singolo file di inoltro nel runner della piattaforma macOS.

macos/Classes/duktape.c

#include "../../src/duktape.c"

Questo file utilizza la potenza del preprocessore C per includere il codice sorgente del codice sorgente nativo che hai configurato nel passaggio precedente. Per ulteriori informazioni sul funzionamento, consulta la pagina macos/ffigen_app.podspec.

Ora l'esecuzione di questa applicazione segue lo stesso pattern che hai utilizzato in Windows e Linux.

$ cd example
$ flutter run -d macos

Visualizzazione di Duktape inizializzata in un&#39;applicazione macOS

Visualizzazione dell&#39;output JavaScript di Duktape in un&#39;applicazione macOS

iOS

Come per la configurazione di macOS, iOS richiede anche l'aggiunta di un singolo file C di inoltro.

ios/Classes/duktape.c

#include "../../src/duktape.c"

Con questo singolo file, il plug-in è ora configurato anche per l'esecuzione su iOS. Eseguilo come al solito.

$ flutter run -d iPhone

Visualizzazione di Duktape inizializzata in un simulatore iOS

Visualizzazione dell&#39;output JavaScript di Duktape in un simulatore iOS

Complimenti! Hai integrato correttamente il codice nativo su cinque piattaforme. È un buon motivo per festeggiare! Forse un'interfaccia utente più funzionale, che creerai nel passaggio successivo.

7. Implementazione del loop di stampa della valutazione della lettura

Interagire con un linguaggio di programmazione è molto più divertente in un ambiente veloce e interattivo. L'implementazione originale di questo ambiente era il Read Eval Print Loop (REPL) del LISP. In questo passaggio implementerai qualcosa di simile con Duktape.

Preparare la produzione

Il codice attuale che interagisce con la libreria C di Duktape presuppone che nulla possa andare storto. Ah, e le librerie di link dinamici Duktape non vengono caricate durante il test. Per rendere pronta la produzione di questa integrazione, devi apportare alcune modifiche a lib/ffigen_app.dart.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
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;
}

Il codice per caricare la libreria di link dinamici è stato esteso per gestire il caso in cui il plug-in venga utilizzato in un runner di test. Ciò consente di scrivere un test di integrazione che esercita questa API come Flutter test. Il codice per valutare una stringa di codice JavaScript è stato esteso per gestire correttamente le condizioni di errore, ad esempio un codice incompleto o errato. Questo codice aggiuntivo mostra come gestire le situazioni in cui le stringhe vengono restituite come array di byte e devono essere convertite in stringhe Dart.

Aggiungere pacchetti

Quando crei un REPL, visualizzerai un'interazione tra l'utente e il motore JavaScript Duktape. L'utente inserisce righe di codice e Duktape risponde con il risultato del calcolo o con un'eccezione. Utilizzerai freezed per ridurre la quantità di codice boilerplate da scrivere. Utilizzerai anche google_fonts per rendere i contenuti visualizzati un po' più attinenti a un tema e flutter_riverpod per la gestione dello stato.

Aggiungi le dipendenze richieste all'app di esempio:

$ cd example
$ dart pub add flutter_riverpod freezed_annotation google_fonts
$ dart pub add -d build_runner freezed

Quindi, crea un file per registrare l'interazione 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;
}

Questa classe usa la funzionalità tipo di unione di freezed per consentire una facile espressione della forma di ogni linea visualizzata nella REPL in uno di tre tipi. A questo punto, probabilmente il codice mostra una qualche forma di errore, perché deve essere generato del codice aggiuntivo. Fallo ora come segue.

$ flutter pub run build_runner build

Questa operazione genera il file example/lib/duktape_message.freezed.dart, su cui si basa il codice appena digitato.

Successivamente, dovrai apportare un paio di modifiche ai file di configurazione di macOS per consentire a google_fonts di effettuare richieste di rete per i dati dei caratteri.

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>

Creazione dell'REPL

Dopo aver aggiornato il livello di integrazione per gestire gli errori e aver creato una rappresentazione dei dati per l'interazione, devi creare l'interfaccia utente dell'app di esempio.

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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Il codice contiene molti argomenti, ma spiegarlo tutto non rientra nell'ambito di questo codelab. Ti consiglio di eseguire il codice e di modificarlo dopo aver esaminato la documentazione appropriata.

$ cd example
$ flutter run

Duktape REPL in esecuzione in un&#39;applicazione Linux

Duktape REPL in esecuzione in un&#39;applicazione Windows

Duktape REPL in esecuzione in un simulatore iOS

Duktape REPL in esecuzione in un emulatore Android

8. Complimenti

Complimenti! Hai creato correttamente un plug-in basato su Flutter FFI per Windows, macOS, Linux, Android e iOS.

Dopo aver creato un plug-in, potresti volerlo condividere online in modo che altri possano utilizzarlo. Puoi trovare la documentazione completa sulla pubblicazione del tuo plug-in in pub.dev nella sezione Sviluppo di pacchetti di plug-in.