1. Введение
FFI (интерфейс внешних функций) Dart позволяет приложениям Flutter использовать существующие собственные библиотеки, которые предоставляют API C. Dart поддерживает FFI на Android, iOS, Windows, macOS и Linux. Для веба Dart поддерживает взаимодействие с JavaScript, но эта тема не рассматривается в этой кодовой лаборатории.
Что вы построите
В этой кодовой лаборатории вы создадите мобильный и десктопный плагин, который использует библиотеку C. С помощью этого API вы напишете пример приложения, которое использует плагин. Ваш плагин и приложение будут:
- Импортируйте исходный код библиотеки C в ваш новый плагин Flutter.
- Настройте плагин, чтобы разрешить его сборку на Windows, macOS, Linux, Android и iOS.
- Создайте приложение, использующее плагин для JavaScript REPL (прочитайте цикл печати reveal)
Чему вы научитесь
В этой лабораторной работе вы получите практические знания, необходимые для создания плагина 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 для Flutter
- Любые необходимые инструменты сборки для платформ, на которых вы будете разрабатывать (например, 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.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!
Убедитесь, что вывод 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.
Вам необходимо внести изменения в example/android/app/build.gradle.kts
чтобы использовать соответствующую версию NDK.
пример/android/app/build.gradle.kts)
android {
// Modify the next line from `flutter.ndkVersion` to the following:
ndkVersion = "27.0.12077973"
// ...
}
Убедитесь, что у вас есть устройство 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
Когда у вас будет работающее устройство Android (физическое устройство или эмулятор), выполните следующую команду:
cd ffigen_app/example flutter run
Flutter спросит вас, на каком устройстве вы хотите его запустить. Выберите подходящее устройство из списка.
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
Когда у вас будет работающее устройство iOS (физическое устройство или симулятор), выполните следующую команду:
cd ffigen_app/example flutter run
Flutter спросит вас, на каком устройстве вы хотите его запустить. Выберите подходящее устройство из списка.
Симулятор iOS имеет приоритет над целевой ОС macOS, поэтому можно не указывать устройство с помощью параметра -d
.
Поздравляем, вы успешно создали и запустили приложение на пяти разных операционных системах. Далее, создание собственного плагина и взаимодействие с ним из Dart с помощью FFI.
5. Используйте Duktape на Windows, Linux и Android
Библиотека C, которую вы будете использовать в этой кодовой лаборатории, — Duktape . Duktape — это встраиваемый движок Javascript, ориентированный на портативность и компактность. На этом этапе вы настроите плагин для компиляции библиотеки Duktape, свяжете его с вашим плагином, а затем получите к нему доступ с помощью Dart's FFI.
Этот шаг настраивает интеграцию для работы в Windows, Linux и Android. Интеграция iOS и macOS требует дополнительной настройки (помимо подробно описанной в этом шаге) для включения скомпилированной библиотеки в финальный исполняемый файл Flutter. Дополнительная требуемая настройка рассматривается в следующем шаге.
Получить Duktape
Сначала получите копию исходного кода 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]
Установить LLVM
Чтобы использовать 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 `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
Эта конфигурация включает в себя заголовочный файл C для передачи в LLVM, выходной файл для генерации, описание для размещения в верхней части файла и раздел преамбулы, используемый для добавления предупреждения lint.
В конце файла есть один элемент конфигурации, который заслуживает дальнейшего объяснения. Начиная с версии 11.0.0 ffigen
генератор привязок по умолчанию не будет генерировать привязки, если при разборе файлов заголовков clang
выдает предупреждения или ошибки.
Заголовочные файлы Duktape, как написано, вызывают clang
на macOS для генерации предупреждений из-за отсутствия спецификаторов типов nullability в указателях 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
, чтобы сгенерировать новую привязку:
$ 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
Вы увидите разные предупреждения на каждой операционной системе. Пока можете их игнорировать, так как 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)
if (ANDROID)
# Support Android 15 16k page size
target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()
Конфигурация 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
Вы должны увидеть работающее приложение следующим образом:
Эти два снимка экрана показывают до и после нажатия кнопки 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. Реализуйте цикл чтения, оценки и печати
Взаимодействие с языком программирования гораздо интереснее в быстрой интерактивной среде. Первоначальная реализация такой среды — LISP's Read Eval Print Loop (REPL). На этом этапе вы реализуете нечто подобное с помощью 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 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;
}
Для этого требуется добавление пакета path
.
flutter pub add path
Код для загрузки библиотеки динамической компоновки был расширен для обработки случая, когда плагин используется в тестовом раннере. Это позволяет написать интеграционный тест, который использует этот API как тест Flutter. Код для оценки строки кода JavaScript был расширен для правильной обработки состояний ошибок, например неполного или неправильного кода. Этот дополнительный код показывает, как обрабатывать ситуации, когда строки возвращаются как массивы байтов и должны быть преобразованы в строки Dart.
Добавить пакеты
При создании REPL вы отобразите взаимодействие между пользователем и движком JavaScript Duktape. Пользователь вводит строки кода, а Duktape отвечает либо результатом вычисления, либо исключением. Вы будете использовать freezed
чтобы сократить объем шаблонного кода, который вам нужно написать. Вы также будете использовать google_fonts
, чтобы сделать отображаемый контент немного более тематическим, и flutter_riverpod
для управления состоянием.
Добавьте необходимые зависимости в пример приложения:
cd example flutter pub add flutter_riverpod freezed_annotation google_fonts flutter 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;
}
Этот класс использует свойство типа union 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) {
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,
),
),
],
),
),
);
}
}
В этом коде происходит много всего, но это выходит за рамки этой кодовой лаборатории, чтобы объяснить все. Я предлагаю вам запустить код, а затем внести изменения в код, просмотрев соответствующую документацию.
cd example flutter run
8. Поздравления
Поздравляем! Вы успешно создали плагин на базе Flutter FFI для Windows, macOS, Linux, Android и iOS!
После создания плагина вы можете захотеть поделиться им в сети, чтобы другие могли его использовать. Полную документацию по публикации вашего плагина на pub.dev вы найдете в разделе Разработка пакетов плагинов .