Java本机接口规范内容 第2章:设计概述

本章重点介绍JNI中的主要设计问题。 本节中的大多数设计问题都与本机方法有关。 

 

本章包括以下主题:

目录

JNI接口函数和指针

编译,加载和链接本机方法

解析本机方法名称

本机方法参数

引用Java对象

全球和本地参考

实现本地引用

访问Java对象

访问原始数组

访问字段和方法

报告编程错误

Java例外

例外和错误代码

异步异常

异常处理


JNI接口函数和指针

本机代码通过调用JNI函数来访问Java VM功能。 JNI函数可通过接口指针获得 。 接口指针是指向指针的指针。 该指针指向一个指针数组,每个指针指向一个接口函数。 每个接口函数都在数组内的预定义偏移处。 下图“ 接口指针”说明了接口指针的组织。

该图包含从左到右出现的三个元素,以及标有“接口函数”的三个椭圆:

  • 文字:“JNI界面指针”
  • 一列两行的表:
    • 第一行:“指针”
    • 第二行:“每线程JNI数据结构”
  • 标题为“指向JNI函数的指针数组”的一列和四行的表:
    • 第一行:“指针”。 箭头从该表格单元格指向标记为“界面功能”的椭圆形。 没有其他箭头指向这个椭圆形。
    • 第二行:“指针”。 箭头从该表格单元格指向标记为“界面功能”的椭圆形。 没有其他箭头指向这个椭圆形。
    • 第三行:“指针”。 箭头从该表格单元格指向标记为“界面功能”的椭圆形。 没有其他箭头指向这个椭圆形。
    • 第四行:“......”(省略号)

箭头连接这三个元素:

  • 从文本“JNI interace pointer”到表格单元格“Pointer”,从一列和两行的表中
  • 从表格单元格“指针”从一列和两行的表到第一行(带有文本“指针”)的表“指向JNI函数的指针数组”

JNI接口的组织方式类似于C ++虚函数表或COM接口。 使用接口表而不是硬连接函数条目的优点是JNI名称空间与本机代码分离。 VM可以轻松提供多个版本的JNI功能表。 例如,VM可能支持两个JNI函数表:

  • 一个人执行彻底的非法参数检查,适合调试;
  • 另一个执行JNI规范所需的最小量检查,因此更有效。

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

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

编译,加载和链接本机方法

由于Java VM是多线程的,因此本机库也应该与多线程感知的本机编译器一起编译和链接。 例如, -mt标志应该用于使用Sun Studio编译器编译的C ++代码。 对于符合GNU gcc编译器的代码,应使用标志-D_REENTRANT-D_POSIX_C_SOURCE 。 有关更多信息,请参阅本机编译器文档。

本机方法使用System.loadLibrary方法加载。 在以下示例中,类初始化方法加载特定于平台的本机库,其中定义了本机方法f :

package pkg; 

class Cls {
    native double f(int i, String s);
    static {
        System.loadLibrary(“pkg_Cls”);
    }
}

System.loadLibrary的参数是程序员任意选择的库名。 系统遵循标准但特定于平台的方法将库名称转换为本机库名称。 例如,Solaris系统将名称pkg_Cls转换为libpkg_Cls.so ,而Win32系统将相同的pkg_Cls名称转换为pkg_Cls.dll 。

程序员可以使用单个库来存储任意数量的类所需的所有本机方法,只要这些类要使用相同的类加载器加载即可。 VM在内部维护每个类加载器的加载本机库列表。 供应商应选择本地库名称,以尽量减少名称冲突的可能性。

本机库可以与VM 静态链接 。 组合库和VM映像的方式取决于实现。 System.loadLibrary或等效的API必须成功才能将此库视为已加载。

当且仅当库导出名为JNI_OnLoad_L的函数时,其图像已与VM组合的库L被定义为静态链接。

如果静态链接库L导出名为JNI_OnLoad_L的函数和名为JNI_OnLoad的函数,则将忽略JNI_OnLoad函数。

如果库L是静态链接的,那么在第一次调用System.loadLibrary("L")或等效的API时,将使用为JNI_OnLoad function指定的相同参数和预期返回值调用JNI_OnLoad function 。

静态链接的库L将禁止动态加载同名库。

当包含静态链接的本机库L的类加载器被垃圾收集时,如果导出这样的函数,VM将调用库的JNI_OnUnload_L函数。

如果静态链接库L导出名为JNI_OnUnLoad_L的函数和名为JNI_OnUnLoad的函数,则将忽略JNI_OnUnLoad函数。

程序员还可以调用JNI函数RegisterNatives()来注册与类关联的本机方法。 RegisterNatives()函数对于静态链接函数特别有用。

解析本机方法名称

动态链接器根据其名称解析条目。 本机方法名称由以下组件连接:

  • 前缀Java_
  • 一个错位的完全限定的类名
  • 下划线(“_”)分隔符
  • 一个受损的方法名称
  • 对于重载的本机方法,两个下划线(“__”)后跟受损的参数签名

VM检查驻留在本机库中的方法的方法名称匹配。 VM首先查找短名称; 也就是说,没有参数签名的名称。 然后它查找长名称,这是带有参数签名的名称。 只有当本机方法使用另一个本机方法重载时,程序员才需要使用长名称。 但是,如果本机方法与非本地方法具有相同的名称,则这不是问题。 非本地方法(Java方法)不驻留在本机库中。

在以下示例中,本机方法g不必使用长名称进行链接,因为其他方法g不是本机方法,因此不在本机库中。

  class Cls1 {
     int g(int i);
     native int g(double d);
 } 

我们采用了一种简单的名称修改方案,以确保所有Unicode字符都转换为有效的C函数名称。 我们使用下划线(“_”)字符代替完全限定类名中的斜杠(“/”)。 由于名称或类型描述符永远不会以数字开头,因此我们可以使用_0 ,..., _9作为转义序列,如下表所示:

Unicode字符转换
逃脱序列表示
_0XXXXUnicode字符XXXX 。 请注意,小写字母用于表示非ASCII Unicode字符,例如_0abcd而不是_0ABCD 。
_1人物 ”_”
_2签名中的字符“;”
_3签名中的字符“[”

本机方法和接口API都遵循给定平台上的标准库调用约定。 例如,UNIX系统使用C调用约定,而Win32系统使用__stdcall。

本机方法参数

JNI接口指针是本机方法的第一个参数。 JNI接口指针的类型为JNIEnv 。 第二个参数根据本机方法是静态方法还是非静态方法而有所不同。 非静态本机方法的第二个参数是对该对象的引用。 静态本机方法的第二个参数是对其Java类的引用。

其余参数对应于常规Java方法参数。 本机方法调用通过返回值将其结果传递回调用例程。 第3章:JNI类型和数据结构描述了Java和C类型之间的映射。

以下代码示例说明如何使用C函数实现本机方法f 。 本机方法f声明如下:

package pkg; 

class Cls {
    native double f(int i, String s);
    // ...
}

具有长错位名称Java_pkg_Cls_f_ILjava_lang_String_2的C函数实现本机方法f :

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
     JNIEnv *env,        /* interface pointer */
     jobject obj,        /* "this" pointer */
     jint i,             /* argument #1 */
     jstring s)          /* argument #2 */
{
     /* Obtain a C-copy of the Java string */
     const char *str = (*env)->GetStringUTFChars(env, s, 0);

     /* process the string */
     ...

     /* Now we are done with str */
     (*env)->ReleaseStringUTFChars(env, s, str);

     return ...
}

请注意,我们总是使用接口指针env操作Java对象。 使用C ++,您可以编写稍微更简洁的代码版本,如以下代码示例所示:

  extern“C”/ *指定C调用约定* / 

 jdouble Java_pkg_Cls_f__ILjava_lang_String_2(

      JNIEnv * env,/ *接口指针* /
      jobject obj,/ *“this”指针* /
      jint i,/ *参数#1 * /
      jstring s)/ *参数#2 * /

 {
      const char * str = env-> GetStringUTFChars(s,0);

      // ...

      env-> ReleaseStringUTFChars(s,str);

      //返回...
 } 

使用C ++,额外的间接级别和接口指针参数从源代码中消失。 但是,底层机制与C完全相同。在C ++中,JNI函数被定义为内联成员函数,它们扩展为C对应函数。

引用Java对象

原始类型(如整数,字符等)在Java和本机代码之间复制。 另一方面,任意Java对象都通过引用传递。 VM必须跟踪已传递给本机代码的所有对象,以便垃圾收集器不会释放这些对象。 反过来,本机代码必须有一种方法来通知VM它不再需要这些对象。 此外,垃圾收集器必须能够移动本机代码引用的对象。

全球和本地参考

JNI将本机代码使用的对象引用分为两类: 本地 引用全局引用 。 本地引用在本机方法调用的持续时间内有效,并在本机方法返回后自动释放。 全局引用在显式释放之前仍然有效。

对象作为本地引用传递给本机方法。 JNI函数返回的所有Java对象都是本地引用。 JNI允许程序员从本地引用创建全局引用。 JNI函数,期望Java对象接受全局和本地引用。 本机方法可以返回对VM的本地或全局引用作为其结果。

在大多数情况下,程序员应该依赖VM在本机方法返回后释放所有本地引用。 但是,有时程序员应该明确地释放本地引用。 例如,考虑以下情况:

  • 本机方法访问大型Java对象,从而创建对Java对象的本地引用。 然后,本机方法在返回调用方之前执行其他计算。 对大型Java对象的本地引用将阻止对象被垃圾回收,即使该对象不再用于计算的其余部分。
  • 本机方法会创建大量本地引用,但并非所有引用都同时使用。 由于VM需要一定的空间来跟踪本地引用,因此创建太多本地引用可能会导致系统内存不足。 例如,本机方法循环遍历大量对象,将元素作为本地引用检索,并在每次迭代时对一个元素进行操作。 在每次迭代之后,程序员不再需要对数组元素的本地引用。

JNI允许程序员在本机方法中的任何点手动删除本地引用。 为了确保程序员可以手动释放本地引用,不允许JNI函数创建额外的本地引用,除了它们作为结果返回的引用。

本地引用仅在创建它们的线程中有效。 本机代码不能将本地引用从一个线程传递到另一个线程。

实现本地引用

为了实现本地引用,Java VM为从Java到本机方法的每次控制转换创建了一个注册表。 注册表将不可移动的本地引用映射到Java对象,并防止对象被垃圾回收。 传递给本机方法的所有Java对象(包括那些作为JNI函数调用结果返回的对象)都会自动添加到注册表中。 在本机方法返回后删除注册表,允许其所有条目被垃圾回收。

有不同的方法来实现注册表,例如使用表,链表或哈希表。 虽然引用计数可用于避免注册表中的重复条目,但JNI实现没有义务检测和折叠重复条目。

请注意,通过保守扫描本机堆栈,无法忠实地实现本地引用。 本机代码可以将本地引用存储到全局或堆数据结构中。

访问Java对象

JNI在全局和本地引用上提供了丰富的访问器函数。 这意味着无论VM如何在内部表示Java对象,相同的本机方法实现都会起作用。 这是JNI可以被各种VM实现支持的关键原因。

通过不透明引用使用访问器函数的开销高于直接访问C数据结构的开销。 我们相信,在大多数情况下,Java程序员使用本机方法来执行非常重要的任务,这些任务会掩盖此接口的开销。

访问原始数组

对于包含许多基本数据类型的大型Java对象(例如整数数组和字符串),此开销是不可接受的。 (考虑用于执行向量和矩阵计算的本机方法。)迭代Java数组并使用函数调用检索每个元素是非常低效的。

一种解决方案引入了“固定”的概念,以便本机方法可以要求VM确定数组的内容。 然后,本机方法接收指向元素的直接指针。 然而,这种方法有两个含义:

  • 垃圾收集器必须支持固定。
  • VM必须在内存中连续布局原始数组。 虽然这是大多数原始数组最自然的实现,但是布尔数组可以实现为打包或解包。 因此,依赖于布尔数组的确切布局的本机代码将不可移植。

我们采取妥协方案,克服上述两个问题。

首先,我们提供了一组函数来复制Java数组的一段和本机内存缓冲区之间的原始数组元素。 如果本机方法只需要访问大型数组中的少量元素,请使用这些函数。

其次,程序员可以使用另一组函数来检索数组元素的固定版本。 请记住,这些功能可能需要Java VM执行存储分配和复制。 这些函数实际上是否复制数组取决于VM实现,如下所示:

  • 如果垃圾收集器支持固定,并且数组的布局与本机方法的预期相同,则不需要复制。
  • 否则,将数组复制到不可移动的内存块(例如,在C堆中)并执行必要的格式转换。 返回指向副本的指针。

最后,该接口提供了通知VM本机代码不再需要访问数组元素的功能。 当您调用这些函数时,系统会取消数组,或者将原始数组与其不可移动的副本进行协调并释放副本。

我们的方法提供灵活性 垃圾收集器算法可以针对每个给定阵列单独决定复制或固定。 例如,垃圾收集器可以复制小对象,但可以固定较大的对象。

JNI实现必须确保在多个线程中运行的本机方法可以同时访问同一个数组。 例如,JNI可以为每个固定数组保留一个内部计数器,这样一个线程就不会取消固定另一个线程固定的数组。 请注意,JNI不需要锁定原始数组以供本机方法独占访问。 同时从不同的线程更新Java数组会导致不确定的结果。

访问字段和方法

JNI允许本机代码访问字段并调用Java对象的方法。 JNI通过符号名称和类型签名来标识方法和字段。 两步过程会从字段名称和签名中分析出定位字段或方法的成本。 例如,要在类cls中调用方法f ,本机代码首先获取方法ID,如下所示:

  jmethodID mid = env-> GetMethodID(cls,“f”,“(ILjava / lang / String;)D”); 

然后,本机代码可以重复使用方法ID,而无需查找方法,如下所示:

  jdouble result = env-> CallDoubleMethod(obj,mid,10,str); 

字段或方法ID不会阻止VM卸载已从中派生ID的类。 卸载类后,方法或字段ID将变为无效。 因此,本机代码必须确保:

  • 保持对基础类的实时引用,或
  • 重新计算方法或字段ID

如果它打算长时间使用方法或字段ID。

JNI不对内部如何实现字段和方法ID施加任何限制。

报告编程错误

JNI不检查编程错误,例如传入NULL指针或非法参数类型。 非法参数类型包括使用普通Java对象而不是Java类对象。 由于以下原因,JNI不检查这些编程错误:

  • 强制JNI函数检查所有可能的错误条件会降低正常(正确)本机方法的性能。
  • 在许多情况下,没有足够的运行时类型信息来执行此类检查。

大多数C库函数都不能防止编程错误。 例如, printf()函数在收到无效地址时通常会导致运行时错误,而不是返回错误代码。 强制C库函数检查所有可能的错误条件可能会导致重复此类检查 - 一次在用户代码中,然后再次在库中。

程序员不得将非法指针或错误类型的参数传递给JNI函数。 这样做可能会导致任意后果,包括系统状态损坏或VM崩溃。

Java例外

JNI允许本机方法引发任意Java异常。 本机代码也可以处理未完成的Java异常。 未处理的Java异常会传播回VM。

例外和错误代码

某些JNI函数使用Java异常机制来报告错误情况。 在大多数情况下,JNI函数通过返回错误代码抛出Java异常来报告错误情况。 错误代码通常是一个特殊的返回值(如NULL),它超出了正常返回值的范围。 因此,程序员可以:

  • 快速检查上次JNI调用的返回值,以确定是否发生了错误,并且
  • 调用一个函数ExceptionOccurred() ,以获取包含错误条件的更详细描述的异常对象。

在两种情况下,程序员需要检查异常而无法首先检查错误代码:

  • 调用Java方法的JNI函数返回Java方法的结果。 程序员必须调用ExceptionOccurred()来检查在执行Java方法期间可能发生的异常。
  • 某些JNI数组访问函数不返回错误代码,但可能抛出ArrayIndexOutOfBoundsExceptionArrayStoreException 。

在所有其他情况下,非错误返回值可确保不会抛出任何异常。

异步异常

在多线程的情况下,除当前线程之外的线程可能发布异步异常。 异步异常不会立即影响当前线程中本机代码的执行,直到:

  • 本机代码调用可能引发同步异常的JNI函数之一,或
  • 本机代码使用ExceptionOccurred()显式检查同步和异步异常。

请注意,只有那些可能引发同步异常的JNI函数才会检查异步异常。

本机方法应在必要的位置插入ExceptionOccurred()检查(例如在没有其他异常检查的紧密循环中),以确保当前线程在合理的时间内响应异步异常。

异常处理

有两种方法可以处理本机代码中的异常:

  • 本机方法可以选择立即返回,从而导致在启动本机方法调用的Java代码中抛出异常。
  • 本机代码可以通过调用ExceptionClear()来清除异常,然后执行自己的异常处理代码。

引发异常后,本机代码必须首先清除异常,然后再进行其他JNI调用。 当存在挂起的异常时,可以安全调用的JNI函数是:

ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道格拉斯范朋克

播种花生牛奶自留田

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值