Нативная мощь: Flutter SDK на C++ ядре. Часть 1

от автора

Меня зовут Александр Максимовский, и я тимлид команды Mobile SDK в 2ГИС. Мы разрабатываем SDK — набор инструментов, который позволяет другим разработчикам внедрять наши технологии (карту, справочник, построение маршрутов и навигатор) в свои мобильные приложения. Благодаря нам можно быстро и удобно интегрировать функциональность 2ГИС, не тратя время на реализацию сложных решений с нуля.

Моя команда уже прошла большой путь. Мы «покорили» iOS и Android, создав для обеих платформ SDK, которые включают кодогенератор (на Swift и Kotlin) и собственные UI-компоненты для SwiftUI, UIKit, Android View и Jetpack Compose. Благодаря этому наши клиенты могут легко создавать свой пользовательский интерфейс.

Теперь пришло время освоить ещё один популярный фреймворк — Flutter. Мы реализовали новое решение: в приложениях на Flutter можно напрямую вызывать C++ код из Dart с помощью FFI. Всё это — в виде коммерческого SDK, который уже работает под Android и iOS. Расскажу, зачем мы это сделали и как всё устроено.

Flutter — это открытый фреймворк для создания красивых, нативно компилируемых, мультиплатформенных приложений из единого кода.

Flutter — это открытый фреймворк для создания красивых, нативно компилируемых, мультиплатформенных приложений из единого кода.

Звучит красиво: «мультиплатформенные приложения», «единая кодовая база». Но под капотом скрывается сложная логика по скрещиванию разных подходов, чтобы всё это могло работать на iOS, Android и ещё Desktop ОС.

Тем не менее, в январе 2023 года мы начали проект по интеграции нашего iOS и Android Mobile SDK для крупного клиента, который не хотел видеть ничего, кроме Flutter и Dart. Это были сложные месяцы реализации различных каналов между Dart и Swift/Kotlin, чтобы обеспечить необходимый функционал. Аппетит клиента рос, и с ним росло количество этих каналов. И проблем. Дополнительно нам пришлось использовать AndroidView и UiKitView для отображения наших платформенных UI-компонент.

Все эти трудности привели нас к решению: создать полноценный Flutter Mobile SDK с кодогенератором C++ ↔ Dart через FFI и удобными Widgets, чтобы новые возможности в ядре нашего продукта автоматически становились доступными для клиентов с Flutter-приложениями.

В сентябре 2024 года мы выпустили Mobile SDK на базе фреймворка Flutter, который позволяет разработчикам внедрять наши карты, поиск и навигацию в свои мобильные приложения на Flutter. 

В этой статье детально рассказываю про основу продукта — кодогенератор для генерации платформенного Dart-кода на основе C++ интерфейсов.

Codegen: генерация Dart API из C++ кода

В одной из наших статей мы рассказывали о нашем продукте Codegen, который позволяет генерировать Swift- и Kotlin-код на основе публичного C++ кода. Чтобы упростить интеграцию нового функционала C++ ядра в Flutter SDK, мы решили доработать Codegen и добавить возможность генерации Dart-кода с FFI-прослойками.

Для Dart уже существует инструмент ffigen, который автоматически создает FFI-биндинги к C/C++ библиотекам. Как и ffigen, наш Codegen взаимодействует только с C-кодом через Dart::FFI. Однако ffigen поддерживает только простые типы, структуры и перечисления, в то время как в нашем проекте активно используются различные контейнеры и собственные типы, такие как Future и Channel.

Кроме того, Codegen уже внедрён и широко используется в Android и iOS SDK для всех типов, применяемых в проекте, поэтому нет смысла переходить на сторонние инструменты.

Основные принципы

Dart::FFI — библиотека для взаимодействия Dart-кода с C. С её помощью можно вызывать C-код ядра из Dart напрямую, без необходимости использовать промежуточные сущности в Swift/Kotlin и без лишних ограничений и преобразований. При этом сохраняются все возможности Dart, такие как Future, CancelableOperation, Stream и т.д.

На выходе — публичный и внутренний интерфейс

Цель кодогенератора — создание интерфейса на Dart на основе существующих интерфейсов на C++. Чем больше часть интерфейса, которая может быть использована пользователями без дополнительной доработки, тем выше степень автоматизации и быстрее процесс разработки. Поэтому кодогенератор проектируется таким образом, чтобы максимально возможная часть интерфейса становилась автоматически публичной.

Те части сгенерированного интерфейса, которые требуют доработки, пользователям недоступны. Кодогенератор помечает их аннотацией @internal, что позволяет скрывать их при экспорте (пока этот процесс выполняется вручную). Эти элементы формируют внутренний интерфейс. На их основе разработчики SDK реализуют недостающий публичный API — уже поверх Dart-интерфейса, а не напрямую C++. В результате в Mobile SDK попадает лишь небольшая часть общего API — всего несколько процентов.

Например, 3D-движку SDK на C++ требуется Surface для рендеринга карты. Чтобы скрыть детали реализации от пользователя, в публичном API предоставляется виджет StatefulWidget MapWidget для рендеринга, а в его реализации используется внутреннее API.

Поэтапное применение

Codegen не требует полностью менять структуру проекта. Чтобы добавить новый интерфейс, достаточно создать специальные файлы, в которых указывается, какие типы подключать к генерации. Это выглядит так:

namespace dgis_bindings::directory {      using dgis::directory::Attribute;     using dgis::directory::ContactInfo;     using dgis::directory::DirectoryFilter;     using dgis::directory::DirectoryObjectId;     using dgis::directory::FormattedAddress;     using dgis::directory::FormattingType;     using dgis::directory::IsOpenNow;  }

Генерация будет выполнена только для типов, перечисленных в пространстве имён dgis_bindings. Это означает, что в Dart-интерфейсе появятся такие типы, как Attribute, ContactInfo и другие (структуры, классы, перечисления). Все эти типы по умолчанию будут публичными, поскольку явно не указано, что они должны быть внутренними.

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

Существующий код на Dart беспрепятственно работает с промежуточным C-кодом с помощью специальных обёрток и Dart::FFI.

Тестовое покрытие

Мы тщательно проверяем, как наши алгоритмы превращают C++ объекты в Dart-код.

Тестируем:

  • компилируемость полученного результата,

  • корректность работы сгенерированного кода,

  • отсутствие утечек ресурсов при преобразованиях.

Для тестирования не требуется собирать весь SDK — достаточно проверить работоспособность на e2e-тестах.

Архитектура Codegen

Техническая основа для работы напрямую с C++ идёт через ClangTool. Этот инструмент использует компиляторный фронтенд Clang и позволяет работать с кодом на C++ в виде конкретизированных структур данных. Без него было бы сложно представить рентабельную работу с C++ на входе.

Этапы преобразования

1. Интерфейсы на C++ подаются в ClangTool, что даёт модель интерфейса в терминах Clang AST (дерева абстрактного синтаксиса).

2. Дальше наши утилиты преобразуют AST в общую для целевых языков абстрактную модель (в нашем случае — Dart).

На этом этапе выполняются преобразования, аналогичные тем, что используются для Swift и Kotlin:

  • переименования типов, функций, полей и методов из С++;

  • добавление новых полей на основе многофункциональных сущностей;

  • превращения функций во вспомогательные конструкторы и методы расширений;

  • выделение «свойств» среди групп геттеров и сеттеров (в самом С++ нет такой штуки);

  • переписывание комментариев.

3. Стандартные шаблонные сущности записываются концептуально, а не в терминах конкретных типов:

  • std::optional<T> → Optional<T>

  • std::vector<T> → Array<T>

  • std::unordered_map<T> → Map<T>

  • pc::future<T> → Future<T>

4. На основе абстрактной модели строится аннотированная C-модель — специализированная модель, описывающая интерфейсы с учётом особенностей C. То есть, описывается интерфейс, доступный из чистого C.

Особенности С:

  • Все методы — свободные функции.

  • Неявный параметр this становится первым параметром обычной функции.

  • Все типы либо примитивны (например, числа), либо являются структурами.

  • Все шаблонные типы должны быть инстанциированы (например, vector<int> и vector<string> — разные типы).

  • Новые типы должны быть предварительно объявлены.

  • Возможность возврата ошибки означает, что функция может вернуть значение более чем одним способом.

  • Для типов с конструкторами необходимы парные функции-деструкторы.

  • Все внутренние C++-типы должны быть скрыты с помощью инкапсуляции.

Аннотированность модели означает, что, несмотря на описание интерфейса в терминах C, у сущностей сохраняются дополнительные пометки из исходной абстрактной модели.

Например, std::vector<std::string> превращается в абстрактный Array<String>. В C это становится типом CArray_CString — самостоятельный тип, не имеющий ничего общего с CArray_int32_t. Но в модели сохраняется пометка, что CArray_CString — это концептуальный Array. Эта пометка ещё пригодится в будущем при пробросе данных в Dart.

Далее процесс продолжается:

5. На основе C-модели пишется текст C-интерфейса. Это прямолинейный процесс: в модели уже есть все необходимые типы, функции и комментарии в нужном порядке. Всё содержимое помещается в один файл CInterface.h (разделение на несколько файлов не дало преимуществ).

Создаются два вспомогательных файла:

  • Внутренний интерфейс — CInterfacePrivate.h (написан на C++, содержит определения структур с C++ типами).

  • Реализация — CInterface.cpp(реализации всех функций из C-интерфейса).

Повторяющиеся действия из CInterface.cpp вынесены в библиотеку поддержки c_support. Она написана с использованием шаблонов, что минимизирует объём генерируемого кода. Механические действия по вызову функций, инициализации структур и перечисления аргументов содержатся в .cpp-файле, а содержательный код преобразования ключевых типов вынесен в c_support и используется повсеместно.

6. На основе аннотированной C-модели строится Dart-модель. Здесь аннотации позволяют вернуть разрозненным сущностям из C-интерфейса типизацию на основе стандартных библиотек Dart.

Например,CArray_CString и CArray_int32_t превращаются в List<String> и List<int>. На выходе получаем родственные типы.

Сгенерированный Dart-код помещается в файл dart_bindings.dart. Это реализация всех описанных функций и типов поверх импортированных из модуля CInterface C-функций и C-типов.

Этот файл экспортируется в dgis.dart с исключением внутренних объектов:

export 'src/generated/dart_bindings.dart'     hide         ApplicationState,         BaseCameraInternalMethods,         ImageLoader,         LocaleChangeNotifier,         MapBuilder,         MapGestureRecognizer,         MapInternalMethods,         MapRenderer,         MapSurfaceProvider,         PlatformLocaleManager,         ProductType,         calculateBearing,         calculateDistance,         createImage,         downloadData,         makeSystemContext,         move,         toLocaleManager;

Как устроена система типов

Сейчас подробнее о составе системы типов модели.

Есть примитивные типы:

  • Целые: int8_tint32_tuint64_tbool.

  • Плавающие: floatdoublelong double.

  • void.

Составные типы:

  • Optional: std::optional

  • Array: std::vectorstd::array

  • Dictionary: std::mapstd::unordered_map

  • Set: std::setstd::unordered_set

Прочие базовые типы (не обязательно стандартные):

  • Строка: std::stringstd::string_view.

  • Сырые данные:std::vector<std::byte>.

  • Временные: std::chrono::durationstd::chrono::time_point.

  • OptionSet: битовая маска.

  • JSON: rapidjson::GenericValue.

  • Future: отложенное значение (portable_concurrency::future).

  • Channel / BufferedChannel / StatefulChannel: поток значений во времени (channels::channel и другие).

Сложные типы на основе базовых:

  • Struct: значение с полями данных

  • Class: ссылочный тип с методами и свойствами

  • Enum: простое перечисление и с ассоциированными значениями

  • Protocol: доступный для реализации пользователем интерфейс

Особые типы:

  • Any: произвольное значение

  • Empty: отсутствие значения (например, вариант enum без ассоциированного значения)

  • Error: ошибка (например, из throws-функции)

Таблица основных типов:

Void

Bool

Int… / UInt…

Float / Double

Struct

Enum

Class

Protocol

Optional

Array

Dictionary

Set

String

Data

TimeInterval

Date

OptionSet

JSON

Future

Channel…

Any

Error

Empty

Также существуют свободные функции и методы расширений.

Any и Protocol

Это единственные типы в текущей системе, которые позволяют передать Dart-объект из Dart в C++ и сохранить его в C++ на неопределённое время.

  • Protocol позволяет реализовать abstract class на Dart и вызывать реализацию в коде на C++.

  • Any позволяет принять в C++ произвольный объект и вернуть обратно в Dart без изменений.

Во всех остальных случаях типы перекодируются в собственные типы C++.

Для вызова Dart-кода из C++ из любого потока используется NativeCallable.

При цепочке вызовов Dart → C++ → Dart в одном потоке существует проблема: это приводит к дедлоку.

Шаблонные типы

Параметризуются типом Vector<T>, Optional<T>. В C ничего подобного нет. Все типы на основе шаблонов C++ должны быть представлены индивидуально.

Рассмотрим пример с необязательной строкой и геоточкой std::optional<std::string> и std::optional<GeoPoint> на входе.

// std::optional<std::string> typedef struct COptional_CString COptional_CString;  struct COptional_CString {     CString value;     bool hasValue; };  // std::optional<GeoPoint> typedef struct COptional_CGeoPoint COptional_CGeoPoint;  struct COptional_CGeoPoint {     CGeoPoint value;     bool hasValue; };

Строка кодируется с помощью CString (C-тип). Тогда необязательный CString можно представить как значение в паре с флагом наличия значения. Можем читать value только тогда, когда hasValue == true.

Аналогично, GeoPoint — это простая структура, описывающая координаты на карте. Мы точно так же подставляем GeoPoint и можем читать его, только если hasValue == true.

У двух полученных типов нет ничего общего с точки зрения C.

Далее эти типы приходят в Dart. Рассмотрим COptional_CString.

final class _COptional_CString extends ffi.Struct {   external _CString value;   @ffi.Bool()   external bool hasValue; }  extension _COptional_CStringBasicFunctions on _COptional_CString {   void _releaseIntermediate() {     _COptional_CString_release(this);   } }  extension _COptional_CStringToDart on _COptional_CString {   String? _toDart() {     if (!this.hasValue) {       return null;     }     return this.value._toDart();   } }  extension _DartTo_COptional_CString on String? {   _COptional_CString _copyFromDartTo_COptional_CString() {     final cOptional = _COptional_CStringMakeDefault();     if (this != null) {       cOptional.value = this!._copyFromDartTo_CString();       cOptional.hasValue = true;     } else {       cOptional.hasValue = false;     }     return cOptional;   } }  // FFI bindings late final _COptional_CStringMakeDefaultPtr =     _lookup<ffi.NativeFunction<_COptional_CString Function()>>('COptional_CStringMakeDefault'); late final _COptional_CStringMakeDefault =     _COptional_CStringMakeDefaultPtr.asFunction<_COptional_CString Function()>();  late final _COptional_CString_releasePtr =     _lookup<ffi.NativeFunction<ffi.Void Function(_COptional_CString)>>('COptional_CString_release'); late final _COptional_CString_release =     _COptional_CString_releasePtr.asFunction<void Function(_COptional_CString)>();

Расширение на String? позволяет связать конкретный тип COptional_CString с обобщённым типом Optional.

  • COptionalCStringMakeDefault — это Dart::FFI-обёртка для вызова C-функции COptional_CStringMakeDefault, создающей C++ объект по умолчанию.

  • COptionalCString_release — Dart::FFI-обёртка для вызова C-функции COptional_CString_release для уничтожения C++ объекта.

Array

Пример списка отличается от Optional. Так выглядит интерфейс std::vector<Color> на C:

typedef struct CArray_CColor CArray_CColor; struct CArray_CColor {     struct CArray_CColorImpl * _Nonnull impl; };  CArray_CColor CArray_CColor_makeEmpty(); void CArray_CColor_release(CArray_CColor self);  size_t CArray_CColor_getSize(CArray_CColor self); void CArray_CColor_addElement(CArray_CColor container, CColor item); void CArray_CColor_forEachWithFunctionPointer(     CArray_CColor self,     void (* _Nonnull nextIter)(CColor item) );

В интерфейсе используется CArray_CColor_forEachWithFunctionPointer для передачи callback из Dart в C, чтобы вычитать все элементы std::vector в Dart::List.

Для передачи Dart::List в C++ используется CArray_CColor_makeEmpty для создания пустого списка, а дальше его заполнение происходит в Dart через CArray_CColor_addElement.

В Dart код будет выглядеть следующим образом:

final class CArrayCColor extends ffi.Struct {   external ffi.Pointer<ffi.Void> _impl; }  extension CArrayCColorToDart on CArrayCColor {   List<Color> toDart() {     return fillFromC();   } }  extension DartToCArray_CColor on List<Color> {   CArrayCColor copyFromDartToCArray_CColor() {     final cArray = CArrayCColormakeEmpty();     forEach((item) {       final cItem = item._copyFromDartTo_CColor();       CArrayCColoraddElement(cArray, cItem);     });     return cArray;   } }  extension CArrayCColorBasicFunctions on CArrayCColor {   void releaseIntermediate() {     CArray_CColor_release(this);   }    static final listToFill = <Color>[];    static void iterate(_CColor item) {     listToFill.add(item.toDart());   }    List<Color> fillFromC() {     forEach_CArray_CColor(this, ffi.Pointer.fromFunction<ffi.Void Function(_CColor)>(_iterate));       final result = List<Color>.from(_listToFill);       _listToFill.clear();       return result;   } }

Структуры

Под структурами в модели Codegen мы понимаем типы данных с семантикой значения.

По нашему внутрикомандному соглашению в структурах C++ содержатся доступные снаружи хранимые поля. Структура полностью эквивалентна другой структуре того же типа (и с теми же значениями полей). То есть любая структура может быть воссоздана точным перечислением её содержимого.

Это очень простые типы. В Dart для них прямолинейно генерируется class с final-полями и const-конструктором, а также методы operator==, hashCode и copyWith.

Пример структуры в С++:

struct Address {     std::vector<AdminDivision> drill_down;     std::vector<AddressComponent> components;     std::optional<std::string> building_name;     std::optional<std::string> post_code;     std::optional<std::string> building_code;     std::optional<std::string> address_comment; };

В C превращается переписыванием всех полей и добавлением деструктора:

typedef struct CAddress CAddress; struct CAddress {     CArray_CAddressAdminDivision drillDown;     CArray_CAddressComponent components;     COptional_CString buildingName;     COptional_CString postCode;     COptional_CString buildingCode;     COptional_CString addressComment; };  // Необходим деструктор, так как обладает полями с деструкторами. void CAddress_release(CAddress self);

В Dart:

class Address {   final List<AddressAdmDiv> drillDown;   final List<AddressComponent> components;   final String? buildingName;   final String? postCode;   final String? buildingCode;   final String? addressComment;    const Address({     required this.drillDown,     required this.components,     required this.buildingName,     required this.postCode,     required this.buildingCode,     required this.addressComment,   });    Address copyWith({     List<AddressAdmDiv>? drillDown,     List<AddressComponent>? components,     Optional<String?>? buildingName,     Optional<String?>? postCode,     Optional<String?>? buildingCode,     Optional<String?>? addressComment,   }) {     return Address(       drillDown: drillDown ?? this.drillDown,       components: components ?? this.components,       buildingName: buildingName != null ? buildingName.value : this.buildingName,       postCode: postCode != null ? postCode.value : this.postCode,       buildingCode: buildingCode != null ? buildingCode.value : this.buildingCode,       addressComment: addressComment != null ? addressComment.value : this.addressComment,     );   }    @override   bool operator ==(Object other) =>       identical(this, other) ||       other is Address &&           other.runtimeType == runtimeType &&           other.drillDown == drillDown &&           other.components == components &&           other.buildingName == buildingName &&           other.postCode == postCode &&           other.buildingCode == buildingCode &&           other.addressComment == addressComment;    @override   int get hashCode {     return Object.hash(       drillDown,       components,       buildingName,       postCode,       buildingCode,       addressComment,     );   } }

Помимо полей с преобразованными типами, добавляется поэлементный конструктор и преобразования из или в C-тип. Генерация обоих преобразований заключается в вызове конструктора целевой структуры, инициализируя каждое поле его преобразованным значением.

Future и Channel

В наших C++ интерфейсах используются конкретные решения:

  • portable_concurrency::future — для единственного отложенного значения (ссылка);

  • channels::channel — для потока произвольного количества значений (ссылка).

В Dart есть аналоги: CancellableOperation для отложенных значений и Stream для потока значений. Благодаря этому все C++ асинхронные сущности были удобно интегрированы в Dart-среду.

Пример класса на C++:

struct ISearchManager {     [[nodiscard]] virtual pc::future<ISuggestResultPtr> suggest(SuggestQueryPtr query) const = 0;     [[nodiscard]] virtual const unicore::stateful_channel<MapDataLoadingState>& data_loading_state() const = 0; };

В Dart подобный класс сгенерируется в класс SearchManager:

class SearchManager implements ffi.Finalizable {   final ffi.Pointer<ffi.Void> _self;   static final _finalizer = ffi.NativeFinalizer(_CSearchManager_releasePtr);    SearchManager._raw(this._self);    factory SearchManager._create(ffi.Pointer<ffi.Void> self) {     final classObject = SearchManager._raw(self);     _finalizer.attach(classObject, self, detach: classObject, externalSize: 10000);     return classObject;   }    @override   bool operator ==(Object other) =>       identical(this, other) ||       other is SearchManager &&           other.runtimeType == runtimeType &&           _CSearchManager_cg_objectIdentifier(this._self) ==               _CSearchManager_cg_objectIdentifier(other._self);    @override   int get hashCode {     final identifier = _CSearchManager_cg_objectIdentifier(this._self);     return identifier.hashCode;   }    CancelableOperation<SuggestResult> suggest(SuggestQuery query) {     var _a1 = query._copyFromDartTo_CSuggestQuery();     _CFuture_CSuggestResult res = _CSearchManager_suggest_CSuggestQuery(       _CSearchManagerMakeDefault().._impl = _self,       _a1,     );     _a1._releaseIntermediate();     final t = res._toDart();     res._releaseIntermediate();     return t;   }    StatefulChannel<MapDataLoadingState> get dataLoadingStateChannel {     _CStatefulChannel_CMapDataLoadingState res =         _CSearchManager_dataLoadingStateChannel(             _CSearchManagerMakeDefault().._impl = _self);     final t = res._toDart();     res._releaseIntermediate();     return t;   } }

StatefulChannel — это обёртка над Stream, которая дополнительно хранит установленное значение в потоке.

Классы

У классов семантика ссылочного типа. В классах нет хранимых полей, только методы и вычисляемые свойства. Генерируемые классы не могут быть отнаследованы пользователем — для этого существуют абстрактные классы. 

Пример класса на C++:

struct IDirectoryObject {     virtual ~IDirectoryObject() = default;     [[nodiscard]] virtual std::vector<ObjectType> types() const = 0;     [[nodiscard]] virtual std::string title() const = 0;     [[nodiscard]] virtual std::string subtitle() const = 0;     [[nodiscard]] virtual std::optional<DirectoryObjectId> id() const = 0; };

Это абстрактный интерфейс. Этот тип можно использовать только по ссылке. В нашем случае интерфейсы возвращаются всегда через ссылку, shared_ptr или unique_ptr.

В С генерируем подобный объект (комментарии ниже только для пояснения, они не являются частью процесса генерации):

typedef struct CDirectoryObject CDirectoryObject; struct CDirectoryObject {   // CDirectoryObjectImpl хранит std::shared_ptr<IDirectoryObject>.   struct CDirectoryObjectImpl * _Nonnull impl; }; // Служебные функции. void CDirectoryObject_release(CDirectoryObject self); CDirectoryObject CDirectoryObject_retain(CDirectoryObject self);  // Функции — методы. CArray_CObjectType CDirectoryObject_types(CDirectoryObject self); CString CDirectoryObject_title(CDirectoryObject self); CString CDirectoryObject_subtitle(CDirectoryObject self); COptional_CDirectoryObjectId CDirectoryObject_id(CDirectoryObject self); void * _Nonnull CDirectoryObject_cg_objectIdentifier(CDirectoryObject self); 

Реализация промежуточного объекта хранит shared_ptr на нужный нам объект. Удерживая временный объект (CDirectoryObject), класс имеет контроль над временем жизни объекта. Все методы экземпляра представляются функциями, принимающими self в качестве первого параметра. Остальные параметры идут следом в том же порядке.

Статические методы тоже поддерживаются, self они не принимают, работают как свободные функции. В Dart это выглядит так:

class DirectoryObject implements ffi.Finalizable {   final ffi.Pointer<ffi.Void> _self;   static final _finalizer = ffi.NativeFinalizer(_CDirectoryObject_releasePtr);    DirectoryObject._raw(this._self);    factory DirectoryObject._create(ffi.Pointer<ffi.Void> self) {     final classObject = DirectoryObject._raw(self);     _finalizer.attach(classObject, self, detach: classObject, externalSize: 10000);     return classObject;   }    List<ObjectType> get types {     _CArray_CObjectType res = _CDirectoryObject_types(       _CDirectoryObjectMakeDefault().._impl = _self,     );     final t = res._toDart();     res._releaseIntermediate();     return t;   }    String get title {     _CString res = _CDirectoryObject_title(       _CDirectoryObjectMakeDefault().._impl = _self,     );     final t = res._toDart();     res._releaseIntermediate();     return t;   }    String get subtitle {     _CString res = _CDirectoryObject_subtitle(       _CDirectoryObjectMakeDefault().._impl = _self,     );     final t = res._toDart();     res._releaseIntermediate();     return t;   }    DgisObjectId? get id {     _COptional_CDgisObjectId res = _CDirectoryObject_id(       _CDirectoryObjectMakeDefault().._impl = _self,     );     return res._toDart();   }    @override   bool operator ==(Object other) =>       identical(this, other) ||       other is DirectoryObject &&           other.runtimeType == runtimeType &&           _CDirectoryObject_cg_objectIdentifier(this._self) ==               _CDirectoryObject_cg_objectIdentifier(other._self);    @override   int get hashCode {     final identifier = _CDirectoryObject_cg_objectIdentifier(this._self);     return identifier.hashCode;   } }  final class _CDirectoryObject extends ffi.Struct {   external ffi.Pointer<ffi.Void> _impl; }  extension _CDirectoryObjectBasicFunctions on _CDirectoryObject {   void _releaseIntermediate() {     _CDirectoryObject_release(_impl);   }    _CDirectoryObject _retain() {     return _CDirectoryObject_retain(_impl);   } }  extension _CDirectoryObjectToDart on _CDirectoryObject {   DirectoryObject _toDart() {     return DirectoryObject._create(_retain()._impl);   } }  extension _DartToCDirectoryObject on DirectoryObject {   _CDirectoryObject _copyFromDartTo_CDirectoryObject() {     return (_CDirectoryObjectMakeDefault().._impl = _self)._retain();   } }

Уникальная способность класса — наличие NativeFinalizer. Так как в Dart нет деструкторов, то именно благодаря NativeFinalizer вызывается release-функция, где отпускается shared_ptr на объект, который был захвачен в конструкторе. Таким образом удаётся автоматически освобождать память от неиспользуемых C++ объектов.

Variant

В C++ есть тип std::variant — он может хранить значение одного из нескольких заранее определенных типов.

Для передачи такого типа в Dart можно было бы сгенерировать отдельные подклассы для sealed-класса, соответствующие каждому варианту из std::variant. Однако это приводит к избыточности, поэтому было принято решение генерировать один Dart-класс, объект которого можно сконструировать с помощью любого типа, указанных в std::variant.

Как пример рассмотрим std::variant WorkTimeFilter.

struct WeekTime {     WeekDay week_day;     DayTime time; };  struct IsOpenNow { };  using WorkTimeFilter CODEGEN_FIELD_NAMES(work_time, is_open_now) = std::variant<WeekTime, IsOpenNow>;

Аннотация CODEGEN_FIELD_NAMES используется для задания инструкции генератору кода для C++ и Dart.

В результате генерации получается следующий Dart-класс:

final class WorkTimeFilter {   final Object? _value;   final int _index;    WorkTimeFilter._raw(this._value, this._index);    WorkTimeFilter.workTime(WeekTime value) : this._raw(value, 0);   WorkTimeFilter.isOpenNow(IsOpenNow value) : this._raw(value, 1);    bool get isWorkTime => this._index == 0;   WeekTime? get asWorkTime => this.isWorkTime ? this._value as WeekTime : null;    bool get isIsOpenNow => this._index == 1;   IsOpenNow? get asIsOpenNow => this.isIsOpenNow ? this._value as IsOpenNow : null;    T match<T>({     required T Function(WeekTime value) workTime,     required T Function(IsOpenNow value) isOpenNow,   }) {     return switch (this._index) {       0 => workTime(this._value as WeekTime),       1 => isOpenNow(this._value as IsOpenNow),       _ => throw NativeException("Unrecognized case index ${this._index}")     };   }    @override   String toString() => "WorkTimeFilter(${this._value})";    @override   bool operator ==(Object other) =>       identical(this, other) ||       other is WorkTimeFilter &&           other.runtimeType == runtimeType &&           other._value == this._value &&           other._index == this._index;    @override   int get hashCode => Object.hash(this._index, this._value); }

Благодаря аннотации удалось сгенерировать Dart-класс WorkTimeFilter с набором конструкторов, соответствующих типам в C++ std::variant.

Итог по Codegen

Поддержка генерации Dart-кода в существующем кодогенераторе значительно ускоряет внедрение новой функциональности при кроссплатформенной разработке: готовность функции на C++ сразу означает её готовность для всех платформенных языков, включая Dart. На базе такого API строится конечный продукт — Flutter SDK с инициализацией, виджетами и всей необходимой логикой.

Об этом напишу в следующей части.


ссылка на оригинал статьи https://habr.com/ru/articles/898804/


Комментарии

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

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