免责声明:我已经实现了自己的面向对象的编程语言,因此熟悉(并教过了)指针和C。内存管理101并不是这个问题。与看起来相似的JNI C ++ API也不相同。谢谢。
当您查看Java的JNI C API时,在jni.h
中,JNIEnv
是指向typedef
的指针的struct JNINativeInterface
。鉴于所有JNI C API都使用JNIEnv*
,这意味着它实际上是JNINativeInterface **
,表示指向struct
的指针的指针。
JNI为什么要使用额外级别的间接访问?
在JNINativeInterface*
中用C模拟一个类似对象的构造就可以了。你可以打电话
env->NewGlobalRef(env, my_object);
那为什么要强迫我们这样做
(*env)->NewGlobalRef(env, my_object);
?间接的额外级别作为第一个参数传递到函数中,因此我想它们可以更新该指针。是吗?
更正:
我最初记错了并通过(*env)
而不是env
作为第一个参数,因此排除了被调用者编辑指针本身的可能性。我已经改正了这个帖子。感谢John Bollinger指出这一点。
答案 0 :(得分:1)
JNIEnv
实际上不是指向指针的指针,而是指向包含其他(专用)线程特定信息的数据结构。 JNINativeInterface*
只是结构中的第一个字段,其余字段不是公共的。这为VM在JNI功能表的实现中提供了更大的灵活性。
一些链接可能会给那些碰到这些问题的人带来好处:
Threads and JNI-在这里说明:
JNI接口指针(JNIEnv *)仅在当前线程中有效。您不得将接口指针从一个线程传递到另一个线程,也不得缓存接口指针并在多个线程中使用它。在从同一线程连续调用本机方法时,Java虚拟机将为您传递相同的接口指针,但是不同的线程会将不同的接口指针传递给本机方法。
答案 1 :(得分:1)
在jni.h
中有一条评论:
我们将内联函数用于C ++,以便程序员可以在C ++中编写:
env->FindClass("java/lang/String")
,而不是在C中编写:(*env)->FindClass(env, "java/lang/String")
。
例如,在struct JNIEnv_
中,如果是c ++,则有一种方法:
jclass FindClass(const char *name) {
return functions->FindClass(this, name);
}
该类具有一个指针:
const struct JNINativeInterface_ *functions;
[这是C唯一可见的东西]。它是虚拟函数表的指针。
因此,AFAICT,正确的[对于C]的deref是:
env->functions->FindClass(env,name)
这是成员函数进行调用的方式,并且还与上面引用的注释匹配。
那么,您确定:
env->functions->FindClass(*env,name)
有效吗?
碰巧(*env)->FindClass(env,name)
起作用是因为functions
是 first 元素[对于C是唯一的元素]。
所以,对我来说,我将创建一个执行deref的宏:
#define DEREF(_env) ((_env)->functions)
DEREF(env)->FindClass(env,name)
答案 2 :(得分:1)
TL; DR :多种因素(包括二进制兼容性,多线程支持和所需的接口特性)导致了JNI和随之而来的C调用范例的设计。
那为什么要强迫我们这样做
(*env)->NewGlobalRef((*env), my_object);
?额外的间接级别不会传递给以下函数: 第一个参数,因此他们无法更新该指针。
这不是JNI调用的正确格式。 As the specification attests,事实上,正如从分布式JNI头文件中清楚看到的那样,正确的格式是
JNIEnv *env = /* ... */;
(*env)->NewGlobalRef(env, my_object);
请注意
JNIEnv
本身就是指针类型(将->
运算符应用于*env
所必需),尤其是env
本身而不是*env
。也就是说,与问题的断言相反,额外的间接调用 传递给了JNI函数。花了一些时间来思考它并阅读了the JNI specification的一些内容丰富的部分,但是,我改变了主意,专门设计了一个干净的C ++接口,但付出了代价。不太干净的C接口。需要将JNI C接口设计为使用调用惯用法的唯一 technical 原因是,这样JNI函数可以将一个环境换成另一个环境,但是没有理由认为任何JNI函数都可以做到这一点,或者可以想象任何人都会这么做。 (稍后将对此设计选择进行更多评论。)
Chapter 1 of the specification提供了尽可能多的官方故事。它讨论了JNI当前(第二次)主要迭代的历史背景和设计目标。特别是,Sun的立场是,精心设计的界面应具有以下优点:
- 每个VM供应商可以支持更大范围的本机代码。
- 工具构建者将不必维护不同种类的本机方法接口。
- 应用程序程序员将能够编写其本地代码的一个版本,并且该版本将在不同的VM上运行。
与各有关方面进行磋商后,他们达到了这些高级设计要求:
- 二进制兼容性-主要目标是本机方法库跨服务器上所有Java VM实现的二进制兼容性。 给定的平台。程序员应仅维护其版本的一个 给定平台的本机方法库。
- 效率-要支持对时间要求严格的代码,本机方法接口必须施加很少的开销。所有已知技术,以确保 VM独立性(以及二进制兼容性)具有一定数量 开销。我们必须以某种方式在效率之间达成妥协 和VM无关。
- 功能-接口必须公开足够的Java VM内部组件,以允许本机方法完成有用的任务。
文档对COM作为实现这些目标的接口技术表示了极大的赞赏,事实上,微软已经为其Java 1 VM创建了COM接口。但是,当然,COM还存在一些问题,不仅涉及相对于Java的技术细节,而且还涉及感兴趣的平台(包括Sun自己的Solaris)上的(不)可用性这一小问题。 。因此,我认为这可能是对提出的问题的真正答案:
尽管Java对象未作为COM公开给本机代码 对象,JNI接口本身与COM二进制兼容。杰尼 使用与COM相同的跳转表结构和调用约定 做。 这意味着,一旦对COM的跨平台支持 可以使用的JNI可以成为Java VM的COM接口。
(原着重点。)
该规范继续提供a high-level description of what having a COM-congruent form means,关键部分是:
本机代码通过调用JNI函数来访问Java VM功能。杰尼 函数可以通过接口指针使用。接口 指针是指向指针的指针。该指针指向一个数组 指针,每个指针都指向一个接口函数。每一个 接口函数位于数组内部的预定义偏移处。
这正是我们实际上看到的,并且规范继续表达它与C ++虚拟函数表和COM接口的相似之处。它还阐明了使用功能表具有以下优点:
此外,它解释说,提供指向函数表的双指针有助于将不同的表呈现给不同的线程:
JNI接口指针仅在当前线程中有效。一种 因此,本机方法一定不能从一个方法传递接口指针 线程到另一个。实施JNI的VM可以分配和存储 JNI接口指针指向的区域中的线程本地数据。
本机方法接收JNI接口指针作为参数。的 保证VM会将相同的接口指针传递给本机方法 当它从同一Java多次调用本机方法时 线。但是,可以从不同的Java调用本机方法 线程,因此可能会收到不同的JNI接口指针。
(“ JNI接口指针”是上述的双指针,其类型在C JNI中表示为JNIEnv *
。)
C调用范例直接来自该数据和接口设计。必须先取消引用JNI接口指针才能获取功能表指针,然后将接口指针本身(而不是功能表指针)传递给每个函数。
完全相同的事情也发生在C ++ API中,但是通过将函数表指针包装在一个类中并将JNI接口指针伪装为该类实例的指针来掩饰它。这也提供了提供包装函数的机会,这些包装函数掩盖了JNI接口指针传递给JNI函数的事实。我认为这是对C ++功能的充分利用,以该语言提供了一个简单自然的接口,而不是C ++优先设计JNI的证据。