Embed java code from a C++ library without needing top-level program to embed it?

362 Views Asked by At

I'm using QtCreator to deploy C++/Java applications on Android. But I think my problem may not be specific to the way QtCreator deploys the app.

I want to create a C++ library providing a specific functionnality. To do so, the library needs to instantiate a Java class, this last one will be used to do some SDK functions class (for stuff that are not available in the NDK/C++ API).

Creating and using java objects from a C++ program works fine. I package the .java file to the application environment during compilation/deployment and then I can use the Java class via two approachs:

  • Declare JNI_OnLoad, load class id, method id, and later call them using jni
  • Use Qt QAndroidJniObject objects (this is specific to QtCreator)

Now the problem comes when I want to create and use java objects from a C++ library. It only works if the .java file is packaged with the top-level application. I could not find a way to package the java with and only with the library itself. Meaning that anyone why needs to use my library will not only have to simply link with the library, but will also need to package the .java file(s) needed by my library. This breaks encapsulation and gives a hard time to the end developer writing programs and simply wanting to load a library and expecting it to embed all it needs to work on its own...

My question is: How can the library embed the java file, so that this java file does not need to be part of the top level program package to let the library use it?

Here is a quick sample: MainWindow constrctor calls 4 functions themselves trying to create and use Java objects. Only the first two calls work...

enter image description here

main.cpp:

#include <QApplication>
#include <QMainWindow>

#include "MyLib.h"

#include <QtAndroidExtras/QAndroidJniObject>
#include "jni.h"

#include <assert.h>

// load java classes from main program

JavaVM* s_javaVM = NULL;
jclass s_classID = 0;
jmethodID s_ctorMethodID = 0;
jmethodID s_callmethodID = 0;

bool loadJava( JNIEnv *env )
{
    jclass clazz = env->FindClass("my/FooPrg");
    if (!clazz)
    {
        qCritical("Can't find FooPrg class");
        return false;
    }
    // keep a global reference to it
    s_classID = (jclass)env->NewGlobalRef(clazz);

    // search for its contructor
    s_ctorMethodID = env->GetMethodID(s_classID, "<init>", "()V");
    if (!s_ctorMethodID )
    {
        qCritical("Can't find class contructor");
        return false;
    }

    // search for a method
    s_callmethodID = env->GetMethodID(s_classID, "Mult", "(I)I");
    if (!s_callmethodID )
    {
        qCritical("Can't find Mult method");
        return false;
    }

    return true;
}

jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
{
    s_javaVM = vm;

    JNIEnv* env = NULL;
    if (s_javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK)
        return -1;

    loadJava( env );

    return JNI_VERSION_1_4;
}

void callJavaFunctionFromPrgWithQt()
{
    if ( QAndroidJniObject::isClassAvailable("my/FooPrg") )
    {
        QAndroidJniObject obj("my/FooPrg","()V");
        if ( obj.isValid() )
        {
            jint res = obj.callMethod<jint>("Mult", "(I)I", 0x0002);
            assert( res == 4 );
        }
        else
        {
            assert( false );
        }
    }
    else
    {
        assert( false );
    }
}

void callJavaFunctionFromPrgWithJniLoad()
{
    if ( s_classID != 0 && s_ctorMethodID != 0 && s_callmethodID != 0 )
    {
        JNIEnv* env = NULL;
        if (s_javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK)
            assert(false);

        jobject j_object = env->NewGlobalRef( env->NewObject(s_classID, s_ctorMethodID ) );
        jint res = env->CallIntMethod(j_object, s_callmethodID, 0x0002 );
        assert( res == 4 );
    }
    else
    {
        assert( false );
    }
}

class MainWindow : public QMainWindow
{
public:
    MainWindow()
    {
        callJavaFunctionFromPrgWithQt();        // works
        callJavaFunctionFromPrgWithJniLoad();   // works
        callJavaFunctionFromLibWithQt();        // fails, assert
        callJavaFunctionFromLibWithJniLoad();   // fails, because libraries JNI_OnLoad can't find FooLib.java!
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

MyLib.h:

#pragma once

void callJavaFunctionFromLibWithQt();
void callJavaFunctionFromLibWithJniLoad();

MyLib.cpp:

#include "MyLib.h"

#include <QtAndroidExtras/QAndroidJniObject>
#include "jni.h"

#include <assert.h>

// load java classes from main program

JavaVM* s_javaVM = NULL;
jclass s_classID = 0;
jmethodID s_ctorMethodID = 0;
jmethodID s_callmethodID = 0;

bool loadJava( JNIEnv *env )
{
    jclass clazz = env->FindClass("my/FooLib");
    if (!clazz)
    {
        qDebug("Can't find FooLib class");
        return false;
    }
    // keep a global reference to it
    s_classID = (jclass)env->NewGlobalRef(clazz);

    // search for its contructor
    s_ctorMethodID = env->GetMethodID(s_classID, "<init>", "()V");
    if (!s_ctorMethodID )
    {
        qDebug("Can't find class contructor");
        return false;
    }

    // search for a method
    s_callmethodID = env->GetMethodID(s_classID, "Mult", "(I)I");
    if (!s_callmethodID )
    {
        qDebug("Can't find Mult method");
        return false;
    }

    return true;
}

jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
{
    s_javaVM = vm;

    JNIEnv* env = NULL;
    if (s_javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK)
        return -1;

    // uncommenting this makes the application crash upon load....
    //loadJava( env );

    return JNI_VERSION_1_4;
}

void callJavaFunctionFromLibWithQt()
{
    if ( QAndroidJniObject::isClassAvailable("my/FooLib") )
    {
        QAndroidJniObject obj("my/FooLib","()V");
        if ( obj.isValid() )
        {
            jint res = obj.callMethod<jint>("Mult", "(I)I", 0x0002);
            assert( res == 4 );
        }
        else
        {
            assert( false );
        }
    }
    else
    {
        assert( false ); // this assertion is reached!
    }
}

void callJavaFunctionFromLibWithJniLoad()
{
    if ( s_classID != 0 && s_ctorMethodID != 0 && s_callmethodID != 0 )
    {
        JNIEnv* env = NULL;
        if (s_javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK)
            assert(false);

        jobject j_object = env->NewGlobalRef( env->NewObject(s_classID, s_ctorMethodID ) );
        jint res = env->CallIntMethod(j_object, s_callmethodID, 0x0002 );
        assert( res == 4 );
    }
    else
    {
        assert( false ); // this assertion is reached!
    }
}

FooPrg.java:

package my;

import java.lang.Integer;

public class FooPrg
{
    public FooPrg()
    {
    }

    public int Mult(int val)
    {
        return val * 2;
    }
}

FooLib.java:

package my;

import java.lang.Integer;

public class FooLib
{
    public FooLib()
    {
    }

    public int Mult(int val)
    {
        return val * 2;
    }
}

jniload.pro:

TARGET = jniload

CONFIG += qt resources

QT += core gui widgets

android: QT += androidextras

SOURCES += src/main.cpp

TEMPLATE = app

INCLUDEPATH += ifc

LIBS +=  \ 
-l$$OUT_PWD/../../lib/jniload_lib/libjniload_lib.so

ANDROID_EXTRA_LIBS +=  \ 
$$OUT_PWD/../../lib/jniload_lib/libjniload_lib.so

ANDROID_PACKAGE_SOURCE_DIR = data/android/root

OTHER_FILES += data/android/root/src/my/FooPrg.java

jniload_lib.pro:

TARGET = jniload_lib

CONFIG += qt resources

QT += core gui widgets

android: QT += androidextras

SOURCES += src/MyLib.cpp

HEADERS += ifc/MyLib.h

TEMPLATE = lib

INCLUDEPATH += ifc

# This does has apparently no effect on library
ANDROID_PACKAGE_SOURCE_DIR = data/android/root

OTHER_FILES += data/android/root/src/my/FooLib.java
1

There are 1 best solutions below

0
On BEST ANSWER

Finaly got a way to work this out.

I removed ANDROID_PACKAGE_SOURCE_DIR line from jniload.pro file and hanlde manual copy of the .java files through custom build steps:

custom_jniload_lib_step.target = jniload_lib_mockup.h
custom_jniload_lib_step.commands = $(COPY_DIR) data\android\root ..\..\android-build
QMAKE_EXTRA_TARGETS += custom_jniload_lib_step
PRE_TARGETDEPS += jniload_lib_mockup.h

custom_jniload_step.target = jniload_mockup.h
custom_jniload_step.commands = $(COPY_DIR) data\android\root ..\..\android-build
QMAKE_EXTRA_TARGETS += custom_jniload_step
PRE_TARGETDEPS += jniload_mockup.h

Then, upon deployment, android-build/src contains both FooLib.java and FooPrg.java and then both library and program can access them!