JNI GetMethodID not working for constructor of inner class
I thought to provide a more involved answer to this question. The following is a simplified version of some experiments I am doing with JNI to learn how to use it. This example is more about exploring how to access objects and fields using JNI rather than to be a recommendation as to use.
Also the Java source is slightly modified removing quite a bit of other source dealing with other JNI uses. However this should provide a starting place. There are best practices for JNI such as caching of field identifiers which are being ignored in this example. Here are some best practices using JNI from IBM.
In this example taken from that source the idea was to have a class, helloworld
, which contained an inner class, ExportedFuncs
, which would have various methods which acted as an interface to a set of native C functions exported from a dynamic link library (DLL). This inner class would in turn have its own inner class, ExportedData
, which would be a data only class.
When an ExportedFuncs
object was created, it would do a native call using JNI to obtain an instance of an ExportedData class.
JNI requires a fully qualified class name
Notice in the JNI Native C source below that both the GetFieldID()
and the FindClass()
functions use a fully qualified class name of "Lhelloworld$ExportedFuncs$ExportedData;"
which has the inner classes separated by the US dollar sign ($).
The GetMethodID()
function must include the parent classes of any inner class. If the method being looked up was within the main class, helloworld
, then the call would look like:
jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "()V");
However since we are wanting to construct an inner class of an inner class we need to specify the parent classes for the inner class we want to construct as in:
jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(Lhelloworld$ExportedFuncs;)V");
One other point is that the constructor for the ExportedData
class is the default constructor which does not take any arguments. If there were arguments then those would need to be added to the method signature used in the GetMethodID()
function call. So if a constructor that took an int
was being used then the signature would look like "(Lhelloworld$ExportedFuncs;I)V"
.
A simple example of Java and JNI with inner class
Assume a simple example Java class with an encapsulated inner class. This example has an inner class that has an inner class.
public class helloworld {
private class ExportedFuncs
{
// declare our private, data only class with some fields
private class ExportedData
{
int theInt;
String theString;
}
public native ExportedData getExportedData();
ExportedData theExportedData;
// constructor for the ExportedFuncs class which gets a copy of the data
ExportedFuncs()
{
theExportedData = getExportedData(); // get an object through native method
}
}
ExportedFuncs myExportedFuncs = new ExportedFuncs();
// .... other fields and methods of the helloworld class follows
}
The JNI native C function would look
JNIEXPORT jobject JNICALL Java_helloworld_00024ExportedFuncs_getExportedData (JNIEnv *env, jobject obj)
{
jfieldID fid = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, obj), "theExportedData", "Lhelloworld$ExportedFuncs$ExportedData;");
jobject newObj = 0;
jclass cls = (*env)->FindClass(env, "Lhelloworld$ExportedFuncs$ExportedData;");
// Get the Method ID of the constructor for this inner class.
// There are two things to notice about this GetMethodID() function call.
// First, the constructor is requested by specifying the special string "<init>"
// Second, the signature of the constructor includes the enclosing class in the signature.
// Also there are no arguments for this constructor. if there were then they would need to be included between the parenthesis
// for example "(Lhelloworld$ExportedFuncs;I)V" for a single int arg.
jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(Lhelloworld$ExportedFuncs;)V");
if (NULL == midInit) return NULL;
// Call the class constructor to allocate a new instance. the default constructor has no arguments.
newObj = (*env)->NewObject(env, cls, midInit);
// now lets set some values in our new object and return it.
if (newObj) {
jfieldID fidAge = (*env)->GetFieldID (env, cls, "theInt", "I");
(*env)->SetIntField (env, newObj, fidAge, 127);
}
return newObj;
}
The function signature for the native JNI code was generated using the javah
utility on the helloworld
class. You may also find the output from the javap
utility helpful as well.
By the way I thought it interesting that the name of the native method of the inner class has the numeric field of five digits, 00024, which is the hex for the US dollar sign ($) in the ANSI/ASCII table. The US dollar sign is used for the separator for inner classes in a fully qualified name used in JNI functions such as GetFieldID()
.
I am not using packages in this contrived example so there is no package component to the native C function name. Ordinarily there would be. And a question I have is what are the limits of the function name length used with that that naming convention.
user2340939's answer help me to find the right way to construct an new object with integer argument of inner class. Here is my reward.
JAVA
package xxx.test_jni;
public class myNDK {
public myNDK() { Log.d("myNDK","myNDK constructor"); }
public myNDK(int a) { Log.d("myNDK","myNDK constructor(int)"); }
static {
System.loadLibrary("myJNI");
}
public class myObj {
int aaa;
public myObj(){
Log.d("myNDK","myObj()");
this.aaa = 333;
}
public myObj(int aaa){
Log.d("myNDK","myObj(int) " + aaa);
this.aaa = aaa;
}
public int getAaa() {
Log.d("myNDK","getAaa()");
return aaa;
}
}
public native myObj getmyObj1();
public native myObj getmyObj2();
}
CPP
JNIEXPORT jobject JNICALL Java_xxx_test_1jni_myNDK_getmyObj1
(JNIEnv *env, jobject thiz){
// Find inner class
jclass innerCls = env->FindClass("xxx/test_jni/myNDK$myObj");
if (innerCls == NULL) {
LOGI("%s, FindClass nullptr\n", __func__);
return NULL;
}
// Get Method ID myObj(), constructor
jmethodID cnstrctr1 = env->GetMethodID(innerCls, "<init>", "(Lxxx/test_jni/myNDK;)V");
if (cnstrctr == NULL) {
LOGI("%s, GetMethodID nullptr\n", __func__);
return NULL;
}
jobject obj1 = env->NewObject(innerCls, cnstrctr1, thiz);
if (obj1 == NULL) {
LOGI("%s, NewObject nullptr\n", __func__);
return NULL;
}
// Get Method ID myObj(int), constructor
jmethodID cnstrctr2 = env->GetMethodID(innerCls, "<init>", "(Lxxx/test_jni/myNDK;I)V");
if (cnstrctr2 == NULL) {
LOGI("%s, GetMethodID2 nullptr\n", __func__);
return NULL;
}
jint a = 5;
jobject obj2 = env->NewObject(innerCls, cnstrctr2, thiz, a);
if (obj2 == NULL) {
LOGI("%s, NewObject2 nullptr\n", __func__);
return NULL;
}
return obj2; // or obj1
}
To NewObject, NOT an inner class
JNIEXPORT jobject JNICALL Java_xxx_test_1jni_myNDK_getmyObj2
(JNIEnv *env, jobject thiz){
jclass cls = env->FindClass("xxx/test_jni/myNDK");
// Get Method ID myNDK(), constructor
jmethodID cnstrctr1 = env->GetMethodID(cls, "<init>", "()V");
jobject obj1 = env->NewObject(cls, cnstrctr1);
// Get Method ID myNDK(int), constructor
jmethodID cnstrctr2 = env->GetMethodID(cls, "<init>", "(I)V");
jint a = 1;
jobject obj2 = env->NewObject(cls, cnstrctr2, a);
return obj2; // or obj1
}
But I still want to know which document tells the NewObject API of inner class have to add parenter class?
You need parent class in the GetMethodID signature, so in my example:
jmethodID methodID = (*env)->GetMethodID(env, cls, "<init>", "(LsomeClass;)V");
And I also needed to add calling class object/pointer to the NewObject function:
jobject obj = (*env)->NewObject(env, cls, methodID, this);