Русский
Русский
English
Статистика
Реклама

Package

Перевод Удобная платформа для подбора библиотек и фреймворков JavaScript openbase

15.10.2020 14:05:55 | Автор: admin
image

Что за зверь?


openbase.io

Находите и сравнивайте пакеты с открытым исходным кодом с отзывами пользователей, категоризацией и беспрецедентной информацией о популярности, надежности, активности пакетов и многом другом

Думаю, сейчас во многих языках программирования есть менеджер пакетов, который позволяет каким-то образом получать информацию о них.

Как я обычно выбираю себе библиотеку



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

  1. Поискать в npm
  2. Подобрать айтемы, подходящие по описанию, имеющие достаточное количество загрузок и получавшие обновления в последние несколько месяцев.
  3. Проверить доступность документации и readme на GitHub; иногда проверять наличие обновлений по ключевым вопросам.
    Кстати, я стараюсь не принимать решение только по наличию или отсутствию документов. Как правило, они могут находиться в процессе релиза или экстренных правок, о чем можно узнать на issue board, где разработчики и юзеры могут контактировать друг с другом.

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

На деле все оказывается не так просто: прочитал Readme файл, подумал: О, вот это было бы полезно, а оказывается, что функции, которые, как я думал, заработают, на деле не поддерживаются, поэтому придется прописать их самому. Всегда хотелось, чтобы были какие-нибудь ревью: ведь есть же люди, которые до меня сталкивались с подобными проблемами.

Плюсы openspace Ревью


Для моих изысканий идеально подошел сервис openbase.io

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

В обзоре содержатся впечатления от использования и количество звезд. Сами обзоры тоже можно оценивать по степени полезности, что, в свою очередь, помогает понять ценность самого обзора.

Например, на React оставлено более 570 отзывов.

Общая информация


image

Ревью


image

Плюсы openbase Можно сразу найти туториалы


На боковой панели есть вкладка tutorial, в которой вам все подробно разъяснят, в том числе через ролики на YouTube.

image

image

Плюсы openbase информация об альтернативных пакетах


По вкладке Alternatives там же, на сайдбаре, можно найти библиотеки, схожие с теми, на которые вы наткнулись на данный момент. На картинке ниже результаты для React, особых откровений не ожидается, но в зависимости от результатов поиска можете наткнуться на интересные открытия. Думаю, у этой функции хороший потенциал, можно делать свои проекты более оригинальными за счет новых, небанальных пакетов.

image

Спасибо за прочтение!

Мы будем очень рады, если вы расскажете нам, понравилась ли вам данная статья, понятен ли перевод, была ли она вам полезна?
Подробнее..

Как мы подружили Flutter с CallKit Call Directory

21.04.2021 14:19:32 | Автор: admin

Flutter+CallKitCallDirectory=Love


Привет!


В этом лонгриде я расскажу о том, как мы в Voximplant пришли к реализации собственного Flutter плагина для использования CallKit во Flutter приложении, и в итоге оказались первыми, кто сделал поддержку блокировки/определения номеров через Call Directory для Flutter.


Что такое CallKit


Apple CallKit это фреймворк для интеграции звонков стороннего приложения в систему.


Если звонок из стороннего приложения отображается как нативный, то тут задействован CallKit. Если звонок из стороннего приложения отображается в списке звонков системного приложения Phone тоже CallKit. Сторонние приложения, выступающие в качестве определителя номера CallKit. Звонки из сторонних приложений, которые не могут пробиться через режим Не беспокоить ну вы поняли.



CallKit предоставляет сторонним разработчикам системный UI для отображения звонков



А что с CallKit на Flutter?


CallKit является частью iOS SDK, во Flutter он не представлен, однако доступ к нему из Flutter возможен путём взаимодействия с нативным кодом. Для использования функциональности этого фреймворка потребуется подключить сторонний плагин, инкапсулирующий взаимодействие Flutter с iOS, или реализовывать всё самостоятельно, например, так:



Пример реализации CallKit сервиса для Flutter, где код iOS приложения (platform code) связывает приложение Flutter с системой




Готовые решения с CallKit на Flutter


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


Существующие плагины частично или полностью оборачивали CallKit API в собственный высокоуровневый API. Таким образом терялась гибкость, а некоторые возможности становились недоступными. Из-за собственной реализации архитектуры и интерфейсов такие плагины содержали свои баги. Документация хромала или отсутствовала, а авторы некоторых из них прекратили поддержку почти сразу, что особенно опасно на быстроразвивающемся Flutter.



Как мы пришли к созданию своего решения


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


Мы задумались о том, чтобы реализовать своё решение с учетом этих недостатков.


Хотелось пойти по пути сохранения архитектуры и интерфейсов CallKit. Таким образом оставить пользователям всю гибкость, возможность использовать оригинальную документацию и оградить от потенциальных багов в собственной реализации.



Наша Реализация


Нам удалось перенести всё CallKit API на Dart с сохранением иерархии классов и механизмов взаимодействия с ними.



Наш плагин закрывает собой всю работу с платформой, при этом предоставляет идентичный интерфейс


Коммуникация между Flutter и iOS асинхронна, так что пришлось поломать голову с реализацией некоторых деталей. Основной сложностью был функционал, требующий синхронного взаимодействия с той или иной стороны.


Например, нативное CallKit API CXProviderDelegate.provider(_:execute:) требует синхронно возвращать Bool значение:


optional func provider(_ provider: CXProvider,     execute transaction: CXTransaction) -> Bool

Этот метод вызывается каждый раз, когда нужно обработать новую транзакцию CXTransaction. Можно вернуть true, чтобы обработать транзакцию самостоятельно и уведомить об этом систему. Вернув false, получим дефолтное поведение, при котором для каждого CXAction, содержащегося в транзакции, будет вызван соответствующий метод обработчик в CXProviderDelegate.


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


Проблемы с асинхронностью возникают и в нативной части. Например, есть iOS фреймворк PushKit, он не является частью CallKit, но часто они используются вместе, так что интеграция с ним была необходима. При получении VoIP пуша требуется немедленно уведомить CallKit о входящем звонке в нативном коде, в противном случае приложение упадет. Для обработки этого нюанса мы решили дать возможность репортить входящие звонки напрямую в CallKit из нативного кода без асинхронного крюка в виде Flutter. В итоге для этой интеграции реализовали несколько хелперов в нативной части плагина (доступны через FlutterCallkitPlugin iOS класс) и несколько на стороне Flutter (доступны через FCXPlugin Dart класс).


Дополнительные возможности плагина мы объявили в его собственном классе, чтобы отделить интерфейс плагина от интерфейса CallKit.

Как зарепортить входящий звонок напрямую в CallKit

При получении VoIP пуша вызывается один из методов PKPushRegistryDelegate.pushRegistry(_: didReceiveIncomingPushWith:). Здесь необходимо создать экземпляр CXProvider и вызвать reportNewIncomingCall для уведомления CallKit о звонке. Так как для дальнейшей работы со звонком необходим тот же экземпляр провайдера, мы добавили метод FlutterCallkitPlugin.reportNewIncomingCallWithUUID с нативной стороны плагина. При его вызове плагин сам зарепортит звонок в CXProvider, а так же вызовет FCXPlugin.didDisplayIncomingCall хендлер на стороне Dart для продолжения работы со звонком.


func pushRegistry(_ registry: PKPushRegistry,                  didReceiveIncomingPushWith payload: PKPushPayload,                  for type: PKPushType,                  completion: @escaping () -> Void) {    // Достаем необходимые данные из пуша    guard let uuidString = payload["UUID"] as? String,        let uuid = UUID(uuidString: uuidString),        let localizedName = payload["identifier"] as? String    else {        return    }    let callUpdate = CXCallUpdate()    callUpdate.localizedCallerName = localizedName    let configuration = CXProviderConfiguration(        localizedName: "ExampleLocalizedName"    )        // Репортим звонок в плагин, а он зарепортит его в CallKit    FlutterCallkitPlugin.sharedInstance.reportNewIncomingCall(        with: uuid,        callUpdate: callUpdate,        providerConfiguration: configuration,        pushProcessingCompletion: completion    )}


Подводя итог: главной фишкой нашего плагина является то, что его использование на Flutter практически не отличается от использования нативного CallKit на iOS.


One more thing


Но оставалось ещё кое-что в Apple CallKit, что мы не реализовали у себя (и не реализовал никто в доступных сторонних решениях). Это поддержка Call Directory App Extension.



Что такое Call Directory


CallKit умеет блокировать и определять номера, доступ к этим возможностям для разработчиков открыт через специальное системное расширение Call Directory. Подробнее про iOS app extensions можно почитать в App Extension Programming Guide.



Call Directory app extension позволяет блокировать и/или идентифицировать номера


Если вкратце, то это отдельный таргет iOS проекта, который запускается независимо от основного приложения по требованию системы.


Например, при получении входящего звонка iOS пытается определить или найти звонящего в списке заблокированных стандартными средствами. Если номер не был найден, система может запросить данные у доступных Call Directory расширений, чтобы так или иначе обработать звонок. В этот момент расширение должно эти номера достать из некого хранилища номеров. Само приложение может заполнять это хранилище номерами из своих баз в любое время. Таким образом, взаимодействия между расширением и приложением нет, обмен данными происходит через общее хранилище.



Пример архитектуры для реализации Call Directory


Примеры с передачей номеров в Call Directory уже есть на хабре: раз и два.


Подробнее про iOS App Extensions: App Extension Programming Guide.



Call Directory Extension на Flutter


Не так давно нам написал пользователь с запросом на добавление поддержки Call Directory. Начав изучать возможность реализации этой фичи, мы выяснили, что сделать Flutter API без необходимости написания пользователями нативного кода не выйдет. Проблема заключается в том, что, как было сказано выше, Call Directory работает в расширении. Оно запускается системой, работает очень короткое время и не зависит от приложения (и в том числе от Flutter). Таким образом, для поддержки этого функционала пользователю плагина так или иначе потребуется реализовать app extension и хранилище данных самостоятельно.



Пример работы с Call Directory во Flutter приложении



Принятое решение


Несмотря на сложности с нативным кодом, мы твёрдо решили сделать использование Call Directory максимально удобным для пользователей нашего фреймворка.


Проверив возможность работы такого расширения в связке с Flutter приложением, мы принялись за проектирование. Решение должно было сохранить все Call Directory Manager API, а также требовать от пользователя минимум написания нативного кода и быть удобным для взаимодействия через Flutter.


Так мы сделали версию 1.2.0 с поддержкой Call Directory Extension.



Как мы реализовывали Call Directory для Flutter


Итак, для реализации этого функционала требовалось учесть несколько аспектов:


  • Перенести интерфейс класса CXCallDirectoryManager (CallKit объект позволяющий управлять Call Directory)
  • Решить, что делать с app extension и хранилищем номеров для него
  • Создать удобный способ передачи данных из Dart в натив и обратно для управления списками номеров из Flutter приложения


Перенос интерфейсов CXCallDirectoryManager во Flutter


Код, приведенный в статье, был специально упрощен для облегчения восприятия, полную версию кода можно найти по ссылкам в конце статьи. Для реализации плагина мы использовали Objective-C, так как он был выбран основным в проекте ранее. Интерфейсы CallKit представлены на Swift для простоты.


Интерфейс


Первым делом посмотрим, что конкретно требуется перенести:


extension CXCallDirectoryManager {    public enum EnabledStatus : Int {        case unknown = 0        case disabled = 1        case enabled = 2    }}open class CXCallDirectoryManager : NSObject {    open class var sharedInstance: CXCallDirectoryManager { get }    open func reloadExtension(        withIdentifier identifier: String,        completionHandler completion: ((Error?) -> Void)? = nil    )    open func getEnabledStatusForExtension(        withIdentifier identifier: String,        completionHandler completion: @escaping (CXCallDirectoryManager.EnabledStatus, Error?) -> Void    )    open func openSettings(        completionHandler completion: ((Error?) -> Void)? = nil    )}

Воссоздадим аналог CXCallDirectoryManager.EnabledStatus энама в Dart:


enum FCXCallDirectoryManagerEnabledStatus {  unknown,  disabled,  enabled}

Теперь можно объявить класс и методы. Необходимости в sharedInstance в нашем интерфейсе нет, так что сделаем обычный Dart класс со static методами:


class FCXCallDirectoryManager {  static Future<void> reloadExtension(String extensionIdentifier) async { }  static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(    String extensionIdentifier,  ) async { }  static Future<void> openSettings() async { }}

Сохранение API важно, но так же важно учитывать платформенные и языковые code-style, чтобы использование интерфейса было понятно и удобно для пользователей плагина.


Для API в Dart мы использовали более короткое название без слов-связок (длинное название пришло из objective-C) и заменили completion блок на Future. Future является стандартным механизмом, используемым для получения результата выполнения асинхронных методов в Dart. Мы также возвращаем Future из большинства Dart методов плагина, потому что коммуникация с нативным кодом происходит асинхронно.


Было getEnabledStatusForExtension(withIdentifier:completionHandler:)


Стало Future getEnabledStatus(extensionIdentifier)




Реализация


Для коммуникации между Flutter и iOS будем использовать FlutterMethodChannel.


Подробнее про особенности работы этого канала связи можно почитать здесь.



On the Flutter side


Создадим объект MethodChannel:


const MethodChannel _methodChannel =  const MethodChannel('plugins.voximplant.com/flutter_callkit');


On the iOS side


Первым делом iOS класс плагина нужно подписать на протокол FlutterPlugin, чтобы иметь возможность взаимодействовать с Flutter:


@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@end

При инициализации плагина создадим FlutterMethodChannel с таким же идентификатором, что мы использовали выше:


+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {    FlutterMethodChannel *channel        = [FlutterMethodChannel           methodChannelWithName:@"plugins.voximplant.com/flutter_callkit"          binaryMessenger:[registrar messenger]];    FlutterCallkitPlugin *instance         = [FlutterCallkitPlugin sharedPluginWithRegistrar:registrar];    [registrar addMethodCallDelegate:instance channel:channel];}

Теперь можно использовать этот канал для вызова iOS методов из Flutter.



Рассмотрим подробно реализацию методов в Dart и нативной части плагина на примере getEnabledStatus.



On the Flutter side


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


Про MethodChannel

MethodChannel API позволяет асинхронно получить результат вызова из нативного кода посредством Future, но накладывает ограничения на передаваемые типы данных.




Итак, нам потребуется передать имя метода (его будем использовать в нативном коде для того, чтобы идентифицировать вызов) и аргумент extensionIdentifier в MethodChannel.invokeMethod, а затем преобразовать результат из простейшего типа int в FCXCallDirectoryManagerEnabledStatus. На случай ошибки в нативном коде следует обработать PlatformException.


static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(  String extensionIdentifier,) async {  try {    // Воспользуемся объектом MethodChannel для вызова    // соответствующего метода в платформенном коде    // с аргументом extensionIdentifier.    int index = await _methodChannel.invokeMethod(      'Plugin.getEnabledStatus',      extensionIdentifier,    );    // Преобразуем результат в энам     // FCXCallDirectoryManagerEnabledStatus    // и вернем его значение пользователю    return FCXCallDirectoryManagerEnabledStatus.values[index];  } on PlatformException catch (e) {    // Если что-то пошло не так, обернем ошибку в собственный тип     // и отдадим пользователю    throw FCXException(e.code, e.message);  }}

Обратите внимание на идентификатор метода который мы использовали:


Plugin.getEnabledStatus


Слово перед точкой используется, для определения модуля ответственного за тот или иной метод.


getEnabledStatus идентично названию метода во Flutter, а не в iOS (или Android).




On the iOS side


Теперь переместимся в платформенный код и реализуем бэкенд для этого метода.


Вызовы через FlutterMethodChannel попадают в метод handleMethodCall:result:.


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


- (void)handleMethodCall:(FlutterMethodCall*)call                  result:(FlutterResult)result {    // Вызовы из Flutter можно идентифицировать по названию,    // которое передается в `FlutterMethodCall.method` проперти    if ([@"Plugin.getEnabledStatus" isEqualToString:call.method]) {        // При передаче аргументов с помощью MethodChannel,         // они упаковываются в `FlutterMethodCall.arguments`        // Извлечем extensionIdentifier, который         // мы передали сюда ранее из Flutter кода        NSString *extensionIdentifier = call.arguments;        if (isNull(extensionIdentifier)) {            // Если аргументы не валидны, вернём ошибку через             // `result` обработчик            // Ошибка должна быть упакована в `FlutterError`            // Она вылетит в виде PlatformException в Dart коде            result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);            return;}        // Теперь, когда метод обнаружен,        // а аргументы извлечены и провалидированы,         // можно реализовать саму логику        // Для взаимодействия с этой функциональностью CallKit // потребуется экземпляр CallDirectoryManager        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        // Вызываем метод CallDirectoryManager        // с требуемой функциональностью        // и ожидаем результата        [manager             getEnabledStatusForExtensionWithIdentifier:extensionIdentifier            completionHandler:^(CXCallDirectoryEnabledStatus status,                                            NSError * _Nullable error) {            // completion с результатом вызова запустился,             // можем пробросить результат в Dart            // предварительно сконвертировав его в подходящие типы,             // так как через MethodChannel можно передавать            // лишь некоторые определенные типы данных.            if (error) {                // Ошибки передаются упакованные в `FlutterError`                result([FlutterError errorFromCallKitError:error]);            } else {                // Номера передаются упакованные в `NSNumber`                // Так как этот энам представлен значениями `NSInteger`,                 // выполним требуемое преобразование                result([self convertEnableStatusToNumber:enabledStatus]);            }}];    }}


По аналогии реализуем оставшиеся два метода FCXCallDirectoryManager



On the Flutter side


static Future<void> reloadExtension(String extensionIdentifier) async {  try {    // Задаем идентификатор, передаем аргумент     // и вызываем платформенный метод    await _methodChannel.invokeMethod(      'Plugin.reloadExtension',      extensionIdentifier,    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}static Future<void> openSettings() async {  try {    // А этот метод не принимает аргументов     await _methodChannel.invokeMethod(      'Plugin.openSettings',    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.reloadExtension" isEqualToString:call.method]) {    NSString *extensionIdentifier = call.arguments;    if (isNull(extensionIdentifier)) {        result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);        return;    }    CXCallDirectoryManager *manager         = CXCallDirectoryManager.sharedInstance;    [manager         reloadExtensionWithIdentifier:extensionIdentifier        completionHandler:^(NSError * _Nullable error) {        if (error) {            result([FlutterError errorFromCallKitError:error]);        } else {            result(nil);        }    }];}if ([@"Plugin.openSettings" isEqualToString:call.method]) {    if (@available(iOS 13.4, *)) {        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        [manager             openSettingsWithCompletionHandler:^(NSError * _Nullable error) {            if (error) {                result([FlutterError errorFromCallKitError:error]);            } else {                result(nil);            }        }];    } else {        result([FlutterError errorLowiOSVersionWithMinimal:@"13.4"]);    }}


Готово, CallDirectoryManager реализован и может быть использован.


Подробнее про Platform-Flutter взаимодействие



App Extension и хранилище номеров


Так как из-за нахождения Call Directory в iOS расширении мы не сможем предоставить его реализацию с плагином, а работа с платформенным кодом обычно непривычна для Flutter разработчиков, не знакомых с нативной разработкой, постараемся по максимуму помочь им с помощью Документации!


Реализуем полноценный пример app extension и хранилища и подключим их к example app нашего плагина.


В качестве простейшего варианта хранилища используем UserDefaults, которые обернем в propertyWrapper.


Примерно так выглядит интерфейс нашего хранилища:


// Доступ к хранилищу из iOS приложения@UIApplicationMainfinal class AppDelegate: FlutterAppDelegate {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]}// Доступ к хранилищу из app extensionfinal class CallDirectoryHandler: CXCallDirectoryProvider {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]    @NullableUserDefault("lastUpdate")    private var lastUpdate: Date?}


Код имплементации хранилища:


UserDefaults


Код iOS приложения:


iOS App Delegate


Код iOS расширения:


iOS App Extension


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


Передача номеров из Flutter в iOS и обратно


Итак, app extension настроен и связан с хранилищем, необходимые методы CallDirectoryManager реализованы, осталась последняя деталь научиться передавать номера из Flutter в платформенное хранилище или, наоборот, запрашивать номера оттуда.


Наиболее простым вариантом кажется взвалить передачу данных на пользователя плагина, тогда ему придется самостоятельно организовывать MethodChannel или использовать другие сторонние решения по управлению хранилищем. И, безусловно, кому-то это даже подойдет! :) А для остальных сделаем простое и удобное API, чтобы пробрасывать номера прямо через наш фреймворк. Этот функционал будем делать опциональным, чтобы не ограничивать тех, кому удобнее использовать свои способы передачи данных.



Интерфейс


Посмотрим, какие интерфейсы могут понадобиться:


  • Добавление блокируемых/идентифицируемых номеров в хранилище
  • Удаление блокируемых/идентифицируемых номеров из хранилища
  • Запрос блокируемых/идентифицируемых номеров из хранилища


On the Flutter side


Для методов-хелперов мы ранее решили использовать классы плагина FCXPlugin (Flutter) и FlutterCallkitPlugin (iOS). Однако Call Directory является узкоспециализированным функционалом, который используется далеко не в каждом проекте. Поэтому хотелось вынести это в отдельный файл, но оставить доступ через объект класса FCXPlugin, для этого подойдет extension:


extension FCXPlugin_CallDirectoryExtension on FCXPlugin {  Future<List<FCXCallDirectoryPhoneNumber>> getBlockedPhoneNumbers()    async { }  Future<void> addBlockedPhoneNumbers(    List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeBlockedPhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllBlockedPhoneNumbers() async { }  Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers()    async { }  Future<void> addIdentifiablePhoneNumbers(List<FCXIdentifiablePhoneNumber> numbers,  ) async { }  Future<void> removeIdentifiablePhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllIdentifiablePhoneNumbers() async { }}


On the iOS side


Чтобы со стороны Flutter получить доступ к номерам, которые находятся в неком хранилище на стороне iOS, пользователю плагина нужно будет как-то связать свою базу номеров с плагином. Для этого дадим ему такой интерфейс:



@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@property(strong, nonatomic, nullable)NSArray<FCXCallDirectoryPhoneNumber *> *(^getBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)NSArray<FCXIdentifiablePhoneNumber *> *(^getIdentifiablePhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddIdentifiablePhoneNumbers)(NSArray<FCXIdentifiablePhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveIdentifiablePhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllIdentifiablePhoneNumbers)(void);@end


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


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


Реализация


Теперь реализуем связь между объявленными методами-хелперами во Flutter и обработчиками в iOS.


Методов много, а работают они +- одинаково, поэтому будем рассматривать два из них с противоположным направлением движения данных.

Get identifiable numbers



On the Flutter side


Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers() async {  try {    // Вызываем платформенный метод и сохраняем результат    List<dynamic> numbers = await _methodChannel.invokeMethod(      'Plugin.getIdentifiablePhoneNumbers',    );    // Типизируем результат и возвращаем пользователю    return numbers      .map(        (f) => FCXIdentifiablePhoneNumber(f['number'], label: f['label']))      .toList();  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.getIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.getIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"getIdentifiablePhoneNumbers"]);        return;    }    // Используя обработчик, запрашиваем номера у пользователя    NSArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = self.getIdentifiablePhoneNumbers();    NSMutableArray<NSDictionary *> *phoneNumbers        = [NSMutableArray arrayWithCapacity:identifiableNumbers.count];    // Оборачиваем каждый номер в словарь,     // чтобы иметь возможность передать их через MethodChannel     for (FCXIdentifiablePhoneNumber *identifiableNumber in identifiableNumbers) {        NSMutableDictionary *dictionary             = [NSMutableDictionary dictionary];        dictionary[@"number"]             = [NSNumber numberWithLongLong:identifiableNumber.number];        dictionary[@"label"]             = identifiableNumber.label;        [phoneNumbers addObject:dictionary];    }    // Отправляем номера во Flutter    result(phoneNumbers);}


Add identifiable numbers



On the Flutter side


Future<void> addIdentifiablePhoneNumbers(  List<FCXIdentifiablePhoneNumber> numbers,) async {  try {    // Готовим номера для передачи через MethodChannel    List<Map> arguments = numbers.map((f) => f._toMap()).toList();    // Отправляем номера в нативный код    await _methodChannel.invokeMethod(      'Plugin.addIdentifiablePhoneNumbers',      arguments    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.addIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.didAddIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"didAddIdentifiablePhoneNumbers"]);        return;    }    // Достаем переданные в аргументах номера    NSArray<NSDictionary *> *numbers = call.arguments;    if (isNull(numbers)) {        // Проверяем их валидность        result([FlutterError errorInvalidArguments:@"numbers must not be null"]);        return;    }    NSMutableArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = [NSMutableArray array];    // Типизируем номера    for (NSDictionary *obj in numbers) {        NSNumber *number = obj[@"number"];        __auto_type identifiableNumber            = [[FCXIdentifiablePhoneNumber alloc] initWithNumber:number.longLongValue                                                                                     label:obj[@"label"]];        [identifiableNumbers addObject:identifiableNumber];    }    // Отдаём типизированные номера в обработчик пользователю    self.didAddIdentifiablePhoneNumbers(identifiableNumbers);    // Сообщаем во Flutter о завершении операции    result(nil);}


Остальные методы реализуются по аналогии, полный код:




Примеры использования


Теперь переместимся на сторону пользователя получившегося плагина и посмотрим, как он может воспользоваться нашими интерфейсами.



Reload extension


Метод reloadExtension(withIdentifier:completionHandler:) используется для перезагрузки расширения Call Directory. Это может потребоваться, например, после добавления новых номеров в хранилище, чтобы они попали в CallKit.


Использование идентично нативному CallKit API: обращаемся к FCXCallDirectoryManager и запрашиваем перезагрузку по заданному extensionIdentifier:


final String _extensionID =  'com.voximplant.flutterCallkit.example.CallDirectoryExtension';Future<void> reloadExtension() async {  await FCXCallDirectoryManager.reloadExtension(_extensionID);}


Get identified numbers



On the Flutter side


Запрашиваем список идентифицируемых номеров через класс плагина из Flutter:


final FCXPlugin _plugin = FCXPlugin();Future<List<FCXIdentifiablePhoneNumber>> getIdentifiedNumbers() async {  return await _plugin.getIdentifiablePhoneNumbers();}


On the iOS side


Добавляем обработчик getIdentifiablePhoneNumbers, который плагин использует для передачи заданных номеров во Flutter. Будем передавать в него номера из нашего хранилища identifiedNumbers:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий запроса номеровcallKitPlugin.getIdentifiablePhoneNumbers = { [weak self] in    guard let self = self else { return [] }    // Возвращаем номера из хранилища в обработчик    return self.identifiedNumbers.map {        FCXIdentifiablePhoneNumber(number: $0.number, label: $0.label)    }}


Теперь номера из пользовательского хранилища будут попадать в обработчик, а из него через плагин во Flutter.



Add identified numbers



On the Flutter side


Передаем номера, которые хотим идентифицировать, в объект плагина:


final FCXPlugin _plugin = FCXPlugin();Future<void> addIdentifiedNumber(String number, String id) async {  int num = int.parse(number);  var phone = FCXIdentifiablePhoneNumber(num, label: id);  await _plugin.addIdentifiablePhoneNumbers([phone]);}


On the iOS side


Добавляем обработчик didAddIdentifiablePhoneNumbers, который плагин использует для уведомления платформенного кода о получении новых номеров из Flutter. В обработчике сохраняем полученные номера в хранилище номеров:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий добавления номеровcallKitPlugin.didAddIdentifiablePhoneNumbers = { [weak self] numbers in    guard let self = self else { return }    // Сохраняем в хранилище номера, переданные плагином в обработчик    self.identifiedNumbers.append(        contentsOf: numbers.map {            IdentifiableNumber(identifiableNumber: $0)        }    )    // Номера в Call Directory обязательно должны быть отсортированы    self.identifiedNumbers.sort()}


Теперь номера из Flutter будут попадать в плагин, из него в обработчик события, а оттуда в пользовательское хранилище номеров. При следующей перезагрузке Call Directory расширения они станут доступны CallKit для идентификации звонков.


Полные примеры:




Итог


У нас получилось дать возможность использовать CallKit Call Directory из Flutter!


Детали платформенных коммуникаций по прежнему скрыты в недрах плагина, нативное API сохранено, а необходимая к написанию пользовательская iOS реализация хорошо задокументирована.


Теперь во Flutter можно относительно просто блокировать и/или определять номера с помощью нативного Call Directory.



Пример работы с Call Directory в Flutter приложении с использованием flutter_callkit_voximplant



Результаты:


  • Интерфейс CallDirectoryManager полностью перенесен
  • Добавлен простой способ передачи номеров из Flutter кода в iOS, оставлена возможность использовать собственные решения передачи данных
  • Архитектура решения описана в README с визуальными схемами для лучшего понимания
  • Добавлен полноценный работоспособный example app, использующий всю функциональность Call Directory, реализующий пример платформенных модулей (таких как iOS расширение и хранилище данных)


Полезные ссылки


Source код flutter_callkit на GitHub


Example app код на GitHub


Полная документация по использованию Call Directory с flutter_callkit


CallKit Framework Documentation by Apple


App Extension Programming Guide by Apple


Writing custom platform-specific code by Flutter

Подробнее..

Extendr вызываем rust из R (и наоборот)

14.06.2021 20:18:14 | Автор: admin

Зачем нужен Rust в R?

Первый вопрос, который должен возникнуть у читателя -- а зачем вообще использовать Rust вместе с R? Ответ довольно прост: Rust -- новый системный язык программирования, спроектированный специально для написания безопасного и легко распараллеливаемого кода. Rust довольно сложен в освоении (в сравнении с другими языками), но при этом предоставляет отличные инструменты для разработки. Rust имеет довольно неплохую ООП систему и очень много заимствует из функциональных языков программирования. Несмотря на дополнительную сложность из-за функциональных/ООП компонентов, Rust позиционируется как zero-cost abstraction язык, так же как и C++.

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

Таким образом, можно привести два основных аргумента в пользу использования Rust: более безопасный и удобочитаемый код и доступ к целому репозиторию Rust-крейтов.

Что нужно, чтобы R код мог вызвать Rust-библиотеку?

На самом деле -- не так уж много. R-пакеты могут содержать директорию src/, в которой находится исходный код на одном из компилируемых языков. С помощью src/Makevars или src/Makevars.win файлов (вариация make) можно контролировать процесс сборки, например, вызвав на одном из шагов cargo (см. пример здесь):

cargo build --release --manifest-path=rustlib/Cargo.toml

При этом Rust -библиотека должна собираться как crate-type = ["staticlib"]. Кроме непосредственной компиляции Rust-кода, нужно предоставить C-обертки к экспортируемым функциям, а так же добавить несколько магических вызовов специальных R-функций, которые объясняют R, какие именно функции и какого типа экспортируются из данной библиотеки (например, вот так).

Основная проблема -- C-обертки и преобразование типов из R SEXP (фактически, специальный указатель) во что-то, совместимое с Rust, учитывая при этом специфику управления памятью в R (все эти ваши PROTECT, UNPORTECT, и т. д.). Как результат -- легко создать примитивный прототип без функционала, практически невозможно написать достаточно большой проект.

Интегрируем R и Rust: три простых шага

Шаг первый: баиндинги для заголовочных файлов R

Взаимодействие с R происходит через специализированный API, доступный обычно ввиде C/ C++ заголовочных файлов (см. $R_HOME\include\). Разумеется, вызывать эти методы можно практически из любого языка, но это неудобно -- загловочные файлы невозможно подключить напрямую к Rust. К счастью, у этой проблемы уже давно есть решение: rust-bindgen (rust-lang/rust-bindgen). bindgen позволяет автоматически генерировать Rust-обертки из заголовочных файлов, и делает это довольно эффективно.

Так появился крейт libR-sys, который предоставляет баиндинги ко всем необходимым внутренним R функциям. Генерация баиндингов -- вещь нетривиальная, bindgen зависит от clangи сложен в конфигурировании, поэтому мы предоставляем pre-computed (заранее сгененрированные) баиндинги для большинства платформ, поддерживающих R. Список включает в себя linux-x64 (созданный с помощью Ubuntu-20.04), win-x86/x64 (с помощью msys2, x86 может иметь проблемы в каких-то пограничных случаях), macOS включая 11 версию (по возможности), x64 и экспериментально arm64 (честно я не знаю, есть ли arm64 сборка R под macOS). Для каждой из упомянутых платформ/архитектур мы стараемся предоставить три версии баиндингов: oldrel, release, и devel, что соответствует "прошлой", "текущей" (сейчас это 4.1.0) и "находящейся в разработке" версиям R.

В качестве альтернативы баиндинги можно сгенерировать непосредственно в системе где компилируется R пакет, при условии что все необходимые зависимости присутствуют (особенная головная боль на Windows). Гипотетически, можно собрать R для неподдерживаемой платформы и прямо на месте сгененрирвоать баиндинги (если Rust поддерживает такую платформу).

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

Шаг второй: автоматизируем преобразование типов и экспорт функций

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

Прежде чем продолжить, я хочу сделать небольшое отступление. Вся идея проекта extendr и, в особенности, имплементация большей части Rust-крейтов, принадлежит Энди Томасону (@andy-thomason). Без его вклада, на мой субъективный взгляд, extendr в том виде, в котором он существует сейчас, был бы невозможен.

Вернемся обратно к коду. Как избавиться от боилерплейта? Легко, надо всего лишь распарсить исходный код Rust. Например, используя syn и подобные крейты. Моей экспертизы недостаточно, чтобы детально описать процесс парсинга и кодогенерации, но для конечного пользователя экспорт Rust функции становится невероятно простым. Во-первых, нужно пометить функции с помощью аттрибута #[extendr]:

#[extendr]fn add_i32(x : i32, y : i32) -> i32 { x + y }#[extendr]fn add_vec(x : &[i32], y : &[i32]) -> Vec<i32> {     x.iter().zip(y.iter()).map(|v| v.0 + v.1).collect()}

Во-вторых, нужно явно объявить экспортируемые функции:

extendr_module! {  mod extendrtest;  fn add_i32;  fn add_vec;}

Мы попытались предусмотреть все возможные случаи экспорта функций, включая экспорт нескольких модулей. Возможно, это не будет работать идеально во всех случаях, но в базовых сценариях проблемы отсутствуют. Несмотря на огромное количество готовых фич, к extendr стоит относится как к WIP.

К сожалению, остается одно небольшое ограничение при интеграции Rust-кода в проект. Дело в том, что если в папке src/ отсутствуют файлы-исходники, то стандартная процедура компиляции R попросут игнорирует все остальное и библиотека не компилируется. Чтобы обойти это, в src/ добавляется единственный файл entrypoint.c, примерно следующего содержания:

void R_init_extendrtest_extendr(void *dll);void R_init_extendrtest(void *dll) {  R_init_extendrtest_extendr(dll);}

Здесь R_init_extendrtest_extendr генерируется автоматически с помощью Rust-крейта, а R_init_extendrtest -- непосредственно вызывается из R. Мы пока что не нашли способа избавиться от этого ограничения.

Некоторых изменений требуют и Makevars-файлы. Вот пример из одного из тестовых проектов:

LIBDIR = ./rust/target/releaseSTATLIB = $(LIBDIR)/libextendrtest.aPKG_LIBS = -L$(LIBDIR) -lextendrtestall: C_clean$(SHLIB): $(STATLIB)$(STATLIB):cargo build --lib --release --manifest-path=./rust/Cargo.tomlC_clean:rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)clean:rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) rust/target

Фактически, мы отдельно компилируем Rust-крейт, а потом создаем совместимую с R библиотеку используя Rust-библиотеку и результат компиляции entrypoint.c.

Аналогично выглядит и версия для Windows, с той лишь разницей что на Windows мы поддерживаем иx86, и x64, из-за чего приходится динамически выбирать правильный путь к STATLIB.

extendr выполняет не только кодогенерацию на стороне Rust, он еще генерирует обертки на стороне R. Если предположить, что приведенный выше Rust код является частью пакета {extendrtest}, то становятся досутпны следующие функции:

extendrtest::add_i32(4L, 11L)# [1] 15extendrtest::add_vec(1:10, 10:1)#  [1] 11 11 11 11 11 11 11 11 11 11

Да, насктолько просто.

Шаг третий: user-friendliness

В своей работе мы вдохновлялись такими проектами как {cpp11} - header-only пакет для интеграции C++11 кода. Так появился на свет {rextendr}, R - пакет без Rust-зависимости, который решает три основные задачи:

  • Создание шаблона пакета, использующего extendr, наподобие {usethis};

  • Компиляция и исполнение Rust - кода на лету, прямо в R-сессии. Именно это демонстрирует Анимация Для Привлечения Внимания;

  • Предоставление специальных knitr-модулей (engines), а именно {extendr} и {extendrsrc}, которые позволяют включать фрагменты Rust-кода (и результаты его выполнения) в ваш Rmarkdown прямо рядом с R-кодом, обеспечивая их взаимодействие.

Сейчас {rextendr} отправился на проверку в CRAN, и мы ждем результатов. Я думаю, самое время продемонстроровать несколько примеров именно с использованием {rextendr}. Сразу оговорюсь, для упрощения и воспроизводимости, я буду использовать{reprex}.

Самый простой пример это, конечно же,

rextendr::rust_function("fn hello_r() -> &'static str { \"Hello R!\" }")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmp259cVM\file10186cb44264'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmp259cVM/file10186cb44264/target/extendr_wrappers.R'.hello_r()#> [1] "Hello R!"

Пример со сложением я уже показывал, но что будет, если явно передать NA?

rextendr::rust_function("fn add_i32(x : i32, y : i32) -> i32 { x + y }")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmp2P2cnQ\file2f7c65e8269a'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmp2P2cnQ/file2f7c65e8269a/target/extendr_wrappers.R'.add_i32(42L, NA)#> Error in add_i32(42L, NA): unable to convert R object to primitive

Сообщения об ошибках еще не очень информативны, мы работаем над этим, но тем не менее мы получаем базовую валидацию просто за счет системы типов -- NA не совместим с i32. Однако, можно написать вот так

rextendr::rust_function("fn add_i32_opt(x : Option<i32>, y : Option<i32>) -> Option<i32> {    match (x, y) {        (Some(a), Some(b)) => Some(a + b),        _ => None    }}")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmpyg3uPw\file6587a897d2a'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmpyg3uPw/file6587a897d2a/target/extendr_wrappers.R'.add_i32_opt(NA, 42L)#> [1] NAadd_i32_opt(42L, 100L)#> [1] 142

Хотите еще больше магии? Макрос R! выполняет внутри R-код, возвращая результат если операция была успешной. Как насчет

x <- 42L y <- 100Lrextendr::rust_eval("R!(x)? * 2 + R!(y)? * 3")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpKeC23J\file32ec53677fc9'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpKeC23J/file32ec53677fc9/target/extendr_wrappers.R'.#> [1] 384

Можно попробовать смешать переменные из R и Rust.

library(tibble)x <- 10:1 # Эта переменная на стороне Rrextendr::rust_eval("call!(\"tibble\", x = R!(x), y = 1..=10)")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpcDWhlk\file45802f52dc5'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpcDWhlk/file45802f52dc5/target/extendr_wrappers.R'.#> # A tibble: 10 x 2#>        x     y#>    <int> <int>#>  1    10     1#>  2     9     2#>  3     8     3#>  4     7     4#>  5     6     5#>  6     5     6#>  7     4     7#>  8     3     8#>  9     2     9#> 10     1    10

Эти макросы полезны, но до сих пор нестабильны. Они лишь демонстрируют потенциальные возможности для взаимодействия R и Rust.

Для безопасной печати в Rout существует отдельный макрос : rprintln!.

x <- 42Lrextendr::rust_eval("rprintln!(\"Hello from Rust! x = {}\", R!(x)?.as_integer().unwrap());")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpWQh3w0\file48e024f161ce'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpWQh3w0/file48e024f161ce/target/extendr_wrappers.R'.#> Hello from Rust! x = 42

Пишем свой extendr-пакет

В этом разделе я просто приведу пример генерации пакета с использование {rextendr} и других стандартных инструментов:

pkg <- file.path(tempfile(), "myextendr")dir.create(pkg, recursive = TRUE)usethis::create_package(pkg)usethis::proj_activate(pkg)rextendr::use_extendr()rextendr::document()rextendr::document()hello_world()
Как это выглядит
pkg <- file.path(tempfile(), "myextendr")dir.create(pkg, recursive = TRUE)usethis::create_package(pkg)#> v Setting active project to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr'#> v Creating 'R/'#> v Writing 'DESCRIPTION'#> Package: myextendr#> Title: What the Package Does (One Line, Title Case)#> Version: 0.0.0.9000#> Authors@R (parsed):#>     * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)#> Description: What the package does (one paragraph).#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a#>     license#> Encoding: UTF-8#> LazyData: true#> Roxygen: list(markdown = TRUE)#> RoxygenNote: 7.1.1#> v Writing 'NAMESPACE'#> v Setting active project to '<no active project>'usethis::proj_activate(pkg)#> v Setting active project to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr'#> v Changing working directory to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr/'rextendr::use_extendr()#> v Creating 'src/rust/src'.#> v Writing 'src/entrypoint.c'#> v Writing 'src/Makevars'#> v Writing 'src/Makevars.win'#> v Writing 'src/.gitignore'#> v Writing 'src/rust/Cargo.toml'.#> v Writing 'src/rust/src/lib.rs'#> v Writing 'R/extendr-wrappers.R'#> v Finished configuring extendr for package myextendr.#> * Please update the system requirement in 'DESCRIPTION' file.#> * Please run `rextendr::document()` for changes to take effect.rextendr::document()#> i Generating extendr wrapper functions for package: myextendr.#> ! No library found at 'src/myextendr.dll', recompilation is required.#> Re-compiling myextendr#>   -  installing *source* package 'myextendr' ...#>      ** using staged installation#>      ** libs#>      rm -Rf myextendr.dll ./rust/target/x86_64-pc-windows-gnu/release/libmyextendr.a entrypoint.o#>      "C:/rtools40/mingw64/bin/"gcc  -I"C:/PROGRA~1/R/R-41~1.0/include" -DNDEBUG          -O2 -Wall  -std=gnu99 -mfpmath=sse -msse2 -mstackrealign  -UNDEBUG -Wall -pedantic -g -O0 -c entrypoint.c -o entrypoint.o#>      cargo build --target=x86_64-pc-windows-gnu --lib --release --manifest-path=./rust/Cargo.toml#>              Updating crates.io index#>             Compiling winapi-build v0.1.1#>       Compiling winapi v0.3.9#>       Compiling winapi v0.2.8#>       Compiling proc-macro2 v1.0.27#>       Compiling unicode-xid v0.2.2#>       Compiling syn v1.0.73#>       Compiling extendr-engine v0.2.0#>       Compiling lazy_static v1.4.0#>             Compiling kernel32-sys v0.2.2#>             Compiling quote v1.0.9#>             Compiling extendr-macros v0.2.0#>             Compiling libR-sys v0.2.1#>             Compiling extendr-api v0.2.0#>             Compiling myextendr v0.1.0 (C:\Users\...\AppData\Local\Temp\RtmpAVW4HZ\file122c180d1953\myextendr\src\rust)#>              Finished release [optimized] target(s) in 33.09s#>      C:/rtools40/mingw64/bin/gcc -shared -s -static-libgcc -o myextendr.dll tmp.def entrypoint.o -L./rust/target/x86_64-pc-windows-gnu/release -lmyextendr -lws2_32 -ladvapi32 -luserenv -LC:/PROGRA~1/R/R-41~1.0/bin/x64 -lR#>      installing to C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/devtools_install_122c37bd1965/00LOCK-myextendr/00new/myextendr/libs/x64#>   -  DONE (myextendr)#> v Writing 'R/extendr-wrappers.R'.#> i Updating myextendr documentation#> i Loading myextendr#> Writing NAMESPACE#> Writing NAMESPACE#> Writing hello_world.Rdrextendr::document()#> i Generating extendr wrapper functions for package: myextendr.#> i 'R/extendr-wrappers.R' is up-to-date. Skip generating wrapper functions.#> i Updating myextendr documentation#> i Loading myextendr#> Writing NAMESPACE#> Writing NAMESPACEhello_world()#> [1] "Hello world!"

hello_world() написана на Rust и автоматически экспортируется в R. Обратите внимание, что hello_world.Rd был создан при вызове rextendr::document() (аналог devtools::document()). Дело в том, что rextendr-парсер воспринимает /// комментарии как R комментарии. Rust функция выглядит вот так

/// Return string `"Hello world!"` to R./// @export#[extendr]fn hello_world() -> &'static str {    "Hello world!"}

Что автоматически генерирует R обертку

#' Return string `"Hello world!"` to R.#' @exporthello_world <- function() .Call(wrap__hello_world)

и, как результат, обновляет документацию и NAMESPACE с помощью {roxygen2}.

Если этого мало

Здесь я хотел бы коротко описать последнюю важную фичу extendr. Крейт позволяет экспортировать не просто функции, а целые типы. Легким движением руки можно пробросить кастомный тип из Rust в R , а инстансы этого типа -- передавать в обе стороны как ссылки. Это позволяет заполучить ООП в R в традиционном (object-first) стиле, модицифируя in-place объекты, созданные и доступные из Rust:

Мутабельный объект
rextendr::rust_source(code = "struct Counter {    n: i32,}#[extendr]impl Counter {    fn new() -> Self {        Self { n: 0 }    }        fn increment(&mut self) {        self.n += 1;    }        fn get_n(&self) -> i32 {        self.n    }}")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpWOu1pt\file5318783e2176'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpWOu1pt/file5318783e2176/target/extendr_wrappers.R'.cntr <- Counter$new()cntr$get_n()#> [1] 0cntr$increment()cntr$increment()cntr$get_n()#> [1] 2

Вместо заключения

Статья получилась гораздо длиннее и сумбурней, чем я ожидал. Тем не менее, я не успел описать все возможности extendr. Этот проект амбициозный и еще далек от завершения, но я считаю, что давно пришло время добавить поддержку Rust в R, а главное сделать взаимодействие этих языков удобным. Мы осторожно надеемся, что в конечном итоге сможем добавить официальную поддержку Rust, на равне с C / C++. К сожалению, сейчас ее отсутствие накладывает на нас некоторые ограничения.

Отдельным вызовом было заставить эту систему работать на Windows. Мы столкнулись со множеством проблем, но на данный момент нам удалось справиться практически со всеми трудностями. Для запуска на Windows extendr требует стандартный Rust - тулчейн, stable-x86_64-pc-windows-msvc, с дополнительными целями (targets) x86_64-pc-windows-gnu и i686-pc-windows-gnu, а также Rtools40v2 (последняя версия на момент написания, отличается от Rtools40).

Скудную документацию можно найти здесь и в репозиториях проекта extendr.

Спасибо что дочитали до конца!

Подробнее..
Категории: Rust , R , Package , Interop , Crate

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru