第二章以一个非常简单的HelloWorld示例演示了使用JNI技术Java调用C,Java程序只是单纯的调用了C程序,C程序不接受任何参数(除了必须的JNIEnv和jobject外),也不提供返回值。但是,真实项目中很少有机会碰到这么简单的使用JNI的情况,大部分情况是C代码接受参数,也有返回值。这样就涉及到了两种语言之间不同类型的转换。如Java中int与C的int可能定义不同,String与char不同,数组也不同,因此,下面仍然结合示例程序,讲一下两种语言如何类型转换。在下面的讲解中,不会重复出现第二章中已经讲过的命令使用方法和配置环境变量等,如果需要请看第二章内容。本章只会贴出Java和C的源代码,并讲解其中的知识点,javac,javah,gcc等命令的用法不再出现。
本章使用到的函数可以在JNI函数列表页面详细了解函数原型、参数解释、返回值及注意事项等。本章只对函数的使用方法和重要的地方做介绍。
一、调用带参数和返回值的原生方法
这个示例程序中,原生方法的原型为:String getLine(String prompt);。Java提供一个字符串,调用getLine函数,该函数打印这个字符串,并从终端获取用户输入,返回给Java程序。结合这个例子,可以讲解Java与C转换字符串类型和基本类型。
1.Java程序
class Prompt {
//原生函数原型
private native String getLine(String prompt);
public static void main(String argv[]) {
Prompt p = new Prompt();
//提供参数调用原生函数,接受返回值并打印。
String input = p.getLine("Type a line: ");
System.out.println("In Java: " + input);
}
static{
System.loadLibrary("Prompt");
}
}
除了调用带参数和返回值的函数外,这个Java程序没有任何新知识,有不明白的地方可以看第二章。
### 2.C语言原生程序
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "Prompt.h"
//该函数出了接受必须的JNIEnv*和jobject参数外,还接受了jstring参数。
//jstring参数就是Java中提供的字符串,也就是String prompt。
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring p)
{
//获取Java String中的内容,即完成Java字符串到C语言char*的转换。
const char *prompt = (*env)->GetStringUTFChars(env, p, NULL);
if(NULL == prompt)
return NULL;
printf("In C: %s", prompt);
//打印之后,不再使用该字符串了,可以释放,以便Java的GC回收内存。
(*env)->ReleaseStringUTFChars(env, p, prompt);
//获取用户输入,并将C语言的char*转换为Java的String。
char buf[256];
memset(buf, 0, sizeof buf);
scanf("%s", buf);
return (*env)->NewStringUTF(env, buf);
}
编译运行这个项目,如下图所示:
下面说一下原生函数必须接受的两个参数。
2.1JNIEnv*:
它是一个指针,指向一个列表的某个元素,这个元素也是一个指针,指向JNI定义的函数功能列表,这个列表的每一个元素都是一个JNI函数指针,原生方法通过调用这些函数指针完成相应的JNI功能,如读取Java中的数组,访问Java类中的成员变量等等。说着有点儿绕,看下图: 在JNIEnv*指向的列表中,除了Pointer,还有Java虚拟机定义的内部数据结构,这些数据结构包括Java String、数组和类在内存中如何布局等等,用户无法直接访问这些底层的数据结构,只能通过JNI函数来获取数据结构中的值。
2.2jobject或jclass
这是原生方法接受的第二个参数,这个参数有两种可能的类型,一种是jobject,另一种是jclass。当原生函数在Java类中声明为非static,如本例中的原型private native String getLine(String prompt);此时,经过javah转换后的原生函数原型的第二个参数就为jobject;如果在Java类中声明为static,如private native static String getLine(String prompt);此时,第二个参数就为jclass。这很好理解,每个类的实例(也叫对象)都有用一份非static类成员的副本,而类的所有实例都共享一个static类成员。所以,如果是非static的原生方法,则需要用jobjcet来确定该方法所属的实例,而如果是static,则通过jclass来确定该方法所属的类即可。
3.类型映射
根据Java中的不同类型,JNI定义了相关的C和C++类型与之对应。Java中有两种类型分类,一种是基本类型,如int、float等;另一种是引用类型,如类、对象、数组等。Java中的String就是java.lang.String类,所以它属于引用类型。对这两种分类,JNI映射机制不同。对于基本类型,JNI直接映射。对于引用类型,对程序员来讲是不透明的,它们指向Java虚拟机中的内部数据结构,对程序员隐藏了这些数据结构的具体布局和存储细节。在原生代码中,必须通过JNIEnv接口指针调用JNI函数使用和管理内部对象。比如,前面代码中提到使用JNI函数GetStringUTFChars()获取jstring引用的数据。JNI引用的父类型是jobject,为了方便和提高类型安全,JNI也定义了一系列jobject的子类型,这些子类型是Java中经常用到的引用类型。如jstring对应String,jobjectArray指向对象数组。
3.1基本类型
对于基本类型,JNI直接映射。如int对应jint(有符号32位整型),float对应jfloat(32位浮点型),double对应jdouble,Boolean对应jboolean等等。直接映射就说明可以在原生函数中直接读取Java中基本类型变量的值。下面的对上面的示例程序稍作修改,代码如下:
3.1.1 Java代码
class Prompt {
private native int getLine(int i);
public static void main(String argv[]) {
Prompt p = new Prompt();
int i = p.getLine(3);
System.out.println("In Java: " + i);
}
static {
System.loadLibrary("Prompt");
}
}
修改了原生函数的原型,接受参数为int,返回int。
##### 3.1.2 C代码
JNIEXPORT jint JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jint i){
int j = i + 2;
printf("In C: i = %d, j = %dn", i, j);
return j;
}
因为参数为jint类型,是C中int与Java中的int直接映射,因此可以直接访问基本类型的变量的值。
编译后运行,效果如下:
[](http://104.131.143.131/blog/wp-content/uploads/2015/04/Screen-Shot-2015-04-26-at-23.31.51.png)
#### 3.2获取字符串值
字符串在Java中为引用类型,不能zhi直接被映射,需要使用JNI函数才能进行访问。所以,类似如下的原生代码肯定会引起错误:
jstring prompt;//prompt是从原生函数的参数中得到的,假设不为空。
printf("%s", prompt);//不能直接访问引用类型,会引起错误甚至虚拟机崩溃。
3.2.1 转换为原生字符串
把jstring对象转换为C/C++字符串,这种转换支持到/从Unicode和UTF-8字符串。可以使用JNI函数GetStringUTFChars()获取jstring的内容。这个函数把Unicode编码的Java jstring对象转换为UTF-8编码的C/C++原生字符串。转换后,就可以调用printf()等函数对其进行操作。代码片段如下:
//获取Java String中的内容,即完成Java字符串到C语言char*的转换。
const char *prompt = (*env)->GetStringUTFChars(env, p, NULL);
if(NULL == prompt)
return NULL;
printf("In C: %s", prompt);
千万不要忘记检查GetStringUTFChars()函数的返回值是否有效。因为转换为UTF-8编码的字符串是需要分配一定空间的,这就存在分配不成功的问题。一旦分配失败,该函数返回NULL,并且抛出OutOfMemoryError异常。后面章节会详细介绍原生代码抛出异常的相关知识,这里简单说一下。在JNI中抛出异常与在Java中不同,抛出的异常不会自动改变原生代码的执行流,因此这里需要显式的使用return NULL;语句立刻从原生代码中返回。返回后,抛出的异常就可以被原生函数的调用者捕获,从而进行一场处理。
##### 3.2.2 及时释放不再使用的字符串资源
当原生代码中不再使用字符串引用时,在本章的例子中就是使用GetStringUTFChars()函数转换后的字符串,必须要及时释放掉。释放字符串资源可以使用JNI函数ReleaseStringUTFChars()。调用这个方法就表示从GetStringUTFChars()函数返回的字符串不再使用,为它分配的内存空间可以被释放。如果调用这个函数失败,会造成内存泄漏,甚至耗光内存。代码片段如下:
//打印之后,不再使用该字符串了,可以释放,以便Java的GC回收内存。
(*env)->ReleaseStringUTFChars(env, p, prompt);
3.2.3 创建新的字符串
可以调用JNI函数NewStringUTF()创建新的Java String字符串对象。改函数把UTF-8编码的C/C++字符串转换为UniCode编码的Java String对象,并返回改对象的引用。同样的,如果内存不足导致分配失败,该函数返回NULL并抛出OutOfMemory异常。但与GetStringUTFChars()函数不同的是,我们不需要检查其返回值,因为在本章中改函数是原生代码中最后一条语句,直接返回其值即可。如果分配成功,就可以把转换后的Java String返回给原生函数的调用者,如果分配失败,就把NULL返回原生函数的调用者,调用者捕获异常并处理异常。代码片段如下:
//获取用户输入,并将C语言的char*转换为Java的String。
char buf[256];
memset(buf, 0, sizeof buf);
scanf("%s", buf);
return (*env)->NewStringUTF(env, buf);
3.2.4 其他JNI字符串函数介绍
除了前面提到的GetStringUTFChars()、ReleaseStringUTFChars()和NewStringUTF()以外,JNI还包括了大量的字符串相关的函数。
GetStringChars与ReleaseStringChars()读取jstring的字符串,将其保存为Unicode编码格式。Unicode编码的字符串不像C字符串那样以’ ’结尾,所以不能使用strlen()等函数计算其长度,要使用JNI函数GetStringLength()方法获取Unicode编码的字符串。而UTF-8编码的字符串是以’ ’结尾,所以既可以使用strlen()函数,也可以使用JNI函数GetStringUTFLength()计算其长度。
GetStringChars()与GetStringUTFChars()函数接受第三个参数,原型如下:
const jchar *GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
const jchar *GetStringUTFChars(JNIEnv *env, jstring str, jboolean *isCopy);
isCopy参数用于向程序员表明这两个函数返回值的情况。如果返回的值是str的副本的指针,也就是说原生代码重新为转换后的字符串分配内存空间,拷贝src的数据,把这个内存空间的地址作为返回值返回,那么isCopy就为JNI_TRUE。如果返回值为src的指针,即不拷贝数据,则isCopy为JNI_FALSE,在这种情况下,原生代码就不能修改src的数据,否则Java程序中相关的String参数的值也会跟着变。通常情况下,我们并不关心返回值是副本还是直接指针,因为我们不去修改,所以isCopy这个参数一般传递NULL作为实参。
通常无法预测虚拟机是返回副本还是返回直接指针。程序员必须假设虚拟机会花费时间分配内存,拷贝数据,返回副本。虚拟机会把对象分配到堆中,一旦返回直接指针,虚拟机就不能再将对象分配到堆中,而是“钉住”(pin)Java String对象。由于过度使用“钉住”这种方式容易导致内存碎片化,因此虚拟机可能根据情况对每个GetString(UTF)Chars()函数返回Java String对象的副本或返回其直接指针。当不再使用GetStringChars()返回的字符串时,必须记得使用ReleaseStringChars()释放,虚拟机根据isCopy的值释放掉分配的内存或unpin对象。
##### 3.2.5 Java 1.2中新的字符串函数
为了提高直接返回指针的可能性(直接返回指针避免了数据拷贝),Java 1.2推出了一对函数:Get/ReleaseStringCritical()。它们与Get/ReleaseString(UTF)Chars()函数一样,尽可能的返回直接指针,否则返回副本。但是使用前者有很重要的约束,那就是在这对函数之间的代码不能够阻塞、引起死锁或动态分配内存等,否则会导致虚拟机崩溃。这一个限制可以让虚拟机在这对函数包含的区域中禁用垃圾回收(Garbage Collection),也就是说虚拟机暂时不再管理这块代码中的内存情况(如对直接指针的pin活unpin),但此时垃圾回收机制被禁用了,此进程的其想要使用垃圾回收机制的线程就会被阻塞,直到执行流跳出这对函数包含的区域。因此,在这段区域中不允许阻塞调用(如等待文件IO)、动态分配对象(malloc或new)和调用任何其他的JNI函数(除了Get/ReleaseStringCritical()和Get/ReleasePrimitiveArrayCritical()函数外),否则可能让虚拟机陷入死锁,导致程序崩溃。这个函数对可以任意嵌套,示例代码如下:
jchar *s1, *s2;
s1 = (*env)->GetStringCritical(env, jstr1);
if (s1 == NULL) {... /* error handling */}
s2 = (*env)->GetStringCritical(env, jstr2);
if (s2 == NULL)
{
(*env)->ReleaseStringCritical(env, jstr1, s1);
... /* error handling */
}
/* use s1 and s2 */
(*env)->ReleaseStringCritical(env, jstr1, s1);
(*env)->ReleaseStringCritical(env, jstr2, s2);
可以看到,s1与s2的函数对没有严格的先后关系,这就是说可以随意嵌套。
另外两个有用的新函数是GetStringUTFRegion()和GetStringRegion()。这两个函数把jstring转换后拷贝到预先定义好了的数组/容器中。3.1.2节的代码可以作如下修改:
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char outbuf[128], inbuf[128];
int len = (*env)->GetStringLength(env, prompt);
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);
printf("%s", outbuf);
scanf("%s", inbuf);
return (*env)->NewStringUTF(env, inbuf);
}
3.2.6 使用字符串函数的建议
最好使用GetString(UTF)Region()函数,因为它不必管理内存,内存的分配和管理交给用户,效率高,不容易出错。如果确定代码块不会造成阻塞或死锁等情况,可以使用Get/ReleaseStringCritical()函数,因为它们可以返回jstring的直接指针,免去数据拷贝。当然,最简单常用的还是Get/ReleaseString(UTF)Chars()函数。
3.3 访问数组
数组包括基本数据类型数组如int[],和对象数组如object[]。在JNI中不存在二维数组,二维数组就是数组的数组,属于对象数组,以此类推。
3.3.1 访问基本数据类型数组
我们以一个例子讲解:在Java中创建int[],调用原生函数计算int[]中所有元素的和。
Java代码如下:
class IntArray {
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {arr[i] = i; }
//调用原生方法sumArray()求给定int[]的和
int sum = p.sumArray(arr);
System.out.println(“sum = “ + sum);
}
static {
System.loadLibrary(“IntArray”);
}
}
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
//读取arr[0]到arr[9]共10个int型元素,并复制到buf中
(env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++)
sum += buf[i];
return sum;
}
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv env, jobject obj, jintArray arr)
{
jint carr;
jint i, sum = 0;
carr = (env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL)
return 0; / exception occurred /
for (i=0; i<10; i++)
sum += carr[i];
(env)->ReleaseIntArrayElements(env, arr, carr, 0);
return sum;
}
对于基础数据类型数组的访问,建议使用Get/Set<Primitive Type>ArrayRegion()函数对读取、写入jarray函数。当确定代码块不会导致阻塞等情况时尽可能使用Get/ReleasePrimitiveArrayCritical()函数对。还可以选择Get/Release<Primitive Type>ArrayElements()函数对。
3.3.2 访问对象数组
JNI为对象数组提供了单独的一对函数用于读写其元素:Get/SetObjectArrayElement()函数。不能一次性拷贝多个对象元素,只能一次获取一个。
下面的例子在原生代码中创建并初始化二维整型数组int[][],在Java中打印元素。注意,二维数组在JNI中表示为数组的数组,因此属于对象数组。
Java代码:
class ObjectArrayTest {
private static native int[][] initInt2DArray(int size);
public static void main(String[] args) {
//调用原生方法initInt2DArray()函数初始化二维数组,参数是维数。
//在这表示33大小的int数组
int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(“ “ + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.loadLibrary(“ObjectArrayTest”);
}
}
JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv env, jclass cls, int size)
{
//定义对象数组。
jobjectArray result;
int i;
//int[]类型的引用
jclass intArrCls = (env)->FindClass(env, “[I”);
if (intArrCls == NULL)
return NULL; / exception thrown /
result = (env)->NewObjectArray(env, size, intArrCls, NULL);
if (result == NULL)
return NULL; / out of memory error thrown /
for (i = 0; i < size; i++)
{
jint tmp[256]; / make sure it is large enough! /
int j;
jintArray iarr = (env)->NewIntArray(env, size);
if (iarr == NULL)
return NULL; / out of memory error thrown /
for (j = 0; j < size; j++)
tmp[j] = i + j;
(env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(env)->SetObjectArrayElement(env, result, i, iarr);
(env)->DeleteLocalRef(env, iarr);
}
return result;
}
0 1 2
1 2 3
2 3 4