Наводим мосты между Flutter и нативными библиотеками

от автора

Все вы знаете, что Flutter реализует несколько абстракций для передачи данных между Dart-кодом и кодом, связанным с оболочкой Flutter Engine на языке платформы (например, Kotlin для Android). Но в действительности у Dart есть еще один инструмент для взаимодействия с внешним миром и он может использоваться для добавления C/C++ библиотек и вызова функций из Dart-кода. Основную сложность представляет разные соглашения по кодированию типизованных числовых значений, строк и структур, но часть задач по преобразованию и работе с памятью выполняют библиотека dart:ffi и пакет package:ffi/ffi.dart, а некоторые из них могут быть выполнены самостоятельно. В статье мы рассмотрим общие принципы подключения внешних библиотек и кодогенерации для создания связываний dart-функции и классов и структур данных C.

Начнем с более простого примера и создадим небольшую библиотеку на C и рассмотрим последовательность действий для ее интеграции во Flutter-приложение.

Создадим новый проект и в нем новый каталог в android/app/src/main/cpp, добавим исходный текст, файл с заголовками и CMakeLists.txt для сборки библиотеки:

#include "string.h" #include "stdlib.h"  int sum(int a, int b) {   return a+b; }  int len(char* str) {   return strlen(str); }  char* reversed(char* str) {   char* newstr = malloc(strlen(str)+1);   str += strlen(str)-1;   while (*str) {     *newstr = *str;     str--;     newstr++;   }   *newstr = 0;   return newstr; }
#ifndef FFI1_SAMPLE_H #define FFI1_SAMPLE_H  int sum(int a, int b);  int len(char* str);  char* reversed(char* str); #endif //FFI1_SAMPLE_H 

И создадим CMakeLists.txt:

cmake_minimum_required(VERSION 3.4.1) project(sample) add_library(sample SHARED sample.c) find_library(log-lib log) target_link_libraries(sample)

И также добавим конфигурацию внешней сборки в app/build.gradle:

... android {   ...     externalNativeBuild {         cmake {             path "src/main/сpp/CMakeLists.txt"         }     } }

После этого при выполнении сборки проекта через flutter build apk в финальный архив будет добавляться so-файл библиотеки, собранный для соответствующих платформ (lib/). Для подключения к библиотеке мы будем использовать возможности dart:ffi (Foreign Function Interface).

dart:ffi определяет внешние (нативные) типы данных и позволяет связать определение dart-функции и внешней функции из импортированной библиотеки. Типы данных включают в себя:

  • Void — пустой тип (может использоваться как результат функции, которая ничего не возвращает)

  • Bool — логическое значение (в C будет представлен целым числом)

  • Int8, Int16, Int32, Int64 — целое число со знаком с соответствующей разрядностью

  • Uint8 (Char), Uint16, Uint32, Uint64 — целое число без знака с соответствующей разрядностью

  • Float, Double — 32-х и 64-х разрядное число с плавающей точкой

  • Pointer — указатель на область памяти в C

  • Array, Struct, Union — составные типы данных (соответственно массив однотипных элементов, структура разнотипных элементов, объединение элементов, занимающих одну область памяти)

Для привязки внешних функций необходимо загрузить библиотеку (DynamicLibrary.open) и выполнить обнаружение функции по сигнатуре и названию. Например, для поиска функции sum можно использовать следующий вызов (при условии, что в переменной library размещен результат вызова DynamicLibrary.open):

typedef DartSumFunction = int sum(int,int);  typedef CSumFunction = ffi.Int64 ffi.Function(ffi.Int64,ffi.Int64)>  DartSumFunction sum = library.lookup('sum').asFunction();  

Для определения структур и объединений необходимо создать класс-расширение от ffi.Struct или ffi.Union и пометить все его свойства ключевым словом external (внешние типы данных задаются через ffi-типы, которые используются как аннотации), например для описания комплексного числа можно создать такую структуру:

class ComplexNumber extends ffi.Struct {   @ffi.Double()      external double x;     @ffi.Double()    external double y;  }

Объединения создаются аналогично (наследованием от ffi.Union). Для управления памятью и определения указателей нужно использовать тип Pointer с уточнением типа значения под указателем (например, Pointer, Pointer или Uint8Pointer может быть использован как указатель на строку, поскольку аналогом в C является тип char*, указатель на целое 8-битное число без знака).

Для получения указателя из typed_data (бывает часто нужно для передачи двоичных данных, например изображения), можно использовать следующий фрагмент кода:

Uint8Pointer getBlob(Uint8List data) {    final blob = calloc(data.length);    final blobBytes = blob.asTypedList(data.length);    blobBytes.setAll(0, data);    return blob;  }

Для определения массива (Array) также нужно использовать уточнение типа или одно из расширений (например, Int64Array).

Для выделения памяти можно использовать функцию calloc(N), которая возвращает указатель на тип T с выделением N элементов (суммарно N*sizeof(T) байт). Также для создания указателя на один элемент (например структуру) можно использовать malloc(). Важно не забывать освобождать выделенную память через malloc.free(ptr).

Для преобразования строк доступны расширения в String toNativeUtf8 (возвращает Pointer) и toDartString() для обратного преобразования. Для извлечения значения под указателем используется метод .value(). Указатели могут быть преобразованы к другому типу методом .cast. Все функции по управлению памятью и работе с указателями доступны в пакете package:ffi/ffi.dart.

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

Процесс создания интерфейса для вызова внешних функций может быть автоматизировать с помощью кодогенерации пакетом ffigen. Установим его как зависимость для разработки:

flutter pub add --dev ffigen

И добавим конфигурацию в pubspec.yaml:

ffigen:      output: 'lib/generated_bindings.dart'      name: SampleNative      headers:          entry-points:            - 'android/app/src/main/сpp/sample.h'

После запуска flutter pub run ffigen в сгенерированном файле будет доступен класс SampleNative, который при создании получает объект загруженной динамической библиотеки и представляет интерфейс с методами, совпадающими с заголовочным файлом sample.h:

import 'package:path/path.dart' as path;   String libraryFilename(String library) {    if (Platform.isMacOs) return "lib${library}.dylib";   if (Platform.isWindows) return "${library}.dll";    return "lib${library}.so";  }  void example() {    ffi.DynamicLibrary library;    if (Platform.isAndroid) {      library = ffi.DynamicLibrary.open(libraryFilename('sample'));    } else if (Platform.isIos) {      library = ffi.DynamicLibrary.process();    } else {      library = ffi.DynamicLibrary.open(path.join(Directory.current.path, libraryFilename('sample')));    }       final nativeLibrary = NativeSample(library);    print(nativeLibrary.sum(2,2)); }  

Теперь, когда мы умеет подключать простые библиотеки, обсудим способы интеграции OpenCV.

Начнем с установки исходных текстов и настройки сборки для получения so-библиотек и необходимых заголовочных файлов:

git clone https://github.com/opencv/opencv.git  git clone https://github.com/opencv/opencv_contrib.git  python3 opencv/platforms/android/build_sdk.py --sdk_path $ANDROID_HOME --ndk_path $ANDROID_NDK_HOME --extra_modules_path opencv_contrib/modules/

Для импорта сгенерированных библиотек и header-файлов можно использовать готовый CMakeLists.txt отсюда. Для правильной сборки нужно указать диалект языка C++ в build.gradle:

externalNativeBuild {    cmake {      cppFlags "-frtti -fexceptions -std=c++17"      abiFilters "armeabi-v7a", "arm64-v8a"    } }

Обертки над функциями могут быть написаны самостоятельно или с использованием ffigen, например для функции cvMaxRect, которая принимает два указателя на прямоугольники и возвращает новый прямоугольник, определение может выглядеть следующим образом:

class CvRect extends Struct {    @Int64() external int x;    @Int64() external int y;    @Int64(); external int width;    @Int64(); external int height;  }  typedef CvRectPtr = Pointer<CvRect>  typedef cv_max_rect_function = NativeFunction<CvRect Function(CvRectPtr, CvRectPtr)>  typedef CvMaxRectFunction = CvRect Function<CvRectPtr, CvRectPtr> final cvMaxRect = library.lookup<cv_max_rect_function>('cvMaxRect').asFunction<CvMaxRectFunction>(); 

При вызовах ffi необходимо помнить, что обращение к функциям происходит синхронно и блокирует основной изолят, поэтому для длительных операций (преобразования изображение, детектирование объектов и др.) предпочтительно использовать изоляты (в простейшем варианте можно применить compute с передачей внешней функции).

Важно помнить, что dart FFI поддерживает только С API, поэтому для библиотек, которые созданы только для C++ потребуется создавать дополнительные функции-обертки для создания и удаления объектов (результатом может быть структура с указателем на объект или иным handle, который можно использовать для обнаружения созданного объекта в C-коде), а также функции-адаптеры для вызова соответствующих методов объекта (указатель или handle-объекта передается параметром функции). Для корректной компиляции обертки (при смешивании C и C++ исходных текстов) перед функциями необходимо добавить extern «C». Примеры создания таких связываний можно посмотреть в этом репозитории.

Во второй части статьи мы рассмотрим более подробно интеграцию нативных Android-библиотек на примере библиотеки обработки и синтеза звука AAudio.

И в заключtние приглашаю всех желающих на бесплатный урок по теме: «Сферический Flutter в вакууме. Создаем свою систему координат для RenderObject».


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/692530/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *