Я поддерживаю пакет Dart с именем mraa, это реализация библиотеки Intel MRAA Linux с использованием механизма Dart FFI. Этому пакету уже около 3 лет, и он был реализован путем ручного создания привязок FFI, необходимых Dart, к API MRAA C, которые поставляются в файлах заголовков MRAA. Это работает достаточно хорошо, но не поддается легкому обслуживанию, любые изменения в MRAA API должны быть проверены, и пакет соответствующим образом обновлен, это отнимает много времени и подвержено ошибкам, требуется гораздо лучшее долгосрочное решение. .

К счастью, в Dart теперь есть пакет ffigen. Этот пакет сканирует заголовочный файл или файлы API на основе C и автоматически создает файл класса Dart с уже сгенерированными необходимыми привязками FFI. Поскольку этот класс генерируется автоматически, любые изменения в MRAA API автоматически подхватываются, и нет необходимости в какой-либо проверке вручную. Это большое преимущество для обслуживания, поэтому я решил использовать привязки FFI, необходимые для пакета mraa, из этого сгенерированного файла.

Однако при этом следует учитывать несколько соображений:

  1. Сгенерированный файл ни в коем случае нельзя трогать, т. е. никакие ручные правки не должны применяться, бессмысленно экономить время на удалении созданного вручную кода привязки FFI, чтобы затем применять ручные правки каждый раз при повторной генерации сгенерированного файла.
  2. Существующая структура API пакета должна быть сохранена, т. е. доступ к различным разделам API MRAA осуществляется через соответствующие именованные классы Dart, например, доступ к функции GPIO осуществляется следующим образом: «mraa.gpio.initialise…». Это позволяет лучше распределять имена между функциональными возможностями, мы не хотим, чтобы API с сотнями функций предоставлялся пользователю таким длинным списком, который присутствует в сгенерированном файле.
  3. Публичный API пакета mraa должен поддерживаться с минимальными изменениями или вообще без изменений, мы не хотим создавать ненужную работу для пользователей, которые обновляют mraa, поэтому любые типы, сгенерированные ffigen, должны быть внедрены в существующую структуру именования типов пакетов.

По сути, нам нужно заменить существующий код привязки FFI, созданный вручную, кодом сгенерированного класса с минимальным воздействием.

Один из способов сделать это — использовать подход, распространенный в мире C++, который называется Pimpl Idiom. Этот метод по существу удаляет детали реализации класса из его объектного представления, помещая их в отдельный класс, доступ к которому осуществляется через непрозрачный указатель. Для любителей паттернов это вариант паттерна Bridge, т. е. способ скрыть реализацию, в первую очередь для разрыва зависимостей компиляции, тогда как полный паттерн Bridge — это способ поддержки нескольких реализаций, хотя идиома Pimpl в C++ использовалась для многих. лет до того, как шаблоны проектирования стали формализованными.

Класс Ffigen’d Pimpl

Давайте начнем с создания нашего класса ffigen, далее именуемого классом 'pimpl', это не статья о ffigen или о том, как его использовать, для этого см. его документацию, поэтому ниже приведены основные моменты из моего ffigen_pubspec. yaml-файл: -

figen:  
  output: 'mraa_impl.dart'  
  name: 'MraaImpl'  
  description: 'Holds ffigen generated implementation bindings to MRAA.'  
  headers:  
    entry-points:  
      - 'mraa/mraa.h'  
    include-directives:  
      - '**mraa/*.h'  
  comments: true  
  preamble: |

Итак, мы создаем файл с именем mraa_impl.dart, который содержит класс с именем MraaImpl из одного заголовочного файла mraa.h, который сам содержит заголовочные файлы для каждой из областей API (стандартный способ сделать это в C), мы сохранение комментариев и добавление преамбулы к классу для лицензирования и т.д. ffigen справляется с этой генерацией без проблем, сгенерированный класс можно увидеть здесь.

Давайте теперь посмотрим, как мы можем обновить пакет mraa, чтобы использовать этот класс.

Основные обновления класса мраа

Начиная с основного класса mraa, мы видим, что исходная структура зависит от базовой общей библиотеки mraa, теперь нам нужно удалить это и сделать класс mraa зависимым от класса pimpl, поэтому:

Mraa() {  
  _lib = DynamicLibrary.open('libmraa.so');  
}

становится :-

Mraa() {  
  _impl = mraaimpl.MraaImpl(DynamicLibrary.open('libmraa.so'));  
}

и мы добавляем: -

// The MRAA Implementation class  
late mraaimpl.MraaImpl _impl;

Индивидуальная конструкция API теперь меняется с:

common = MraaCommon(_lib, noJsonLoading, useGrovePi);

to :-

common = MraaCommon(_impl, noJsonLoading, useGrovePi);

Заменив зависимость общей библиотеки mraa на наш класс pimpl, обратите внимание, что изменения общедоступного API не затрагиваются.

Обновления класса API

Теперь нам нужно обновить наши отдельные классы API, взяв в качестве примера Common API, конструкция изменится на:

MraaCommon(this._impl, this._noJsonLoading, this._useGrovePi) {  
  // Set up the pin offset for grove pi usage.  
  if (_useGrovePi) {  
    _grovePiPinOffset = Mraa.grovePiPinOffset;  
  }  
}  
  
// The MRAA implementation  
final mraaimpl.MraaImpl _impl;

то есть теперь мы используем наш класс pimpl, глядя на метод инициализации, мы видим, что он изменился: -

MraaReturnCode initialise() => returnCode.fromInt(_initFunc());

to

MraaReturnCode initialise() => MraaReturnCode.
returnCode(_impl.mraa_init());

Обратите внимание, что тип возвращаемого значения не изменился, поэтому это не влияет на общедоступный API. Базовый вызов C теперь вызывается через класс pimpl, а не разделяемую библиотеку через наши привязки FFI, созданные вручную, это позволяет нам удалить сотни строк кода, которые существовали в исходном классе, количество строк теперь составляет 316, а не 658. Также обратите внимание на некоторую возню с выводом кода возврата, это связано с тем, что мы также обновили перечисления пакетов, подробнее об этом позже.

Теперь давайте посмотрим на метод, который передает параметр и возвращает тип:

String platformVersion(int platformOffset) {  
  final ptr = _platformVersionFunc(platformOffset);  
  if (ptr != nullptr) {  
    ptr.toDartString();  
  }  
  return 'Platform Version is unavailable';  
}

становится

String platformVersion(int platformOffset) {  
  final ptr = _impl.mraa_get_platform_version(platformOffset);  
  if (ptr != nullptr) {  
    return ptr.cast<ffi.Utf8>().toDartString();  
  }  
  return 'Platform Version is unavailable';  
}

Заметьте еще раз, что сигнатура общедоступного метода не изменилась, произошла небольшая перекодировка обработки указателя, но этого и следовало ожидать, теперь мы удалили любую поддержку этого из нашего существующего класса. Эти обновления просты и интуитивно понятны и, что более важно, находятся внутри наших общедоступных методов.

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

Обновления общедоступного типа

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

Многие вызовы API принимают параметр «контекст», это непрозрачный тип, который инициализируется и впоследствии передается методам API по мере необходимости, что является обычной практикой в ​​​​API на основе C, примером является тип контекста класса GPIO, который был определен как :-

class MraaGpioContext extends Opaque {}

Непрозрачный тип FFI, мы не можем использовать его сейчас, мы должны использовать определение для этого, предоставленное классом pimpl, так что теперь это выглядит так:

typedef MraaGpioContext = mraaimpl.mraa_gpio_context;

mraa_gpio_context из класса pimpl сам по себе является typedef, поэтому сейчас мы создаем typedef из typedef (аккуратно) и, что важно, сохраняем существующее имя типа. Однако это оказывает разрушающее влияние на общедоступный API, typedef, объявленный в классе pimpl, является указателем,

ffi.Pointer<_gpio>; 

поскольку существующий класс был непрозрачным типом, в любом случае нет членов для доступа, что нормально, но любые пользователи, объявляющие это,

Pointer<MraaGpioContext>

теперь увидит поломку, так как наш новый typedef уже является указателем, поэтому теперь это использование нужно будет изменить на «MraaGpioContext». Это упрощающее изменение, и, поскольку эти типы инициализируются только один раз, не должно вызывать слишком много обновлений, я считаю, что это приемлемо.

Точно так же мы также должны заменить наши существующие типы, основанные на классах ручной работы, на типы из класса pimpl, взяв в качестве примера класс MraaGpioEvent, мы изменим оригинал:

class MraaGpioEvent extends Struct {  
  /// Construction  
  MraaGpioEvent(int id, int timestamp) {  
    id = id;  
    timestamp = timestamp;  
  }  
  
  @Int32()  
  
  /// Id  
  external int id;  
  
  @Int64()  
  
  /// Timestamp  
  external int timestamp;  
}

to

typedef MraaGpioEvent = mraaimpl.mraa_gpio_event;

т. е. мы снова используем определение из класса pimpl, в этом случае mraa_gpio_event — это класс, а не typedef, поэтому мы все еще можем использовать подобные объявления из набора модульных тестов, например:

final events = <MraaGpioEvent>[];

и по-прежнему получать доступ к членам класса, что значительно упрощает наш код без изменения существующего кода.

Остальные типы были соответствующим образом обновлены в этом направлении. Это оставляет еще одну большую область для решения, перечисления.

Обновление общедоступных перечислений

Объявления C API для таких пакетов, как MRAA, содержат множество значений конфигурации для выбора пороговых значений напряжения, направления выводов, частот и т. д., используемых различными функциями C API. Взяв в качестве примера направления выводов, мы видим, что заголовок MRAA GPIO C содержит следующее:

typedef enum {  
    MRAA_GPIO_OUT = 0,      /**< Output. A Mode can also be set */  
    MRAA_GPIO_IN = 1,       /**< Input */
    .....

Есть и другие способы сделать это, в некоторых случаях используются директивы прямого определения. Это представлено как: -

abstract class mraa_gpio_dir_t {  
  /// < Output. A Mode can also be set  
  static const int MRAA_GPIO_OUT = 0;  
  
  /// < Input  
  static const int MRAA_GPIO_IN = 1;
...

В нашем классе pimpl от ffigen, поэтому теперь нам нужно включить эти значения в наш пакет mraa.

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

Теперь, с появлением расширенных перечислений в Dart, мы можем значительно упростить это и значительно сократить усилия по обслуживанию. Перечисление MraaGpioDirection теперь становится: -

enum MraaGpioDirection {  
  /// Out  
  out(mraaimpl.mraa_gpio_dir_t.MRAA_GPIO_OUT),  
  
  /// In  
  inn(mraaimpl.mraa_gpio_dir_t.MRAA_GPIO_IN),
  ....

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

Экономия на обслуживании здесь не может быть недооценена, поскольку необходимость каким-то образом проверять, что ни одно из этих значений не изменилось от выпуска к выпуску MRAA, является душераздирающей работой, и надежда на лучшее не является жизнеспособной альтернативой.

Обратите внимание на перечисление «inn» выше, это должно быть «in» в качестве направления, однако у нас не может быть «in», его подсветка синтаксиса с «in» не может использоваться в качестве идентификатора, потому что это ключевое слово. ‘. Я не могу иметь все, я полагаю!

Исход

Итак, удалось ли нам обновить пакет в соответствии с изложенными выше соображениями, т. е. оказали ли мы минимальное влияние на наших пользователей или вообще не повлияли на них? Один из способов быстро взглянуть на это — проверить файлы, в которых мы находим пользователей общедоступного API, которые в этом пакете представляют собой набор модульных тестов и каталог примера.

Мы видим одно обновление в примере mraa_uart_details.dart:

'${returnCode.asString(ret)}');

становится

'$ret)');

упрощение в результате новой реализации enum.

Во всем наборе модульных тестов у нас есть только одно влияние в mraa_common_test.dart: -

mraa.common.resultPrint(returnCode.asInt(MraaReturnCode.success));  
mraa.common  
    .resultPrint(returnCode.asInt(MraaReturnCode.errorInvalidHandle));

становится

mraa.common.resultPrint(MraaReturnCode.success.code);  
mraa.common.resultPrint(MraaReturnCode.errorInvalidHandle.code);

опять же упрощение, никаких других обновлений для тестового набора не требовалось, и он работал без ошибок.

В целом, я буду считать это победой для рассмотрения. 3. Использование пакета в других проектах может привести к некоторым другим несоответствиям, но было бы чудесно, если бы он не оказал абсолютно никакого влияния на пользователя, мы стремимся к «минимальным изменениям или их отсутствию». ', а вовсе нет.

Соображения 1 и 2, приведенные выше, кажутся полностью соблюденными.

Заключение

Если вы выполняете какую-либо серьезную работу с FFI и API на основе C, ffigen — это то, что вам нужно, я нашел его простым в использовании и интуитивно понятным, его генерация кода была завершена, не требовалось ручных украшений, никаких сюрпризов и просто работал.

Однако я бы порекомендовал, хотя вы и могли бы, вы не должны напрямую выставлять свой класс ffigen своим пользователям, обернуть его под другим слоем Dart, в долгосрочной перспективе это того стоит.

Счастливого фигенинга!