1. Einführung
Mithilfe von FFI (Foreign Function Interface) von Dart können Flutter-Apps vorhandene native Bibliotheken nutzen, die eine C API bereitstellen. Dart unterstützt FFI unter Android, iOS, Windows, macOS und Linux. Für das Web unterstützt Dart JavaScript-Interop, aber dieses Thema wird in diesem Codelab nicht behandelt.
Inhalt
In diesem Codelab erstellen Sie ein Plug-in für Mobilgeräte und Desktops, das eine C-Bibliothek verwendet. Mit dieser API schreiben Sie eine einfache Beispielanwendung, die das Plug-in verwendet. Für das Plug-in und die App gilt Folgendes:
- Quellcode der C-Bibliothek in Ihr neues Flutter-Plug-in importieren
- Plug-in für Windows, macOS, Linux, Android und iOS anpassen
- Erstellen Sie eine Anwendung, die das Plug-in für eine JavaScript-REPL (Lesen, Zeige Druckschleife) verwendet.
Aufgaben in diesem Lab
In diesem Codelab lernen Sie die praktischen Kenntnisse kennen, die erforderlich sind, um ein FFI-basiertes Flutter-Plug-in auf Desktop- und mobilen Plattformen zu erstellen. Dazu gehören:
- Dart-FFI-basierte Vorlage für Flutter-Plug-in generieren
ffigen
-Paket zum Generieren von Bindungscode für eine C-Bibliothek verwenden- Mit CMake ein Flutter-FFI-Plug-in für Android, Windows und Linux erstellen
- Mit CocoaPods ein Flutter-FFI-Plug-in für iOS und macOS erstellen
Voraussetzungen
- Android Studio 4.1 oder höher für die Android-Entwicklung
- Xcode 13 oder höher für die iOS- und macOS-Entwicklung
- Visual Studio 2022 oder Visual Studio Build Tools 2022 mit der Desktopentwicklung mit C++ Arbeitslast für die Windows-Desktopentwicklung
- Flutter-SDK
- Alle erforderlichen Build-Tools für die Plattformen, auf denen Sie entwickeln (z. B. CMake, CocoaPods usw.)
- LLVM für die Plattformen, auf denen Sie entwickeln. Die LLVM-Compiler-Toolsuite wird von
ffigen
verwendet, um die C-Header-Datei zu parsen, um die in Dart exponierte FFI-Bindung zu erstellen. - Einen Code-Editor wie Visual Studio Code
2. Erste Schritte
Das ffigen
-Tool wurde vor Kurzem zu Flutter hinzugefügt. Mit dem folgenden Befehl können Sie prüfen, ob in Ihrer Flutter-Installation die aktuelle stabile Version ausgeführt wird.
$ 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!
Prüfen Sie, ob in der Ausgabe von flutter doctor
angegeben ist, dass Sie sich auf der stabilen Version befinden und ob keine neueren stabilen Flutter-Releases verfügbar sind. Wenn Sie nicht in der stabilen Version arbeiten oder neuere Versionen verfügbar sind, führen Sie die beiden folgenden Befehle aus, um Ihre Flutter-Tools auf den neuesten Stand zu bringen.
$ flutter channel stable $ flutter upgrade
Sie können den Code in diesem Codelab auf jedem der folgenden Geräte ausführen:
- Ihren Entwicklungscomputer (für Desktop-Builds Ihres Plug-ins und der Beispiel-App)
- Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden ist und sich im Entwicklermodus befindet
- iOS-Simulator (Installation von Xcode-Tools erforderlich)
- Android-Emulator (Einrichtung in Android Studio erforderlich)
3. Plug-in-Vorlage generieren
Erste Schritte bei der Entwicklung des Flutter-Plug-ins
Im Lieferumfang von Flutter sind Vorlagen für Plug-ins enthalten, die den Einstieg erleichtern. Wenn Sie die Plug-in-Vorlage generieren, können Sie angeben, welche Sprache Sie verwenden möchten.
Führen Sie den folgenden Befehl in Ihrem Arbeitsverzeichnis aus, um Ihr Projekt mit der Plug-in-Vorlage zu erstellen:
$ flutter create --template=plugin_ffi \ --platforms=android,ios,linux,macos,windows ffigen_app
Der Parameter --platforms
gibt an, welche Plattformen Ihr Plug-in unterstützt.
Sie können das Layout des generierten Projekts mit dem Befehl tree
oder dem Datei-Explorer Ihres Betriebssystems prüfen.
$ 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
Nehmen Sie sich einen Moment Zeit, um sich die Verzeichnisstruktur anzusehen, um ein Gefühl dafür zu bekommen, was erstellt wurde und wo es sich befindet. Die Vorlage plugin_ffi
platziert den Dart-Code für das Plug-in unter lib
, unter den plattformspezifischen Verzeichnissen android
, ios
, linux
, macos
und windows
und vor allem unter einem example
-Verzeichnis.
Für Entwickler, die mit der normalen Flutter-Entwicklung vertraut sind, kann diese Struktur seltsam erscheinen, da keine ausführbare Datei auf oberster Ebene definiert ist. Ein Plug-in ist für die Aufnahme in andere Flutter-Projekte vorgesehen. Sie müssen den Code jedoch im Verzeichnis example
ausarbeiten, um sicherzustellen, dass der Plug-in-Code funktioniert.
Jetzt kann es losgehen!
4. Beispiel erstellen und ausführen
Damit das Build-System und die Voraussetzungen korrekt installiert sind und für jede unterstützte Plattform funktionieren, erstellen Sie die generierte Beispiel-App für jedes Ziel und führen Sie sie aus.
Windows
Achten Sie darauf, dass Sie eine unterstützte Windows-Version verwenden. Dieses Codelab funktioniert bekanntermaßen unter Windows 10 und Windows 11.
Sie können die Anwendung entweder in Ihrem Code-Editor oder über die Befehlszeile erstellen.
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=/
Es sollte ein Fenster mit einer laufenden App angezeigt werden, das in etwa so aussieht:
Linux
Achten Sie darauf, dass Sie eine unterstützte Linux-Version verwenden. In diesem Codelab wird Ubuntu 22.04.1
verwendet.
Nachdem Sie alle in Schritt 2 aufgeführten Voraussetzungen installiert haben, führen Sie die folgenden Befehle in einem Terminal aus:
$ 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=/
Es sollte ein Fenster mit einer laufenden App angezeigt werden, das in etwa so aussieht:
Android
Unter Android können Sie Windows, macOS oder Linux zur Kompilierung verwenden. Prüfen Sie zuerst, ob ein Android-Gerät mit Ihrem Entwicklungscomputer verbunden ist oder eine Android Emulator-Instanz (AVD) ausgeführt wird. Prüfen Sie mit dem folgenden Befehl, ob Flutter eine Verbindung zum Android-Gerät oder zum Emulator herstellen kann:
$ 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 und iOS
Für die Entwicklung von macOS und iOS Flutter benötigen Sie einen macOS-Computer.
Führen Sie zuerst die Beispiel-App unter macOS aus. Prüfen Sie noch einmal, welche Geräte Flutter sieht:
$ 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
Führen Sie die Beispielanwendung mit dem generierten Plug-in-Projekt aus:
$ cd ffigen_app/example $ flutter run -d macos
Es sollte ein Fenster mit einer laufenden App angezeigt werden, das in etwa so aussieht:
Für iOS können Sie den Simulator oder ein echtes Hardwaregerät verwenden. Wenn Sie den Simulator verwenden, starten Sie ihn zuerst. Der Befehl flutter devices
listet den Simulator jetzt als eines der verfügbaren Geräte auf.
$ 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
Sobald der Simulator gestartet wurde, führen Sie flutter run
aus.
$ cd ffigen_app/example $ flutter run -d iphone
Der iOS-Simulator hat Vorrang vor dem macOS-Ziel, sodass Sie die Angabe eines Geräts mit dem Parameter -d
überspringen können.
Herzlichen Glückwunsch! Sie haben erfolgreich eine Anwendung für fünf verschiedene Betriebssysteme erstellt und ausgeführt. Als Nächstes wird das native Plug-in erstellt und mithilfe von FFI über Dart eine Verbindung hergestellt.
5. Duktape unter Windows, Linux und Android verwenden
Die C-Bibliothek, die Sie in diesem Codelab verwenden, ist Duktape. Duktape ist eine integrierbare JavaScript-Engine, die auf Portabilität und kompaktem Platzbedarf ausgelegt ist. In diesem Schritt konfigurieren Sie das Plug-in so, dass die Duktape-Bibliothek kompiliert, mit Ihrem Plug-in verknüpft und dann über Dart FFI darauf zugegriffen wird.
Mit diesem Schritt wird die Integration für Windows, Linux und Android konfiguriert. Die Integration von iOS und macOS erfordert über die in diesem Schritt beschriebenen zusätzliche Konfiguration hinaus, um die kompilierte Bibliothek in die endgültige ausführbare Flutter-Datei aufzunehmen. Die zusätzlich erforderliche Konfiguration wird im nächsten Schritt behandelt.
Duktape abrufen
Laden Sie zuerst eine Kopie des duktape
-Quellcodes von der duktape.org-Website herunter.
Unter Windows können Sie PowerShell mit Invoke-WebRequest
verwenden:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
Unter Linux ist wget
eine gute Wahl.
$ 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]
Die Datei ist ein tar.xz
-Archiv. Unter Windows können Sie die 7Zip-Tools herunterladen und so verwenden:
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
Sie müssen 7z zweimal ausführen. Erstens, um die XZ-Komprimierung zu dearchivieren, und dann das TAR-Archiv erweitern.
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
In modernen Linux-Umgebungen extrahiert tar
den Inhalt in einem Schritt so.
$ 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]
LLVM installieren
Wenn Sie ffigen
verwenden möchten, müssen Sie LLVM installieren, die von ffigen
zum Parsen von C-Headern verwendet wird. Führen Sie unter Windows den folgenden Befehl aus.
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
Konfigurieren Sie Ihre Systempfade, um C:\Program Files\LLVM\bin
zu Ihrem binären Suchpfad hinzuzufügen, um die Installation von LLVM auf Ihrem Windows-Computer abzuschließen. So können Sie testen, ob es richtig installiert wurde.
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
Für Ubuntu kann die LLVM-Abhängigkeit so installiert werden. Andere Linux-Distributionen haben ähnliche Abhängigkeiten für LLVM und 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) ...
So testen Sie Ihre LLVM-Installation unter Linux:
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
ffigen
wird konfiguriert
Die auf oberster Ebene generierte Vorlage pubpsec.yaml
enthält möglicherweise veraltete Versionen des ffigen
-Pakets. Führen Sie den folgenden Befehl aus, um die Dart-Abhängigkeiten im Plug-in-Projekt zu aktualisieren:
$ flutter pub upgrade --major-versions
Da das ffigen
-Paket nun auf dem neuesten Stand ist, konfigurieren Sie als Nächstes, welche Dateien ffigen
nutzen soll, um die Bindungsdateien zu generieren. Ändern Sie den Inhalt der Datei ffigen.yaml
Ihres Projekts so, dass er wie folgt aussieht.
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
Diese Konfiguration enthält die C-Header-Datei, die an LLVM übergeben werden soll, die zu generierende Ausgabedatei, die Beschreibung, die an den Anfang der Datei gestellt werden soll, und einen Präambelabschnitt, der zum Hinzufügen einer Lint-Warnung verwendet wird.
Am Ende der Datei befindet sich ein Konfigurationselement, das näher erläutert werden muss. Ab Version 11.0.0 von ffigen
generiert der Bindungsgenerator standardmäßig keine Bindungen, wenn beim Parsen der Headerdateien Warnungen oder Fehler von clang
generiert werden.
Die Duktape-Headerdateien in der vorliegenden Form lösen unter macOS clang
aus, um Warnungen zu generieren, da in den Duktape-Zeigern keine Typbezeichner für die Null-Zulässigkeit vorhanden sind. Damit Duktape unter macOS und iOS vollständig unterstützt wird, müssen diese Typspezifizierer der Duktape-Codebasis hinzugefügt werden. In der Zwischenzeit werden diese Warnungen ignoriert, indem wir das Flag ignore-source-errors
auf true
setzen.
In einer Produktionsanwendung sollten Sie alle Compiler-Warnungen eliminieren, bevor Sie Ihre Anwendung versenden. Diese Vorgehensweise für Duktape wird in diesem Codelab jedoch nicht behandelt.
Weitere Informationen zu den anderen Schlüsseln und Werten finden Sie in der Dokumentation zu ffigen
.
Sie müssen bestimmte Duktape-Dateien aus der Duktape-Distribution an den Speicherort kopieren, an dem ffigen
entsprechend konfiguriert ist.
$ 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/
Technisch gesehen müssen Sie nur duktape.h
für ffigen
kopieren, aber Sie sind im Begriff, CMake so zu konfigurieren, dass die Bibliothek erstellt wird, die alle drei benötigt. Führen Sie ffigen
aus, um die neue Bindung zu generieren:
$ 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
Je nach Betriebssystem werden unterschiedliche Warnungen angezeigt. Diese können Sie vorerst ignorieren, da Duktape 2.7.0 bekanntermaßen mit clang
unter Windows, Linux und macOS kompiliert wird.
CMake konfigurieren
CMake ist ein System zum Generieren von Build-Systemen. Dieses Plug-in verwendet CMake, um das Build-System für Android, Windows und Linux zu generieren und Duktape in das generierte Flutter-Binärprogramm einzubinden. So ändern Sie die durch Vorlage generierte CMake-Konfigurationsdatei:
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)
Durch die CMake-Konfiguration werden die Quelldateien hinzugefügt und, noch wichtiger, das Standardverhalten der generierten Bibliotheksdatei unter Windows so geändert, dass standardmäßig alle C-Symbole exportiert werden. Dies ist eine CMake-Problemumgehung, die dazu beiträgt, Bibliotheken im Unix-Stil, wie Duktape, in die Windows-Welt zu übertragen.
Ersetzen Sie den Inhalt von lib/ffigen_app.dart
durch Folgendes.
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;
}
Diese Datei ist verantwortlich für das Laden der dynamischen Link-Bibliotheksdatei (.so
für Linux und Android, .dll
für Windows) und für die Bereitstellung eines Wrappers, der dem zugrunde liegenden C-Code eine eher von Dart idiomatische Schnittstelle offenlegt.
Da diese Datei das Paket ffi
direkt importiert, müssen Sie das Paket von dev_dependencies
nach dependencies
verschieben. Dazu können Sie einfach den folgenden Befehl ausführen:
$ dart pub add ffi
Ersetzen Sie den Inhalt des main.dart
-Objekts des Beispiels durch Folgendes:
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)}';
});
},
),
],
),
),
),
),
);
}
}
Sie können die Beispiel-App jetzt mit folgendem Befehl noch einmal ausführen:
$ cd example $ flutter run
Die Anwendung sollte wie folgt ausgeführt werden:
Diese beiden Screenshots zeigen den Vorher-Nachher-Effekt des Klickens auf die Schaltfläche JavaScript ausführen. Hier sehen Sie, wie JavaScript-Code in Dart ausgeführt und das Ergebnis auf dem Bildschirm angezeigt wird.
Android
Android ist ein Kernel-basiertes Linux-Betriebssystem und ähnelt Desktop-Linux-Distributionen in etwa. Das Build-System von CMake kann die meisten Unterschiede zwischen den beiden Plattformen verbergen. Damit du Apps unter Android erstellen und ausführen kannst, muss der Android Emulator ausgeführt werden oder das Android-Gerät muss verbunden sein. Führen Sie die App aus. Hier einige Beispiele:
$ cd example $ flutter run -d emulator-5554
Sie sollten jetzt die Beispiel-App unter Android sehen:
6. Duktape unter macOS und iOS verwenden
Jetzt ist es an der Zeit, Ihr Plug-in auf macOS und iOS, zwei eng verwandte Betriebssysteme, umzusetzen. Mit macOS starten CMake unterstützt zwar macOS und iOS, aber Sie werden Ihre Arbeit für Linux und iOS nicht wiederverwenden. Android verwendet, wie Flutter unter macOS und iOS, CocoaPods für den Import von Bibliotheken.
Bereinigen
Im vorherigen Schritt haben Sie eine funktionierende Anwendung für Android, Windows und Linux erstellt. In der ursprünglichen Vorlage sind jedoch noch einige Dateien übrig, die Sie jetzt bereinigen müssen. Entfernen Sie diese jetzt wie folgt.
$ rm src/ffigen_app.c $ rm src/ffigen_app.h $ rm ios/Classes/ffigen_app.c $ rm macos/Classes/ffigen_app.c
macOS
Flutter auf der macOS-Plattform verwendet CocoaPods zum Importieren von C- und C++-Code. Das bedeutet, dass dieses Paket in die CocoaPods-Build-Infrastruktur integriert werden muss. Um die Wiederverwendung des C-Codes zu aktivieren, den Sie im vorherigen Schritt bereits für die Erstellung mit CMake konfiguriert haben, müssen Sie dem macOS-Plattform-Runner eine einzelne Weiterleitungsdatei hinzufügen.
macos/Classes/duktape.c
#include "../../src/duktape.c"
Diese Datei nutzt die Leistungsfähigkeit des C-Präprozessors, um den Quellcode aus dem nativen Quellcode einzubinden, den Sie im vorherigen Schritt eingerichtet haben. Weitere Informationen zur Funktionsweise finden Sie unter macos/ffigen_app.podspec.
Für die Ausführung dieser Anwendung gilt nun dasselbe Muster wie unter Windows und Linux.
$ cd example $ flutter run -d macos
iOS
Ähnlich wie bei macOS muss auch bei iOS eine einzelne C-Weiterleitungsdatei hinzugefügt werden.
ios/Classes/duktape.c
#include "../../src/duktape.c"
Mit dieser Datei ist Ihr Plug-in nun auch für die Ausführung unter iOS konfiguriert. Führen Sie sie wie gewohnt aus.
$ flutter run -d iPhone
Glückwunsch! Sie haben nativen Code erfolgreich auf fünf Plattformen integriert. Das ist ein Grund zum Feiern! Vielleicht sogar eine funktionalere Benutzeroberfläche, die Sie im nächsten Schritt erstellen werden.
7. Lese-Eval-Print-Schleife implementieren
Die Interaktion mit einer Programmiersprache macht in einer schnellen interaktiven Umgebung viel mehr Spaß. Die ursprüngliche Implementierung einer solchen Umgebung war die Read Eval Print Loop (REPL) des LISP. In diesem Schritt implementieren Sie etwas Ähnliches mit Duktape.
Produktionsvorbereitung
Der aktuelle Code, der mit der Duktape C-Bibliothek interagiert, geht davon aus, dass nichts schiefgehen kann. Außerdem werden die dynamischen Linkbibliotheken von Duktape beim Testen nicht geladen. Damit diese Integration für die Produktion bereit ist, musst du ein paar Änderungen an lib/ffigen_app.dart
vornehmen.
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;
}
Der Code zum Laden der dynamischen Linkbibliothek wurde für den Fall erweitert, dass das Plug-in in einem Test-Runner verwendet wird. Dadurch kann ein Integrationstest geschrieben werden, der diese API als Flutter-Test ausführt. Der Code zum Auswerten einer JavaScript-Code-Zeichenfolge wurde erweitert, damit Fehlerbedingungen korrekt verarbeitet werden können, z. B. unvollständiger oder falscher Code. Dieser zusätzliche Code zeigt, wie mit Situationen umzugehen ist, in denen Strings als Byte-Arrays zurückgegeben werden und in Dart-Strings konvertiert werden müssen.
Pakete hinzufügen
Beim Erstellen einer REPL zeigen Sie eine Interaktion zwischen dem Nutzer und der Duktape-JavaScript-Engine. Der Nutzer gibt Codezeilen ein und Duktape antwortet entweder mit dem Ergebnis der Berechnung oder mit einer Ausnahme. Sie verwenden freezed
, um die Menge an Boilerplate-Code zu reduzieren, die Sie schreiben müssen. Außerdem verwenden Sie google_fonts
, um die angezeigten Inhalte themenbezogen zu gestalten, und flutter_riverpod
für die Statusverwaltung.
Fügen Sie der Beispielanwendung die erforderlichen Abhängigkeiten hinzu:
$ cd example $ dart pub add flutter_riverpod freezed_annotation google_fonts $ dart pub add -d build_runner freezed
Erstellen Sie als Nächstes eine Datei, um die REPL-Interaktion aufzuzeichnen:
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;
}
Diese Klasse verwendet die Union-Typ-Funktion von freezed
, um einen einfachen Ausdruck der Form jeder in der REPL angezeigten Zeile als einen von drei Typen zu ermöglichen. An dieser Stelle wird in Ihrem Code wahrscheinlich ein Fehler angezeigt, da zusätzlicher Code generiert werden muss. Gehen Sie dazu so vor:
$ flutter pub run build_runner build
Dadurch wird die Datei example/lib/duktape_message.freezed.dart
generiert, auf der sich der gerade eingegebene Code basiert.
Als Nächstes müssen Sie ein paar Änderungen an den macOS-Konfigurationsdateien vornehmen, damit google_fonts
Netzwerkanfragen für Schriftdaten senden kann.
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>
REPL erstellen
Nachdem Sie nun die Integrationsebene zur Fehlerbehandlung aktualisiert und eine Datendarstellung für die Interaktion erstellt haben, können Sie die Benutzeroberfläche der Beispielanwendung erstellen.
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,
),
),
],
),
),
);
}
}
In diesem Code passiert viel, aber es würde den Rahmen dieses Codelab sprengen, um alles zu erklären. Wir empfehlen Ihnen, den Code auszuführen und dann Änderungen am Code vorzunehmen, nachdem Sie die entsprechende Dokumentation gelesen haben.
$ cd example $ flutter run
8. Glückwunsch
Glückwunsch! Sie haben erfolgreich ein Flutter-FFI-basiertes Plug-in für Windows, macOS, Linux, Android und iOS erstellt.
Nachdem Sie ein Plug-in erstellt haben, können Sie es online freigeben, damit andere es verwenden können. Die vollständige Dokumentation zum Veröffentlichen Ihres Plug-ins in pub.dev finden Sie unter Plug-in-Pakete entwickeln.