JNI接口函数与指针
平台相关代码是通过调用 JNI 函数来访问 Java 虚拟机功能的。JNI 函数可通过接口指针来获得。接口指针是指针的指针,它指向一个指针数组,而指针数组中的每个元素又指向一个接口函数。每个接口函数都处在数组的某个预定偏移量中。下图 说明了接口指针的组织结构:
JNI 接口的组织类似于 C++ 虚拟函数表或 COM 接口。使用接口表而不使用硬性编入的函数表的好处是使 JNI 名字空间与平台相关代码分开。虚拟机可以很容易地提供多个版本的 JNI 函数表。例如,虚拟机可支持以下两个 JNI 函数表:
- 一个表对非法参数进行全面检查,适用于调试程序;
- 另一个表只进行 JNI 规范所要求的最小程度的检查,因此效率较高。
JNI 接口指针只在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程中。实现 JNI 的虚拟机可将本地线程的数据分配和储存在 JNI 接口指针所指向的区域中。
本地方法将 JNI 接口指针当作参数来接受。虚拟机在从相同的 Java 线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNI 接口指针。
加载和链接本地方法
对本地方法的加载通过 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。
程序员可用单个库来存放任意数量的类所需的所有本地方法,只要这些类将被相同的类加载器所加载。虚拟机在其内部为每个类加载器保护其所加载的本地库清单。提供者应该尽量选择能够避免名称冲突的本地库名。
如果底层操作系统不支持动态链接,则必须事先将所有的本地方法链接到虚拟机上。这种情况下,虚拟机实际上不需要加载库即可完成 System.loadLibrary 调用。
程序员还可调用 JNI 函数 RegisterNatives() 来注册与类关联的本地方法。在与静态链接的函数一起使用时,RegisterNatives() 函数将特别有用。
解析本地方法名
动态链接程序是根据项的名称来解析各项的。本地方法名由以下几部分串接而成:
- 前缀 Java_
- mangled 全限定的类名
- 下划线(“_”)分隔符
- mangled 方法名
- 对于重载的本地方法,加上两个下划线(“__”),后跟 mangled 参数签名
虚拟机将为本地库中的方法查找匹配的方法名。它首先查找短名(没有参数签名的名称),然后再查找带参数签名的长名称。只有当某个本地方法被另一个本地方法重载时程序员才有必要使用长名。但如果本地方法的名称与非本地方法的名称相同,则不会有问题。因为非本地方法(Java 方法)并不放在本地库中。
下例中,不必用长名来链接本地方法 g,因为另一个方法 g 不是本地方法,因而它并不在本地库中。
class Cls1 {
int g(int i);
native int g(double d);
}
我们采取简单的名字搅乱方案,以保证所有的 Unicode 字符都能被转换为有效的 C 函数名。我们用下划线(“_”) 字符来代替全限定的类名中的斜杠(“/”)。
本地方法和接口 API 都要遵守给定平台上的库调用标准约定。例如,UNIX 系统使用 C 调用约定,而 Win32 系统使用 __stdcall。
本地方法的参数
JNI 接口指针是本地方法的第一个参数。其类型是 JNIEnv。第二个参数随本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用。
其余的参数对应于通常 Java 方法的参数。本地方法调用利用返回值将结果传回调用程序中。
代码示例说明了如何用 C 函数来实现本地方法 f。对本地方法 f 的声明如下:
package pkg;
class Cls {
native double f(int i, String s);
...
}
具有长 mangled 名称 Java_pkg_Cls_f_ILjava_lang_String_2 的 C 函数实现本地方法f:
代码示例 2-1: 用 C 实现本地方法
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* 接口指针 */
jobject obj, /* “this”指针 */
jint i, /* 第一个参数 */
jstring s) /* 第二个参数 */
{
/* 取得 Java 字符串的 C 版本 */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* 处理该字符串 */
...
/* 至此完成对 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, /* 第一个参数 */
jstring s) /* 第二个参数 */
{
const char *str = env->GetStringUTFChars(s, 0);
...
env->ReleaseStringUTFChars(s, str);
return ...
}
使用 C++ 后,源代码变得更为直接,且接口指针参数消失。但是,C++ 的内在机制与 C 的完全一样。在 C++ 中,JNI 函数被定义为内联成员函数,它们将扩展为相应的 C 对应函数。