Вызов Java методов из C/C++ кода при помощи JNI на Android.
В прошлой статье посвященной 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
#include
#include
#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, «», «(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!
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.