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

Why does the JNI C API use a pointer-to-pointer instead of straight pointers for JNIEnv?

免责声明:我实现了自己的面向对象编程语言,因此熟悉(并教授过)指针和 C。内存管理 101 是不是这个问题是关于什么的。这也不是关于看起来相似的 JNI C++ API。谢谢。

当您查看 Java 的 JNI C API 时,在 jni.h 中,JNIEnv 是指向 [= 的指针的 typedef 16=]。鉴于所有 JNI C API 都采用 JNIEnv*,这意味着它实际上是一个 JNINativeInterface **,表示指向 struct.

的指针

为什么 JNI 使用额外的间接级别?

在 C 中模拟一个类似对象的结构在 JNINativeInterface* 中工作得很好。你可以打电话

env->NewGlobalRef(env, my_object);

所以为什么要强迫我们做

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

?额外的间接级别作为第一个参数传递给函数,所以我猜他们可以更新那个指针。是吗?

更正: 我最初记错了并传递 (*env) 而不是 env 作为第一个参数,因此排除了被调用者编辑指针本身的可能性。我已经更正了 post。感谢 John Bollinger 指出这一点。

JNIEnv 并不是真正指向指针的指针,而是指向包含其他(私有)线程特定信息的数据结构。 JNINativeInterface* 只是结构中的第一个字段,其余的不是 public。这使得 VM 对 JNI 函数表的实现更加灵活。

这里的一些链接是为了那些可能会遇到这个问题的人:

  1. Threads and JNI - 此处说明:

    The JNI interface pointer (JNIEnv *) is only valid in the current thread. You must not pass the interface pointer from one thread to another, or cache an interface pointer and use it in multiple threads. The Java Virtual Machine will pass you the same interface pointer in consecutive invocations of a native method from the same thread, but different threads pass different interface pointers to native methods.

  2. JNI spec

来自jni.h,有评论:

We use inlined functions for C++ so that programmers can write: env->FindClass("java/lang/String") in C++ rather than: (*env)->FindClass(env, "java/lang/String") in C.

比如在struct JNIEnv_中,如果是c++,有一个方法:

jclass FindClass(const char *name) {
    return functions->FindClass(this, name);
}

class有一个指针:

const struct JNINativeInterface_ *functions;

[这是 C 唯一可见的东西]。它是一个指向虚函数的指针 table.

所以,AFAICT,正确的 deref [for C] 是:

env->functions->FindClass(env,name)

成员函数是这样调用的,也符合上面引用的注释。

那么,您确定

env->functions->FindClass(*env,name)

有效吗?

碰巧 (*env)->FindClass(env,name) 有效,因为 functions 第一个 元素 [并且对于 C 是唯一的元素]。

所以,对我来说,我会创建一个执行取消引用的宏:

#define DEREF(_env) ((_env)->functions)
DEREF(env)->FindClass(env,name)

TL;DR:二进制兼容性、多线程支持和所需的接口特性等多种因素导致了 JNI 的设计以及由此而来的 C 调用范例.

C JNI调用惯用语

so why force us to do

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

? The extra level of indirection is not passed into the functions as the first argument, so they can't update that pointer.

这不是 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 接口为代价。需要将 JNI C 接口设计为使用调用习惯用法的唯一 技术 原因是 JNI 函数可以将一个环境换成另一个环境,但是有没有理由认为任何 JNI 函数都会这样做,或者认为任何 JNI 函数都会这样做。 (稍后将对此设计选择进行更多评论。)

Chapter 1 of the specification 提供尽可能多的官方故事。它讨论了 JNI 当前(第二次)主要迭代的历史背景和设计目标。特别是,Sun 的立场是,设计良好的界面将具有以下优点:

  • Each VM vendor can support a larger body of native code.
  • Tool builders will not have to maintain different kinds of native method interfaces.
  • Application programmers will be able to write one version of their native code and this version will run on different VMs.

在与各相关方协商后,他们得出了这些高级设计要求:

  • Binary compatibility - The primary goal is binary compatibility of native method libraries across all Java VM implementations on a given platform. Programmers should maintain only one version of their native method libraries for a given platform.
  • Efficiency - To support time-critical code, the native method interface must impose little overhead. All known techniques to ensure VM-independence (and thus binary compatibility) carry a certain amount of overhead. We must somehow strike a compromise between efficiency and VM-independence.
  • Functionality - The interface must expose enough Java VM internals to allow native methods to accomplish useful tasks.

文档表达了对 COM 作为实现这些目标的接口技术的高度赞赏,事实上,Microsoft 已经为其 Java 1 VM 创建了一个 COM 接口。但当然,COM 也有一些问题,不仅是技术细节 相对于 vis Java,还有平台上(非)可用性的小问题感兴趣的,包括 Sun 自己的 Solaris。因此,我认为这可能是所提出问题的真正答案:

Although Java objects are not exposed to the native code as COM objects, the JNI interface itself is binary-compatible with COM. JNI uses the same jump table structure and calling convention that COM does. This means that, as soon as cross-platform support for COM is available, the JNI can become a COM interface to the Java VM.

(强调在原文中。)

最终的 JNI 设计

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

Native code accesses Java VM features by calling JNI functions. JNI functions are available through an interface pointer. An interface pointer is a pointer to a pointer. This pointer points to an array of pointers, each of which points to an interface function. Every interface function is at a predefined offset inside the array.

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

  • 将 JNI 命名空间与本机命名空间隔离
  • 允许同一个 VM 在不同的上下文中提供替代功能 table

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

The JNI interface pointer is only valid in the current thread. A native method, therefore, must not pass the interface pointer from one thread to another. A VM implementing the JNI may allocate and store thread-local data in the area pointed to by the JNI interface pointer.

Native methods receive the JNI interface pointer as an argument. The VM is guaranteed to pass the same interface pointer to a native method when it makes multiple calls to the native method from the same Java thread. However, a native method can be called from different Java threads, and therefore may receive different JNI interface pointers.

(A "JNI interface pointer"就是前面提到的双指针,其类型在C JNI中表示为JNIEnv *。)

结论

C 调用范例直接遵循该数据和接口设计。必须取消引用 JNI 接口指针以获得函数 table 指针,并且接口指针本身,而不是函数 table 指针,被传递给每个函数。

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