thisObj in JNI function called from constructor refers to the class and not to the instance

83 Views Asked by At

I'm trying to implement a JNI library. I've noticed that thisObj passed to JNI function when called from a constructor differs from the same function called from a method.

Here is my minimal code:

public final class Test {
    static {
        System.loadLibrary("jnitest");
    }

    private native void jni_test(int i);

    public Test() {
        jni_test(0);
    }

    public void m() {
        jni_test(1);
    }
}

I call it like this:

Test t = new Test();
t.m();

JNI side looks like this:

static void jni_test(JNIEnv *env, jobject thisObj, int i) {
    printf("jni_test %i %p\n", i, thisObj);
}

The output is:

jni_test 0 0x7f3bb0288898
jni_test 1 0x7f3bb02888e0

As you see, thisObj is not the same. To be more precise, thisObj refers to the Test class when called from the constructor. And it refers to the instance of the Test when called from a method.

Why is this?

How to workaround it (except from explicitly passing this as one more parameter to the jni function)?

2

There are 2 best solutions below

4
On BEST ANSWER

As you see, thisObj is not the same. To be more precise, thisObj refers to the Test class when called from the constructor. And it refers to the instance of the Test when called from a method.

Why is this?

You are incorrect in your theory. What you're printing out in the C code is an object handle, not the this pointer. Pointers to java objects are not exposed directly to native code. This wouldn't work as the garbage collector can move the objects around, even at the same time that the native code is executed.

Instead the VM will allocate an object handle, which can be thought of as a token that indirectly refers to the Java object, and only the GC knows how to access correctly (which is encapsulated by the JNI api). Across multiple calls, this means that the value of the handle can change, because a new handle is created for every call.

But in both cases, the handle will refer to the this object. This has nothing to do with the place from where the method is called. Because jni_test is an instance method, it requires a receiver argument. Both of the calls to jni_test(...) in your Java code, are just short for this.jni_test(...). And that this is what the thisObj handle refers to in the native code.

This is also explained in the JNI specification:

The JNI interface pointer is the first argument to native methods. The JNI interface pointer is of type JNIEnv. The second argument differs depending on whether the native method is static or nonstatic. The second argument to a nonstatic native method is a reference to the object. The second argument to a static native method is a reference to its Java class.

The jni_test method is not static, so the second argument refers to the object.

How to workaround it (except from explicitly passing this as one more parameter to the jni function)?

I'm not sure what you're trying to achieve, or what kind of workaround you're looking for. Rest assured though, that there's no way to expose a stable native address pointing at an arbitrary Java object.

9
On

You declared your method as private native void jni_test(int i);, which means the corresponding C signature is:

void Java_<package>_jni_1test(JNIEnv *env, jobject obj, jint i);

regardless of where it is called from, with obj being an instance of class Test. You can safely pass obj to other methods or call methods on obj, but the same caveats on doing stuff inside constructors apply.

The other answer already explained why your test is testing the wrong thing so I will not repeat it.

EDIT: I wrote the following code to disprove your statement: Added the following main method to Test:

public static void main(String[] args) {
  Test t = new Test();
  t.m();
}

and the following inside the jni_test method:

JNIEXPORT void JNICALL Java_Test_jni_1test(JNIEnv * env, jobject obj, jint i) {
    jclass cls_Test = env->FindClass("Test");
    printf("Inside call with i=%d\n", i);
    printf("\tobj instanceOf Test: %d\n", env->IsInstanceOf(obj, cls_Test));
    printf("\tobj.getClass() == Test: %d\n", env->IsSameObject(cls_Test, env->GetObjectClass(obj)));

    jmethodID mid_Test_toString = env->GetMethodID(cls_Test, "toString", "()Ljava/lang/String;");
    jstring str = (jstring)env->CallObjectMethod(obj, mid_Test_toString);
    const char * str_output = env->GetStringUTFChars(str, nullptr);
    printf("\tobj.toString(): %s\n", str_output);
    env->ReleaseStringUTFChars(str, str_output);
}

which produces the following output:

Inside call with i=0
    obj instanceOf Test: 1
    obj.getClass() == Test: 1
    obj.toString(): Test@1eb44e46
Inside call with i=1
    obj instanceOf Test: 1
    obj.getClass() == Test: 1
    obj.toString(): Test@1eb44e46

This conclusively proves that yes, even within a constructor, this refers to the object being constructed. I do not know how you arrived at a different conclusion, but I can only assume your test code is faulty somehow.