Нативная мощь: 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. Расскажу, зачем мы это сделали и как всё устроено.

Звучит красиво: «мультиплатформенные приложения», «единая кодовая база». Но под капотом скрывается сложная логика по скрещиванию разных подходов, чтобы всё это могло работать на 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→Optionalstd::vector→Arraystd::unordered_map→Mappc::future→Future
4. На основе абстрактной модели строится аннотированная C-модель — специализированная модель, описывающая интерфейсы с учётом особенностей C. То есть, описывается интерфейс, доступный из чистого C.
Особенности С:
Все методы — свободные функции.
Неявный параметр
thisстановится первым параметром обычной функции.Все типы либо примитивны (например, числа), либо являются структурами.
Все шаблонные типы должны быть инстанциированы (например,
vectorиvector— разные типы).Новые типы должны быть предварительно объявлены.
Возможность возврата ошибки означает, что функция может вернуть значение более чем одним способом.
Для типов с конструкторами необходимы парные функции-деструкторы.
Все внутренние C++-типы должны быть скрыты с помощью инкапсуляции.
Аннотированность модели означает, что, несмотря на описание интерфейса в терминах C, у сущностей сохраняются дополнительные пометки из исходной абстрактной модели.
Например,
std::vectorпревращается в абстрактныйArray. В 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иList. На выходе получаем родственные типы.
Сгенерированный 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_t,int32_t,uint64_t,bool.Плавающие:
float,double,long double.void.
Составные типы:
Optional:
std::optionalArray:
std::vector,std::arrayDictionary:
std::map,std::unordered_mapSet:
std::set,std::unordered_set
Прочие базовые типы (не обязательно стандартные):
Строка:
std::string,std::string_view.Сырые данные:
std::vector.Временные:
std::chrono::duration,std::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, Optional. В C ничего подобного нет. Все типы на основе шаблонов C++ должны быть представлены индивидуально.
Рассмотрим пример с необязательной строкой и геоточкой std::optional и std::optional на входе.
// std::optional
typedef struct COptional_CString COptional_CString;
struct COptional_CString {
CString value;
bool hasValue;
};
// std::optional
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>('COptional_CStringMakeDefault');
late final _COptional_CStringMakeDefault =
_COptional_CStringMakeDefaultPtr.asFunction<_COptional_CString Function()>();
late final _COptional_CString_releasePtr =
_lookup>('COptional_CString_release');
late final _COptional_CString_release =
_COptional_CString_releasePtr.asFunction(); Расширение на String? позволяет связать конкретный тип COptional_CString с обобщённым типом Optional.
COptionalCStringMakeDefault— это Dart: FFI-обёртка для вызова C-функцииCOptional_CStringMakeDefault, создающей C++ объект по умолчанию.COptionalCString_release— Dart: FFI-обёртка для вызова C-функцииCOptional_CString_releaseдля уничтожения C++ объекта.
Array
Пример списка отличается от Optional. Так выглядит интерфейс std::vector на 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 _impl;
}
extension CArrayCColorToDart on CArrayCColor {
List toDart() {
return fillFromC();
}
}
extension DartToCArray_CColor on List {
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 = [];
static void iterate(_CColor item) {
listToFill.add(item.toDart());
}
List fillFromC() {
forEach_CArray_CColor(this, ffi.Pointer.fromFunction(_iterate));
final result = List.from(_listToFill);
_listToFill.clear();
return result;
}
} Структуры
Под структурами в модели Codegen мы понимаем типы данных с семантикой значения.
По нашему внутрикомандному соглашению в структурах C++ содержатся доступные снаружи хранимые поля. Структура полностью эквивалентна другой структуре того же типа (и с теми же значениями полей). То есть любая структура может быть воссоздана точным перечислением её содержимого.
Это очень простые типы. В Dart для них прямолинейно генерируется class с final-полями и const-конструктором, а также методы operator==, hashCode и copyWith.
Пример структуры в С++:
struct Address {
std::vector drill_down;
std::vector components;
std::optional building_name;
std::optional post_code;
std::optional building_code;
std::optional 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 drillDown;
final List 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? drillDown,
List? components,
Optional? buildingName,
Optional? postCode,
Optional? buildingCode,
Optional? 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 suggest(SuggestQueryPtr query) const = 0;
[[nodiscard]] virtual const unicore::stateful_channel& data_loading_state() const = 0;
}; В Dart подобный класс сгенерируется в класс SearchManager:
class SearchManager implements ffi.Finalizable {
final ffi.Pointer _self;
static final _finalizer = ffi.NativeFinalizer(_CSearchManager_releasePtr);
SearchManager._raw(this._self);
factory SearchManager._create(ffi.Pointer 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 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 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 types() const = 0;
[[nodiscard]] virtual std::string title() const = 0;
[[nodiscard]] virtual std::string subtitle() const = 0;
[[nodiscard]] virtual std::optional id() const = 0;
}; Это абстрактный интерфейс. Этот тип можно использовать только по ссылке. В нашем случае интерфейсы возвращаются всегда через ссылку, shared_ptr или unique_ptr.
В С генерируем подобный объект (комментарии ниже только для пояснения, они не являются частью процесса генерации):
typedef struct CDirectoryObject CDirectoryObject;
struct CDirectoryObject
{
// CDirectoryObjectImpl хранит std::shared_ptr.
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 _self;
static final _finalizer = ffi.NativeFinalizer(_CDirectoryObject_releasePtr);
DirectoryObject._raw(this._self);
factory DirectoryObject._create(ffi.Pointer self) {
final classObject = DirectoryObject._raw(self);
_finalizer.attach(classObject, self, detach: classObject, externalSize: 10000);
return classObject;
}
List 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 _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; Аннотация 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({
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 с инициализацией, виджетами и всей необходимой логикой.
Об этом напишу в следующей части.
Habrahabr.ru прочитано 24980 раз
