Utilizzo di FFI in un plug-in Flutter

1. Introduzione

L'interfaccia di funzioni esterne (FFI) di Dart consente alle app Flutter di utilizzare le 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à con JavaScript, ma questo argomento non è trattato in questo codelab.

Cosa creerai

In questo codelab, crei un plug-in per dispositivi mobili e computer che utilizza una libreria C. Con questa API, scriverai un'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 compilazione su Windows, macOS, Linux, Android e iOS
  • Crea un'applicazione che utilizzi il plug-in per un REPL (read reveal print loop) JavaScript

REPL Duktape in esecuzione come applicazione macOS

Obiettivi didattici

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

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

Che cosa ti serve

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

2. Per iniziare

Gli strumenti ffigen sono stati aggiunti di recente a Flutter. Puoi verificare che l'installazione di Flutter esegua la release stabile corrente eseguendo il seguente comando.

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-AU)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] IntelliJ IDEA Community Edition (version 2024.3.1.1)
[✓] VS Code (version 1.101.0)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

Verifica che l'output di flutter doctor indichi che sei sul canale stabile e che non sono disponibili release Flutter stabili più recenti. Se non utilizzi la versione stabile o sono disponibili release più recenti, esegui i due comandi riportati di seguito per aggiornare gli strumenti Flutter.

flutter channel stable
flutter upgrade

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

  • Il computer di sviluppo (per le build desktop del plug-in e dell'app di esempio)
  • Un dispositivo Android o iOS fisico 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 del plug-in

Iniziare a sviluppare plug-in Flutter

Flutter viene fornito con modelli per i plug-in che ti consentono di iniziare. Quando generi il modello del plug-in, puoi specificare la lingua che vuoi utilizzare.

Esegui il seguente comando nella tua 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 tuo plug-in.

Puoi esaminare il layout del progetto generato utilizzando il comando tree o 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 po' di tempo alla struttura della directory per avere un'idea di cosa è stato creato e dove si trova. Il modello plugin_ffi inserisce il codice Dart del plug-in in lib, in directory specifiche della piattaforma denominate android, ios, linux, macos e windows e, soprattutto, in una directory example.

Per uno sviluppatore abituato allo sviluppo normale di Flutter, questa struttura potrebbe sembrare strana, in quanto non è definito alcun file eseguibile a livello superiore. Un plug-in è pensato per essere incluso in altri progetti Flutter, ma dovrai completare il codice nella directory example per verificare che funzioni.

È il momento 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, compila ed esegui l'app di esempio generata per ogni target.

Windows

Verifica di utilizzare una versione di Windows supportata. Questo codelab è noto per funzionare su Windows 10 e Windows 11.

Puoi compilare 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 in esecuzione come app Windows

Linux

Verifica 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 seguenti comandi 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 in esecuzione come applicazione Linux

Android

Per Android puoi utilizzare Windows, macOS o Linux per la compilazione.

Devi apportare una modifica a example/android/app/build.gradle.kts per utilizzare la versione NDK appropriata.

example/android/app/build.gradle.kts)

android {
    // Modify the next line from `flutter.ndkVersion` to the following:
    ndkVersion = "27.0.12077973"
    // ...
}

Assicurati di avere un dispositivo Android connesso al computer di sviluppo o di eseguire un'istanza di Android Emulator (AVD). Verifica che Flutter sia in grado di connettersi al dispositivo Android o all'emulatore eseguendo quanto segue:

$ 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

Dopo aver avviato un dispositivo Android, che si tratti di un dispositivo fisico o di un emulatore, esegui il seguente comando:

cd ffigen_app/example
flutter run

Flutter ti chiederà su quale dispositivo vuoi eseguirlo. Seleziona il dispositivo appropriato tra quelli elencati.

App FFI generata da modello in esecuzione in un emulatore Android

macOS e iOS

Per lo sviluppo di Flutter per macOS e iOS, devi utilizzare un computer macOS.

Inizia eseguendo l'app di esempio su macOS. Verifica di nuovo i dispositivi rilevati 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 del 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 in esecuzione come applicazione Linux

Per iOS puoi utilizzare il simulatore o un dispositivo hardware reale. Se utilizzi il simulatore, avvialo prima. Il comando flutter devices ora elenca il simulatore come uno dei 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

Dopo aver avviato un dispositivo iOS, che si tratti di un dispositivo fisico o di un simulatore, esegui il seguente comando:

cd ffigen_app/example
flutter run

Flutter ti chiederà su quale dispositivo vuoi eseguirlo. Seleziona il dispositivo appropriato tra quelli elencati.

App FFI generata da modello in esecuzione in un simulatore iOS

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

Complimenti, hai creato ed eseguito correttamente un'applicazione su cinque diversi sistemi operativi. A questo punto, crea il plug-in nativo e interagisci con esso da Dart utilizzando FFI.

5. Utilizzare Duktape su Windows, Linux e Android

La libreria C che utilizzerai in questo codelab è Duktape. Duktape è un motore JavaScript incorporabile, incentrato sulla portabilità e su un ingombro ridotto. In questo passaggio, configurerai il plug-in per compilare la libreria Duktape, collegarla al plug-in e accedervi utilizzando FFI di Dart.

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

Recuperare Duktape

Per prima cosa, 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 tar.xz. Su Windows, un'opzione è scaricare gli strumenti 7Zip e utilizzarli 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, la prima per estrarre la compressione xz e la seconda 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 solo 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]

Installa LLVM

Per utilizzare ffigen, devi installare LLVM, che ffigen utilizza per analizzare gli header C. Su Windows, esegui il seguente 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 in modo da aggiungere C:\Program Files\LLVM\bin al percorso di ricerca dei binari per completare l'installazione di LLVM sul computer Windows. Per verificare se è stato installato 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 sopra, puoi testare l'installazione di LLVM su Linux come segue.

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

Configura ffigen

Il modello generato di primo livello pubpsec.yaml potrebbe avere versioni obsolete del pacchetto ffigen. Esegui il seguente 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 binding. Modifica i contenuti del file ffigen.yaml del progetto in modo che corrispondano a quanto segue.

ffigen.yaml

# Run with `dart run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
  Bindings for `src/duktape.h`.

  Regenerate bindings with `dart 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 nella parte superiore del file e una sezione di preambolo utilizzata per aggiungere un avviso lint.

Alla fine del file è presente un elemento di configurazione che merita ulteriori spiegazioni. A partire dalla versione 11.0.0 di ffigen, per impostazione predefinita il generatore di binding non genera associazioni se clang genera avvisi o errori durante l'analisi dei file di intestazione.

I file di intestazione di Duktape, così come sono scritti, attivano clang su macOS per generare avvisi a causa della mancanza di specificatori di tipo di nullità sui puntatori di Duktape. Per supportare completamente macOS e iOS, Duktape ha bisogno che questi specificatori di tipo vengano aggiunti al codice sorgente di Duktape. Nel frattempo, abbiamo deciso di ignorare questi avvisi impostando il flag ignore-source-errors su true.

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

Per ulteriori dettagli sulle altre chiavi e sui valori, 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 duktape.h per ffigen, ma stai per configurare CMake per compilare la libreria che ha bisogno di tutti e tre. Esegui ffigen per generare la nuova associazione:

$ dart run ffigen --config ffigen.yaml
Building package executable... (1.5s)
Built ffigen:ffigen.
[INFO]   : Running in Directory: '/Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05'
[INFO]   : Input Headers: [file:///Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/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 '__builtin_va_list' starts with '_' and therefore will be private.
[INFO]   : Finished, Bindings generated in /Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/lib/duktape_bindings_generated.dart

Vedrai avvisi diversi su ogni sistema operativo. Per il momento puoi ignorarli, poiché è noto che Duktape 2.7.0 si compila con clang su Windows, Linux e macOS.

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

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()

La configurazione CMake aggiunge i file di origine e, cosa più importante, modifica il comportamento predefinito del file della libreria generato su Windows in modo da esportare tutti i simboli C per impostazione predefinita. Si tratta di una soluzione alternativa CMake per facilitare il porting delle librerie in stile Unix, come Duktape, al mondo di Windows.

Sostituisci i contenuti di lib/ffigen_app.dart con quanto segue.

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 collegamento dinamico (.so per Linux e Android, .dll per Windows) e del fornire un wrapper che esponga un'interfaccia più idiomatica di Dart al codice C sottostante.

Poiché questo file importa direttamente il pacchetto ffi, devi spostarlo da dev_dependencies a dependencies. Un modo rapido per farlo è eseguire il seguente comando:

dart pub add ffi

Sostituisci i contenuti di main.dart dell'esempio con quanto segue.

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 l'app in esecuzione come segue:

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

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

Questi due screenshot mostrano lo stato prima e dopo aver premuto il pulsante Esegui JavaScript. Questo dimostra l'esecuzione di codice JavaScript da Dart e la visualizzazione del risultato sullo schermo.

Android

Android è un sistema operativo basato su kernel Linux ed è in qualche modo simile alle distribuzioni Linux per computer. Il sistema di compilazione CMake può nascondere la maggior parte delle differenze tra le due piattaforme. Per compilare 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

Ora dovresti vedere l'app di esempio in esecuzione su Android:

Viene mostrato Duktape inizializzato in un emulatore Android

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

6. Utilizzare Duktape su macOS e iOS

Ora è il momento di far funzionare il plug-in su macOS e iOS, due sistemi operativi strettamente correlati. Inizia con macOS. Sebbene CMake supporti macOS e iOS, non potrai riutilizzare il lavoro svolto per Linux e Android, poiché Flutter su macOS e iOS utilizza CocoaPods per l'importazione delle librerie.

Esegui la pulizia

Nel passaggio precedente hai creato un'applicazione funzionante per Android, Windows e Linux. Tuttavia, ci sono un paio di file rimanenti del modello originale che ora devi ripulire. Rimuovili ora come segue.

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 codice C e C++. Ciò significa che questo pacchetto deve essere integrato nell'infrastruttura di compilazione di CocoaPods. Per consentire il riutilizzo del codice C che hai già configurato per la compilazione con CMake nel passaggio precedente, dovrai 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 dal codice sorgente nativo configurato nel passaggio precedente. Per maggiori dettagli su come funziona, consulta macos/ffigen_app.podspec.

L'esecuzione di questa applicazione ora segue lo stesso schema che hai visto su Windows e Linux.

cd example
flutter run -d macos

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

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

iOS

Come per la configurazione di macOS, anche iOS richiede 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 per funzionare anche su iOS. Eseguilo come al solito.

flutter run -d iPhone

Visualizzazione di Duktape inizializzato in un simulatore iOS

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

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

7. Implementare il loop di stampa di valutazione della lettura

Interagire con un linguaggio di programmazione è molto più divertente in un ambiente interattivo rapido. L'implementazione originale di un ambiente di questo tipo era il ciclo di lettura, valutazione e stampa (REPL) di LISP. In questo passaggio implementerai qualcosa di simile con Duktape.

Preparare tutto per la produzione

Il codice attuale che interagisce con la libreria C Duktape presuppone che non possa succedere nulla di male. Inoltre, non carica le librerie di collegamento dinamico Duktape durante il test. Per rendere questa integrazione pronta per la produzione, 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 switch (Abi.current()) {
        Abi.windowsArm64 => DynamicLibrary.open(
          p.canonicalize(
            p.join(r'build\windows\arm64\runner\Debug', '$_libName.dll'),
          ),
        ),
        Abi.windowsX64 => DynamicLibrary.open(
          p.canonicalize(
            p.join(r'build\windows\x64\runner\Debug', '$_libName.dll'),
          ),
        ),
        _ => throw 'Unsupported platform',
      };
    }
    // 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;
}

Ciò richiede l'aggiunta del pacchetto path.

flutter pub add path

Il codice per caricare la libreria di link dinamica è stato esteso per gestire il caso in cui il plug-in venga utilizzato in un programma di test. In questo modo è possibile scrivere un test di integrazione che eserciti questa API come test Flutter. Il codice per valutare una stringa di codice JavaScript è stato esteso per gestire correttamente le condizioni di errore, ad esempio 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, viene visualizzata 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ù in tema e flutter_riverpod per la gestione dello stato.

Aggiungi le dipendenze richieste all'app di esempio:

cd example
flutter pub add flutter_riverpod freezed_annotation google_fonts
flutter pub add -d build_runner freezed

Successivamente, 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 utilizza la funzionalità di tipo unione di freezed per consentire l'espressione sicura del tipo della forma di ogni riga visualizzata nella REPL come uno di tre tipi. A questo punto, il codice probabilmente mostra una qualche forma di errore, in quanto è necessario generare codice aggiuntivo. Procedi nel seguente modo.

flutter pub run build_runner build

Viene generato il file example/lib/duktape_message.freezed.dart, su cui si basa il codice che hai 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>

Crea il REPL

Ora che hai aggiornato il livello di integrazione per gestire gli errori e hai creato una rappresentazione dei dati per l'interazione, è il momento di 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) {
                    return switch (messages[idx]) {
                      DuktapeMessageCode code => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          '> ${code.code}',
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleMedium,
                          ),
                        ),
                      ),
                      DuktapeMessageResponse response => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          '= ${response.result}',
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleMedium,
                            color: Colors.blue[800],
                          ),
                        ),
                      ),
                      DuktapeMessageError error => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          error.log,
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleSmall,
                            color: Colors.red[800],
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                      DuktapeMessage message => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          'Unhandled message $message',
                          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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Questo codice è molto complesso, ma non rientra nell'ambito di questo codelab spiegarlo tutto. Ti consiglio di eseguire il codice e di apportare le modifiche necessarie dopo aver esaminato la documentazione appropriata.

cd example
flutter run

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

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

REPL Duktape in esecuzione in un simulatore iOS

REPL Duktape in esecuzione in un emulatore Android

8. Complimenti

Complimenti! Hai creato correttamente un plug-in Flutter basato su 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 plug-in su pub.dev in Sviluppare pacchetti di plug-in.