1. Wprowadzenie
FFI (interfejs funkcji obcej) w Dart umożliwia aplikacjom Flutter wykorzystywanie istniejących bibliotek natywnych, które udostępniają interfejs API C. Dart obsługuje FFI na urządzeniach z systemami Android, iOS, Windows, macOS i Linux. W przypadku internetu Dart obsługuje interoperacyjność języka JavaScript, ale ten zagadnienie nie został omówiony w tym ćwiczeniu z programowania.
Co utworzysz
W ramach tego ćwiczenia w Codelabs utworzysz wtyczkę na urządzenia mobilne i komputery, która korzysta z biblioteki C. Za pomocą tego interfejsu API napiszesz prostą przykładową aplikację korzystającą z wtyczki. Wtyczka i aplikacja będą:
- Zaimportuj kod źródłowy biblioteki C do nowej wtyczki Flutter
- Dostosuj wtyczkę, aby mogła działać w systemach Windows, macOS, Linux, Android i iOS.
- Utwórz aplikację, która korzysta z wtyczki JavaScriptu do obsługi REPL (odczytywanej pętli drukowania).
Czego się nauczysz
W ramach tego ćwiczenia w programie poznasz praktyczną wiedzę niezbędną do stworzenia opartej na FFI wtyczki Flutter na komputery i platformy mobilne, w tym:
- generowanie szablonu wtyczki Flutter opartego na Dart FFI,
- Użycie pakietu
ffigen
do generowania kodu powiązania dla biblioteki C - Korzystanie z CMake do tworzenia wtyczki Flutter FFI na systemy Android, Windows i Linux
- Korzystanie z CocoaPods do utworzenia wtyczki Flutter FFI na iOS i macOS
Czego potrzebujesz
- Android Studio 4.1 lub nowszy na potrzeby programowania na Androida
- Xcode 13 lub nowsza wersja na potrzeby programowania na iOS i macOS;
- Visual Studio 2022 lub Visual Studio Build Tools 2022 z kursem „Tworzenie komputerów w C++” zadania związane z programowaniem na komputery z systemem Windows
- Pakiet SDK Flutter
- Wszelkie wymagane narzędzia do tworzenia dla platform, na których będziesz programować (np. CMake, CocoaPods itp.).
- LLVM na potrzeby platform, na których będziesz programować. Zestaw narzędzi do kompilatora LLVM jest używany przez
ffigen
do analizowania pliku nagłówka C w celu utworzenia wiązania FFI udostępnianego w Dart. - Edytor kodu, np. Visual Studio Code.
2. Pierwsze kroki
Narzędzie ffigen
zostało niedawno dodane do Flutter. Aby sprawdzić, czy w instalacji Flutter działa bieżąca stabilna wersja, uruchom następujące polecenie.
$ 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!
Sprawdź, czy sygnał wyjściowy flutter doctor
wskazuje, że używasz kanału stabilnego i że nie są dostępne nowsze wersje Flutter. Jeśli nie masz wersji stabilnej lub dostępne są nowsze wersje, uruchom następujące 2 polecenia, aby przyspieszyć działanie narzędzi Flutter.
$ flutter channel stable $ flutter upgrade
Kod w ramach tego ćwiczenia z programowania możesz uruchomić na dowolnym z tych urządzeń:
- komputer programisty (na potrzeby komputerowych kompilacji wtyczki i przykładowej aplikacji);
- Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty
- Symulator iOS (wymaga zainstalowania narzędzi Xcode)
- Emulator Androida (wymaga konfiguracji w Android Studio).
3. Generowanie szablonu wtyczki
Pierwsze kroki z tworzeniem wtyczki Flutter
Flutter zawiera szablony wtyczek, które ułatwiają rozpoczęcie pracy. Podczas generowania szablonu wtyczki możesz określić język, którego chcesz używać.
Uruchom to polecenie w katalogu roboczym, aby utworzyć projekt przy użyciu szablonu wtyczki:
$ flutter create --template=plugin_ffi \ --platforms=android,ios,linux,macos,windows ffigen_app
Parametr --platforms
określa platformy, które obsługuje Twoja wtyczka.
Układ wygenerowanego projektu możesz sprawdzić za pomocą polecenia tree
lub eksploratora plików systemu operacyjnego.
$ 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
Warto poświęcić chwilę na przyjrzenie się strukturze katalogów, aby poczuć, co zostało utworzone i gdzie się znajduje. Szablon plugin_ffi
umieszcza kod Dart wtyczki w katalogu lib
, czyli w katalogach przeznaczonych dla danej platformy o nazwach android
, ios
, linux
, macos
i windows
, a co najważniejsze – w katalogu example
.
Dla programistów przyzwyczajonych do normalnego programowania Flutter ta struktura może wydawać się dziwna, ponieważ na najwyższym poziomie nie ma pliku wykonywalnego. Wtyczka ma być dodana do innych projektów Flutter, ale musisz uzupełnić kod w katalogu example
, by mieć pewność, że kod wtyczki działa.
Czas zaczynać!
4. Tworzenie i uruchamianie przykładu
Aby mieć pewność, że system kompilacji i wymagania wstępne są prawidłowo zainstalowane i działają na każdej obsługiwanej platformie, skompiluj i uruchom wygenerowaną przykładową aplikację dla każdego środowiska docelowego.
Windows
Upewnij się, że używasz obsługiwanej wersji systemu Windows. Ćwiczenia z programowania działają w systemach Windows 10 i Windows 11.
Aplikację możesz utworzyć w edytorze kodu lub w wierszu poleceń.
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=/
Zobaczysz działające okno aplikacji podobne do tego:
Linux
Upewnij się, że używasz obsługiwanej wersji systemu Linux. W tym ćwiczeniu w Codelabs wykorzystuje się Ubuntu 22.04.1
.
Po zainstalowaniu wszystkich wymagań wstępnych wymienionych w kroku 2 uruchom w terminalu te polecenia:
$ 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=/
Zobaczysz działające okno aplikacji podobne do tego:
Android
W przypadku Androida do kompilacji możesz użyć systemów Windows, macOS lub Linux. Najpierw upewnij się, że masz urządzenie z Androidem podłączone do komputera programistycznego lub masz uruchomione wystąpienie emulatora Androida (AVD). Sprawdź, czy Flutter może połączyć się z urządzeniem z Androidem lub z emulatorem, uruchamiając to polecenie:
$ 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 i iOS
W przypadku programowania w systemie macOS i iOS Flutter musisz używać komputera z systemem macOS.
Zacznij od uruchomienia przykładowej aplikacji na urządzeniu z macOS. Ponownie potwierdź urządzenia, które widzi 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
Uruchom przykładową aplikację przy użyciu wygenerowanego projektu wtyczki:
$ cd ffigen_app/example $ flutter run -d macos
Zobaczysz działające okno aplikacji podobne do tego:
W przypadku iOS możesz użyć symulatora lub prawdziwego urządzenia. Jeśli używasz symulatora, najpierw go uruchom. Polecenie flutter devices
wyświetla teraz symulator jako jedno z dostępnych urządzeń.
$ 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
Po uruchomieniu symulatora uruchom polecenie: flutter run
.
$ cd ffigen_app/example $ flutter run -d iphone
Symulator iOS ma pierwszeństwo przed docelowym systemem macOS, więc możesz pominąć określanie urządzenia za pomocą parametru -d
.
Gratulujemy! Udało Ci się utworzyć i uruchomić aplikację w 5 różnych systemach operacyjnych. Następnie utwórz natywną wtyczkę i połącz ją z Dart za pomocą FFI.
5. Korzystanie z Duktape w systemach Windows, Linux i Android
Biblioteka C, której użyjesz w tym ćwiczeniu, to Duktape. Duktape to mechanizm JavaScript, który można umieścić na stronie internetowej i skoncentrować się na przenośności i kompaktowej obudowie. W tym kroku skonfigurujesz wtyczkę do skompilowania biblioteki Duktape, połączenia jej z wtyczką i uzyskania do niej dostępu za pomocą FFI Dart.
W tym kroku skonfigurujesz integrację do działania w systemach Windows, Linux i Android. Integracja z iOS i macOS wymaga dodatkowej konfiguracji (oprócz instrukcji opisanych w tym kroku), aby uwzględnić skompilowaną bibliotekę w końcowym pliku wykonywalnym Flutter. Dodatkową wymaganą konfigurację opisujemy w następnym kroku.
Pobieram duktape
Najpierw pobierz kopię kodu źródłowego duktape
, pobierając go ze strony duktape.org.
W systemie Windows możesz używać PowerShell z funkcją Invoke-WebRequest
:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
W przypadku Linuksa dobrym rozwiązaniem jest wget
.
$ 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]
Plik jest archiwum tar.xz
. W systemie Windows możesz np. pobrać narzędzia 7Zip i użyć ich w następujący sposób.
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
Musisz dwukrotnie uruchomić 7z. Najpierw musisz cofnąć archiwizację kompresji XZ, a potem rozwinąć archiwum 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
W nowoczesnych środowiskach z systemem Linux tar
wyodrębnia zawartość w jednym kroku w następujący sposób.
$ 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]
Instalowanie LLVM
Aby użyć narzędzia ffigen
, musisz zainstalować maszynę wirtualną LLVM, której ffigen
używa do analizowania nagłówków C. W systemie Windows uruchom następujące polecenie:
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
Aby dokończyć instalację LLVM na komputerze z systemem Windows, skonfiguruj ścieżki systemowe i dodaj C:\Program Files\LLVM\bin
do ścieżki wyszukiwania binarnego. Aby sprawdzić, czy kod został poprawnie zainstalowany, wykonaj poniższe czynności.
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
W przypadku Ubuntu zależność LLVM można zainstalować w następujący sposób. Inne dystrybucje Linuksa mają podobne zależności LLVM i 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) ...
Jak powyżej możesz przetestować instalację LLVM w systemie Linux w następujący sposób.
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
Konfiguruję zasób ffigen
Wygenerowany przez szablon element pubpsec.yaml
najwyższego poziomu może mieć nieaktualne wersje pakietu ffigen
. Uruchom to polecenie, aby zaktualizować zależności Dart w projekcie wtyczki:
$ flutter pub upgrade --major-versions
Teraz, gdy pakiet ffigen
jest aktualny. Skonfiguruj teraz, które pliki ffigen
wykorzysta do wygenerowania plików powiązań. Zmodyfikuj zawartość pliku ffigen.yaml
projektu, tak aby była zgodna z podanymi niżej informacjami.
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
Ta konfiguracja obejmuje plik nagłówka C, który ma zostać przekazany do LLVM, plik wyjściowy do wygenerowania, opis, który należy umieścić na początku pliku, oraz sekcję wprowadzenia służącą do dodawania ostrzeżenia o lintowaniu.
Na końcu pliku jest jeden element konfiguracji, który wymaga wyjaśnienia. Od wersji 11.0.0 ffigen
generator powiązań domyślnie nie będzie generować powiązań, jeśli podczas analizowania plików nagłówka clang
wystąpi ostrzeżenia lub błędy.
Zapisane pliki nagłówków Duktape wywołują funkcję clang
w systemie macOS, aby generować ostrzeżenia z powodu braku specyfikatorów typu dopuszczalności null we wskaźnikach Duktape. Aby w pełni obsługiwać systemy macOS i iOS Duktape, do bazy kodu Duktape niezbędne są dodanie tego typu specyfikatorów typu. Tymczasem postanowiliśmy ignorować te ostrzeżenia, ustawiając flagę ignore-source-errors
na true
.
W aplikacji produkcyjnej przed wysłaniem aplikacji należy wyeliminować wszystkie ostrzeżenia dotyczące kompilatora. Jednak wykonanie tego zadania w przypadku Duktape nie jest objęte tym ćwiczeniem.
Więcej informacji o innych kluczach i wartościach znajdziesz w dokumentacji usługi ffigen
.
Musisz skopiować konkretne pliki Duktape z dystrybucji Duktape do lokalizacji, w której skonfigurowano usługę ffigen
, aby je znaleźć.
$ 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/
Z technicznego punktu widzenia wystarczy skopiować dane między duktape.h
w ffigen
, ale zamierzasz skonfigurować CMake, aby biblioteka potrzebowała tych trzech elementów. Uruchom ffigen
, aby wygenerować nowe powiązanie:
$ 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
W każdym systemie operacyjnym będą wyświetlane różne ostrzeżenia. Możesz je na razie zignorować, ponieważ Duktape 2.7.0 jest znany z kompilacji z użyciem clang
w systemach Windows, Linux i macOS.
Konfigurowanie CMake
CMake to system generowania systemu kompilacji. Ta wtyczka korzysta z CMake do generowania systemu kompilacji dla systemów Android, Windows i Linux, w których do wygenerowanego pliku binarnego Flutter zostanie dodany kod Duktape. Musisz zmodyfikować wygenerowany przez szablon plik konfiguracji CMake w następujący sposób.
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)
Konfiguracja CMake dodaje pliki źródłowe, a co ważniejsze – modyfikuje domyślne zachowanie wygenerowanego pliku biblioteki w systemie Windows, aby domyślnie eksportować wszystkie symbole C. Jest to opracowanie CMake, które ma pomóc w przenoszeniu bibliotek Unix (których jest Duktape) w świecie systemu Windows.
Zamień zawartość pola lib/ffigen_app.dart
na taką:
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;
}
Ten plik odpowiada za wczytanie pliku biblioteki linków dynamicznych (.so
dla systemów Linux i Android oraz .dll
w systemie Windows) oraz za udostępnienie kodu, który ujawnia interfejs idiomatyczny Dart dla bazowego kodu C.
Ten plik bezpośrednio importuje pakiet ffi
, więc musisz go przenieść z dev_dependencies
do dependencies
. Aby to zrobić, uruchom następujące polecenie:
$ dart pub add ffi
Zastąp zawartość pola main.dart
przykładu poniższym fragmentem.
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)}';
});
},
),
],
),
),
),
),
);
}
}
Teraz możesz ponownie uruchomić przykładową aplikację, używając:
$ cd example $ flutter run
Aplikacja powinna działać w następujący sposób:
Te 2 zrzuty ekranu pokazują czynności wykonywane przed naciśnięciem przycisku Uruchom JavaScript i po jego naciśnięciu. Pokazuje wykonywanie kodu JavaScript w Dart i wyświetlenie wyniku na ekranie.
Android
Android to system operacyjny Linux oparty na jądrze, który różni się nieco do dystrybucji Linuksa na komputery. System kompilacji CMake może ukrywać większość różnic między tymi 2 platformami. Aby kompilować i uruchamiać aplikację na Androidzie, upewnij się, że emulator Androida jest włączony (lub urządzenie z Androidem jest połączone). Uruchom aplikację. Na przykład:
$ cd example $ flutter run -d emulator-5554
Powinna być teraz widoczna przykładowa aplikacja działająca na Androidzie:
6. Korzystanie z Duktape w systemie macOS i iOS
Nadszedł czas, aby Twoja wtyczka zaczęła działać w systemie macOS i iOS – dwóch ściśle powiązanych systemach operacyjnych. Zacznij od systemu macOS. Choć CMake obsługuje systemy macOS i iOS, nie będzie można ponownie wykorzystać tych samych informacji, które udało Ci się znaleźć w systemach Linux Android, a w systemie macOS i iOS – korzysta z CocoaPods do importowania bibliotek.
Czyszczenie
W poprzednim kroku utworzyliśmy działającą aplikację na systemy Android, Windows i Linux. Zostało jednak kilka plików z oryginalnego szablonu, które trzeba wyczyścić. Usuń je teraz w ten sposób.
$ rm src/ffigen_app.c $ rm src/ffigen_app.h $ rm ios/Classes/ffigen_app.c $ rm macos/Classes/ffigen_app.c
macOS,
Flutter na platformie macOS używa CocoaPods do importowania kodu w języku C i C++. Oznacza to, że ten pakiet musi zostać zintegrowany z infrastrukturą kompilacji CocoaPods. Aby umożliwić ponowne wykorzystanie kodu C już skonfigurowanego na potrzeby kompilacji przy użyciu CMake w poprzednim kroku, musisz dodać pojedynczy plik do przekazywania dalej w uruchamiającym platformę macOS.
macos/Classes/duktape.c
#include "../../src/duktape.c"
Ten plik wykorzystuje możliwości wstępnego procesora C, aby uwzględnić kod źródłowy z natywnego kodu źródłowego skonfigurowanego w poprzednim kroku. Więcej informacji o tym znajdziesz na stronie macos/ffigen_app.podspec.
Uruchamianie tej aplikacji przebiega teraz według tego samego wzorca co w systemach Windows i Linux.
$ cd example $ flutter run -d macos
iOS,
Podobnie jak w przypadku konfiguracji macOS, iOS również wymaga dodania pojedynczego pliku przekierowania C.
ios/Classes/duktape.c
#include "../../src/duktape.c"
W przypadku tego pojedynczego pliku wtyczka jest teraz skonfigurowana tak, aby działać w systemie iOS. Uruchom go w zwykły sposób.
$ flutter run -d iPhone
Gratulacje! Udało Ci się zintegrować kod natywny na 5 platformach. To jest miejsce do świętowania! Być może nawet bardziej funkcjonalny interfejs użytkownika, który utworzysz w następnym kroku.
7. Wdróż pętlę drukowania oceny odczytu
Interakcja z językiem programowania jest znacznie przyjemniejsza w przypadku szybkiego, interaktywnego środowiska. Pierwotną implementacją takiego środowiska była funkcja REPL (Read Eval Print Loop). W tym kroku wdrożysz coś podobnego w Daktape.
Przygotowanie do produkcji
Obecny kod, który wchodzi w interakcje z biblioteką Duktape C, zakłada, że nic nie może pójść nie tak. I nie wczytuje bibliotek linków dynamicznych Duktape podczas testowania. Aby przygotować tę integrację w środowisku produkcyjnym, musisz wprowadzić kilka zmian w usłudze 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;
}
Kod wczytujący bibliotekę linków dynamicznych został rozszerzony, aby obsługiwać przypadki użycia wtyczki w programie uruchamiającym test. Dzięki temu można zapisać test integracji, który będzie wykorzystywał ten interfejs API jako test Flutter. Kod do oceny ciągu kodu JavaScript został rozszerzony, aby zapewnić prawidłową obsługę warunków błędów, np. niekompletnego lub nieprawidłowego kodu. Ten dodatkowy kod pokazuje, jak obsługiwać sytuacje, w których ciągi są zwracane jako tablice bajtów i muszą być przekonwertowane na ciągi znaków DART.
Dodawanie pakietów
Podczas tworzenia obiektu REPL wyświetlisz interakcję między użytkownikiem a mechanizmem JavaScriptu Duktape. Użytkownik wpisuje wiersze kodu, a Duktape w odpowiedzi podaje wynik obliczeń lub wyjątek. Użyj freezed
, aby zmniejszyć ilość kodu stałego, który musisz napisać. Użyjesz narzędzia google_fonts
, aby wyświetlane treści były bardziej powiązane z motywem, i flutter_riverpod
do zarządzania stanem.
Dodaj wymagane zależności do przykładowej aplikacji:
$ cd example $ dart pub add flutter_riverpod freezed_annotation google_fonts $ dart pub add -d build_runner freezed
Następnie utwórz plik rejestrujący interakcję 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;
}
W tej klasie wykorzystywana jest funkcja typu union (freezed
), która umożliwia łatwe wyrażenie kształtu każdej linii wyświetlanej w REPL jako jeden z 3 typów. W tym momencie Twój kod prawdopodobnie wyświetla jakiś rodzaj błędu, ponieważ wymaga to wygenerowania dodatkowego kodu. Zrób to teraz w ten sposób.
$ flutter pub run build_runner build
Spowoduje to wygenerowanie pliku example/lib/duktape_message.freezed.dart
, na którym opiera się wpisany przed chwilą kod.
Następnie musisz wprowadzić parę modyfikacji w plikach konfiguracji macOS, aby umożliwić usłudze google_fonts
wysyłanie żądań sieciowych dotyczących danych czcionek.
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>
Tworzenie REPL
Po zaktualizowaniu warstwy integracji pod kątem błędów i utworzeniu reprezentacji danych dla interakcji nadszedł czas na zbudowanie interfejsu użytkownika przykładowej aplikacji.
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,
),
),
],
),
),
);
}
}
W tym kodzie dużo się dzieje, ale wyjaśnienie nie wykracza poza zakres tego ćwiczenia. Zalecamy uruchomienie kodu, a potem jego modyfikację po zapoznaniu się z odpowiednią dokumentacją.
$ cd example $ flutter run
8. Gratulacje
Gratulacje! Udało Ci się utworzyć wtyczkę Flutter FFI dla systemów Windows, macOS, Linux, Android i iOS.
Po utworzeniu wtyczki możesz ją udostępnić online, aby inni mogli z niej korzystać. Pełną dokumentację dotyczącą publikowania wtyczki w witrynie pub.dev znajdziesz w artykule Tworzenie pakietów wtyczek.