Informationen zu diesem Codelab
1. Einführung
Mit der Foreign Function Interface (FFI) von Dart können Flutter-Apps vorhandene native Bibliotheken nutzen, die eine C API bereitstellen. Dart unterstützt FFI auf Android, iOS, Windows, macOS und Linux. Für das Web unterstützt Dart die JavaScript-Interoperabilität. Dieses Thema wird in diesem Codelab jedoch nicht behandelt.
Aufgaben
In diesem Codelab erstellen Sie ein Plug-in für Mobilgeräte und Computer, das eine C-Bibliothek verwendet. Mit dieser API schreiben Sie eine Beispiel-App, die das Plug-in verwendet. Ihr Plug-in und Ihre App bieten folgende Funktionen:
- Quellcode der C-Bibliothek in Ihr neues Flutter-Plug-in importieren
- Das Plug-in so anpassen, dass es unter Windows, macOS, Linux, Android und iOS erstellt werden kann
- Eine Anwendung erstellen, die das Plug-in für eine JavaScript-REPL (Read Reveal Print Loop) verwendet
Lerninhalte
In diesem Codelab lernen Sie das praktische Wissen, das Sie zum Erstellen eines FFI-basierten Flutter-Plug-ins sowohl auf Desktop- als auch auf Mobilplattformen benötigen. Dazu gehören:
- Flutter-Plug-in-Vorlage auf Basis von Dart FFI generieren
- Bindungscode für eine C-Bibliothek mit dem
ffigen
-Paket generieren - 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 Entwicklung von iOS- und macOS-Apps
- Visual Studio 2022 oder Visual Studio Build Tools 2022 mit der Arbeitslast „Desktopentwicklung mit C++“ für die Windows-Desktopentwicklung
- Flutter SDK
- Alle erforderlichen Build-Tools für die Plattformen, auf denen Sie entwickeln werden (z. B. CMake, CocoaPods usw.).
- LLVM für die Plattformen, auf denen Sie entwickeln werden. Die LLVM-Compiler-Toolsuite wird von
ffigen
verwendet, um die C-Headerdatei zu parsen und die in Dart freigegebene FFI-Bindung zu erstellen. - Einen Code-Editor wie Visual Studio Code
2. Erste Schritte
Die ffigen
-Tools wurden erst vor Kurzem zu Flutter hinzugefügt. Mit dem folgenden Befehl können Sie prüfen, ob in Ihrer Flutter-Installation der aktuelle stabile Release ausgeführt wird.
$ 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!
Prüfen Sie, ob in der Ausgabe von flutter doctor
angegeben ist, dass Sie den stabilen Kanal verwenden und es keine neueren stabilen Flutter-Releases gibt. Wenn Sie nicht die stabile Version verwenden oder neuere Releases verfügbar sind, führen Sie die folgenden beiden 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 einem der folgenden Geräte ausführen:
- Ihr Entwicklungscomputer (für Desktop-Builds Ihres Plug-ins und Ihrer Beispiel-App)
- Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist
- iOS-Simulator (Installation der Xcode-Tools erforderlich)
- Der Android-Emulator (erfordert die Einrichtung in Android Studio)
3. Plug-in-Vorlage generieren
Erste Schritte mit der Entwicklung von Flutter-Plug-ins
Flutter enthält Vorlagen für Plug-ins, die den Einstieg erleichtern. Beim Generieren der Plug-in-Vorlage können Sie angeben, welche Sprache verwendet werden soll.
Führen Sie den folgenden Befehl in Ihrem Arbeitsverzeichnis aus, um das Projekt mit der Plug-in-Vorlage zu erstellen:
flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows ffigen_app
Mit dem Parameter --platforms
wird angegeben, welche Plattformen dein 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
Sehen Sie sich die Verzeichnisstruktur an, um einen Eindruck davon zu bekommen, was erstellt wurde und wo es sich befindet. Die plugin_ffi
-Vorlage platziert den Dart-Code für das Plug-in unter lib
, in den plattformspezifischen Verzeichnissen android
, ios
, linux
, macos
und windows
und vor allem in einem example
-Verzeichnis.
Für Entwickler, die mit der normalen Flutter-Entwicklung vertraut sind, mag diese Struktur seltsam erscheinen, da auf oberster Ebene keine ausführbare Datei definiert ist. Ein Plug-in soll in anderen Flutter-Projekten eingebunden werden. Sie werden den Code im Verzeichnis example
jedoch ausarbeiten, um zu prüfen, ob Ihr Plug-in-Code funktioniert.
Es ist an der Zeit, loszulegen.
4. Beispiel erstellen und ausführen
Um sicherzustellen, dass das Build-System und die Voraussetzungen richtig installiert sind und für jede unterstützte Plattform funktionieren, erstellen und führen Sie die generierte Beispiel-App für jedes Ziel aus.
Windows
Prüfen Sie, ob Sie eine unterstützte Version von Windows verwenden. Dieses Codelab funktioniert unter Windows 10 und Windows 11.
Sie können die Anwendung entweder über Ihren 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
Prüfen Sie, ob Sie eine unterstützte Version von Linux 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
Für Android können Sie Windows, macOS oder Linux zur Kompilierung verwenden.
Sie müssen eine Änderung an example/android/app/build.gradle.kts
vornehmen, um die richtige NDK-Version zu verwenden.
example/android/app/build.gradle.kts)
android {
// Modify the next line from `flutter.ndkVersion` to the following:
ndkVersion = "27.0.12077973"
// ...
}
Sie benötigen ein Android-Gerät, das mit Ihrem Entwicklungscomputer verbunden ist, oder eine Android-Emulator-Instanz (AVD). Prüfen Sie, ob Flutter eine Verbindung zum Android-Gerät oder Emulator herstellen kann. Führen Sie dazu Folgendes aus:
$ 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
Führen Sie auf einem laufenden Android-Gerät (physisch oder emuliert) den folgenden Befehl aus:
cd ffigen_app/example flutter run
Sie werden von Flutter gefragt, auf welchem Gerät Sie die App ausführen möchten. Wählen Sie das entsprechende Gerät aus der Liste aus.
macOS und iOS
Für die Flutter-Entwicklung unter macOS und iOS benötigen Sie einen macOS-Computer.
Führen Sie zuerst die Beispiel-App unter macOS aus. Prüfen Sie noch einmal, ob die Geräte, die Flutter erkennt, korrekt sind:
$ 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 geöffnet werden, das in etwa so aussieht:
Unter iOS können Sie den Simulator oder ein echtes Hardwaregerät verwenden. Wenn Sie den Simulator verwenden, starten Sie ihn zuerst. Der Simulator wird jetzt im Befehl flutter devices
als eines der verfügbaren Geräte aufgeführt.
$ 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
Wenn Sie ein iOS-Gerät haben, das entweder ein physisches Gerät oder ein Simulator ist, führen Sie den folgenden Befehl aus:
cd ffigen_app/example flutter run
Sie werden von Flutter gefragt, auf welchem Gerät Sie die App ausführen möchten. Wählen Sie das entsprechende Gerät aus der Liste aus.
Der iOS-Simulator hat Vorrang vor dem macOS-Ziel. Sie können also die Angabe eines Geräts mit dem Parameter -d
überspringen.
Herzlichen Glückwunsch! Sie haben eine Anwendung auf fünf verschiedenen Betriebssystemen erstellt und ausgeführt. Als Nächstes erstellen wir das native Plug-in und stellen über FFI eine Verbindung zu ihm her.
5. Duktape unter Windows, Linux und Android verwenden
Die C-Bibliothek, die Sie in diesem Codelab verwenden, ist Duktape. Duktape ist eine einbettbare JavaScript-Engine, die auf Portabilität und einen kompakten Speicherplatz ausgerichtet ist. In diesem Schritt konfigurieren Sie das Plug-in so, dass die Duktape-Bibliothek kompiliert, mit Ihrem Plug-in verknüpft und dann über die FFI von Dart darauf zugegriffen wird.
Mit diesem Schritt wird die Integration für Windows, Linux und Android konfiguriert. Für die iOS- und macOS-Integration ist eine zusätzliche Konfiguration erforderlich, die über die in diesem Schritt beschriebenen Schritte hinausgeht, um die kompilierte Bibliothek in die endgültige Flutter-Ausführbare Datei aufzunehmen. Die zusätzliche erforderliche Konfiguration wird im nächsten Schritt beschrieben.
Duktape abrufen
Laden Sie zuerst eine Kopie des duktape
-Quellcodes von der Website duktape.org 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
Für 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, einmal, um die xz-Komprimierung zu extrahieren, und das zweite Mal, um das Tar-Archiv zu entpacken.
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. ffigen
verwendet LLVM, um C-Header zu parsen. 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 so, dass C:\Program Files\LLVM\bin
zum Binärsuchpfad hinzugefügt wird, um die Installation von LLVM auf Ihrem Windows-Computer abzuschließen. So können Sie prüfen, ob die Installation korrekt war:
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
Unter 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) ...
Wie oben können Sie Ihre LLVM-Installation unter Linux so testen:
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
ffigen
konfigurieren
Die von der Vorlage generierte pubpsec.yaml
-Datei der obersten Ebene 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
Nachdem das ffigen
-Paket auf dem neuesten Stand ist, konfigurieren Sie als Nächstes, welche Dateien ffigen
zum Generieren der Bindungsdateien verwenden soll. Ändern Sie den Inhalt der ffigen.yaml
-Datei Ihres Projekts so, dass er Folgendes enthält:
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
Diese Konfiguration enthält die C-Headerdatei, die an LLVM übergeben werden soll, die zu generierende Ausgabedatei, die Beschreibung, die oben in die Datei eingefügt werden soll, und einen Abschnitt mit einer Präambel, mit der eine Lint-Warnung hinzugefügt wird.
Am Ende der Datei befindet sich ein Konfigurationselement, das näher erläutert werden sollte. Ab Version 11.0.0 von ffigen
generiert der Binding-Generator standardmäßig keine Bindungen, wenn beim Parsen der Headerdateien von clang
Warnungen oder Fehler generiert werden.
Die Duktape-Headerdateien lösen aufgrund fehlender Nullbarkeitstypspezifizierer für die Duktape-Pointer Warnungen von clang
unter macOS aus. Für die vollständige Unterstützung von macOS und iOS müssen diese Typspezifier der Duktape-Codebase hinzugefügt werden. In der Zwischenzeit ignorieren wir diese Warnungen und setzen das Flag ignore-source-errors
auf true
.
Bei einer Produktionsanwendung sollten Sie alle Compilerwarnungen beseitigen, bevor Sie die Anwendung ausliefern. Für Duktape ist dies jedoch nicht möglich.
Weitere Informationen zu den anderen Schlüsseln und Werten finden Sie in der ffigen
-Dokumentation.
Sie müssen bestimmte Duktape-Dateien aus der Duktape-Distribution an den Speicherort kopieren, an dem ffigen
sie finden soll.
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
in ffigen
kopieren. Sie konfigurieren jedoch CMake zum Erstellen der Bibliothek, für die alle drei benötigt werden. Führen Sie ffigen
aus, um die neue Bindung zu generieren:
$ 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
Je nach Betriebssystem werden unterschiedliche Warnungen angezeigt. Sie können diese Warnungen vorerst ignorieren, da Duktape 2.7.0 unter Windows, Linux und macOS mit clang
kompiliert werden kann.
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, um Duktape in die generierte Flutter-Binärdatei aufzunehmen. Sie müssen die von der Vorlage generierte CMake-Konfigurationsdatei so ändern:
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()
Die CMake-Konfiguration fügt die Quelldateien hinzu und ändert vor allem das Standardverhalten der generierten Bibliotheksdatei unter Windows, um standardmäßig alle C-Symbole zu exportieren. Dies ist eine CMake-Umgehung, die beim Portieren von Unix-Bibliotheken wie Duktape auf Windows hilft.
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 für das Laden der Dynamic Link Library-Datei (.so
für Linux und Android, .dll
für Windows) und für die Bereitstellung eines Wrappers verantwortlich, der dem zugrunde liegenden C-Code eine für Dart typischere Schnittstelle zur Verfügung stellt.
Da diese Datei das ffi
-Paket direkt importiert, müssen Sie das Paket von dev_dependencies
nach dependencies
verschieben. Sie können dazu einfach den folgenden Befehl ausführen:
dart pub add ffi
Ersetzen Sie den Inhalt von main.dart
im Beispiel 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 noch einmal mit folgendem Befehl ausführen:
cd example flutter run
Die App sollte so aussehen:
Diese beiden Screenshots zeigen den Zustand vor und nach dem Drücken der Schaltfläche JavaScript ausführen. Hier wird gezeigt, wie JavaScript-Code aus Dart ausgeführt und das Ergebnis auf dem Bildschirm angezeigt wird.
Android
Android ist ein Linux-Kernel-basiertes Betriebssystem und ähnelt in gewisser Weise Desktop-Linux-Distributionen. Das CMake-Buildsystem kann die meisten Unterschiede zwischen den beiden Plattformen verbergen. Damit Sie auf Android-Geräten erstellen und ausführen können, muss der Android-Emulator ausgeführt werden (oder das Android-Gerät muss verbunden sein). Führen Sie die App aus. Beispiel:
cd example flutter run -d emulator-5554
Die Beispiel-App sollte jetzt auf Android ausgeführt werden:
6. Duktape unter macOS und iOS verwenden
Jetzt ist es an der Zeit, Ihr Plug-in für macOS und iOS zu optimieren, zwei eng miteinander verwandte Betriebssysteme. Beginnen Sie mit macOS. CMake unterstützt zwar macOS und iOS, aber Sie können Ihre Arbeit für Linux und Android nicht wiederverwenden, da Flutter unter macOS und iOS CocoaPods zum Importieren von Bibliotheken verwendet.
Bereinigen
Im vorherigen Schritt haben Sie eine funktionierende Anwendung für Android, Windows und Linux erstellt. Es gibt jedoch einige Dateien aus der ursprünglichen Vorlage, die Sie jetzt bereinigen müssen. Entfernen Sie sie jetzt so:
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, um C- und C++-Code zu importieren. Das bedeutet, dass dieses Paket in die CocoaPods-Build-Infrastruktur eingebunden werden muss. Damit der C-Code, den Sie im vorherigen Schritt für den Build mit CMake konfiguriert haben, wiederverwendet werden kann, müssen Sie dem macOS-Plattform-Runner eine einzelne Weiterleitungsdatei hinzufügen.
macos/Classes/duktape.c
#include "../../src/duktape.c"
In dieser Datei wird der C-Vorprozessor verwendet, um den Quellcode aus dem nativen Quellcode einzubinden, den Sie im vorherigen Schritt eingerichtet haben. Weitere Informationen dazu finden Sie unter macos/ffigen_app.podspec.
Das Ausführen dieser Anwendung folgt jetzt demselben Muster wie unter Windows und Linux.
cd example flutter run -d macos
iOS
Ähnlich wie bei der macOS-Einrichtung muss auch für iOS eine einzelne C-Weiterleitungsdatei hinzugefügt werden.
ios/Classes/duktape.c
#include "../../src/duktape.c"
Mit dieser einzelnen Datei ist Ihr Plug-in jetzt auch für die Ausführung auf iOS-Geräten konfiguriert. Führen Sie es wie gewohnt aus.
flutter run -d iPhone
Glückwunsch! Sie haben nativen Code auf fünf Plattformen erfolgreich eingebunden. Das ist ein Grund zum Feiern! Vielleicht sogar eine funktionalere Benutzeroberfläche, die Sie im nächsten Schritt erstellen.
7. Read Eval Print Loop 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) von LISP. In diesem Schritt implementieren Sie etwas Ähnliches mit Duktape.
Für die Produktion vorbereiten
Der aktuelle Code, der mit der Duktape-C-Bibliothek interagiert, geht davon aus, dass nichts schiefgehen kann. Außerdem werden die Duktape-Bibliotheken für dynamische Links nicht geladen, wenn der Test ausgeführt wird. Damit diese Integration für die Produktion bereit ist, müssen Sie einige Ä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 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;
}
Dazu muss das Paket path
hinzugefügt werden.
flutter pub add path
Der Code zum Laden der Dynamic Link Library wurde erweitert, um den Fall zu behandeln, in dem das Plug-in in einem Test-Runner verwendet wird. So kann ein Integrationstest geschrieben werden, der diese API als Flutter-Test ausführt. Der Code zur Auswertung eines JavaScript-Strings wurde erweitert, um Fehlerbedingungen wie unvollständigen oder falschen Code richtig zu behandeln. Dieser zusätzliche Code zeigt, wie Sie mit Situationen umgehen, in denen Strings als Bytearrays zurückgegeben werden und in Dart-Strings umgewandelt werden müssen.
Pakete hinzufügen
Wenn Sie eine REPL erstellen, wird eine Interaktion zwischen dem Nutzer und der Duktape-JavaScript-Engine angezeigt. Der Nutzer gibt Codezeilen ein und Duktape antwortet entweder mit dem Ergebnis der Berechnung oder einer Ausnahme. Mit freezed
können Sie den zu schreibenden Boilerplate-Code reduzieren. Außerdem verwenden Sie google_fonts
, um die angezeigten Inhalte besser auf das Thema abzustimmen, und flutter_riverpod
für die Statusverwaltung.
Fügen Sie der Beispielanwendung die erforderlichen Abhängigkeiten hinzu:
cd example flutter pub add flutter_riverpod freezed_annotation google_fonts flutter 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-Typfunktion von freezed
, um die Form jeder Zeile, die in der REPL als einer von drei Typen angezeigt wird, typsicher auszudrücken. An dieser Stelle wird in Ihrem Code wahrscheinlich eine Art Fehler angezeigt, da zusätzlicher Code generiert werden muss. Gehen Sie dazu so vor:
flutter pub run build_runner build
Dadurch wird die example/lib/duktape_message.freezed.dart
-Datei generiert, auf die der gerade eingegebene Code angewiesen ist.
Als Nächstes müssen Sie einige Änderungen an den macOS-Konfigurationsdateien vornehmen, damit google_fonts
Netzwerkanfragen für Schriftartendaten stellen 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 die Integrationsebene für die Fehlerbehandlung aktualisiert und eine Datendarstellung für die Interaktion erstellt haben, ist es an der Zeit, die Benutzeroberfläche der Beispiel-App zu 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) {
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,
),
),
],
),
),
);
}
}
In diesem Code passiert viel, aber es würde den Rahmen dieses Codelabs sprengen, alles zu erklären. Ich empfehle Ihnen, den Code auszuführen und dann Änderungen daran vorzunehmen, nachdem Sie sich die entsprechende Dokumentation angesehen haben.
cd example flutter run
8. Glückwunsch
Glückwunsch! Sie haben 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 auf pub.dev finden Sie unter Plug-in-Pakete entwickeln.