在构造函数中调用的 JNI 函数中的 thisObj 指代的是类而不是实例。

huangapple go评论72阅读模式
英文:

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

问题

我正在尝试实现一个JNI库。我注意到,当从构造函数调用时,传递给JNI函数的thisObj与从方法调用的相同函数不同。

这是我的最小代码示例:

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);
    }
}

我像这样调用它:

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

JNI端看起来像这样:

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

输出是:

jni_test 0 0x7f3bb0288898
jni_test 1 0x7f3bb02888e0

正如您所看到的,thisObj不相同。更准确地说,thisObj 在从构造函数调用时引用Test类,而在从方法调用时引用Test的实例

为什么会这样?

如何解决这个问题(除了显式将this作为jni函数的另一个参数传递之外)?

英文:

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)?

答案1

得分: 2

> 如您所见,thisObj 不相同。更准确地说,当从构造函数调用时,thisObj 指的是 Test 类,而当从方法调用时,它指的是 Test 的实例。
>
> 为什么会这样?

您的理论是不正确的。您在 C 代码中打印出的是一个对象句柄,而不是 this 指针。指向 Java 对象的指针不会直接暴露给本地代码。这是行不通的,因为垃圾收集器甚至在本地代码执行的同时也可以移动对象。

相反,虚拟机将分配一个对象句柄,可以将其视为间接引用 Java 对象的令牌,只有垃圾收集器知道如何正确访问它(这由 JNI API 封装)。在多次调用之间,这意味着句柄的值可以更改,因为每次调用都会创建一个新的句柄。

但在这两种情况下,句柄将引用this 对象。这与调用方法的位置无关。因为 jni_test 是实例方法,它需要一个接收器参数。您 Java 代码中对 jni_test(...) 的两次调用只是 this.jni_test(...) 的简写。而在本地代码中,thisObj 句柄引用的就是这个 this

这也在 JNI 规范 中有解释:

> JNI 接口指针是本地方法的第一个参数。JNI 接口指针的类型是 JNIEnv。第二个参数取决于本地方法是静态还是非静态的。非静态本地方法的第二个参数是对对象的引用。静态本地方法的第二个参数是对其 Java 类的引用。

jni_test 方法不是 static,所以第二个参数引用的是对象。

> 如何解决这个问题(除了显式将 this 作为额外参数传递给 JNI 函数)?

我不确定您试图实现什么,或者您正在寻找什么样的解决方法。但请放心,没有办法公开指向任意 Java 对象的稳定本地地址。

英文:

> 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.

答案2

得分: 1

你将方法声明为 private native void jni_test(int i);,这意味着相应的C签名是:

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

无论从哪里调用它,obj 都是 Test类的一个实例。您可以安全地将 obj 传递给其他方法或调用 obj 上的方法,但在构造函数内执行操作时要注意相同的注意事项。

另一个答案已经解释了为什么您的测试正在测试错误的事情,因此我不会重复。

编辑:我编写了以下代码来反驳您的说法:
在Test类中添加了以下main方法:

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

并在jni_test方法中添加了以下内容:

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);
}

这将产生以下输出:

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 也是指向正在构造的对象的。我不知道您是如何得出不同结论的,但我只能假设您的测试代码在某种方式上存在问题。

英文:

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.

huangapple
  • 本文由 发表于 2023年6月15日 21:46:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76483156.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定