Содержание
В прошлой статье посвященной JNI на Android я описывал как начать работу с Android NDK, в том числе, как вызывать нативные методы из Java, на этот раз я опишу как вызывать Java-методы из нативного кода.
Для лучшего понимания этой статьи, рекомендую предварительно ознакомиться с основами JNI в Android. Сам процесс вызова Java-метода из нативного кода далее мы будем называть обратным вызовом, так как такие вызовы чаще всего используются для оповещения Java-стороны приложения о некоторых событиях, просходящих «в нативе». Код доступен на нашем репозитории на GitHub. Для нетерпеливых есть быстрое решение
Общее описание механизма
И так, перед нами стала задача «дернуть» Java-метод из нативного кода. Каков будет порядок действий?
- Опеределить какой метод и у какого класса вы хотите вызвать. Банально, да.
- Получить дескриптор нужного класса.
- Описать сигнатуру метода на языке JNI-примитивов (не так страшно, как звучит).
- Получить идентификатор метода (что-то типа ссылки на метод).
- Вызвать метод у нужного объекта (если метод экземплярный) или класса (если метод статический).
Определяемя с методами
Я предлагаю всегда делать методами обратного вызова методы интерфейса, а не конкретного класса. У такого подхода есть преимуществ, начиная с того, что можно будет без труда подменять 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». В скобках указаны входные параметры, после них — возвращаемый тип. Таким образом первый метод не имеет входных параметров и ничего не возвращает, а второй принимает строку и тоже ничего не возвращает. Ниже приведена таблица типов параметров и пару примеров описания методов.
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!