Informacje o tym ćwiczeniu (w Codelabs)
1. Wprowadzenie
Interfejs FFI (foreign function interface) w języku Dart umożliwia aplikacjom Flutter korzystanie z dotychczasowych natywnych bibliotek, które udostępniają interfejs C API. Dart obsługuje FFI na Androidzie, iOS, Windows, macOS i Linuxie. W przypadku internetu Dart obsługuje interoperacyjność z JavaScriptem, ale ten temat nie jest omawiany w tym CodeLab.
Co utworzysz
W tym ćwiczeniu tworzysz wtyczkę na urządzenia mobilne i komputery, która korzysta z biblioteki C. Za pomocą tego interfejsu API napiszesz przykładową aplikację, która korzysta z wtyczki. Wtyczka i aplikacja:
- Importowanie kodu źródłowego biblioteki C do nowego wtyczki Flutter
- Spersonalizuj wtyczkę, aby umożliwić jej kompilację w systemach Windows, macOS, Linux, Android i iOS
- Utwórz aplikację, która używa wtyczki do pętli REPL (read reveal print) w JavaScript.
Czego się nauczysz
W tym ćwiczeniu z programowania poznasz praktyczne informacje potrzebne do tworzenia wtyczki Flutter na podstawie FFI na platformach komputerowych i mobilnych, w tym:
- Generowanie szablonu wtyczki Flutter na podstawie Dart FFI
- Generowanie kodu wiązania dla biblioteki C za pomocą pakietu
ffigen
- Kompilowanie wtyczki FFI Flutter na Androida, Windowsa i Linuxa za pomocą CMake
- Tworzenie wtyczki FFI Flutter na iOS i macOS za pomocą CocoaPods
Czego potrzebujesz
- Android Studio w wersji 4.1 lub nowszej do tworzenia aplikacji na Androida
- Xcode 13 lub nowsza wersja do tworzenia aplikacji na iOS i macOS
- Visual Studio 2022 lub Visual Studio Build Tools 2022 z pakietem „Programowanie na komputery z systemem Windows w C++” do tworzenia aplikacji na komputery z systemem Windows
- Pakiet SDK Flutter
- Wszystkie wymagane narzędzia do kompilacji na platformy, na których będziesz prowadzić prace programistyczne (np. CMake, CocoaPods itp.).
- LLVM na platformy, na których będziesz tworzyć. Zestaw narzędzi kompilatora LLVM jest używany przez
ffigen
do analizowania pliku nagłówka C w celu utworzenia powiązania FFI udostępnianego w Dart. - Edytor kodu, np. Visual Studio Code.
2. Pierwsze kroki
Narzędzie ffigen
zostało niedawno dodane do Fluttera. Aby sprawdzić, czy zainstalowana wersja Fluttera działa w ramach bieżącej stabilnej wersji, uruchom to polecenie.
$ 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!
Sprawdź, czy w wyniku flutter doctor
widnieje informacja, że używasz kanału stabilnego i że nie ma nowszych stabilnych wersji Fluttera. Jeśli nie używasz wersji stabilnej lub są dostępne nowsze wersje, uruchom te 2 polecenia, aby zaktualizować narzędzia Flutter.
flutter channel stable flutter upgrade
Kod w tym CodeLab możesz uruchomić na dowolnym z tych urządzeń:
- Twój komputer programistyczny (do tworzenia wersji wtyczki i przykładowej aplikacji na komputer)
- fizyczne urządzenie z Androidem lub iOS połączone z komputerem i ustawione w trybie dewelopera.
- symulatora iOS (wymaga zainstalowania narzędzi Xcode);
- emulator Androida (wymaga konfiguracji w Android Studio);
3. Generowanie szablonu wtyczki
Pierwsze kroki z rozwojem wtyczki Flutter
Flutter zawiera szablony wtyczek, które ułatwiają rozpoczęcie pracy. Podczas generowania szablonu wtyczki możesz określić, którego języka chcesz użyć.
Aby utworzyć projekt za pomocą szablonu wtyczki, uruchom to polecenie w katalogu roboczym:
flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows ffigen_app
Parametr --platforms
określa, na których platformach będzie obsługiwany Twój wtyczka.
Układ wygenerowanego projektu możesz sprawdzić za pomocą polecenia tree
lub Eksploratora plików w systemie operacyjnym.
$ 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 zapoznanie się ze strukturą katalogów, aby dowiedzieć się, co zostało utworzone i gdzie się znajduje. Szablon plugin_ffi
umieszcza kod Dart w pluginie w katalogach lib
, android
, ios
, linux
, macos
i windows
, a co najważniejsze, w katalogu example
.
Dla dewelopera przyzwyczajonego do zwykłego tworzenia aplikacji w Flutterze ta struktura może wydawać się dziwna, ponieważ na najwyższym poziomie nie ma pliku wykonywalnego. Wtyczka ma być dołączona do innych projektów Fluttera, ale kod będziesz uzupełniać w katalogu example
, aby sprawdzić, czy działa.
Czas zacząć!
4. Kompilowanie 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 celu.
Windows
Sprawdź, czy używasz obsługiwanej wersji systemu Windows. To ćwiczenie z programowania w języku Java działa w systemach Windows 10 i Windows 11.
Aplikację możesz skompilować w edytorze kodu lub z wiersza 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=/
Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:
Linux
Sprawdź, czy używasz obsługiwanej wersji systemu Linux. To ćwiczenie z programowania korzysta z Ubuntu 22.04.1
.
Gdy zainstalujesz wszystkie wymagania wstępne wymienione 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=/
Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:
Android
W przypadku Androida możesz użyć systemu Windows, macOS lub Linux do skompilowania aplikacji.
Aby używać odpowiedniej wersji NDK, musisz wprowadzić zmianę w example/android/app/build.gradle.kts
.
example/android/app/build.gradle.kts)
android {
// Modify the next line from `flutter.ndkVersion` to the following:
ndkVersion = "27.0.12077973"
// ...
}
Upewnij się, że masz urządzenie z Androidem połączone z komputerem programisty lub że uruchomiono instancję emulatora Androida (AVD). Aby sprawdzić, czy Flutter może połączyć się z urządzeniem z Androidem lub emulatorem, uruchom te polecenia:
$ 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
Po uruchomieniu urządzenia z Androidem (fizycznego lub w emulatorze) uruchom to polecenie:
cd ffigen_app/example flutter run
Flutter zapyta Cię, na jakim urządzeniu chcesz uruchomić aplikację. Wybierz odpowiednie urządzenie z listy.
macOS i iOS
Do tworzenia aplikacji Flutter na macOS i iOS musisz używać komputera z systemem macOS.
Zacznij od uruchomienia przykładowej aplikacji na macOS. Ponownie sprawdź 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ę za pomocą wygenerowanego projektu wtyczki:
cd ffigen_app/example flutter run -d macos
Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:
W przypadku iOS możesz użyć symulatora lub prawdziwego urządzenia. Jeśli korzystasz z 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 urządzenia z iOS (fizycznego lub na symulatorze) uruchom to polecenie:
cd ffigen_app/example flutter run
Flutter zapyta Cię, na jakim urządzeniu chcesz uruchomić aplikację. Wybierz odpowiednie urządzenie z listy.
Symulator iOS ma wyższy priorytet niż docel macOS, więc możesz pominąć określenie urządzenia za pomocą parametru -d
.
Gratulacje, udało Ci się utworzyć i uruchomić aplikację na 5 różnych systemach operacyjnych. Następnie utwórz natywny wtyczkę i zaimplementuj interfejs za pomocą FFI.
5. Korzystanie z Duktape w systemach Windows, Linux i Android
Biblioteka C, której użyjesz w tym laboratorium kodu, to Duktape. Duktape to mechanizm JavaScript, który można umieszczać w innych aplikacjach. Jego zalety to przenośność i mała wielkość. W tym kroku skonfigurujesz wtyczkę, aby skompilować bibliotekę Duktape, powiązać ją z wtyczką, a potem uzyskać do niej dostęp za pomocą interfejsu FFI Darta.
Ten krok konfiguruje integrację do działania w systemach Windows, Linux i Android. Integracja z iOS i macOS wymaga dodatkowej konfiguracji (oprócz tej opisanej w tym kroku), aby uwzględnić skompilowaną bibliotekę w końcowym pliku wykonywalnym Fluttera. W następnym kroku omówimy wymaganą dodatkową konfigurację.
Odzyskiwanie Duktape
Najpierw pobierz kopię kodu źródłowego duktape
, pobierając go z witryny duktape.org.
W systemie Windows możesz użyć PowerShell z Invoke-WebRequest
:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
W przypadku systemu Linux dobrym wyborem 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 pobrać narzędzia 7-Zip i użyć ich w ten 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 uruchomić 7z dwa razy: pierwszy raz, aby rozpakować kompresję xz, a drugi, aby rozpakować 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 Linuxa tar
wyodrębnia zawartość w jednym kroku w ten 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żywać ffigen
, musisz zainstalować LLVM, którego ffigen
używa do analizowania nagłówków C. W systemie Windows uruchom to 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 zakończyć instalację LLVM na komputerze z systemem Windows, skonfiguruj ścieżki systemowe, dodając C:\Program Files\LLVM\bin
do ścieżki wyszukiwania binarnego. Aby sprawdzić, czy został prawidłowo zainstalowany, wykonaj te 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 Ubuntu zależność LLVM można zainstalować w ten sposób: Inne dystrybucje Linuksa mają podobne zależności w przypadku 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 wspomniano powyżej, możesz przetestować instalację LLVM w systemie Linux w ten sposób:
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
Skonfiguruj ffigen
Szablon wygenerowany na najwyższym poziomie pubpsec.yaml
może zawierać nieaktualne wersje pakietu ffigen
. Aby zaktualizować zależności Dart w projekcie wtyczki, uruchom to polecenie:
flutter pub upgrade --major-versions
Teraz, gdy pakiet ffigen
jest aktualny, skonfiguruj pliki, których ffigen
będzie używać do generowania plików wiązania. Zmodyfikuj zawartość pliku ffigen.yaml
projektu, aby odpowiadała poniższemu.
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
Ta konfiguracja zawiera plik nagłówka C, który należy przekazać do LLVM, plik wyjściowy do wygenerowania, opis do umieszczenia na początku pliku oraz sekcję wstępną, która służy do dodawania ostrzeżeń lint.
Na końcu pliku znajduje się jeden element konfiguracji, który wymaga wyjaśnienia. Od wersji 11.0.0 generatora powiązań domyślnie nie generuje powiązań, jeśli podczas analizowania plików nagłówków wystąpiły ostrzeżenia lub błędy generowane przez clang
.ffigen
Pliki nagłówka Duktape, w sposób, w jaki zostały napisane, powodują, że clang
w systemie macOS generuje ostrzeżenia z powodu braku specyfikatorów typu „null” w wskaźnikach Duktape. Aby w pełni obsługiwać systemy macOS i iOS, Duktape musi dodać te specyfikatory typów do kodu źródłowego Duktape. Tymczasem postanowiliśmy zignorować te ostrzeżenia, ustawiając flagę ignore-source-errors
na true
.
W przypadku aplikacji produkcyjnej przed jej udostępnieniem musisz wyeliminować wszystkie ostrzeżenia kompilatora. Jednak w przypadku Duktape jest to poza zakresem tego ćwiczenia.
Więcej informacji o innych kluczach i wartościach znajdziesz w dokumentacji ffigen
.
Musisz skopiować określone pliki Duktape z dystrybucji Duktape do lokalizacji, w której ffigen
ma 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/
Teoretycznie wystarczy skopiować duktape.h
do ffigen
, ale skonfigurujesz CMake, aby skompilować bibliotekę, która potrzebuje wszystkich 3 wersji. Aby wygenerować nowe powiązanie, uruchom ffigen
:
$ 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
W każdym systemie operacyjnym zobaczysz inne ostrzeżenia. Na razie możesz je zignorować, ponieważ Duktape 2.7.0 kompiluje się z clang
w systemach Windows, Linux i macOS.
Konfigurowanie CMake
CMake to system generowania systemu kompilacji. Ta wtyczka używa CMake do generowania systemu kompilacji dla Androida, Windowsa i Linuxa, aby uwzględnić Duktape w wygenerowanym binarnym pliku Fluttera. Musisz zmodyfikować wygenerowany przez szablon plik konfiguracji CMake w ten 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)
if (ANDROID)
# Support Android 15 16k page size
target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()
Konfiguracja CMake dodaje pliki źródłowe, a co ważniejsze, modyfikuje domyślne zachowanie wygenerowanego pliku biblioteki w Windows, aby domyślnie eksportować wszystkie symbole C. Jest to obejście CMake, które ułatwia przenoszenie bibliotek w stylu Unix, do których należy Duktape, do świata Windows.
Zamień zawartość pliku lib/ffigen_app.dart
na tę:
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 wczytywanie pliku biblioteki dynamicznych (.so
w przypadku systemu Linux i Androida, .dll
w przypadku systemu Windows) oraz za udostępnianie owijki, która udostępnia bardziej idiomatyczny interfejs Dart do kodu C.
Ponieważ ten plik importuje bezpośrednio pakiet ffi
, musisz przenieść go z poziomu dev_dependencies
do dependencies
. Najszybszym sposobem jest uruchomienie tego polecenia:
dart pub add ffi
Zamień zawartość przykładu main.dart
na tę:
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ę, korzystając z jednego z tych sposobów:
cd example flutter run
Aplikacja powinna działać w ten sposób:
Te 2 zrzuty ekranu przedstawiają sytuację przed i po naciśnięciu przycisku Uruchom JavaScript. Ten przykład pokazuje wykonywanie kodu JavaScript z Dartu i wyświetlanie wyniku na ekranie.
Android
Android to system operacyjny oparty na jądrze Linuksa i jest nieco podobny do dystrybucji Linuksa na komputery. System kompilacji CMake może ukryć większość różnic między tymi platformami. Aby skompilować i uruchomić aplikację na Androidzie, upewnij się, że emulowany Android jest uruchomiony (lub że urządzenie z Androidem jest podłączone). Uruchom aplikację. Na przykład:
cd example flutter run -d emulator-5554
Powinna się teraz uruchomić przykładowa aplikacja na Androida:
6. Korzystanie z Duktape w systemie macOS i iOS
Teraz czas uruchomić wtyczkę na macOS i iOS, czyli dwóch powiązanych ze sobą systemach operacyjnych. Zacznij od macOS. CMake obsługuje systemy macOS i iOS, ale nie można ponownie użyć kodu napisanego na potrzeby Linuxa i Androida, ponieważ Flutter w systemach macOS i iOS używa CocoaPods do importowania bibliotek.
Czyszczenie
W poprzednim kroku utworzyliśmy działającą aplikację na Androida, Windowsa i Linuxa. Pozostało jednak kilka plików z pierwotnego szablonu, które musisz teraz usunąć. 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 C i C++. Oznacza to, że ten pakiet musi zostać zintegrowany z infrastrukturą kompilacji CocoaPods. Aby umożliwić ponowne użycie kodu C skompilowanego w CMake w poprzednim kroku, musisz dodać jeden plik przekierowujący w Runnerze platformy macOS.
macos/Classes/duktape.c
#include "../../src/duktape.c"
Ten plik korzysta z kompilatora C, aby uwzględnić kod źródłowy z natywnego kodu źródłowego skonfigurowanego w poprzednim kroku. Więcej informacji o tym, jak to działa, znajdziesz w pliku macos/ffigen_app.podspec.
Uruchomienie tej aplikacji odbywa się teraz w taki sam sposób jak w Windows i Linuxie.
cd example flutter run -d macos
iOS
Podobnie jak w przypadku konfiguracji systemu macOS, iOS wymaga dodania pojedynczego pliku C do przekierowania.
ios/Classes/duktape.c
#include "../../src/duktape.c"
Dzięki temu jednemu plikowi wtyczka jest teraz skonfigurowana tak, aby działać na iOS. Uruchom go jak zwykle.
flutter run -d iPhone
Gratulacje! Udało Ci się zintegrować kod natywny na 5 platformach. To powód do świętowania! Może nawet bardziej funkcjonalny interfejs użytkownika, który utworzysz w następnym kroku.
7. Wdrażanie pętli odczytu i wydruku
Interakcje z językiem programowania są dużo przyjemniejsze w szybkim, interaktywnym środowisku. Pierwotna implementacja takiego środowiska to pętla odczytu, oceny i wydruku (REPL) w języku LISP. Na tym etapie zaimplementujesz coś podobnego za pomocą Duktape.
Przygotowanie do wdrożenia
Obecny kod, który współpracuje z biblioteką C Duktape, zakłada, że nic nie może pójść nie tak. Podczas testowania nie wczytuje bibliotek linków dynamicznych Duktape. Aby przygotować tę integrację do wdrożenia, musisz wprowadzić kilka zmian w lib/ffigen_app.dart
.
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p; // Add this import
import 'duktape_bindings_generated.dart';
const String _libName = 'ffigen_app';
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(
'build/macos/Build/Products/Debug/$_libName/$_libName.framework/$_libName',
);
}
// To here.
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(
'build/linux/x64/debug/bundle/lib/lib$_libName.so',
);
}
// To here.
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return switch (Abi.current()) {
Abi.windowsArm64 => DynamicLibrary.open(
p.canonicalize(
p.join(r'build\windows\arm64\runner\Debug', '$_libName.dll'),
),
),
Abi.windowsX64 => DynamicLibrary.open(
p.canonicalize(
p.join(r'build\windows\x64\runner\Debug', '$_libName.dll'),
),
),
_ => throw 'Unsupported platform',
};
}
// To here.
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final DuktapeBindings _bindings = DuktapeBindings(_dylib);
class Duktape {
Duktape() {
ctx = _bindings.duk_create_heap(
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
}
// Modify this function
String evalString(String jsCode) {
var nativeUtf8 = jsCode.toNativeUtf8();
final evalResult = _bindings.duk_eval_raw(
ctx,
nativeUtf8.cast<Char>(),
0,
0 |
DUK_COMPILE_EVAL |
DUK_COMPILE_SAFE |
DUK_COMPILE_NOSOURCE |
DUK_COMPILE_STRLEN |
DUK_COMPILE_NOFILENAME,
);
ffi.malloc.free(nativeUtf8);
if (evalResult != 0) {
throw _retrieveTopOfStackAsString();
}
return _retrieveTopOfStackAsString();
}
// Add this function
String _retrieveTopOfStackAsString() {
Pointer<Size> outLengthPtr = ffi.calloc<Size>();
final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
final returnVal = errorStrPtr.cast<ffi.Utf8>().toDartString(
length: outLengthPtr.value,
);
ffi.calloc.free(outLengthPtr);
return returnVal;
}
void dispose() {
_bindings.duk_destroy_heap(ctx);
ctx = nullptr;
}
late Pointer<duk_hthread> ctx;
}
Wymaga to dodania pakietu path
.
flutter pub add path
Kod służący do wczytywania biblioteki linków dynamicznych został rozszerzony, aby obsługiwać przypadki, w których wtyczka jest używana w ramach testów. Umożliwia to napisanie testu integracji, który testuje ten interfejs API jako test Fluttera. Kod służący do oceny ciągu kodu JavaScript został rozszerzony, aby prawidłowo obsługiwać stany błędów, np. niekompletny lub niepoprawny kod. Ten dodatkowy kod pokazuje, jak radzić sobie z sytuacjami, w których ciągi znaków są zwracane jako tablice bajtów i muszą zostać przekonwertowane na ciągi znaków w języku Dart.
Dodawanie pakietów
Podczas tworzenia REPL wyświetlasz interakcję między użytkownikiem a silnikiem JavaScript Duktape. Użytkownik podaje wiersze kodu, a Duktape odpowiada wynikiem obliczeń lub wyjątkiem. Aby ograniczyć ilość kodu stałego, który musisz napisać, użyjesz freezed
. Użyjesz też atrybutu google_fonts
, aby wyświetlane treści były bardziej związane z tematem, oraz atrybutu flutter_riverpod
do zarządzania stanami.
Dodaj do przykładowej aplikacji wymagane zależności:
cd example flutter pub add flutter_riverpod freezed_annotation google_fonts flutter pub add -d build_runner freezed
Następnie utwórz plik, w którym zapiszesz interakcję z 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;
}
Ta klasa korzysta z funkcji freezed
typu zjednoczenia, aby umożliwić bezpieczne wyrażanie kształtu każdej linii wyświetlanej w repl jako jeden z 3 typów. W tym momencie Twój kod prawdopodobnie wyświetla błąd, ponieważ musi zostać wygenerowany dodatkowy kod. 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ę kod, który właśnie wpisujesz.
Następnie musisz wprowadzić kilka zmian w plikach konfiguracji macOS, aby umożliwić 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 w celu obsługi błędów i utworzeniu reprezentacji danych dla interakcji nadszedł czas na stworzenie interfejsu 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) {
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,
),
),
],
),
),
);
}
}
Ten kod jest bardzo rozbudowany, ale nie da się go w pełni omówić w ramach tego Codelab. Zalecamy uruchomienie kodu, a potem wprowadzenie w nim zmian po zapoznaniu się z odpowiednią dokumentacją.
cd example flutter run
8. Gratulacje
Gratulacje! Udało Ci się utworzyć wtyczkę Flutter FFI na potrzeby systemów Windows, macOS, Linux, Android i iOS.
Po utworzeniu wtyczki możesz udostępnić ją online, aby inni mogli z niej korzystać. Pełną dokumentację dotyczącą publikowania wtyczki na pub.dev znajdziesz w artykule Tworzenie pakietów wtyczek.