Pull to refresh

Comments 16

Не очень понятно. Чем такой подход лучше общепринятого.
На стороне java:


static HashMap<Long, Listener> listeners = new HashMap<>();

static void nativeEvent(long handle, String msg) {
Listener listener = listeners.get(handle);
if (listener == null){
return;
}
listener.print(msg);
}

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

У меня в нэйтиве много классов с различными листенерами, с разным набором параметров и возвращаемых значений. В вашем примере придется делать отдельный NativeEvent для каждой функции (которых даже в одном отдельном листенере может быть больше одной). Да и самих листенеров может быть много - какие-то из них необязательные и тд. В общем, если листенер один с одной единственной функцией - ваше решение, пожалуй, и правда будет рациональнее. Если нативных классов много и у каждого свой листенер со своим набором функций - моя реализация кажется мне удобнее.

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

Спасибо за вопрос, возможно позже дополню статью пояснениями.

Так как в статье упоминается android то "общепринятым" подразумевается подход google.
Если посмотреть исходники фреймворка то там так и есть.
static методы в которых первым идет int с типом коллбека и обычно куча еще параметров, который из натива чаще всего приходят как null.
Ну и потом портянки switch-case :)

Не хочу оспаривать методы Google, но для собственных проектов мой вариант кажется мне красивее и удобнее, чем "портянки switch-case" и функция с кучей параметров. С точки зрения масштабирования - тоже. В проект могут добавляться новые классы с новыми листенерами, всякий раз менять кол-во параметров static функции и переписывать соответствующие участки кода? Не, я слишком ленив, да простит меня Гугл

Опять же, возвращаемое значение... Возвращать как Object, а потом плясать с бубном ради получения простого типа? Зачем, если большинство функций возвращают int, а одна-две - скажем, коллекцию или строку... в них как раз можно и заморочиться, а большую часть кода оставить простой и чистой

Запоздало вспомнил еще одно преимущество моего метода: легкость переноса на другие системы. К примеру, С++ связывается со Swift без плясок с JNI, но общая структура кода остается похожей. Я просто беру, немного правлю нативный код, немного - код оболочки (да, перевод на другой язык, но логика сохраняется полностью), и вуаля - у меня уже аналогичные обертки под IOS. С протестированной логикой, что тоже весомый фактор по сравнению с тем, чтобы на каждой системе городить собственный огород и затем заново тестировать.

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

Не нужно менять существующие функции.
Создаете новый класс и нативную обработку прямо в нем делаете.
Static поля и методы инициализируются в момент загрузки класса.
Т.е. даже если не создавали ни одного листенера и класс еще не загружался при поиске метода из натива все равно будет загружен класс и инициалированы static поле и метод.
Единственный минус что GC не может их удалить, пока ссылка лежит в HashMap.
Так что этот момент нужно обработаывать самому.
Все в одном месте и натив ничего не знает про листенеры кроме того что их класс может получать nativeEvent:

сlass Listener() {
private static HashMap<Long, Listener> listeners = new HashMap<>();

private static void nativeEvent(long handle, String msg){...}

private final long handle;

public Lisener(long handle) {
this.handle = handle;
listeners.put(handle, this);
}

public void cancel(){
listeners.remove(handle);
}
}

Насчет переписывания: в ios у меня схожая реализация оберток, только проще, без игры с jni. В итоге при переносе достаточно почистить готовый код, логика (набор классов и методов для оберток, как в нативной части, так и в оболочке) остается прежний. Лично я считаю это преимуществом.

И если кол-во параметров можно сделать с запасом чтобы не переписывать при расширении, то вопрос с возвращаемыми значениями остается открытым. Скажем, у меня большинство каллбэков либо void, либо простые типы. Но есть и коллекции, и String. Возвращать Object для всех и получать из него простой тип в вызывающем коде каждого каллбэка - усложнение кода, как по мне

Подытоживая, о вашем методе: общий вызывающий код, с единым jmethodid, НО прослойка с необходимостью поиска, switch case, наследования всех листенеров от единого и приведение к наследникам в коде вызова. Плюсом возвращаемое значение Object, необходимость доп манипуляций для получения простого типа. Параметры, кстати, тоже Object, что требует конвертации всякий раз даже если передаются простые типы.

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

Я не очень понимаю что за проблема с "преобразованием и возвращаемым значением". Для каждого вызова можно сделать свои static функции которые будет получать или возвращать то что нужно.
Но главное в этом всем одно - ваш подход это только если вы сами автор и натива и java кода. И можете сразу все править для точного соответствия классов и натива.
Если ваш натив предполагает быть библиотекой для широкого использования или даже просто использоваться в нескольких собственных проктах то это породит кучу прослоек под каждый чих.

Мой натив используется в нескольких собственных проектах, общая библиотека, чтобы не переписывать имена функций, просто создаю пакет и класс с импортами вида com.myprog.native, при пакете приложения, скажем, com.myprog.myappname, никаких проблем нет.

Но даже если имена бы менялись - при использовании обертки Java для cpp класса (начало статьи, без всчких каллбэков) все имена функций итак зависят от пакета, поэтому зависимость некоторая будет иметься всегда. Да и при несовпадении имен, разве трудно воспользоваться поиском с заменой, если это собственный проект…

Если же это является проблемой, то подход с оберткой едва ли применим в принципе, тут уже нужно пилить какое-то апи с номерами функций и общую функцию-обработчик, а не работать полноценно с cpp ООП из java. Тут уже зависит от целей, я лишь показал свой подход, который пользую в своих проектах и который удобен для меня.

Про кучу прослоек на каждый чих не понял, при доступе и к нативу и к Java коду.

Еще одно, пожалуй, самое важное преимущество моего подхода. В моих приложениях отдельные листенеры не только выводят результат, но и возвращают значения, которые получают от пользователя. То есть, вызывающий поток подвешивается пока пользователь не жмакнет по кнопочке. В моем случае сколько листенеров, столько и очередей выполнения с вызовом AttachCurrentThread. В вашем - очередь одна (чтобы сохранить преимущество общего вызывающего кода, можно конечно принимать в функции вызова и ThreadPool, но тогда пропадает главное преимущество вашего подхода - единоразовый инит и последующее обращение к общей функции)

Ps: преобразования типов - ну как минимум, лишнее действие на каждый чих, необходимость очистки, лишний код (а ведь главное, на мой взгляд, преимущество вашего подхода - вроде как меньший обьем). Не назову чем-то критичным, но все же, чем компактнее вызывающий код и меньше прослоек, тем лучше, имхо

Подход интересный, но на десктопе, наверное, уже проще Panama FFM использовать (Java 17+). Там можно передавать java-функции, как ссылки для upcall из нативного кода через MethodHandle. Классический пример с qsort с компаратором реализованный на java.

За десктоп не скажу, этот подход для Android использую, где UI на java - не выбор, а данность. Для десктопных версий своих приложений пишу UI на C++, под gtk4.

Может имеет смысл рассмотреть переход на flutter.
Там и биндинги к нативу присать проще и GUI для всех систем на одном языке получится.

Я противник всяких фреймворков и тяжеловесных библиотек. Во-первых, все мои приложения весят считанные мегабайты, и я пребываю в откровенном недоумении когда вижу условный блокнот весом в 100мб. Во-вторых, я крайне не люблю брать сторонние библиотеки: это зависимость от чужого проекта, как от его дальнейшей поддержки, так и от лицензии и планов разработчика, которые могут измениться (допустим, решат брать плату за использование или кто-то их выкупит), исключением я считаю библиотеки криптопротоколов и тому прочее, что прямым образом связано с безопасностью. В-третьих (дополняя первый пункт), я сторонник минимализма: эффективно использовать память и дисковое пространство. В-четвертых, я уже создал собственные фреймворки для быстрого построения интерфейса под Java, Swift, gtk с одинаковым насколько это возможно API. Они хорошо протестированы и прекрасно работают, как и решение из статьи. Да, на это ушло время когда-то, зато теперь работать легко.

Опять же, я понимаю, что описанные выше пункты довольно дискуссионны, и многие компании прикручивают к своим проектам сотни библиотек ради одного единственного вызова и используют всевозможные фреймворки. Но, учитывая, что я занимаюсь собственными проектами, могу позволить себе путь, соответствующий моей идеологии :) Тем более, что именно за этим я и занялся программированием: чтобы решать интересные задачи, используя минимум.

Насчет эффективного использования дискового пространства и сторонних либ наброшу еще. Когда-то в своем сканере сети я реализовал функцию получения Netbios имен буквально в 20 строчек. На связь со мной вышел человек, который писал похожее приложение, чтобы узнать, как я это сделал, и по итогу... прикрутил библиотеку с полной поддержкой Samba протокола лишь ради этой функции. В дальнейшем, с доливанием собственной DNS либы для моих сетевых утилит, я усовершенствовал и эту функцию, тк там используется как раз DNS протокол, но вся моя собственная библиотека - это ~700 строчек.

Беда в том что FFM пока нет в Andorid. В бета android 17 подтянули до java25, но на приемлемый процент пользователей это раскатится лет через 5 минимум.
А делать две обертки на одном и том же языке совсем не разумно.

Sign up to leave a comment.

Articles