序
这四种情况下你会用到本书:
1、 在 Java 程序中复用以前写过的 C/C++代码。
2、 自己实现一个 java 虚拟机
3、 学习不同语言如何进行协作,尤其是如何实现垃圾回收和多线程。
4、 把一个虚拟机实现整合到用 C/C++写的程序中。
本书是写给开发者的。JNI 在 1997 年第一次发布,本书总结了 SUN 工程师和大量开发者两
年来积累的经验。
本书介绍了 JNI 的设计思想,对这种思想的理解是使用 JNI 的各种特性的基础。
本书有一部分是 JAVA2 平台上面的 JNI 特征的规范说明。JNI 程序员可以把这部分用作一个
手册。JVM 开发者在实现虚拟机的时候必须遵守这些规范。
JNI 的部分设计思想来源于 Netscape 的 Java Runtime Interface(JRI)。
第一章 简介
JNI 是 JAVA 平台的一个重要特征,使用它我们可以重用以前用 C/C++写的大量代码。本书
既是一个编程指南也是一个 JNI 手册。本书共包括三部分:
1、 第二章通过一个简单的例子介绍了 JNI。它的对象是对 JNI 不熟悉的初学者。
2、 3~10 章对 JNI 的特征进行了系统的介绍。我们会举大量的例子来说明 JNI 的各个特征,
这些特征都是 JNI 中重要且常用的。
3、 11~13 章是关于 JNI 的技术规范。可以把这两章当作一个手册。
本书尽量去满足各类读者的需要。指南面向初学者,手册面向有经验的人和自己实现 JNI
规范的人。大部分读者可能是用 JNI 来写程序的开发者。本书会假设你有 JAVA,C/C++基
础。
本章的剩余部分介绍了 JNI 的背景,扮演的角色和 JNI 的演化。
1.1 JAVA 平台和系统环境(Host Environment)
系统环境代指本地操作系统环境,它有自己的本地库和 CPU 指令集。本地程序(Native
Applications)使用 C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的
二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系
统环境。比如,一个系统下编译出来的 C 程序不能在另一个系统中运行。
1.2 JNI 扮演的角色
JNI 的强大特性使我们在使用 JAVA 平台的同时,还可以重用原来的本地代码。作为虚拟机
实现的一部分,JNI 允许 JAVA 和本地代码间的双向交互。
图 1.1 JNI 的角色
JNI 可以这样与本地程序进行交互:
1、 你可以使用 JNI 来实现“本地方法”(native methods),并在 JAVA 程序中调用它们。
2、 JNI 支持一个“调用接口”(invocation interface),它允许你把一个 JVM 嵌入到本地程序
中。本地程序可以链接一个实现了 JVM 的本地库,然后使用“调用接口”执行 JAVA 语
言编写的软件模块。例如,一个用 C 语言写的浏览器可以在一个嵌入式 JVM 上面执行
从网上下载下来的 applets
1.3 JNI 的副作用
请记住,一旦使用 JNI,JAVA 程序就丧失了 JAVA 平台的两个优点:
1、 程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
2、 程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。
一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了 JAVA 和 C 之间
的耦合性。
1.4 什么场合下应该使用 JNI
当你开始着手准备一个使用 JNI 的项目时,请确认是否还有替代方案。像上一节所提到的,
应用程序使用 JNI 会带来一些副作用。下面给出几个方案,可以避免使用 JNI 的时候,达到
与本地代码进行交互的效果:
1、 JAVA 程序和本地程序使用 TCP/IP 或者 IPC 进行交互。
2、 当用 JAVA 程序连接本地数据库时,使用 JDBC 提供的 API。
3、 JAVA 程序可以使用分布式对象技术,如 JAVA IDL API。
这些方案的共同点是,JAVA 和 C 处于不同的线程,或者不同的机器上。这样,当本地程序
崩溃时,不会影响到 JAVA 程序。
下面这些场合中,同一进程内 JNI 的使用无法避免:
1、 程序当中用到了 JAVA API 不提供的特殊系统环境才会有的特征。而跨进程操作又不
现实。
2、 你可能想访问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存,
数据传递方面。
3、 JAVA 程序当中的一部分代码对效率要求非常高,如算法计算,图形渲染等。
总之,只有当你必须在同一进程中调用本地代码时,再使用 JNI。
1.5 JNI 的演化
JDK1.0 包含了一个本地方法接口,它允许 JAVA 程序调用 C/C++写的程序。许多第三方的
程序和 JAVA 类库,如:java.lang,java.io,java.net 等都依赖于本地方法来访问底层系统环境的
特征。
不幸的是,JDK1.0 中的本地方法有两个主要问题:
1、 本地方法像访问 C 中的结构(structures)一样访问对象中的字段。尽管如此,JVM 规范
并没有定义对象怎么样在内存中实现。如果一个给定的 JVM 实现在布局对象时,和本
地方法假设的不一样,那你就不得不重新编写本地方法库。
2、 因为本地方法可以保持对 JVM 中对象的直接指针,所以,JDK1.0 中的本地方法采用了
一种保守的 GC 策略。
JNI 的诞生就是为了解决这两个问题,它可以被所有平台下的 JVM 支持:
1、 每一个 VM 实现方案可以支持大量的本地代码。
2、 开发工具作者不必处理不同的本地方法接口。
3、 最重要的是,本地代码可以运行在不同的 JVM 上面。
JDK1.1 中第一次支持 JNI,但是,JDK1.1 仍在使用老风格的本地代码来实现 JAVA 的 API。
这种情况在 JDK1.2 下被彻底改变成符合标准的写法。
1.6 例子程序
本书包含了大量的代码示例,还教我们如何使用 javah 来构建 JNI 程序。
第二章 开始。 。。
本章通过一个简单的例子来示例如何使用 JNI。我们写一个 JAVA 程序,并用它调用一个 C
函数来打印“Hello World!”。
2.1 概述
图 2.1 演示了如何使用 JAVA 程序调用 C 函数来打印“Hello World!”。这个过程包含下面几
步:
1、 创建一个类(HelloWorld.java)声明本地方法。
2、 使用 javac 编译源文件 HollowWorld.java,产生 HelloWorld.class。使用 javah –jni 来生成
C 头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。
3、 用 C 代码写函数原型的实现。
4、 把 C 函数实现编译成一个本地库,创建 Hello-World.dll 或者 libHello-World.so。
5、 使用 java 命令运行 HelloWorld 程序,类文件 HelloWorld.class 和本地库(HelloWorld.dll
或者 libHelloWorld.so)在运行时被加载。
图 2.1 编写并运行“HelloWorld”程序
本章剩余部分会详细解释这几步。
第三章 基本类型、字符串、数组
开发者使用 JNI 时最常问到的是 JAVA 和 C/C++之间如何传递数据,以及数据类型之间如何
互相映射。本章我们从整数等基本类型和数组、字符串等普通的对象类型开始讲述。至于如
何传递任意对象,我们将在下一章中进行讲述。
3.1 一个简单的本地方法
JAVA 端源代码如下:
class Prompt {
// native method that prints a prompt and reads a line
private native String getLine(String prompt);
public static void main(String args[]) {
Prompt p = new Prompt();
String input = p.getLine("Type a line: ");
System.out.println("User typed: " + input);
}
static {
System.loadLibrary("Prompt");
}
}
3.1.1 本地方法的 C 函数原型
Prompt.getLine 方法可以用下面这个 C 函数来实现:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
其中,JNIEXPORT 和 JNICALL 这两个宏(被定义在 jni.h)确保这个函数在本地库外可见,
并且 C 编译器会进行正确的调用转换。C 函数的名字构成有些讲究,在 11.3 中会有一个详
细的解释。
3.1.2 本地方法参数
第一个参数 JNIEnv 接口指针,指向一个个函数表,函数表中的每一个入口指向一个 JNI 函
数。本地方法经常通过这些函数来访问 JVM 中的数据结构。图 3.1 演示了 JNIEnv 这个指针:
图 3.1 JNIEnv 接口指针
第二个参数根据本地方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方
法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本
地方法所在的对象。我们的例子当中,Java_Prompt_getLine 是一个实例方法,因此 jobject
参数指向方法所在的对象。
3.1.3 类型映射
本地方法声明中的参数类型在本地语言中都有对应的类型。JNI 定义了一个 C/C++类型的集
合,集合中每一个类型对应于 JAVA 中的每一个类型。
JAVA 中有两种类型:基本数据类型(int,float,char 等)和引用类型(类,对象,数组等)。
JNI 对基本类型和引用类型的处理是不同的。基本类型的映射是一对一的。例如 JAVA 中的
int 类型直接对应 C/C++中的 jint(定义在 jni.h 中的一个有符号 32 位整数)。12.1.1 包含了
JNI 中所有基本类型的定义。
JNI 把 JAVA 中的对象当作一个 C 指针传递到本地方法中,这个指针指向 JVM 中的内部数
据结构,而内部数据结构在内存中的存储方式是不可见的。本地代码必须通过在 JNIEnv 中
选择适当的 JNI 函数来操作 JVM 中的对象。例如,对于 java.lang.String 对应的 JNI 类型是
jstring,但本地代码只能通过 GetStringUTFChars 这样的 JNI 函数来访问字符串的内容。
所有的 JNI 引用都是 jobject 类型,对了使用方便和类型安全,JNI 定义了一个引用类型集合,
集合当中的所有类型都是 jobject 的子类型。这些子类型和 JAVA 中常用的引用类型相对应。
例如,jstring 表示字符串,jobjectArray 表示对象数组。
3.2 访问字符串
Java_Prompt_getLine 接收一个 jstring 类型的参数 prompt,jstring 类型指向 JVM 内部的一个
字符串,和常规的 C 字符串类型 char*不同。你不能把 jstring 当作一个普通的 C 字符串。
3.2.1 转换为本地字符串
本地代码中,必须使用合适的 JNI 函数把 jstring 转化为 C/C++字符串。JNI 支持字符串在
Unicode 和 UTF-8 两种编码之间转换。Unicode 字符串代表了 16-bit 的字符集合。UTF-8 字
符串使用一种向上兼容 7-bit ASCII 字符串的编码协议。UTF-8 字符串很像 NULL 结尾的 C
字符串,在包含非 ASCII 字符的时候依然如此。所有的 7-bitASCII 字符的值都在 1~127 之
间,这些值在 UTF-8 编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多
字节字符(16-bitUnicode 值)。
函数 Java_Prompt_getLine 通过调用 JNI 函数 GetStringUTFChars 来读取字符串的内容。
GetStringUTFChars 可以把一个 jstring 指针(指向 JVM 内部的 Unicode 字符序列)转化成一
个 UTF-8 格式的 C 字符串。如何你确信原始字符串数据只包含 7-bit ASCII 字符,你可以把
转化后的字符串传递给常规的C 库函数使用,如printf。我们会在 8.2 中讨论如何处理非ASCII
字符串。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than
* 127 characters */
scanf("%s", buf);
return
不要忘记检查 GetStringUTFChars。因为 JVM 需要为新诞生的 UTF-8 字符串分配
内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars 会返
回 NULL,并抛出一个 OutOfMemoryError 异常(对异常的处理在第 6 章)。这些
JNI 抛出的异常与 JAVA 中的异常是不同的。一个由 JNI 抛出的未决的异常不会
改变程序执行流,因此,我们需要一个显示的 return 语句来跳过 C 函数中的剩
余 语 句 。Java_Prompt_getLine 函 数 返 回 后 , 异 常 会 在 Prompt.main
(Prompt.getLine 这个发生异常的函数的调用者)中抛出,
3.2.2 释放本地字符串资源
从 GetStringUTFChars 中获取的 UTF-8 字符串在本地代码中使用完毕后,要使用
ReleaseStringUTFChars 告诉 JVM 这个 UTF-8 字符串不会被使用了,因为这个
UTF-8 字符串占用的内存会被回收。
3.2.3 构造新的字符串
你 可 以 通 过 JNI 函 数 NewStringUTF 在 本 地 方 法 中 创 建 一 个 新 的
java.lang.String 字符串对象。这个新创建的字符串对象拥有一个与给定的
UTF-8 编码的 C 类型字符串内容相同的 Unicode 编码字符串。
如果一个 VM 不能为构造 java.lang.String 分配足够的内存,NewStringUTF 会
抛出一个 OutOfMemoryError 异常,并返回一个 NULL。在这个例子中,我们不必
检 查 它 的返 回 值 ,因 为 本 地方 法 会 立即 返 回 。如 果 NewStringUTF 失 败 ,
OutOfMemoryError 这个异常会被在 Prompt.main(本地方法的调用者)中抛出。
如果 NeweStringUTF 成功,它会返回一个 JNI 引用,这个引用指向新创建的
java.lang.String 对 象 。 这 个 对 象 被 Prompt.getLine 返 回 然 后 被 赋 值 给
Prompt.main 中的本地 input。
3.2.4 其它 JNI 字符串处理函数
JNI 支持许多操作字符串的函数,这里做个大致介绍。
GetStringChars 和 ReleaseStringChars 获取以 Unicode 格式编码的字符串。当
操作系统支持 Unicode 编码的字符串时,这些方法很有用。
UTF-8 字符串以’\0’结尾,而 Unicode 字符串不是。如果 jstring 指向一个
Unicode 编 码 的 字 符 串 , 为 了 得 到 这 个 字 符 串 的 长 度 ,可 以 调 用
GetStringLength。如果一个 jstring 指向一个 UTF-8 编码的字符串,为了得到
这个字符串的字节长度,可以调用标准 C 函数 strlen。或者直接对 jstring 调
用 JNI 函数 GetStringUTFLength,而不用管 jstring 指向的字符串的编码格式。
GetStringChars 和 GetStringUTFChars 函数中的第三个参数需要更进一步的解
释:
const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
当从 JNI 函数 GetStringChars 中返回得到字符串 B 时,如果 B 是原始字符串
java.lang.String 的拷贝,则 isCopy 被赋值为 JNI_TRUE。如果 B 和原始字符串
指向的是 JVM 中的同一份数据,则 isCopy 被赋值为 JNI_FALSE。当 isCopy 值为
JNI_FALSE 时,本地代码决不能修改字符串的内容,否则 JVM 中的原始字符串也
会被修改,这会打破 JAVA 语言中字符串不可变的规则。
通常,因为你不必关心 JVM 是否会返回原始字符串的拷贝,你只需要为 isCopy
传递 NULL 作为参数。
JVM 是否会通过拷贝原始 Unicode 字符串来生成 UTF-8 字符串是不可以预测的,
程序员最好假设它会进行拷贝,而这个操作是花费时间和内存的。一个典型的
JVM 会在 heap 上为对象分配内存。一旦一个 JAVA 字符串对象的指针被传递给本
地代码,GC 就不会再碰这个字符串。换言之,这种情况下,JVM 必须 pin 这个对
象。可是,大量地 pin 一个对象是会产生内存碎片的,因为,虚拟机会随意性地
来选择是复制还是直接传递指针。
当你不再使用一个从 GetStringChars 得到的字符串时,不管 JVM 内部是采用复
制还是直接传递指针的方式,都不要忘记调用 ReleaseStringChars。根据方法
GetStringChars 是复制还是直接返回指针,ReleaseStringChars 会释放复制对
象时所占的内存,或者 unpin 这个对象。
3.2.5 JDK1.2 中关于字符串的新 JNI 函数
为了提高 JVM 返回字符串直接指针的可能性,JDK1.2 中引入了一对新函数,
Get/ReleaseStringCritical。表面上,它们和 Get/ReleaseStringChars 函数差
不多,但实际上这两个函数在使用有很大的限制。
使用这两个函数时,你必须两个函数中间的代码是运行在"critical region"(临界区)
的,即,这两个函数中间的本地代码不能调用任何会让线程阻塞或等待 JVM 中的其它线程
的本地函数或 JNI 函数。
有了这些限制, JVM 就可以在本地方法持有一个从 GetStringCritical 得到的字符串的直接
指针时禁止 GC。当 GC 被禁止时,任何线程如果触发 GC 的话,都会被阻塞。而
Get/ReleaseStringCritical 这两个函数中间的任何本地代码都不可以执行会导致阻塞的调用或者
为新对象在 JVM 中分配内存。否则,JVM 有可能死锁,想象一下这样的场景中:
1、 只有当前线程触发的 GC 完成阻塞并释放 GC 时,由其它线程触发的 GC 才可能由阻塞
中释放出来继续运行。
2、 在这个过程中,当前线程会一直阻塞。因为任何阻塞性调用都需要获取一个正被其它线
程持有的锁,而其它线程正等待 GC。
Get/ReleaseStringCritical 的交迭调用是安全的,这种情况下,它们的使用必
须有严格的顺序限制。而且,我们一定要记住检查是否因为内存溢出而导致它的
返回值是 NULL。因为 JVM 在执行 GetStringCritical 这个函数时,仍有发生数
据复制的可能性,尤其是当 JVM 内部存储的数组不连续时,为了返回一个指向连
续内存空间的指针,JVM 必须复制所有数据。
总之,为了避免死锁,在 Get/ReleaseStringCritical 之间不要调用任何 JNI
函数。Get/ReleaseStringCritical 和 Get/ReleasePrimitiveArrayCritical 这
两个函数是可以的。
下面代码演示了这对函数的正确用法:
jchar *s1, *s2;