В прошлой статье посвященной JNI на Android я описывал как начать работу с Android NDK, в том числе, как вызывать нативные методы из Java, на этот раз я опишу как вызывать Java-методы из нативного кода.

Для лучшего понимания этой статьи, рекомендую предварительно ознакомиться с основами JNI в Android. Сам процесс вызова Java-метода из нативного кода далее мы будем называть обратным вызовом, так как такие вызовы чаще всего используются для оповещения Java-стороны приложения о некоторых событиях, просходящих «в нативе». Код доступен на нашем репозитории на GitHub. Для нетерпеливых есть быстрое решение :)

Общее описание механизма

И так, перед нами стала задача «дернуть» Java-метод из нативного кода. Каков будет порядок действий?

  1. Опеределить какой метод и у какого класса вы хотите вызвать. Банально, да.
  2. Получить дескриптор нужного класса.
  3. Описать сигнатуру метода на языке JNI-примитивов (не так страшно, как звучит).
  4. Получить идентификатор метода (что-то типа ссылки на метод).
  5. Вызвать метод у нужного объекта (если метод экземплярный) или класса (если метод статический).

Определяемя с методами

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

Пусть у нас есть следующий интерфейс NativeCallListener:


public interface NativeCallListener {
public void onNativeVoidCall();
public void onNativeStringCall(String arg);
}

Данному интерфейсу в нативном коде на C++ будет соответстововать следующий класс JniNativeCallListener:


class JniNativeCallListener {
public:
JniNativeCallListener(JNIEnv* pJniEnv, jobject pWrapperInstance);
/* for onNativeVoidCall()*/
void callJavaVoidMethod();
/* for onNativeStringCall(String) */
void callJavaStringMethod(jobject pString);
~JniNativeCallListener();
private:
JNIEnv* getJniEnv();
/* Method descriptors*/
jmethodID mOnNativeVoidCallmID;
jmethodID mOnNativeStringCallmID;
/* Reference to Java-object*/
jobject mObjectRef;
JavaVM* mJVM;
};

Заранее хочу обратить внимание, что действия по инциализации объекта будут местами избыточны для случая однопоточного приложения, но обязательны при использовании многопоточности и pthread.

Теперь перейдем к непосредственной инициализации и получению дескрипторов.

Получаем дескриптор класса и методов

К получению дескриптора класса есть два подхода. Первый мне нравится:

jclass clazz = pJniEnv->GetObjectClass(pWrappedInstance);

Второй не очень:

jclass clazz = pEnv->FindClass("by/idev/jni/javacall/MainActivity");

Преимущества первого подхода в отсутсвии хард-кода, второй подход позволяет получить дескриптор (jclass) не имея ссылки на объект. Так что решение о том, какой из них использоват зависит от ситуации.

Полностью инициализация будет выглядеть так:


JniNativeCallListener::JniNativeCallListener(JNIEnv* pJniEnv, jobject pWrappedInstance) {
pJniEnv->GetJavaVM(&mJVM);
mObjectRef = pJniEnv->NewGlobalRef(pWrappedInstance);
jclass clazz = pJniEnv->GetObjectClass(pWrappedInstance);
mOnNativeVoidCallmID = pJniEnv->GetMethodID(clazz, "onNativeVoidCall", "()V");
mOnNativeStringCallmID = pJniEnv->GetMethodID(clazz, "onNativeStringCall", "(Ljava/lang/String;)V");
log_debug("JniNativeCallListener created.");
}

В данном случая, лаконичности ради, я провожу инициализацию в конструкторе, что является плохим тоном. Гораздо лучшим вариантом был бы отдельный метод инициализации, потому что мы помним, что логика и понетциальные ошибки в конструкторе — это плохо, а при вызовах GetMethodID и GetObjectClass и им подобных могут возникать ошибки (они могут вернуть NULL, хотя со мной такого не случалось).

Логирование определено среди утилит в файле Util.h


#include <android/log.h>
#include <jni.h>
#include <stddef.h>
#define LOG_TAG "NativeLog"
#ifndef UTIL_H_
#define UTIL_H_

void log_debug(const char* pMessage) {
__android_log_write(ANDROID_LOG_DEBUG, LOG_TAG, pMessage);
}
void log_error(const char* pMessage) {
__android_log_write(ANDROID_LOG_ERROR, LOG_TAG, pMessage);
}
/*
* Some utility methods
*/
void makeGlobalRef(JNIEnv* pEnv, jobject* pRef) {
if (*pRef) {
jobject globalRef = pEnv->NewGlobalRef(*pRef);
pEnv->DeleteLocalRef(*pRef);
//TODO NULL-check for globalRef
*pRef = globalRef;
}
}
void deleteGlobalRef(JNIEnv* pEnv, jobject* pRef) {
if (*pRef != NULL) {
pEnv->DeleteGlobalRef(*pRef);
*pRef = NULL;
}
}

#endif /* UTIL_H_ */

Вы можете спросить, а зачем нужны эти строки:


pJniEnv->GetJavaVM(&mJVM);
mObjectRef = pJniEnv->NewGlobalRef(pWrappedInstance);

А я напомню, что ссылки на объекты, переданные в JNI-метод, действительны только в пределах времени выполнения этого метода. То есть при попытки обратится к mObjectRef или pJniEnv после выполнения метода мы потерпим фиаско. Поэтому мы создаем глобальную ссылку на mObjectRef, чтобы потому вызвать у него нужный метод, и получаем ссылку на Java-машину mJVM, что при помощи нее в будещем получать JNIEnv. Вот так все запутанно, но походу дела все станет понятнее :) Чтобы получать JNIEnv используется метод getJniEnv():


JNIEnv* JniNativeCallListener::getJniEnv() {
JavaVMAttachArgs attachArgs;
attachArgs.version = JNI_VERSION_1_6;
attachArgs.name = ">>>NativeThread__Any";
attachArgs.group = NULL;
JNIEnv* env;
if (mJVM->AttachCurrentThread(&env, &attachArgs) != JNI_OK) {
env = NULL;
}
return env;
}

Определение сигнатуры и получение идентификатора метода

Этим целом служат строки которые вы уже видели:


mOnNativeVoidCallmID = pJniEnv->GetMethodID(clazz, "onNativeVoidCall", "()V");
mOnNativeStringCallmID = pJniEnv->GetMethodID(clazz, "onNativeStringCall", "(Ljava/lang/String;)V");

В качестве параметров GetMethodID служат дескриптор класса, имя метода и дикое, на первый взгляд, описание списка параметров и возвращаемого типа «()V» и «(Ljava/lang/String;)V». В скобках указаны входные параметры, после них — возвращаемый тип. Таким образом первый метод не имеет входных параметров и ничего не возвращает, а второй принимает строку и тоже ничего не возвращает. Ниже приведена таблица типов параметров и пару примеров описания методов.

Поддерживаемые JNI типы данных и их коды
Java JNI JNI array Код Код массива
boolean jboolean jbooleanArray Z [Z
byte jbyte jbyteArray B [B
char jchar jcharArray C [C
double jdouble jdoubleArray D [D
float jfloat jfloatArray F [F
int jint jintArray I [I
long jlong jlongArray J [J
short jshort jshortArray S [S
Object jobject jobjectArray L [L
Class jclass нет L [L
String jstring нет L [L
void void нет V нет

//Получение конструтора. Да, конструктор - тоже метод.
jmethod constructor = pEnv->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");
//Аргемент метода - произвольный класс SomeClass
jmethodID customObjectMethod= pEnv->GetMethodID(clazz,"doWork", "(Lby/idev/native/SomeClass;)V");
//Получение знаменитого метода equals(object)
jmethodID equals = pEnv->GetMethodID(clazz,"equals", "(Ljava/lang/Object;)Z");

Вызов метода обратного вызова

Дескрипторы получены, теперь можно написать код который используя их и JNIEnv произведет нужные действия:


void JniNativeCallListener::callJavaVoidMethod() {
JNIEnv* jniEnv = getJniEnv();
jniEnv->CallVoidMethod(mObjectRef, mOnNativeVoidCallmID);
}

void JniNativeCallListener::callJavaStringMethod(jobject pString) {
JNIEnv* jniEnv = getJniEnv();
jniEnv->CallVoidMethod(mObjectRef, mOnNativeStringCallmID, pString);
}

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


#include "JniNativeCall.hpp"
#include "JniNativeCall.hpp"
#include "by_idev_jni_javacall_MainActivity.h"
#ifndef JNIGLUE_CPP_
#define JNIGLUE_CPP_
JNIEXPORT void JNICALL Java_by_idev_jni_javacall_MainActivity_makeNativeCall (JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener) {
JniNativeCallListener listener(pEnv, pNativeCallListener);
jstring jStr = pEnv->NewStringUTF("Hello from native!");
//Call Java callbacks
listener.callJavaStringMethod(jStr);
listener.callJavaVoidMethod();
}

#endif /* JNIGLUE_CPP_ */

На Java-стороне код будет таким:

 

public class MainActivity extends Activity
implements NativeCallListener, OnClickListener {

private int mCount = 0;
private Button mButton;
private TextView mText;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(this);

mText = (TextView) findViewById(R.id.text);
}

@Override
public void onNativeVoidCall() {
mCount++;
mText.setText(String.valueOf(mCount));
}
@Override
public void onNativeStringCall(String arg) {
makeNoise("Native call with string: " + arg);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
makeNativeCall(this);
break;
}
}

private void makeNoise(String text) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show();
}

native private void makeNativeCall(NativeCallListener nativeCallListener);

static {
System.loadLibrary("jnicall");
}
}

Все вышесказанное, но очень быстро

Весь код можно сократить до жалких трех строк логики:


JNIEXPORT void JNICALL Java_by_idev_jni_javacall_MainActivity_makeNativeCall (JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener) {
//получение дескриптора класса
jclass clazz = pEnv->GetObjectClass(pNativeCallListener);
//получение идентификатора метода
jmethodID voidVoidMethod = pEnv->GetMethodID(clazz,"onNativeVoidCall", "()V");
//вызов метода
pEnv->CallVoidMethod(pNativeCallListener, voidVoidMethod);
}

Если заморачиваться не нужно — то такой подход сработает, для больших систем я бы все-таки рекомендовал реализовать все на должном уровне :) Так же не очень разумно делать «циклические вызовы», когда Java-код вызывает С++ код, который вызывает Java-код, который снова вызывает C++ код. Четвертый шаг тут явно избыточный.

Заключение

JNI — замечательный, но требующий бережного отношения к себе механизм, дающий понимание работы многих аспектов Java и программирования в целом. В следующий раз мы с вами займемся доступом к полям объекта. Код для данной статьи тут.

Happy Coding! :)