Использование FFI в плагине Flutter

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)

Duktape REPL, работающий как приложение macOS

Чему вы научитесь

В этой лабораторной работе вы получите практические знания, необходимые для создания плагина 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=/

Вы должны увидеть окно работающего приложения, подобное следующему:

Приложение FFI, созданное по шаблону и работающее как приложение Windows

Линукс

Убедитесь, что вы используете поддерживаемую версию 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=/

Вы должны увидеть окно работающего приложения, подобное следующему:

Приложение FFI, созданное по шаблону и работающее как приложение Linux

андроид

Для 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 спросит вас, на каком устройстве вы хотите его запустить. Выберите подходящее устройство из списка.

Приложение FFI, созданное по шаблону и работающее в эмуляторе Android

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

Вы должны увидеть окно работающего приложения, подобное следующему:

Приложение FFI, созданное по шаблону и работающее как приложение Linux

Для 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 спросит вас, на каком устройстве вы хотите его запустить. Выберите подходящее устройство из списка.

Приложение FFI, созданное на основе шаблона и работающее в симуляторе iOS

Симулятор 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

Вы должны увидеть работающее приложение следующим образом:

Показан Duktape, инициализированный в приложении Windows

Отображение вывода JavaScript Duktape в приложении Windows

Эти два снимка экрана показывают до и после нажатия кнопки Run JavaScript . Это демонстрирует выполнение кода JavaScript из Dart и отображение результата на экране.

андроид

Android — это Linux, основанная на ядре ОС, которая чем-то похожа на настольные дистрибутивы Linux. Система сборки CMake может скрыть большинство различий между двумя платформами. Для сборки и запуска на Android убедитесь, что эмулятор Android запущен (или устройство Android подключено). Запустите приложение. Например:

cd example
flutter run -d emulator-5554

Теперь вы должны увидеть пример приложения, работающего на Android:

Показан Duktape, инициализированный в эмуляторе Android

Отображение вывода JavaScript Duktape в эмуляторе 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

Показан Duktape, инициализированный в приложении macOS

Отображение вывода JavaScript Duktape в приложении macOS

iOS

Подобно настройке macOS, для iOS также требуется добавить один файл переадресации C.

ios/Классы/duktape.c

#include "../../src/duktape.c"

С помощью этого единственного файла ваш плагин теперь также настроен для работы на iOS. Запустите его как обычно.

flutter run -d iPhone

Демонстрация инициализации Duktape в симуляторе iOS

Демонстрация вывода JavaScript Duktape в симуляторе iOS

Поздравляем! Вы успешно интегрировали собственный код на пяти платформах. Это повод для празднования! Возможно, даже более функциональный пользовательский интерфейс, который вы создадите на следующем этапе.

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

Duktape REPL, работающий в приложении Linux

Duktape REPL, работающий в приложении Windows

Duktape REPL, запущенный в симуляторе iOS

Duktape REPL, работающий в эмуляторе Android

8. Поздравления

Поздравляем! Вы успешно создали плагин на базе Flutter FFI для Windows, macOS, Linux, Android и iOS!

После создания плагина вы можете захотеть поделиться им в сети, чтобы другие могли его использовать. Полную документацию по публикации вашего плагина на pub.dev вы найдете в разделе Разработка пакетов плагинов .