JNI Часть 2: Примеры

Всем привет! Меня зовут Роман Аймалетдинов, я разрабатываю клиентское приложение Ситимобил. Продолжаю свою серию статей по 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 в нативном методе

  1. В файле AwesomeLib, который содержит наши нативные методы, создадим метод getRandom, который возвращает int.

public class AwesomeLib { static { System.loadLibrary("nativeLib"); } public native void helloHabr(); public native int getRandom(); // <- new method
}
  1. Затем генерируем заголовок командой 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
  1. Теперь напишем код на С++, который будет возвращать случайное число. Подключим необходимую для этого библиотеку #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>>?

  1. Создаем ещё один метод в нашей крутой библиотеке:

public class AwesomeLib { // code public native void printMatrix(float[][] matrix);
}
  1. Но в заголовке (nativelib_AwesomeLib.h) будет jobjectArray — совсем не то, что мы хотели бы увидеть. Но придётся с этим жить.

/* * Class: nativelib_AwesomeLib * Method: printMatrix * Signature: ([[F)V */
JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix( JNIEnv *, jobject, jobjectArray
);
  1. Пишем реализацию на С++:


JNIEXPORT void JNICALL Java_nativelib_AwesomeLib_printMatrix( JNIEnv * env, jobject obj, jobjectArray matrix
) { std::cout << "C++ code: print jobjectArray: " << matrix << std::endl;
}
  1. Передали, попробуем запустить программу (не забывайте, что после каждого изменения .cpp необходимо запустить в консоли команды для обновления .dll, это описано в первой статье):

Видим только адрес, а если будет matrix[0] или matrix[0][0] ? На самом деле, ни то, ни другое просто не скомпилируется, хотя будет нормально работать для jintArray. Всё дело в jobjectArray: сначала нужно получить из массива необходимые типы, скастить и только потом печатать. Так мы подошли к следующей теме.

Пройтись циклом по массиву в native

Чтобы пройтись по циклу, нужно:

  1. Узнать длину массива GetArrayLength.

  2. Получить элемент массива объектов GetObjectArrayElement.

  3. Скастить полученный jobjectArray в необходимый нам jfloatArray.

  4. Написать типичный код на С++.

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. Как провернуть такой трюк?

  1. В нашем классе 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 ); }
}
  1. Идем в 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-метод, а не возвращаемый.

  • Вызываем метод.

  1. Обновляем .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 есть смысл, а когда нет.

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