Меня зовут Александр Максимовский, и я тимлид команды 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<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_t
,int32_t
,uint64_t
,bool
.Плавающие:
float
,double
,long double
.void.
Составные типы:
Optional:
std::optional
Array:
std::vector
,std::array
Dictionary:
std::map
,std::unordered_map
Set:
std::set
,std::unordered_set
Прочие базовые типы (не обязательно стандартные):
Строка:
std::string
,std::string_view
.Сырые данные:
std::vector<std::byte>
.Временные:
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<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 с инициализацией, виджетами и всей необходимой логикой.
Об этом напишу в следующей части.