1 绪论
JNI 是一个原生编程接口。它允许在 Java 虚拟机(JVM)内运行的 Java 代码与用其它编程语言(如 C、C++ 和汇编)编写的应用程序和库进行互操作。
JNI 最重要的好处是它对底层 JVM 的实现没有限制。因此,JVM 供应商可以添加对 JNI 的支持,而不会影响 JVM 的其它部分。程序员可以编写一个原生应用程序或库的一个版本,并期望它与支持 JNI 的所有 JVM 一起工作。
虽然可以完全用 Java 编写应用程序,但在某些情况下,仅靠 Java 无法满足应用程序的需求。程序员使用 JNI 编写 Java 原生方法来处理应用程序不能完全用 Java 编写的情况。
以下示例说明了何时需要使用 Java 原生方法:
- 标准 Java 类库不支持应用程序所需的依赖于平台的功能。
- 已经有一个用另一种语言编写的库,并希望通过 JNI 使 Java 代码可以访问它。
- 希望用低级语言(如汇编语言)实现一小部分时间关键型代码。
通过 JNI 编程,您可以使用原生方法:
- 创建、检查和更新 Java 对象(包括数组和字符串)。
- 调用 Java 方法。
- 捕获并抛出异常。
- 加载类并获取类信息。
- 执行运行时类型检查。
2 JNI 的设计
2.1 JNI 接口函数和指针
原生代码通过调用 JNI 函数来访问 JVM 特性。JNI 函数可通过接口指针使用。接口指针是指向指针的指针。此指针指向一个指针数组,每个指针都指向一个接口函数。每个接口函数都位于数组内的预定义偏移处。下图 “接口指针” 说明了接口指针的组织。
JNI 接口的组织方式类似于 C++ 虚拟函数表。使用接口表而不是硬连线函数条目的优点是 JNI 名称空间与原生代码分离。VM 可以轻松提供多个版本的 JNI 函数表。例如,VM 可能支持两个 JNI 函数表:
- 一个执行彻底的非法参数检查,适合调试;
- 另一个执行 JNI 规范所需的最小检查量并且因此更高效。
JNI 接口指针仅在当前线程中有效。因此,本机方法不得将接口指针从一个线程传递到另一个线程。实现 JNI 的 VM 可以在 JNI 接口指针指向的区域中分配和存储线程本地数据。
原生方法接收 JNI 接口指针作为参数。当 VM 从同一 Java 线程对原生方法进行多次调用时,它保证会将相同的接口指针传递给原生方法。但是,可以从不同的 Java 线程调用原生方法,因此可能会接收不同的 JNI 接口指针。
2.2 编译、加载和链接原生方法
原生方法使用System.loadLibrary
方法加载。在以下示例中,类初始化方法加载了一个特定于平台的原生库,其中定义了原生方法f
:
package p.q.r;
class A {
native double f(int i, String s);
static {
System.loadLibrary("p_q_r_A");
}
}
System.loadLibrary
的参数是程序员任意选择的库名称。该系统遵循一种标准但特定于平台的方法,将库名称转换为原生库名称。例如,Linux 系统将名称p_q_r_A
转换为libp_q_r_A.so
,而 Windows 系统将相同的p_q_r_A
名称转换为p_q_r_A.dll
。
程序员可以使用单个库来存储任意数量的类所需的所有原生方法,只要这些类要用相同的类加载器加载。VM 在内部为每个类加载器维护一个已加载的原生库列表。
2.2.1 解析原生方法名称
JNI 定义了一个 1:1 的映射,从 Java 中声明的原生方法的名称到驻留在原生库中的原生方法的名称。VM 使用此映射将原生方法的 Java 调用动态链接到原生库中的对应实现。
映射通过连接从原生方法声明派生的以下组件来生成原生方法名称:
- 前缀
Java_
- 给定声明原生方法的类的内部形式的二进制名称:转义名称的结果。
- 下划线(
_
) - 转义的方法名
- 如果原生方法声明重载:两个下划线(
__
)后跟方法声明的转义参数描述符(JVMS 4.3.3)。
转义使每个字母数字 ASCII 字符(A-Za-z0-9)保持不变,并用相应的转义序列替换下表中的每个UTF-16代码单元。如果要转义的名称包含代理项对,则高代理项代码单元和低代理项代码单位将分别转义。转义的结果是一个仅由ASCII字符a-Za-z0-9和下划线组成的字符串。
转义是必要的,原因有二。首先,确保 Java 源代码中的类和方法名(可能包含 Unicode 字符)在 C 源代码中转换为有效的函数名。其次,确保使用;
和[
字符对参数类型进行编码的原生方法的参数描述符可以用 C 函数名进行编码。
当 Java 程序调用原生方法时,VM 首先通过查找原生方法名称的短版本(即没有转义参数签名的名称)来搜索原生库。如果找不到具有短名称的原生方法,则 VM 会查找原生方法名称的长版本,即包含转义参数签名的名称。
首先查找短名称可以更容易地在原生库中声明实现。例如,给定 Java 中的原生方法:
package p.q.r;
class A {
native double f(int i, String s);
}
对应的 C 函数可以命名为Java_p_q_r_A_f
,而不是Java_p_q_r_A_f__ILjava_lang_String_2
。
只有当类中的两个或多个原始呢个方法具有相同名称时,才需要在原生库中声明具有长名称的实现。例如,给定 Java 中的这些原生方法:
package p.q.r;
class A {
native double f(int i, String s);
native double f(int i, Object s);
}
对应的 C 函数必须命名为Java_p_q_r_A_f__ILjava_lang_String_2
和Java_p_q_r_A_f__ILjava_lang _Object_2
,因为原生方法已重载。
如果 Java 中的原生方法仅被非原生方法重载,则原生库中的长名称是不必要的。在下面的示例中,原生方法g
不必使用长名称链接,因为另一个方法g
不是原生方法,因此不驻留在原生库中。
package p.q.r;
class B {
int g(int i);
native int g(double d);
}
请注意,转义序列可以安全地以_0
、_1
等开头,因为 Java 源代码中的类名和方法名从不以数字开头。然而,在不是从 Java 源代码生成的类文件中,情况并非如此。为了保留与原生方法名称的 1:1 映射,VM 按如下方式检查结果名称。如果从原生方法声明(类或方法名,或参数类型)中转义任何前体字符串的过程导致前体字符串中的“0”、“1”、“2”或“3”字符在结果中紧随下划线之后或转义字符串的开头(它将在完全组装的名称中的下划线之后)保持不变,则称转义过程“失败”。在这种情况下,不会执行原生库搜索,尝试链接原生方法调用将抛出UnsatisfiedLinkError。有可能将目前的简单映射方案扩展到涵盖此类情况,但复杂性成本将超过任何收益。
原生方法和接口API都遵循给定平台上的标准库调用约定。例如,UNIX系统使用C调用约定,而Win32系统使用__stdcall
。
原生方法也可以使用RegisterNatives函数显式链接。请注意,RegisterNatives可以通过更改给定原生Java方法要执行的原生代码来更改JVM的记录行为(包括加密算法、正确性、安全性、类型安全性)。因此,请谨慎使用具有使用RegisterNatives函数的原生库的应用程序。
2.2.2 原生方法参数
JNI 接口指针是原生方法的第一个参数。JNI 接口指针的类型为JNIEnv
。第二个参数因原生方法是静态的还是非静态的而异。非静态原生方法的第二个参数是对对象的引用。静态原生方法的第二个参数是对其Java类的引用。
其余的参数对应于常规的Java方法参数。原生方法调用通过返回值将其结果传递回调用例程。
以下代码示例说明了如何使用 C 函数实现原生方法f
。原生方法的声明如下:
package p.q.r;
class A {
native double f(int i, String s);
// ...
}
长名称为Java_p_q_r_A_f__ILjava_lang_String_2
的 C 函数实现了原生方法f
:
jdouble Java_p_q_r_A_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" /* specify the C calling convention */
jdouble Java_p_q_r_A_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
const char *str = env->GetStringUTFChars(s, 0);
// ...
env->ReleaseStringUTFChars(s, str);
// return ...
}
在C++中,额外的间接层和接口指针参数从源代码中消失了。然而,底层机制与C完全相同。在C++中,JNI函数被定义为可扩展到C对应函数的内联成员函数。