Основы Android NDK: вызов Java-методов из C/C++ кода при помощи JNI

В прошлой статье кратко рассмотрел Android NDK, показал как вызывать нативные методы из Java. Разумным продолжением той статьи будет описание того, как вызвать Java методы из C++.

Введение

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

Определение методов

Просто определить метод у класса можно, но лучше использовать интерфейсы и реализовать их в нашем классе. Для начала придумаем более-менее практическую задачу, ибо писать примеры на какие-то абстрактные темы не интересно. Предположим, у вас в C++ коде есть таймер, который что-то делает и вызывает через определённые промежутки Java метод. Как пример, рендеринг в игровом движке или опрос сервера и т.д.

Для простоты, скажем, этот таймер будет сообщать о том, сколько времени прошло после запуска. Теперь определим наш интерфейс в Java с этим методом.

public interface NativeCalls {
   public void sendTime(int time);
}

Теперь реализуем его в нашем главном Activity.

@Override 
public void sendTime(int time){
   //просто выводим текст в текст бокс
   ((TextView)findViewById(R.id.timerText)). setText(Long.toString(time));
}

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

class NativeCallListener {
public:
	//сколько прошло времени
	int timeLeft;
    //активен ли таймер
	bool timerOn;
	NativeCallListener(JNIEnv* pJniEnv, jobject pWrapperInstance);
	NativeCallListener() {}
	//апуск таймера
	void startTimer();
	//передать значение в Java метод
    void sendTime(int time);
    //отключаем таймер
    void destroy();
	~NativeCallListener(){}
private:
	JNIEnv* getJniEnv();

    //ссылка на метод
	jmethodID sendTimeID;
	//ссылка на объект
	jobject mObjectRef;
	JavaVM* mJVM;
};

Далее необходимо получить дескриптор класса.

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

Для этого есть два способа. Первый использует ссылку на объект: jclass cl = pJniEnv->GetObjectClass(pWrappedInstance);

Второй позволяет получить дескриптор по имени: jclass cl = pEnv->FindClass(«ru/suvitruf/androidndk/MainActivity»);

Второй метод не одобряю, ибо в случае смены реализации на стороне java, придётся менять и нативный код, но зато не требуется ссылка для получения дескриптора. Первый метод более удобен, как я считаю.

Полностью инициализация (получение дескриптора класса и ссылки на метод) будет такой:

NativeCallListener::NativeCallListener(JNIEnv* pJniEnv, jobject pWrappedInstance) {
	pJniEnv->GetJavaVM(&mJVM);
	mObjectRef = pJniEnv->NewGlobalRef(pWrappedInstance);
	jclass cl = pJniEnv->GetObjectClass(pWrappedInstance);
	sendTimeID = pJniEnv->GetMethodID(cl, "sendTime", "(I)V");

}

Cсылки на объекты, переданные в JNI-метод, действительны только в пределах времени выполнения этого метода. При попытки обратится к mObjectRef или pJniEnv после выполнения метода, там будет NULL. Поэтому мы и создаём глобальные ссылки на ни, чтобы потом использовать. Теперь нужен метод для получение JNIEnv:

JNIEnv* NativeCallListener::getJniEnv() {
	JavaVMAttachArgs attachArgs;
	attachArgs.version = JNI_VERSION_1_6;
	attachArgs.name = ">>>TimerThread";
	attachArgs.group = NULL;

	JNIEnv* env;
	if (mJVM->AttachCurrentThread(&env, &attachArgs) != JNI_OK) {
		env = NULL;
	}

	return env;
}

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

Для этого у нас написано это: sendTimeID = pJniEnv->GetMethodID(cl, "sendTime", "(I)V");

В качестве параметров GetMethodID служат дескриптор класса, имя метода, список параметров и возвращаемого типа «(I)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 нет

Вызвать метод у нужного объекта

Теперь необходимо написать код, который использует эти дескрипторы:

void NativeCallListener::sendTime(int time) {
	JNIEnv* jniEnv = getJniEnv();
	jniEnv->CallIntMethod(mObjectRef, sendTimeID, time);
}

Осталось написать только метод по инициализации всего этого дела.

JNIEXPORT void JNICALL Java_ru_suvitruf_androidndk_MainActivity_startTimer(JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener) {

	listener = NativeCallListener(pEnv, pNativeCallListener);

	LOGI("NativeCall");
	listener.startTimer();

}

void NativeCallListener::startTimer(){
	timerOn = true;
	timeLeft = 0;
	LOGI("Start Timer");

	while (timerOn) {
		sleep(1);
		timeLeft+=1;
		LOGI("timeLeft %i",timeLeft);
		sendTime(timeLeft);
	}
}

В Jave определяем метод для инициализации и грузим либу:

native public void startTimer(NativeCalls nativeCallListener);

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

//отдельный поток
class MyRunnable implements Runnable {
	MainActivity activity;
	public MyRunnable(MainActivity activity){
		this.activity = activity;
	}
	 public void run() {
		 activity.startTimer(activity);
	 }
}

Теперь вешаем обработчик на кнопку, создаём поток, где и вызываем этот метод с бесконечным циклом. Казалось бы, всё сделали но…будет ошибка:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views

Довольно частое явление при работе с ndk, когда вы вызываете java из C++ и что-то делаете во вьюхе. Вот только проблема в том, что это разные потоки. В том же GLSurfaceView, когда рендерер создаёте, он будет в новом потоке. И из него для манипуляций с вьюхой надо немного по другому всё делать. Создаём свой хэндел, и через него вызываем метод:

protected Handler handler = new Handler(){
   @Override
   public void handleMessage(Message msg) {
      setTime(msg.what);
   }
};

@Override 
public void sendTime(int time){
   handler.sendEmptyMessage(time);
}

Теперь в текст боксе будет отображаться время прошедшее с запуска таймера.

Можете скачать исходники AndroidNDK2.rar.