Меня зовут Александр Максимовский, и я тимлид команды 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::optionalArray:
std::vector,std::arrayDictionary:
std::map,std::unordered_mapSet:
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 с инициализацией, виджетами и всей необходимой логикой.
Об этом — в следующей части.
