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

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 (читайте цикл печати).

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
  • Любые необходимые инструменты сборки для платформ, на которых вы будете разрабатывать (например, 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=/

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

Приложение 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. Сначала убедитесь, что к вашему компьютеру для разработки подключено устройство 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

Приложение 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

После запуска симулятора запустите: flutter run .

$ cd ffigen_app/example
$ flutter run -d iphone

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

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

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

Отображение инициализации Duktape в приложении Windows

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

На этих двух снимках экрана показано, что происходит до и после нажатия кнопки «Выполнить JavaScript» . Это демонстрирует выполнение кода JavaScript из Dart и отображение результата на экране.

Андроид

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

$ cd example
$ flutter run -d emulator-5554

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

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

Отображение вывода Duktape JavaScript в эмуляторе 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. Реализуйте цикл печати чтения 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

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

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

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

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

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

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

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