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