Всем привет! Меня зовут Роман Аймалетдинов, я разрабатываю клиентское приложение Ситимобил. Продолжаю свою серию статей по JNI, так как технология используется редко, но иногда она бывает очень полезной (или просто интересной). В этот раз я покажу примеры решений на JNI, которые совсем немного сложнее, чем hello world. И если вы не знакомы с JNI, то советую начать с первой части.
Содержание
JNI-типы.
Return
в нативном методе.Как передать
List<List>
.Как пройтись по циклу в нативе.
Вызов Java-метода из С++.
JNI-типы
В JNI мы вынуждены использовать специальные типы для native-окружения. Мы не можем просто так передать int
и работать с ним в С++, хотя это и кажется логичным. Таким образом, типы существующие в Java для JNI, дублируются с префиксом j
. Например:
boolean → jboolean
byte → jbyte
char → jchar
И дело не ограничивается одними лишь примитивными типами, как описано в документации Oracle. Вам также придётся иметь дело с некоторыми неудобствами, то есть трансформациями.
Как с этим работать — рассмотрим в следующей главе.
Return в нативном методе
В файле AwesomeLib, который содержит наши нативные методы, создадим метод
getRandom
, который возвращаетint
.
public class AwesomeLib {
static {
System.loadLibrary("nativeLib");
}
public native void helloHabr();
public native int getRandom(); // <- new method
}
Затем генерируем заголовок командой
javac -h . AwesomeLib.java
, наш файл.h
обновится и появится новый метод:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class nativelib_AwesomeLib */
#ifndef _Included_nativelib_AwesomeLib
#define _Included_nativelib_AwesomeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
Class: nativelib_AwesomeLib
Method: helloHabr
Signature: ()V
*/
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
JNIEnv *,
jobject
);
/*
(Наш новый метод!)
Class: nativelib_AwesomeLib
Method: getRandom
Signature: ()I
*/
JNIEXPORT jint JNICALL Java_nativelib_AwesomeLib_getRandom(
JNIEnv *,
jobject
);
#ifdef __cplusplus
}
#endif
#endif
Теперь напишем код на С++, который будет возвращать случайное число. Подключим необходимую для этого библиотеку
#include <ctime>
, затем реализуем простейшее решение получения числа в С++:
#include "nativelib_AwesomeLib.h"
#include <iostream>
#include <ctime> // new lib
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
JNIEnv* env,
jobject thisObject
) {
std::cout << "Hello Habr! This is C++ code!!" << std::endl;
}
// Новый метод
JNIEXPORT jint JNICALL Java_nativelib_AwesomeLib_getRandom(
JNIEnv* env,
jobject obj
) {
std::srand(std::time(nullptr));
int randomValue = std::rand();
return randomValue;
}
Как видно из примера (JNIEXPORT jint
), возвращаем не int
, а jint
, хотя и в этом примере всё будет работать отлично, но в некоторых случаях придется делать каст к JNI-формату. Например, так: return (jint) variable;
.
Как передать List
Как передать в конструктор простой тип? Проблем нет. А что делать, если нужно передать что-то посложнее? Например, List<List<Float>>
?
Создаем ещё один метод в нашей крутой библиотеке:
public class AwesomeLib {
// code
public native void printMatrix(float[][] matrix);
}
Но в заголовке (
nativelib_AwesomeLib.h
) будетjobjectArray
— совсем не то, что мы хотели бы увидеть. Но придётся с этим жить.
/*
* Class: nativelib_AwesomeLib
* Method: printMatrix
* Signature: ([[F)V
*/
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
JNIEnv *,
jobject,
jobjectArray
);
Пишем реализацию на С++:
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
JNIEnv * env,
jobject obj,
jobjectArray matrix
) {
std::cout << "C++ code: print jobjectArray: " << matrix << std::endl;
}
Передали, попробуем запустить программу (не забывайте, что после каждого изменения
.cpp
необходимо запустить в консоли команды для обновления.dll
, это описано в первой статье):
Видим только адрес, а если будет matrix[0]
или matrix[0][0]
? На самом деле, ни то, ни другое просто не скомпилируется, хотя будет нормально работать для jintArray
. Всё дело в jobjectArray
: сначала нужно получить из массива необходимые типы, скастить и только потом печатать. Так мы подошли к следующей теме.
Пройтись циклом по массиву в native
Чтобы пройтись по циклу, нужно:
Узнать длину массива
GetArrayLength
.Получить элемент массива объектов
GetObjectArrayElement
.Скастить полученный
jobjectArray
в необходимый намjfloatArray
.Написать типичный код на С++.
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix(
JNIEnv * env,
jobject obj,
jobjectArray matrix
) {
std::cout << "C++ code: print jobjectArray:" << std::endl;
int sizeFirstArr = env->GetArrayLength(matrix);
for (int i = 0; i < sizeFirstArr; i++) {
jfloatArray secondArr = (jfloatArray) env->GetObjectArrayElement(matrix, i);
jfloat *elements = env->GetFloatArrayElements(secondArr, 0);
int sizeSecondArr = env->GetArrayLength(secondArr);
for (int k = 0; k < sizeSecondArr; k++) {
float value = elements[k];
std::cout << value << ", ";
}
std::cout << std::endl;
}
}
Запустим наш код и посмотрим, что получилось:
Классно, напечаталось. Но знаете, что ещё? Это же C++, никаких тебе тут gc и магии, будь добр, подчищай за собой.
env->ReleaseFloatArrayElements(secondArr, elements, 0);
env->DeleteLocalRef(secondArr);
Необходимо высвобождать память самостоятельно. Пусть этот код вызывается и из Java, но тут нет сборщика мусора, и утечки — ваша головная боль. Справедливости ради, считаю, что сложным кодом на C++ должна заниматься другая команда, которая его хорошо знает, но если это ваш pet-project и вы один, то не забывайте и об особенностях.
Как вызвать Java-метод из C++?
Мы научились простым, но основным операциям. Как вызвать метод, как вернуть значение и передать в конструктор. Но иногда хочется из С++ дёрнуть нечто полезное из Java. Как провернуть такой трюк?
В нашем классе
AwesomeLib.java
создадим обычный метод, который вызовем из C++. Чтобы было интереснее, передадим в него float и int. Вызовем изMain
наш метод из первой статьи —helloHabr
. Немного его модифицируем и вызовем из него Java-метод. Таким образом у нас получится последовательность вызовов:Java → C++ → Java
.
public class AwesomeLib {
// from article: JNI Part 1
public native void helloHabr();
// new code
public void printNativeResult(float value1, int value2) {
System.out.println(
"Java code: value1: " + value1 + " value2: " + value2
);
}
}
Идем в
AwesomeLib.cpp
и изменяем метод из первой статьи:
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_helloHabr(
JNIEnv* env,
jobject thisObject
) {
std::cout << "Hello Habr! This is C++ code!!" << std::endl;
jclass cls_awesome_lib = env -> GetObjectClass(thisObject);
jmethodID mid_compare = env->GetMethodID(
cls_awesome_lib,
"printNativeResult",
"(FI)V"
);
// call method
env->CallVoidMethod(
thisObject,
mid_compare,
2.0,
3
);
}
Что тут происходит?
Сначала находим и получаем Java-объект, чтобы потом вызывать его методы.
Получаем id метода, чтобы обратиться к нему. Мы указываем название метода и то, что в нашем случае он содержит в конструкторе
“(FI)”
, поскольку у нас в нём float и int.V
говорит о том, что это void-метод, а не возвращаемый.Вызываем метод.
Обновляем .dll и запускаем наш код:
Ура, мы умеем запускать код Java из native-кода!
Чуть подробнее про GetMethodID
:
“**(FI)V**”
→ void someFunc(float, int)“**(FF)V**”
→ void someFunc(float, float)“**(FI)Z**”
→ boolean someFunc(float, int)“**()Z**”
→ boolean someFunc()
Обращаемся к информации с сайта Oracle:
Заключение
Напоследок скажу, что JNI — это не совсем обычный C/C++. Но не стоит бояться, нужно исследовать! В моей небольшой серии про JNI осталась последняя статья, в ней я расскажу про самое важное и интересное: производительность! Напишу простые синтетические тесты и покажу, когда в JNI есть смысл, а когда нет.
Комментарии (5)
RyAtex
11.02.2022 18:18Спасибо, очень интересно. А вопрос, в Qt (С++) есть сигналы и слоты. Их можно как-то связять с JNI?
igormich88
11.02.2022 18:22+1Возник такой вопрос можно ли передать лямбду параметром native метода и есть ли в этом смысл (в первую очередь в плане производительности)?
kamasit
спасибо за статью. а схему String -> wstring -> String можно реализовать также как в первой части статьи?
Evleaps Автор
Здравствуйте, простите меня за мой непрофессионализм в c/c++, но вопрос я не понял.
Насколько я знаю, wstring в плюсах используется для unicode строк, а string для ASCII строк. Но какое это отношение имеет к моей серии статей? :)
Попробуйте переформулировать вопрос, больше деталей. Возможно я, или кто-то из сообщества даст ответ.
equeim
Не совсем верно (а точнее, совсем не верно). string и wstring никакого отношение к кодировкам и символам не имеют. Это просто массив байтов (char) либо значений wchar_t. То, в какой кодировке закодированы (простите) эти байты (или значения wchar_t если кодировка это позволяет) зависит только от того как вы их используете (их содержимое также может в принципе не быть текстом).
Например, если вы получаете пользовательский ввод от системы в виде байтов и сохраняете его в string, то ее кодировка будет зависит от системы и ее конфигурации. На юниксах обычно будет UTF-8, а на windows локальная 8-битная кодировка (например CP-1251). Но в обоих случаях возможны любые варианты.
Более того wchar_t на разных платформах имеет разный размер так что wstring в принципе имеет смысл использовать только коде завязанном на windows.
Также по этой причине wstring нельзя использовать для java String в кросс-платформенном JNI коде потому что размер jchar фиксирован (16 бит) а wchar_t - нет.