Найти в Дзене
Nuances of programming

Rust и разработка кроссплатформенных решений для мобильных устройств

Оглавление

Источник: Nuances of Programming

Недавно я начал изучать Android и iOS на предмет возможности обмена между ними бизнес-логикой. Этот поиск привёл меня к Rust — очень интересному и относительно новому языку программирования. Поэтому я решил попробовать его.

Что такое Rust?

Два самых важных момента, которые я нашёл в документации:

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

Это язык нативного уровня, как и C++.

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

Его компилятор убережёт вас от типичных ошибок при работе с памятью.

Он популярен?

Согласно опросу 2019 года, Rust — один из самых любимых и желанных языков среди инженеров-разработчиков:

Источник
Источник
Источник
Источник

Хотя общая динамика не так оптимистична:

Источник
Источник

RUST появился в 2010 году почти одновременно с Go (2009). Версия 1.0 была выпущена в 2015 году, но её создатели и не думают останавливаться и добавляют всё больше новых функциональных возможностей, откликаясь на пожелания пользователей.

К сожалению, пока что Rust используется лишь в нескольких крупных компаниях.

Насколько он хорош?

Первое, на что вам следует обратить внимание, — это производительность. Rust является, вероятно, одним из лучших в этом смысле. Вот несколько тестов производительности (слева направо):
— Rust против Go;
— Rust против Swift;
— Rust против C++.

Источник
Источник
Источник
Источник
Источник
Источник
Источник
Источник

В целом он сопоставим с C/C++ и, возможно, немного быстрее, чем Swift. Конечно, всё зависит от задачи и реализации.
Go или Java обычно на 10 позиций ниже, чем Rust.

Читаемость кода

Давайте проверим следующий фрагмент кода — реализацию сортировки пузырьком:

Слева направо: C++, Rust, Swift. Источник
Слева направо: C++, Rust, Swift. Источник
Слева направо: C++, Rust, Swift. Источник
Слева направо: C++, Rust, Swift. Источник
Слева направо: C++, Rust, Swift. Источник
Слева направо: C++, Rust, Swift. Источник
  • По синтаксису он близок к Swift.
  • Сделан скорее идиоматически: читаемо и понятно.

Безопасность

Ещё одна распространённая на C++ проблема, которая решается в Rust, — это обеспечение безопасной работы с памятью. Rust гарантирует безопасное использование памяти во время компиляции и затрудняет возникновение утечки памяти (хотя её возможность остаётся). В то же время он предоставляет широкий набор средств для самостоятельного управления памятью — оно может быть безопасным или небезопасным.

Применение в приложениях

Я просмотрел официальные примеры Rust и многие другие проекты на GitHub, но они определённо далеки от реального сценария применения мобильного приложения. Поэтому было очень непросто оценить сложность реальных проектов или объём усилий, связанных с переходом на Rust. Именно поэтому я решил создать пример, в котором будут освящены наиболее важные для меня аспекты, а именно:
— организация сетевого взаимодействия;
— многопоточность;
— сериализация данных.

Бэкенд

Ради упрощения работы для бэкенда я решил выбрать API StarWars
Вы можете создать простой сервер Rust на основе этого официального примера.

Среда

Настроить среду и создать приложение для IOS и Android можно, используя очень подробные и простые официальные примеры:

Пример для Android немного устарел. Если вы используете NDK 20+, вам не нужно создавать собственный набор инструментальных средств и можно пропустить этот этап:

Вместо этого добавьте в PATH свой комплект разработчика и предварительно скомпилированный пакет инструментальных средств:

export NDK_HOME=/Users/$USER/Library/Android/sdk/ndk-bundle
export PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
/bin:$PATH

И поместите все это в cargo-config.toml:

Многопоточность, HTTP-клиент и сериализация данных

Rust предоставляет довольно надёжный API для организации сетевого взаимодействия с использованием следующих библиотек:

Вот пример того, как всё это можно сочетать для создания клиента SWAPI (StarWars API) в нескольких строках кода:

lazy_statlic — макрос для объявления statics с использованием ленивых (отложенных) вычислений.

Взаимодействие

Мы подходим к самой сложной части: взаимодействию между IOS/Android и Rust. 
Здесь мы будем использовать механизм
FFI. Для осуществления взаимодействия он использует C-interop и поддерживает только совместимые с C типы. Взаимодействие с помощью C-interop может быть не таким простым. IOS и Android имеют собственные ограничения, справляются с которыми они тоже по-своему. Давайте посмотрим, как это происходит.

Для упрощения передачи данных также можно использовать протоколы побайтовой передачи: ProtoBuf, FlatBuffer. Оба протокола поддерживают Rust, но я исключил их из рассмотрения, потому что они имеют накладные расходы на производительность.

Android

Взаимодействие с Java-средой осуществляется через экземпляр JNIEnv. Вот простой пример, который возвращает строку в обратном вызове в том же потоке:

Выглядит просто, но у этого метода есть ограничение. JNIEnv не может быть просто разделён между потоками, потому что он не реализует типаж `Send` (типаж == протокол/интерфейс). Если вы обернёте call_method в отдельный поток, он завершится с соответствующей ошибкой. Вы, конечно, можете реализовать Send самостоятельно, так же как Copy и Clone, но во избежание шаблонного кода мы можем использовать rust_swig.
Rust swig основан на тех же принципах, что и SWIG: чтобы предоставить вам реализацию, он использует DSL и генерацию кода. Вот пример псевдокода для Rust SwapiClient, который мы определили ранее:

Кроме обёртки RUST, он также сгенерирует для вас Java-код. Вот пример автоматически сгенерированного класса SwapiClient:

Единственное ограничение здесь в том, что вам нужно будет объявить отдельный метод геттер для каждого поля DTO. Хорошо то, что его можно объявить внутри DSL. Библиотека имеет обширный список конфигураций, которые можно найти в документации.

Кроме того, в репозитории rust-swig в android-example можно найти интеграцию Gradle.

IOS

Поскольку в Swift для взаимодействия с Rust не требуется никаких прокси (типа JNIEnv), мы можем использовать непосредственно FFI. Тем не менее существует множество вариантов доступа к данным:

  • Предоставление DTO, совместимых с C. 
    Для каждого такого объекта DTO нужно создать совместимую с C копию и сопоставить её с ним перед отправкой в Swift.
  • Предоставление указателя на структуру без каких-либо полей.
    Для каждого поля в FFI создаётся геттер, который в качестве параметра принимает указатель на объект хоста. 
    Здесь есть ещё два возможных подварианта: 
    2.1. Метод может вернуть (return) результат от геттера.
    2.2. Или вы можете передать указатель и загрузить значение в качестве параметра
    (для строки C вам понадобится указатель на начало символьного массива и его длину).

Давайте проверим реализацию обоих подходов.

Подход 1

Swapi-клиент и загрузка обратного вызова:

На стороне Swift нам нужно будет использовать UnsafePointer и другие вариации обычного указателя для снятия обёртки с данных:

Здесь возникает резонный вопрос: зачем нам класс PeopleResponse в Swift и соответствующая структура PeopleCallback? Главным образом чтобы избежать вот этого:

Вам нужно отправить объект обратного вызова в машинный код и вернуть его обратно с результатом:

-12

Подход 2

В этом случае вместо `PeopleNative` мы будем использовать People (исходную структуру Rust), не предоставляя клиенту поле, а создавая методы, которые будут принимать указатель на DTO и возвращать требующийся элемент. Обратите внимание, что нам всё равно нужно будет обернуть массивы и обратные вызовы, как в предыдущем примере.

Это касается только геттеров, то есть методов получателя, всё остальное практически то же самое:

Создание заголовков

Завершив определение FFI, можно сгенерировать заголовок:

cargo install cbindgen //Устанавливаем cbindgen, если его ещё нет
//Создаём заголовок, который нужно включить в IOS проект cbindgen -l C -o src/swapi.h

Чтобы автоматизировать этот процесс, можно создать конфигурацию сборки в build.rs:

If Android {} else IOS {}

Чтобы разделить логику, присущую приложениям на IOS и Android, зависимости и прочее, можно использовать макросы (пример):

#[cfg(target_os=”android”)]

#[cfg(target_os=”ios”)]

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

Как можете сделать вы.
Как можете сделать вы.
Как сделал я.
Как сделал я.

Тестирование производительности

Размер

Оба проекта оценивались только с использованием кода и пользовательского интерфейса на Rust.

Отладчик API на Android и общие библиотеки:

Отладчик API на Android и общие библиотеки, Мб
Отладчик API на Android и общие библиотеки, Мб

Отладчик приложения на IOS и общая библиотека:

Размер отладчика приложения и общей библиотеки, Мб
Размер отладчика приложения и общей библиотеки, Мб

Скорость

Время загрузки автономного решения Rust и его мостов, вызываемых через Android и iOS, а также реализации нативных решений Swift и Kotlin одного и того же сетевого вызова:

Выполнение в миллисекундах, среднее по 10 запросам/замер времени на стороне клиента после получения обратного вызова с сериализованными данными
Выполнение в миллисекундах, среднее по 10 запросам/замер времени на стороне клиента после получения обратного вызова с сериализованными данными

Как видите, почти никакой разницы нет между вызовом автономного решения Rust и вызовом его через Android и Swift. А значит, FFI не создаёт никаких накладных расходов на производительность.

Примечание: скорость запроса сильно зависит от временной задержки сервера (то есть от количества времени, уходящего на обработку запроса).
Обе реализации можно найти в проекте на GitHub.

Проект

Полный пример проекта доступен на GitHub.

Пользовательский интерфейс IOS и Android

-18
-19

Заключение

Rust — это очень перспективный язык, который даёт чрезвычайно высокую скорость при решении типичных для C++ проблем, связанных с использованием памяти. Надёжный и простой API облегчает его освоение и использование. Выбирая между ним и C++, я отдал бы предпочтение Rust, хотя он остаётся для меня более сложным, чем Swift или Kotlin.
А самое сложное — создать правильный мост между Rust и фреймворками для разработки клиентской части проекта или приложения. Если вы сможете его сделать, у вас будет отличное решение для мобильных устройств.

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

Вот что мне удалось раскопать: Go + Gomobile для Android и IOS.
Реализация и тестирование производительности.

Читайте также:

Читайте нас в телеграмме и vk

Перевод статьи Igor Steblii: Rust & cross-platform mobile development