Меня зовут Александр Максимовский, и я тимлид команды 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 с инициализацией, виджетами и всей необходимой логикой.

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

Комментарии (0)