Бруклинский мост. Зачем генерируем C++ на Kotlin

от автора

Нелегко на рынке найти senior разработчика для конкретной сферы. Каждый из них имеет уникальные знания в своем языке и фреймворке, будь это Java, Kotlin, С++, JS или Swift. Каждый накапливал свои знания годами. Но найти senior разработчика, который одинаково хорош и C++ и в Kotlin, утром переписывает openssl, а по вечерам пинает мидлов за неправильное использование корутин, почти нереально.

Для каждого проекта нужны высококлассные специалисты, поддерживающие проект на плаву, развивающие его, предоставляя клиенту новые фичи, а заказчику большие деньги. Уход каждого специалиста для компании приносит новые убытки, но вот уход ndk специалиста может привести проект к краху. Ладно, не все так драматично. Если у вас все под контролем.

Почему вообще мы говорим о ndk, и для чего нужен мост C++ в Android. Мир нативных разработок и библиотек огромен, а некоторые вещи порой просто так не сделаешь полностью в Protobuf vs Jni. И раз тут интриги нет, то Схема работы JNI

Схема работы JNI

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

jclass jCounterClass = env->FindClass("com/github/klee0kai/kotlinndktest/JniCounter"); jmethodID initMethod = env->GetMethodID(jClassIndex->jCounterClass, "<init>", "()V");

Для kotlin разработчиков рефлексия может вызвать еще большую боль. При использовании вы можете теперь встретить различные преобразования типов в коде и в реалтайме. Тип kotlinx.Int может быть представлен как примитивный тип int, а вот kotlinx.Int? будет представлен как java.lang.Integer. Различные методы, поля, в особенности те, которые используют уникальные для котлина типы: UByte, UInt, могут в реалтайме приобрести уже уникальные имена.

Кроме рефлексии, вы можете столкнуться с проблемами контроля памяти, поиском утечек памяти. К примеру, для jvm можно выполнять слепки памяти, просматривать графы использования экземпляра класса до GC Root, в Android можно даже реализовать утилиту leakcanary.

 Удержание объекта в MainActivity. Инструмент Memory Profiler.

Удержание объекта в MainActivity. Инструмент Memory Profiler.

В C++ мире отработаны механизмы подобно ASan. Но если у вас в одном процессе и C++ и jvm, то отследить утечки памяти сложнее. По jvm у вас объект удерживается сразу GC Root, а как это соотнести с кодом. ASan — тоже особо информации не предоставляет.

Объект, созданный в C++, не отслеживается в Memory Profiler

Объект, созданный в C++, не отслеживается в Memory Profiler

Подобные утечки ресурсов могут появится на ровном месте. Для любого маппинга модели из jvm в C++ и обратно может потребоваться копирование текстовых переменных, временных массивов и много другого. Все это проскочит ревью, не проявится на тестах, и только у пользователя с особым желанием потыкать приложение может выскочить ошибка памяти. Особым видам к таким ошибкам может быть к примеру использование указателей на объекты jobject, jclass не по ЖЦ. Это приводит не к привычной ошибке @JniMirror object NdkEngine { init { Brooklyn.load("kotlinndktest") } fun helloFromJvm(): String = "hello from jvm" external fun helloFromCppPro(): String }

был представлен его отображением в нативной библиотеке.

class ComGithubKlee0kaiKotlinndktestNdkEngine { public:        static std::string helloFromJvm();     static std::string helloFromCppPro();    private:     static jobject jvmSelf; };

Вообще «Бруклин» возник не на пустом месте. Автору довелось неделями заниматься маппингом моделей из Kotlin в C++, проверяя все вызовы, все преобразования строк и массивов. В конченом итоге, написывая одно и тоже по несколько раз, формируется уже четкий шаблон однообразных действий. Но так как такого кода в любом случае получается много, и даже набитая рука сможет наделать ошибок. Лучше, если за нас будет писать плагин.

Каждая итерация компиляции проекта теперь позволяет отслеживать все изменения для дата классов и зеркалированных классов. Kotlin compiler plugin, подключаемый напрямую в gradle сборку, теперь самостоятельно генерирует файлы для индексирования jvm. Конечно мы не можем избавиться от падений в реалтайме при использовании рефлексии, но свести к минимуму, перенеся всю рефлексию только на этап инициализации библиотеки — можно.

int init(JNIEnv *env) {     if (comGithubKlee0kaiKotlinndktestNdkEngineIndex) return 0;     comGithubKlee0kaiKotlinndktestNdkEngineIndex = std::make_shared<ComGithubKlee0kaiKotlinndktestNdkEngineIndexStructure>();     comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls = (jclass) env->NewGlobalRef(             env->FindClass("com/github/klee0kai/kotlinndktest/NdkEngine"));     comGithubKlee0kaiKotlinndktestNdkEngineIndex->init1 = env->GetMethodID(             comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls, "<init>", "()V");     if (!comGithubKlee0kaiKotlinndktestNdkEngineIndex->init1) return -1;     comGithubKlee0kaiKotlinndktestNdkEngineIndex->helloFromJvm1 = env->GetMethodID(             comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls, "helloFromJvm",             "()Ljava/lang/String;");     if (!comGithubKlee0kaiKotlinndktestNdkEngineIndex->helloFromJvm1) return -1;     comGithubKlee0kaiKotlinndktestNdkEngineIndex->helloFromCppPro1 = env->GetMethodID(             comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls, "helloFromCppPro",             "()Ljava/lang/String;");     if (!comGithubKlee0kaiKotlinndktestNdkEngineIndex->helloFromCppPro1) return -1;     comGithubKlee0kaiKotlinndktestNdkEngineIndex->hashCode1 = env->GetMethodID(             comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls, "hashCode", "()I");     if (!comGithubKlee0kaiKotlinndktestNdkEngineIndex->hashCode1) return -1;     comGithubKlee0kaiKotlinndktestNdkEngineIndex->toString1 = env->GetMethodID(             comGithubKlee0kaiKotlinndktestNdkEngineIndex->cls, "toString",             "()Ljava/lang/String;");     if (!comGithubKlee0kaiKotlinndktestNdkEngineIndex->toString1) return -1;     return 0; }

А вот реалтайм ошибки типа No implementation found for можно вообще не боятся. При любом переносе exported метода генерируется новый интерфейсный метод для нативной библиотеки, и дальнейший проброс вызова в stub класс в C++.

extern "C" JNIEXPORT jstring JNICALL Java_com_github_klee0kai_kotlinndktest_NdkEngine_helloFromCppPro(         JNIEnv *env,         jobject jObject ) {     brooklyn::bindEnv(env);     auto mirror = brooklyn::ComGithubKlee0kaiKotlinndktestNdkEngine(jObject);     auto jvmResultObj = mirror.helloFromCppPro();     return brooklyn::mapper::mapToJString(env, std::make_shared<std::string>(jvmResultObj)); }

Остается последнее действие. Скорректировать последовательность сборки проекта, к примеру для Android проектов.

afterEvaluate {     val kotlinCompileTasks = tasks.filterIsInstance<JavaCompile>()     val cmakeTasks =         (tasks.filterIsInstance<com.android.build.gradle.tasks.ExternalNativeBuildTask>() +                 tasks.filterIsInstance<com.android.build.gradle.tasks.ExternalNativeBuildJsonTask>()                 )      cmakeTasks.forEach { cmakeTask ->         kotlinCompileTasks.forEach { kotlinTask ->             cmakeTask.mustRunAfter(kotlinTask)         }     } }

Сухой остаток

Надеюсь бруклинский мост кому-нибудь спасет горящие релизы. Главное позволит полноценно разделить подпроекты на самостоятельные тестируемые модули. Так kotlin код можно тестировать в kotlin, C++ код можно тестировать посредством GTest или других инструментов плюсов. Мост же уже протестирован в библиотеке, а значит ваше приложение будет ближе к стабильности.

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


ссылка на оригинал статьи https://habr.com/ru/articles/773434/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *