Пути внедрения поддержки скриптов в Dart/Flutter
Итак, здравствуйте! Меня зовут Никита Синявин, я разработчик мобильных приложений. С Dart и Flutter возможно почти всё — за это я и люблю этот стек. А самое важное, что до сих пор в рамках технологии остается пространство для творчества. Поэтому я всегда нахожусь в поиске смелых решений и новых идей. Приглашаю вас ознакомиться с материалами этой статьи и вместе разобраться в том, возможно ли внедрение скриптов в Dart/Flutter приложениях!
Введение
Некоторое время назад в разговоре с коллегой мы обсуждали мой пет-проект и он предложил потенциально интересную фичу — внедрить поддержку выполнения скриптов. Сперва эта функция была отложена в плане реализации в долгий ящик, но тем не менее постоянно маячила на периферии сознания. Со временем я вернулся к этой теме, начал изучать доступные варианты реализации, экспериментировать. Эта статья является обобщением моих мыслей и результатов тестов.
При первом рассмотрении темы возникает закономерный вопрос: «Зачем нужен скриптинг в клиентском приложении?». Можно выделить два основных направления применения подобного подхода:
Специфичные продукты, предоставляющие пользователям функционал по моделированию предметной области (процессов и моделей).
Приложения, которым требуется часто и/или динамически менять бизнес-логику, выполняемую на клиенте.
Одной из причин для добавления подобной функциональности в клиентское приложение я вижу увеличение гибкости при разработке и поддержке. Наличие скриптов позволяет отделять изменчивую бизнес-логику от приложения и «упаковывать» ее в обновляемые по сети скрипты. В свою очередь такая модель обновления не требует заново деплоить приложение в сторы и ожидать проверки. Многие знают про SDUI (server-driven UI), а это какой-то SDBL (server-driven busines logic) получается :)
В качестве примера того, что можно было бы реализовать, используя описанный выше подход, можно привести динамические функции-валидаторы полей ввода Low-code/No-code платформ. Пользователь платформы может создавать правила валидации полей формы в установленном формате.
Возможные ограничения
С Google Play и OC Android проблем чаще всего не возникает. Наличие альтернативных сторов и возможность скачивать и устанавливать apk-файлы напрямую из интернета позволяет Android-разработчикам распространять свои приложения по разным каналам. С App Store, как водится, все сложнее. Ручная установка ipa-файлов способна лишь оттолкнуть клиентов, а модерация приложения может занимать весьма продолжительное время. Поэтому в рамках статьи мы рассмотрим именно требования Apple.
Читаю App Store Review Guideline, раздеал Software Requirements, пункт 2.5.2:
Apps should be self-contained in their bundles, and may not read or write data outside the designated container area, nor may they download, install, or execute code which introduces or changes features or functionality of the app, including other apps. Educational apps designed to teach, develop, or allow students to test executable code may, in limited circumstances, download code provided that such code is not used for other purposes. Such apps must make the source code provided by the app completely viewable and editable by the user.
Особенно важной для нас является часть, отмеченная жирным шрифтом. Там говорится о том, что »… (приложения)не могут загружать, устанавливать или выполнять код, который представляет или изменяет функции или функциональность приложения, включая другие приложения».
На мой взгляд, формулировка этого пункта очень уж широка. Я понял это так: запрещено загружать и выполнять код, который способен изменить функционал приложения. Я бы отнес сюда как сторонние среды выполнения (далее «рантаймы»), так и разного рода интерпретаторы. Но прямо сейчас в продакшене находятся сотни приложений для iOS, которые нарушают это правило!
Я говорю о React Native и одной интересной технологии под названием CodePush. Задача CodePush достаточно проста — подменить js-bundle уже установленного приложения на новый. Это позволяет бесшовной обновлять приложения на React Native, в тех случаях, когда не требуется обновление нативных зависимостей.
Схема работы CodePush
Это означает, что ваше приложения для просмотра фотографий котят может по щелчку пальцев превратится в приложение для заказа доставки пиццы, что противоречит правилам Apple. Но то, что Apple допускает использование этой технологии меня в некоторой степени обнадеживает: мы всегда может попробовать «в тихую» реализовать поддержку скриптов и не быть пойманными :)
Краткий обзор
Перед началом работы я выделил три основных способа реализации, в которых я видел перспективы:
FFI и «скрипты» на С/С++
Использование стороннего рантайма
Использование интерпретатора
У каждого из вариантов есть важное общее ограничение — мы должны соблюдать установленный API параметров функций и их возвращаемых значений. То есть, чтобы две версии одного скрипта оставались взаимозаменяемыми, они не должны ломать установленный контракт. Иначе, как мне кажется, смысл от скриптов будет утерян — приложение все равно придется обновлять.
Оценивать решения я буду по четырем основным пунктам по десятибальной шкале:
Поддержка — насколько предлагаемые инструменты поддерживаются официально или же сообществом.
Сложность использования — оценка общих требований для использования.
Сложность отладки.
Идиоматичность — насколько подход «правильный» в контексте Dart/Flutter
Ниже мы более подробно рассмотрим каждый из пунктов, оценим его эффективность и обозначим потенциальные плюсы и минусы. Поехали!
Важно: ни один из озвученных подходов не был применен в продакшене. Вся эта статья лишь «фантазии на тему» и лишь помочает рассмотреть варианты для реализации.
C/C++ и dart: ffi
FFI (Foreign Function Interface) — это механизм, с помощью которого программа, написанная на одном языке программирования, может вызывать подпрограммы, написанные на другом языке. FFI часто используется при вызовах из бинарной динамически подключаемой библиотеки (вики).
В Dart библиотека dart: ffi предназначена для взаимодействия с кодом, написанном на языке С (и другими языками, которые линкуются с С), а также выполняет часть задач по преобразованию типов и работе с памятью.
Так выглядит определение функции на стороне Dart:
typedef GoodFirstFunction = ffi.Int Function(ffi.Int);
typedef GoodFirstFunctionDart = int Function(int);
final dylib = ffi.DynamicLibrary.open(binaryFile.path);
final GoodFirstFunctionDart gff = dylib
.lookup>('goodFirstFunction')
.asFunction();
Код «скрипта» на С++:
#include
extern "C"
{
int goodFirstFunction(int x)
{
return x + x;
};
}
Проблем при реализации выявлено не было. Я успешно загрузил бинарь с сервера и сохранил его. Потенциальные проблемы могут возникнуть на этапе загрузки (DynamicLibrary.open
) динамической библиотеки. В документации по С interop сказано следующее:
On macOS, executables, including the Dart VM (
dart
), can load only signed libraries. For more information on signing libraries, see Apple’s Code Signing Guide.
Пользователи macOS зачастую знакомы с этой ошибкой, когда скачивают какой-либо софт из интернета. Для его запуска требуется «дать понять» ОС, что софт безопасен. Но с точки зрения клиентского ПО, необходимость вручную лезть в директорию где лежит сохраненный .dylib и говорить ОС «все норм, я знаю что делаю» ужасна. Если же речь идет о более «лояльных» в плане политик безопасности ОС и специфичном ПО (например, приложения для «промышленных планшетов» на каком-либо производстве), то этим пунктом можно пренебречь.
Плюсы:
Гарантировано работает. FFI плагин поддерживается командой dart, а значит не будет забыт и брошен (наверное).
Компактный формат, который может эффективно передаваться по сети.
Высокая производительность и широкие возможности.
Минусы:
Высокая сложность. Приведенный пример кода не отображает всей сложности использования плагина для связывания с С++ кодом. Часто необходимо вручную реализовывать структуры, которые будут передаваться между Dart и C++. Есть хорошая статья, где иллюстрируются некоторые тонкости этого процесса. К тому же, надо еще сам С++ код качественно написать :)
Политики безопасности ОС. Возможны самые необычные и неожиданные баги, связанные с ограничениями ОС.
Высокая сложность отладки.
Сложности кросс-компиляции исходного кода и необходимость хранить на сервере бинари под все поддерживаемые платформы.
Мнение:
Использование С/С++ и dart: ffi в контексте динамических скриптов — идея сомнительная. Задача со звездочкой. Выделение бизнес-логики в те же С/С++ библиотеки не новшество на рынке и используется в проде, но все же это следует делать привычными методами (сборка и компиляция вместе с приложением), имхо.
Поддержка | Сложность использования | Сложность отладки | Идиоматичность |
10 | 8 | 9 | 7 |
Встраивание в приложение сторонней среды выполнения
Runtime — среда исполнения программного кода, которая может включать в себя инструменты и библиотеки.
В нашем случае мы подразумеваем то, что в рамках нашего приложения будет развернута дополнительная среда выполнения, в которой будет исполняться код на нужном нам языке. Предлагаю обратиться к опыту разработчиков пакета flutter_js (предоставляет собой js-runtime на базе QuickJS на Android и JavascriptCore на IOS) или же lua_darbo (виртуальная машина Lua, написанная на Dart).
Эти пакеты поддерживают вызов скриптов в строковом представлении, передачу параметров в скрипт и возвращение результата выполнения на сторону Dart.
Сама идея «вживления» чужеродного рантайма в приложение не нова. Тот же React Native под капотом использует Hermes для исполнения js. Это дает надежду на то, что такой подход не является нарушением политики сторов.
Но Flutter в текущий момент — мощная и самодостаточная платформа и на этом фоне использование дополнительной среды выполнения может выглядеть избыточно и очень экзотично.
Плюсы:
Относительная безопасность. Скрипты не имеют прямого доступа к системным API и не могут нарушить работу системы.
Возможность добавления поддержки широкого спектра интерпретируемых ЯП.
Минусы:
Нет production-ready решения. Доработка может потребовать больших временных затрат, а использование сопряжено с рисками.
Нет официальной поддержки от разработчиков dart.
Потенциальное увеличение размера приложения.
Экзотические API.
Сложность отладки.
Мнение:
Необходимость масштабных доработок существующих библиотек (или разработка собственной) может убить весь предполагаемый профит от использования скриптов. Потенциально низкий перформанс и раздутие тушки приложения тоже не добавляет оптимизма.
Поддержка | Сложность использования | Сложность отладки | Идиоматичность |
1 | 5 | 9 | 2 |
Использование интерпретаторов разного уровня
Интерпретатор в широком смысле — это программа, которая выполняет код, написанный на языке программирования. Но в нашем случае, говоря об интерпретаторах, я подразумеваю процесс интерпретации мета-описания задачи (например, описание необходимых для выполнения функций с помощью json) или некой структуры в команды Dart.
В качестве примера простейшей «интерпретации» можно привести функцию, которая на основании значения переданной строки (названия метода) вызывает связанную функцию и передает в нее необходимые параметры. Сложно сказать, насколько этот пример валидный (скорее нет), но он нужен для наглядности.
{
"method1": {
"param1": 100
},
"method2": {
"param1": 200
}
}
final scriptCommands = {
"method1": (dynamic params) {
//...logic
},
"method2": (dynamic params) {
//...logic
},
};
void execute(Map json) {
for (final entry in json.entries) {
final keyName = entry.key;
final params = entry.value;
final fn = scriptCommands[keyName];
fn?.call(params);
}
}
Но такой подход совершенно не решает поставленную задачу. Для изменения функциональности нам так или иначе придется снова отправлять приложения для проверки в стор.
Как пример изящного (более-менее) и функционального инструмента можно выделить язык программирования Hetu Script, легковесный скриптовый язык, написанный на dart и предназначенный для работы в Flutter приложениях.
В рамках своего пет-проекта я реализовал небольшое расширение для добавления поддержки скриптов в приложении и остался приятно удивлен скоростью и простотой использования. Вот несколько качества проекта, которые мне импонируют:
Знаешь Dart — знаешь Hetu. Хету написан на Dart и их синтаксис практически идентичен, за исключением некоторых мелочей. Например, объявление функций с помощью ключевого слова fun (привет, Kotlin).
Кеширование байткода — +/- можно быть уверенным, что компиляция и бандлинг не будет выполнен повторно без дополнительных команд.
Возможность передачи в Hetu объектов из Dart AS-IS и обратно.
Возможность связывания функций из Dart и использовать их в скриптах (external fun).
Пример простейшей функции на языке Hetu:
fun sum(a, b) {
return a + b;
}
Плюсы (в контексте Хету):
Не требуется дополнительно изучать другие ЯП.
Простая интеграция.
Относительная безопасность. Скрипты не имеют прямого доступа к системным API и не могут нарушить работу системы.
Минусы:
Библиотека не production-ready.
Нет официальной поддержки от разработчиков dart. А китайский мейнтейнер не слишком активен. По его словам, это его «домашнее развлечение» :)
Потенциальные доработки потребуют глубокого погружения.
Сложность отладки.
Мнение:
Это решение выглядит немного привлекательнее, чем предыдущий вариант (вживление рантайма), но все еще не дотягивает до отметки «надежный, как швейцарские часы». Подкупает простота интеграции и использования, но надо здраво оценивать все риски при использовании подобных библиотек.
Поддержка | Сложность использования | Сложность отладки | Идиоматичность |
3 | 2 | 5 | 4 |
Резюме
Объясняю гениальную идею
Каждое направление по своему привлекательно для реализации и использования, но и зачастую тащит за собой массу сложностей, будь то необходимость хорошего уровня знания С/С++ или же других ЯП или «сырость» используемого решения. Несмотря на очевидные сложности, внедрение скриптов в ваши приложения может значительно расширить их возможности.
В случае, если у вас есть большой зоопарк устройств, которые требуют регулярно обновления специфичного ПО, то скрипты, с моей точки зрения, выглядят неплохим решением для доставки обновлений.
Как я писал выше, ни один из описанных выше способов реализации не был опробован в продакшене и внедрять скрипты в ваши приложения следует лишь тщательно взвесив все риски и преимущества.
Итоговые оценки:
Подход | Поддержка | Сложность использования | Сложность отладки | Идиоматичность |
dart: ffi и C/C++ | 10 | 8 | 9 | 7 |
Сторонний рантайм | 1 | 5 | 9 | 2 |
Интерптерация (Hetu Script) | 4 | 2 | 5 | 4 |
Оценки не претендуют на объективность и лишь отражают мнение автора*
Полезные ссылки:
И конечно же:
Вступай в сообщество мобильных разработчиков Mobile Broadcast!
Подписывайся на мой телеграм-канал Boltology Tech