1. Введение
FFI (интерфейс внешних функций) Dart позволяет приложениям Flutter использовать существующие собственные библиотеки, предоставляющие C API . Dart поддерживает FFI на Android, iOS, Windows, macOS и Linux. В Интернете Dart поддерживает взаимодействие с JavaScript, но эта тема не рассматривается в данной лаборатории.
Что ты построишь
В этой лаборатории кода вы создадите плагин для мобильных и настольных компьютеров, использующий библиотеку C. С помощью этого API вы напишете простой пример приложения, использующего плагин. Ваш плагин и приложение будут:
- Импортируйте исходный код библиотеки C в ваш новый плагин Flutter.
- Настройте плагин, чтобы он мог работать на Windows, macOS, Linux, Android и iOS.
- Создайте приложение, которое использует плагин для JavaScript REPL (читайте цикл печати).
Что вы узнаете
В этой лаборатории вы изучите практические знания, необходимые для создания плагина Flutter на основе FFI как для настольных, так и для мобильных платформ, в том числе:
- Создание шаблона плагина Flutter на основе Dart FFI
- Использование пакета
ffigen
для создания кода привязки для библиотеки C. - Использование CMake для создания плагина Flutter FFI для Android , Windows и Linux
- Использование CocoaPods для создания плагина Flutter FFI для iOS и macOS
Что вам понадобится
- Android Studio 4.1 или более поздней версии для разработки под Android.
- Xcode 13 или новее для разработки iOS и macOS.
- Visual Studio 2022 или Visual Studio Build Tools 2022 с рабочей нагрузкой «Разработка настольных компьютеров на C++» для разработки настольных компьютеров Windows.
- Флаттер SDK
- Любые необходимые инструменты сборки для платформ, на которых вы будете разрабатывать (например, CMake, CocoaPods и т. д.).
- LLVM для платформ, на которых вы будете разрабатывать . Набор инструментов компилятора LLVM используется
ffigen
для анализа заголовочного файла C для создания привязки FFI, представленной в Dart. - Редактор кода, например Visual Studio Code .
2. Начало работы
Инструмент ffigen
— недавнее дополнение к Flutter. Вы можете убедиться, что в вашей установке Flutter используется текущая стабильная версия, выполнив следующую команду.
$ 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!
Убедитесь, что в выводе flutter doctor
указано, что вы находитесь на стабильном канале и что более поздние стабильные выпуски Flutter недоступны. Если у вас не стабильная версия или доступны более свежие версии, выполните следующие две команды, чтобы ускорить работу инструментов Flutter.
$ flutter channel stable $ flutter upgrade
Вы можете запустить код в этой лаборатории кода, используя любое из этих устройств:
- Ваш компьютер разработки (для настольных сборок вашего плагина и примера приложения)
- Физическое устройство Android или iOS, подключенное к вашему компьютеру и переведенное в режим разработчика.
- Симулятор iOS (требуется установка инструментов Xcode)
- Эмулятор Android (требуется установка в Android Studio)
3. Создайте шаблон плагина.
Начало работы с разработкой плагина Flutter
Flutter поставляется с шаблонами для плагинов , которые упрощают начало работы. Когда вы создаете шаблон плагина, вы можете указать, какой язык вы хотите использовать.
Запустите следующую команду в своем рабочем каталоге, чтобы создать проект с использованием шаблона плагина:
$ flutter create --template=plugin_ffi \ --platforms=android,ios,linux,macos,windows ffigen_app
Параметр --platforms
указывает, какие платформы будет поддерживать ваш плагин.
Вы можете проверить макет созданного проекта с помощью команды tree
или файлового менеджера вашей операционной системы.
$ 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
Стоит потратить немного времени на изучение структуры каталогов, чтобы понять, что было создано и где оно находится. Шаблон plugin_ffi
помещает код Dart для плагина в lib
, каталоги для конкретных платформ с именами android
, ios
, linux
, macos
и windows
и, что наиболее важно, в каталог example
.
Для разработчика, привыкшего к обычной разработке Flutter, эта структура может показаться странной, поскольку на верхнем уровне не определен исполняемый файл. Плагин предназначен для включения в другие проекты Flutter, но вы доработаете код в каталоге example
, чтобы убедиться, что код вашего плагина работает.
Пришло время начать!
4. Создайте и запустите пример.
Чтобы убедиться, что система сборки и необходимые компоненты правильно установлены и работают для каждой поддерживаемой платформы, соберите и запустите созданный пример приложения для каждой цели.
Окна
Убедитесь, что вы используете поддерживаемую версию Windows. Известно, что эта кодовая лаборатория работает в Windows 10 и Windows 11.
Вы можете создать приложение из редактора кода или из командной строки.
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=/
Вы должны увидеть окно работающего приложения, подобное следующему:
Линукс
Убедитесь, что вы используете поддерживаемую версию Linux. В этой кодовой лаборатории используется Ubuntu 22.04.1
.
После установки всех необходимых компонентов, перечисленных в шаге 2, выполните в терминале следующие команды:
$ 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=/
Вы должны увидеть окно работающего приложения, подобное следующему:
Андроид
Для Android вы можете использовать для компиляции Windows, macOS или Linux. Сначала убедитесь, что к вашему компьютеру для разработки подключено устройство Android или запущен экземпляр эмулятора Android (AVD). Убедитесь, что Flutter может подключиться либо к устройству Android, либо к эмулятору, выполнив следующую команду:
$ 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 и iOS
Для разработки Flutter для macOS и iOS вам необходимо использовать компьютер с macOS.
Начните с запуска примера приложения на macOS. Еще раз подтвердите устройства, которые видит 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
Запустите пример приложения, используя сгенерированный проект плагина:
$ cd ffigen_app/example $ flutter run -d macos
Вы должны увидеть окно работающего приложения, подобное следующему:
Для iOS вы можете использовать симулятор или реальное аппаратное устройство. Если вы используете симулятор, сначала запустите его. Команда flutter devices
теперь указывает симулятор как одно из доступных устройств.
$ 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
После запуска симулятора запустите: flutter run
.
$ cd ffigen_app/example $ flutter run -d iphone
Симулятор iOS имеет приоритет над целевым объектом macOS, поэтому вы можете пропустить указание устройства с помощью параметра -d
.
Поздравляем, вы успешно создали и запустили приложение в пяти разных операционных системах. Далее создаем собственный плагин и взаимодействуем с ним из Dart с помощью FFI.
5. Использование Duktape в Windows, Linux и Android
Библиотека C, которую вы будете использовать в этой лаборатории кода, — Duktape . Duktape — это встраиваемый Javascript-движок, ориентированный на портативность и компактность. На этом этапе вы настроите плагин для компиляции библиотеки Duktape, свяжете ее со своим плагином, а затем получите к ней доступ с помощью Dart FFI.
На этом этапе интеграция настраивается для работы в Windows, Linux и Android. Интеграция iOS и macOS требует дополнительной настройки (помимо того, что подробно описано на этом этапе) для включения скомпилированной библиотеки в окончательный исполняемый файл Flutter. Дополнительная необходимая конфигурация рассматривается на следующем шаге.
Получение дуктейпа
Сначала получите копию исходного кода duktape
, загрузив ее с сайта duktape.org .
Для Windows вы можете использовать PowerShell с Invoke-WebRequest
:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
Для Linux 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]
Файл представляет собой архив tar.xz
В Windows один из вариантов — загрузить инструменты 7Zip и использовать их следующим образом.
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
Вам нужно запустить 7z дважды: сначала, чтобы разархивировать сжатие xz, затем, чтобы расширить архив 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
В современных средах Linux tar
извлекает содержимое за один шаг следующим образом.
$ 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]
Установка ЛЛВМ
Чтобы использовать ffigen
, вам необходимо установить LLVM , который ffigen
использует для анализа заголовков C. В Windows выполните следующую команду.
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
Настройте системные пути, добавив C:\Program Files\LLVM\bin
в путь двоичного поиска, чтобы завершить установку LLVM на вашем компьютере с Windows. Вы можете проверить, правильно ли он установлен, следующим образом.
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
Для Ubuntu зависимость LLVM можно установить следующим образом. Другие дистрибутивы Linux имеют аналогичные зависимости для LLVM и 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) ...
Как указано выше, вы можете протестировать установку LLVM в Linux следующим образом.
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
Настройка ffigen
Созданный шаблон верхнего уровня pubpsec.yaml
может содержать устаревшие версии пакета ffigen
. Запустите следующую команду, чтобы обновить зависимости Dart в проекте плагина:
$ flutter pub upgrade --major-versions
Теперь, когда пакет ffigen
обновлен, настройте файлы, которые ffigen
будет использовать для создания файлов привязки. Измените содержимое файла ffigen.yaml
вашего проекта так, чтобы оно соответствовало следующему.
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
Эта конфигурация включает в себя заголовочный файл C для передачи в LLVM, выходной файл для создания, описание, помещаемое в начало файла, и раздел преамбулы, используемый для добавления предупреждения о некорректности.
В конце файла есть один элемент конфигурации, который заслуживает дальнейшего объяснения. Начиная с версии ffigen
11.0.0, генератор привязок по умолчанию не будет генерировать привязки, если при анализе файлов заголовков возникают предупреждения или ошибки, создаваемые clang
.
Заголовочные файлы Duktape, как написано, запускают clang
в macOS для генерации предупреждений из-за отсутствия спецификаторов типа, допускающих значение NULL, в указателях Duktape. Для полной поддержки macOS и iOS Duktape необходимо добавить эти спецификаторы типов в кодовую базу Duktape. Тем временем мы принимаем решение игнорировать эти предупреждения, установив для флага ignore-source-errors
значение true
.
В рабочем приложении перед отправкой приложения следует устранить все предупреждения компилятора. Однако выполнение этого для Duktape выходит за рамки данной лаборатории.
Дополнительную информацию о других ключах и значениях см. в документации ffigen
Вам необходимо скопировать определенные файлы Duktape из дистрибутива Duktape в папку, где ffigen
настроен для их поиска.
$ 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/
Технически вам нужно скопировать только duktape.h
для ffigen
, но вы собираетесь настроить CMake для создания библиотеки, которой нужны все три. Запустите ffigen
, чтобы сгенерировать новую привязку:
$ 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
В каждой операционной системе вы увидите разные предупреждения. Вы можете пока их игнорировать, поскольку известно, что Duktape 2.7.0 компилируется с clang
в Windows, Linux и macOS.
Настройка CMake
CMake — это система генерации системы сборки. Этот плагин использует CMake для создания системы сборки для Android, Windows и Linux для включения Duktape в сгенерированный двоичный файл Flutter. Вам необходимо изменить созданный шаблоном файл конфигурации CMake следующим образом.
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)
Конфигурация CMake добавляет исходные файлы и, что более важно, изменяет поведение по умолчанию сгенерированного файла библиотеки в Windows для экспорта всех символов C по умолчанию. Это обходной путь CMake, помогающий портировать библиотеки в стиле Unix, которыми является Duktape, в мир Windows.
Замените содержимое lib/ffigen_app.dart
следующим.
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;
}
Этот файл отвечает за загрузку файла библиотеки динамической компоновки ( .so
для Linux и Android, .dll
для Windows) и за предоставление оболочки, которая предоставляет более идиоматический интерфейс Dart для базового кода C.
Поскольку этот файл напрямую импортирует пакет ffi
, вам необходимо переместить пакет из dev_dependencies
в dependencies
. Самый простой способ сделать это — запустить следующую команду:
$ dart pub add ffi
Замените содержимое файла main.dart
примера следующим.
пример/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)}';
});
},
),
],
),
),
),
),
);
}
}
Теперь вы можете снова запустить пример приложения, используя:
$ cd example $ flutter run
Вы должны увидеть, что приложение работает следующим образом:
На этих двух снимках экрана показано, что происходит до и после нажатия кнопки «Выполнить JavaScript» . Это демонстрирует выполнение кода JavaScript из Dart и отображение результата на экране.
Андроид
Android — это операционная система Linux на основе ядра, которая чем-то похожа на дистрибутивы Linux для настольных компьютеров. Система сборки CMake может скрыть большую часть различий между двумя платформами. Перед сборкой и запуском на Android убедитесь, что эмулятор Android запущен (или устройство Android подключено). Запустите приложение. Например:
$ cd example $ flutter run -d emulator-5554
Теперь вы должны увидеть пример приложения, работающего на Android:
6. Использование Duktape на macOS и iOS
Пришло время заставить ваш плагин работать на macOS и iOS, двух тесно связанных операционных системах. Начните с macOS. Хотя CMake поддерживает macOS и iOS, вы не будете повторно использовать работу, которую вы проделали для Linux и Android, поскольку Flutter на macOS и iOS использует CocoaPods для импорта библиотек.
Очистка
На предыдущем шаге вы создали работающее приложение для Android, Windows и Linux. Однако от исходного шаблона осталось несколько файлов, которые теперь необходимо очистить. Удалите их сейчас следующим образом.
$ rm src/ffigen_app.c $ rm src/ffigen_app.h $ rm ios/Classes/ffigen_app.c $ rm macos/Classes/ffigen_app.c
macOS
Flutter на платформе macOS использует CocoaPods для импорта кода C и C++. Это означает, что этот пакет необходимо интегрировать в инфраструктуру сборки CocoaPods. Чтобы включить повторное использование кода C, который вы уже настроили для сборки с помощью CMake на предыдущем шаге, вам необходимо добавить один файл пересылки в средство запуска платформы macOS.
macos/Классы/duktape.c
#include "../../src/duktape.c"
Этот файл использует возможности препроцессора C для включения исходного кода из собственного исходного кода, который вы установили на предыдущем шаге. Дополнительную информацию о том, как это работает, см. в macos/ffigen_app.podspec.
Запуск этого приложения теперь происходит по той же схеме, которую вы видели в Windows и Linux.
$ cd example $ flutter run -d macos
iOS
Подобно настройке macOS, iOS также требует добавления одного файла C для пересылки.
iOS/Классы/duktape.c
#include "../../src/duktape.c"
Благодаря этому единственному файлу ваш плагин теперь также настроен для работы на iOS. Запустите его как обычно.
$ flutter run -d iPhone
Поздравляем! Вы успешно интегрировали собственный код на пяти платформах. Это повод для праздника! Возможно, даже более функциональный пользовательский интерфейс, который вы создадите на следующем этапе.
7. Реализуйте цикл печати чтения Eval
Взаимодействовать с языком программирования гораздо интереснее в быстрой интерактивной среде. Первоначальной реализацией такой среды был цикл чтения Eval Print Loop (REPL) LISP. На этом этапе вы собираетесь реализовать нечто подобное с помощью Duktape.
Подготавливаем производство к производству
Текущий код, взаимодействующий с библиотекой Duktape C, предполагает, что все может пойти не так. Да, и он не загружает библиотеки динамической компоновки Duktape во время тестирования. Чтобы подготовить эту интеграционную продукцию, вам необходимо внести несколько изменений в 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;
}
Код для загрузки библиотеки динамической компоновки был расширен для обработки случая, когда плагин используется в программе запуска тестов. Это позволяет написать интеграционный тест, который использует этот API как тест Flutter. Код для оценки строки кода JavaScript был расширен для правильной обработки ошибок, например неполного или неправильного кода. Этот дополнительный код показывает, как обрабатывать ситуации, когда строки возвращаются в виде массивов байтов и их необходимо преобразовать в строки Dart.
Добавление пакетов
Создавая REPL, вы будете отображать взаимодействие между пользователем и движком JavaScript Duktape. Пользователь вводит строки кода, а Duktape отвечает либо результатом вычислений, либо исключением. Вы будете использовать freezed
, чтобы уменьшить объем шаблонного кода, который вам нужно написать. Вы также будете использовать google_fonts
, чтобы отображаемый контент был более тематическим, и flutter_riverpod
для управления состоянием.
Добавьте необходимые зависимости в пример приложения:
$ cd example $ dart pub add flutter_riverpod freezed_annotation google_fonts $ dart pub add -d build_runner freezed
Затем создайте файл для записи взаимодействия REPL:
пример/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;
}
В этом классе используется функция объединения типов freezed
, позволяющая легко выразить форму каждой строки, отображаемой в REPL, как один из трех типов. На этом этапе ваш код, вероятно, показывает какую-то ошибку в этом коде, поскольку необходимо сгенерировать дополнительный код. Сделайте это сейчас следующим образом.
$ flutter pub run build_runner build
При этом создается файл example/lib/duktape_message.freezed.dart
, на который опирается только что набранный вами код.
Далее вам нужно будет внести пару изменений в файлы конфигурации macOS, чтобы позволить google_fonts
отправлять сетевые запросы на данные шрифтов.
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
Теперь, когда вы обновили уровень интеграции для обработки ошибок и создали представление данных для взаимодействия, пришло время создать пользовательский интерфейс примера приложения.
пример/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,
),
),
],
),
),
);
}
}
В этом коде происходит много всего, но объяснение всего этого выходит за рамки данной лаборатории. Я предлагаю вам запустить код, а затем внести в него изменения, просмотрев соответствующую документацию.
$ cd example $ flutter run
8. Поздравления
Поздравляем! Вы успешно создали плагин на основе Flutter FFI для Windows, macOS, Linux, Android и iOS!
После создания плагина вы можете опубликовать его в Интернете, чтобы другие могли его использовать. Полную документацию по публикации вашего плагина на pub.dev вы можете найти в разделе Разработка пакетов плагинов .