为什么JNI C API对于JNIEnv使用指针对指针而不是直接指针?

时间:2020-03-17 16:17:11

标签: java c java-native-interface programming-languages jnienv

免责声明:我已经实现了自己的面向对象的编程语言,因此熟悉(并教过了)指针和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指出这一点。

3 个答案:

答案 0 :(得分:1)

JNIEnv实际上不是指向指针的指针,而是指向包含其他(专用)线程特定信息的数据结构。 JNINativeInterface*只是结构中的第一个字段,其余字段不是公共的。这为VM在JNI功能表的实现中提供了更大的灵活性。

一些链接可能会给那些碰到这些问题的人带来好处:

  1. Threads and JNI-在这里说明:

    JNI接口指针(JNIEnv *)仅在当前线程中有效。您不得将接口指针从一个线程传递到另一个线程,也不得缓存接口指针并在多个线程中使用它。在从同一线程连续调用本机方法时,Java虚拟机将为您传递相同的接口指针,但是不同的线程会将不同的接口指针传递给本机方法。

  2. JNI spec

答案 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调用范例的设计。

C JNI调用习惯用语

那为什么要强迫我们这样做

(*env)->NewGlobalRef((*env), my_object);

?额外的间接级别不会传递给以下函数: 第一个参数,因此他们无法更新该指针。

这不是JNI调用的正确格式As the specification attests,事实上,正如从分布式JNI头文件中清楚看到的那样,正确的格式是

JNIEnv *env = /* ... */;

(*env)->NewGlobalRef(env, my_object);

请注意

  1. 在C API中,JNIEnv本身就是指针类型(将->运算符应用于*env所必需),尤其是
  2. 传递给JNI函数的是env本身而不是*env。也就是说,与问题的断言相反,额外的间接调用 传递给了JNI函数

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接口。

(原着重点。)

最终JNI设计

该规范继续提供a high-level description of what having a COM-congruent form means,关键部分是:

本机代码通过调用JNI函数来访问Java VM功能。杰尼 函数可以通过接口指针使用。接口 指针是指向指针的指针。该指针指向一个数组 指针,每个指针都指向一个接口函数。每一个 接口函数位于数组内部的预定义偏移处。

这正是我们实际上看到的,并且规范继续表达它与C ++虚拟函数表和COM接口的相似之处。它还阐明了使用功能表具有以下优点:

  • 从本地名称空间隔离JNI名称空间
  • 允许同一台VM在不同的上下文中提供备用功能表

此外,它解释说,提供指向函数表的双指针有助于将不同的表呈现给不同的线程:

JNI接口指针仅在当前线程中有效。一种 因此,本机方法一定不能从一个方法传递接口指针 线程到另一个。实施JNI的VM可以分配和存储 JNI接口指针指向的区域中的线程本地数据。

本机方法接收JNI接口指针作为参数。的 保证VM会将相同的接口指针传递给本机方法 当它从同一Java多次调用本机方法时 线。但是,可以从不同的Java调用本机方法 线程,因此可能会收到不同的JNI接口指针。

(“ JNI接口指针”是上述的双指针,其类型在C JNI中表示为JNIEnv *。)

结论

C调用范例直接来自该数据和接口设计。必须先取消引用JNI接口指针才能获取功能表指针,然后将接口指针本身(而不是功能表指针)传递给每个函数。

完全相同的事情也发生在C ++ API中,但是通过将函数表指针包装在一个类中并将JNI接口指针伪装为该类实例的指针来掩饰它。这也提供了提供包装函数的机会,这些包装函数掩盖了JNI接口指针传递给JNI函数的事实。我认为这是对C ++功能的充分利用,以该语言提供了一个简单自然的接口,而不是C ++优先设计JNI的证据。

相关问题