2010-06-23

Android-lighthouse: вызов Java-методов из C++/Qt-кода

По адресу http://qt.gitorious.org/~taipan/qt/android-lighthouse лежит репозитарий с Qt для Android'а. Проект более менее активно пилится его создателем и на данный момент его вполне уже можно использовать. Однако, там не хватает такой вещи как вызов Java-методов из кода, написанного на C++/Qt. В частности, это очень нужно для работы с GPS. Но. Никто не мешает нам слегка поправить проект и впилить туда нужный нам функционал. Этим и займемся.
Внимание! Все исходники основаны на версии репозитария на момент 23 июня (последний push на текущий момент сделан 14 июня).

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

Постановка задачи
Работа с андроидом накладывает несколько ограничений:
  1. Мы не можем создать еще одну ява-машину, нам надо работать в той же (на процесс разрешена только одна ява-машина)
  2. ClassLoader для ява-машины в андроиде кастомизирован и не позволяет искать свои классы (системные ищутся и находятся на ура) из любого места отличного от функции OnLoad(). 
Первое ограничение нас, в принципе, особо не интересует, так как нам все равно нужны инстанции тех же объектов, которые были в нашем Java-приложении, а значит нужна именно та ява-машина.
Второе ограничение похуже и по сути не дает нам просто найти интересующие нас классы из нашего Qt-приложения и работать с ними. Опять же, делать поиск в OnLoad() и позже использовать найденные классы не получится, так как OnLoad() находится в lighthouse и кастомизировать его под каждое приложение не очень хорошо.
Следовательно, нам нужно:
  1. Найти общее решение для вызова своих Java-методов
  2. Реализовать его 

Поиск решения
Первое что приходит на ум, это сделать общее название класса-хелпера и использовать его во всех проектах. Но это опять же не очень хорошо в том плане, что это непереносимое на других пользователей библиотеки исправление.
Вместо этого мы будем передавать объект нужного класса в lighthouse, а оттуда уже будем забирать его из нашего кода на C++/Qt.

Общая структура запуска приложения на Qt под Android'ом
В FAQ'е от автора проекта есть несколько способов запуска Qt-приложений, но мне больше всего нравится способ с подгрузкой библиотеки с Qt-приложением из Java-приложения для Android'а.
Для этого надо создать класс-наследник от com.nokia.qt.QtActivity и в его конструкторе вызвать метод setApplication("appName"), где appName - это название нашего приложения на Qt. Ну и не забыть положить .so-файл в Java-проект. При запуске приложения оно само все подгрузит и выполнит main() из нашего .so.
Теперь рассмотрим что происходит "под капотом".
QtActivity - наследник от обычного Activity и по сути является оберткой для класса QtApplication, который и делает все необходимое. Необходимое заключается, во-первых, в подгрузке Qt-библиотек; во-вторых, в подгрузке библиотеки с нашим приложением и, в-третьих, в запуске нашего приложения. Первое выполняется методом loadLibraries(String[] libraries), а второе и третье методом loadApplication(String lib). Первый этап загрузки нас не интересует, как и второй, а вот третий как раз то что нам надо.
Выполнен запуск приложения также через JNI, посредством вызова нативного метода startQtApp() из Java-кода. Этот нативный метод в свою очередь выполняет некоторые подготовки и запускает отдельный поток с нашим приложением, где уже выполняется наша функция main().

Реализация: изменения в Java-части
Нам нужно вызвать нативный метод запуска с дополнительным параметром. Для этого заведем в com.nokia.qt.QtActivity поле jniProxyObject и сеттер для него, который будем вызывать из конструктора нашего наследника этого класса (там же где выставляется имя подгружаемого нативного приложения). В сигнатуру метода QtApplication.loadApplication() добавим еще один параметр Object jniProxyObject, который собственно и будем передавать в startQtApp(). Также надо поправить сигнатуру объявления нативного метода startQtApp() в этом же классе на
public static native void startQtApp(Object jniProxyObject);

* This source code was highlighted with Source Code Highlighter.
И также поменяем вызов метода loadApplication() в QtActivity.onCreate() на
QtApplication.loadApplication(appName, (jniProxyObject != null) ? jniProxyObject : this);

* This source code was highlighted with Source Code Highlighter.
На этом Java-часть закончена.

Реализация: изменения в Android-Lighthouse
Нам нужно сохранить переданный объект и потом его отдавать нашему приложению. Все нужные нам файлы лежат в директории src/plugins/platforms/android. Если конкретнее, то это файлы androidjnimain.cpp и androidjnimain.h. По сути это и есть то, что вызывается из Java-кода. В хедер в неймспейс QtAndroid сразу добавим объявления функций для получения переданного из Java объекта и ява-машины:
JavaVM *getJavaVM();
jobject getJniProxyObject();

* This source code was highlighted with Source Code Highlighter.
В cpp-шнике добавим переменную для нашего объекта и реализации методов, описанных в хедере:
static jobject m_jniProxyObject = NULL;
//...
namespace QtAndroid
{
//...
  JavaVM *getJavaVM()
  {
    return m_javaVM;
  }
  jobject getJniProxyObject()
  {
    return m_jniProxyObject;
  }
}

* This source code was highlighted with Source Code Highlighter.
Теперь нам нужно поменять метод startQtApp() и его регистрацию в JNI. Второе делается исправлением строковой константы в массиве JNINativeMethod methods[], найдем там элемент {"startQtApp", "()V", (void *)startQtApp} и замением его на {"startQtApp", "(Ljava/lang/Object;)V", (void *)startQtApp}. Метод startQtApp() будет выглядеть следующим образом:
static jboolean startQtApp(JNIEnv* env, jobject /*object*/, jobject jniProxyObject)
{
  qDebug()<<"startQtApp";
  m_surfaces.clear();
  mAndroidGraphicsSystem=0;
  m_requestResize=false;
  m_pauseApplication=false;
  m_applicationControl = new ApplicationControl();
  m_jniProxyObject = env->NewGlobalRef(jniProxyObject);
  pthread_t appThread;
  return pthread_create(&appThread, NULL, startMainMethod, NULL)==0;
}


* This source code was highlighted with Source Code Highlighter.
Осталось только пробросить этот хедер в инклюды, видимые в приложениях. Добавим файл include/QtAndroid/AndroidJniMain, в котором заинклюдим androidjnimain.h.

Тестовое приложение
Java-часть
package org.example;
import com.nokia.qt.QtActivity;

public class ExampleMainActivity extends QtActivity
{
  public ExampleMainActivity() {
    setApplication("jnitest");
    setJniProxyObject(this);
  }

  public static int testMethod(String value) {
    return Integer.parseInt(value);
  }

  public int testMethod2(String value) {
    return Integer.parseInt(value)+5;
  }
}

* This source code was highlighted with Source Code Highlighter.
C++/Qt-часть
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include < QtAndroid/AndroidJniMain >

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  showMaximized();
  JavaVM *jvm = QtAndroid::getJavaVM();
  int res;
  env = NULL;
  if (jvm != NULL)
  {
    JavaVMAttachArgs args;
    args.name = NULL;
    args.group = NULL;
    args.version = JNI_VERSION_1_4;
    res = jvm->AttachCurrentThread(&env, &args);
    if (isJavaExceptionOccured())
      return;
  }

  jobject jniProxyObject = QtAndroid::getJniProxyObject();
  jclass jniProxyClass = env->GetObjectClass(jniProxyObject);
  if (isJavaExceptionOccured())
    return;

  jmethodID staticMethodId = env->GetStaticMethodID(jniProxyClass,"testMethod","(Ljava/lang/String;)I");
  if (isJavaExceptionOccured())
    return;
  jint res1 = env->CallStaticIntMethod(jniProxyClass, staticMethodId, env->NewStringUTF("42"));
  if (isJavaExceptionOccured())
    return;

  jmethodID methodId = env->GetMethodID(jniProxyClass,"testMethod2","(Ljava/lang/String;)I");
  if (isJavaExceptionOccured())
    return;
  jint res2 = env->CallIntMethod(jniProxyObject, methodId, env->NewStringUTF("56"));
  if (isJavaExceptionOccured())
    return;

  QString output = QString("%1;%2").arg(res1).arg(res2);
  ui->plainTextEdit->appendPlainText(output);
}

MainWindow::~MainWindow()
{
  delete ui;
}

inline bool MainWindow::isJavaExceptionOccured()
{
  if (env->ExceptionOccurred()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
    ui->plainTextEdit->appendPlainText("Exception occured");
    return true;
  }
  return false;
}

* This source code was highlighted with Source Code Highlighter.
То есть, у нас есть два Java-метода (static и обычный), которые возвращают число, переданное в строке (не статичный метод при этом еще прибавляет 5, чтобы отличаться от static-метода). Qt-приложение, просто вызывает эти два метода и выводит результат в текстовое поле на формочке. При запуске, как и ожидалось, выдастся нужный нам результат "42;61".

UPD: Сейчас эти изменения влиты в Android-Lighthouse в виде QtAndroidBridge. Эти меры временные, до появления полноценного порта QtMobility под Android.

1 комментарий:

  1. Ну неплохо как введение в JNI. Конечно для серьёзного применения нужно делать умный указатель для jobject, пробрасывать исключения, транслировать строки, и т.д. Большая работа, на самом деле.

    ОтветитьУдалить