Step By Step(Java 本地方法篇)

    本篇是该系列的最后一篇文章,就让我们以JNI作为结束吧!

    众所周知,使用多种语言协同开发时,经常会导致一些未知问题的发生,也会给我们程序的调试带来一定的负担。鉴于此,我们也只有在充分了解到必要性之后再决定用C/C++来替换部分Java代码,以达到我们预期的目的,考虑使用本地代码主要有以下三个理由:
    1)    你的应用需要访问系统的各个特性和设备,这些特性和设备通过Java平台是无法访问的;
    2)    你已经有了大量的测试过和调试过得用另一种语言编写的代码,并且知道如何将其导出到所有的目标平台上;
    3)    通过基准测试,你已经发现所编写的Java代码比用其他语言编写的等价代码要慢得多。
    在Java中提供了用于和本地C语言代码进行互操作的API,称为Java本地接口(JNI)。
       
    1.    第一个本地方法:
    1)    声明一个含有本地方法的类:

1     public class HelloNative {
2 public static native void greeting();
3 }

    注意以上代码中的native关键字,以及在函数声明的末尾直接用分号结束该函数的声明。
    2)    将该.java文件编译成.class文件,如果使用eclipse直接用IDE编译即可,否则可以使用命令行:
    javac HelloNative.java
    将会发现在当前目录下生成了相应的HelloNative.class文件。
    3)    通过%JAVA_HOME%/bin/javah命令在命令行窗口为HelloNative.class文件生成相应的C语言使用的头文件。
    javah HelloNative  #这里不要添加.class后缀,执行目录为HelloNative.class所在的目录。
    需要注意的是,在我们的例子中并没有给HelloNative方法添加包名,如:

1     package mytest;
2 public class HelloNative {
3 public static native void greeting();
4 }

    针对该种情况在基于.class文件生成C语言要求的头文件时,需要做出以下调整:
    javah mytest.HelloNative     #这里在类的前面添加了包名mytest,包名和类名之间使用dot作为连接符。还需要注意的是执行当前命令的目录为HelloNative.class所在目录的上一级目录,如果包的名字为package mytest.first; 则执行目录为HelloNative.class的上两级目录,以此类推。执行成功后将会在当前执行目录下生成一个名为mytest_HelloNative.h(针对包名为mytest的case)。该文件中包含了和HelloNative.greeting()对应的C语言导出函数的声明。为了实现本地代码,要求生成的C函数名符合Java在JNI中定义的命名规则,如:
    1)    使用完整的Java类名.方法名,如:HelloNative.greeting。如果类属于某个包,那么在前面添加包名,如mytest.HelloNative.greeting;
    2)    用下划线替换掉所有的dot,并加上Java_前缀,如:Java_mytest_HelloNative_greeting;
    3)    如果你重载本地方法,既使用相同名字提供多个本地方法,那么必须在名称后附加两个下划线,后面再加上已编码的参数类型。如:Java_mytest_HelloNative_greeting__表示当前没有参数的greeting,如果在Java中同时定义了另一个greeting(int repeat),其对应的C函数声明为Java_mytest_HelloNative_greeting__I,这里的I表示参数类型,更为具体的描述,后面会逐步给出。
    以下为该头文件的代码:   

 1     /* DO NOT EDIT THIS FILE - it is machine generated */
2 #include <jni.h>
3 /* Header for class mytest_HelloNative */
4 #ifndef _Included_mytest_HelloNative
5 #define _Included_mytest_HelloNative
6 #ifdef __cplusplus
7 extern "C" {
8 #endif
9 /*
10 * Class: mytest_HelloNative
11 * Method: greeting
12 * Signature: ()V
13 */
14 JNIEXPORT void JNICALL Java_mytest_HelloNative_greeting
15 (JNIEnv *, jclass);
16
17 #ifdef __cplusplus
18 }
19 #endif
20 #endif

    我们后面需要做的是为该头文件创建一个对应的HelloNative.c文件包含导出函数的实现代码,如:

1     #include "HelloNative.h"
2 #include <stdio.h>
3 JNIEXPORT void JNICALL Java_mytest_HelloNative_greeting(JNIEnv* env, jclass cl) {
4 printf("Hello Native World!\n");
5 }

    注意如果使用了C++的代码,由于Java并未提供和C++的交互能力,因此需要在该函数前加extern "C"来表示按照C语言的方法导出,以阻止编译器生成C++特有的代码,如:

1     extern "C"    
2 JNIEXPORT void JNICALL Java_mytest_HelloNative_greeting(JNIEnv* env, jclass cl) {
3 cout << "Hello, Native World!" << endl;
4 }

    我们下一步需要做的是利用平台已有的编译工具在命令行窗口编译该文件,如:
    Linux:gcc -fPIC -I jdk/include -I jdk/include/linux -shared -o libHelloNative.so HelloNative.c
    Windows: c1 -I jdk/include -I jdk/include/win32 -LD HelloNative.c -FeHelloNative.dll (也可以通过VC的IDE完成)
    生成动态库文件之后,需要将其拷贝到path(win32)或LD_LIBRARY_PATH(Linux)环境变量包含的目录下,以便于执行时操作系统可以定位到该动态库,也可以将当前动态库所在的目录添加到path(win32)或LD_LIBRARY_PATH(Linux)中。
    最后一步我们需要做的是实现调用该本地方法的Java代码,如:

 1     package mytest;
2 public class MyTest {
3 //这里调用loadLibrary方法用于加载该动态库到JVM中,以保证在使用时该方法已经被加载。
4 static {
5 System.loadLibrary("HelloNative");
6 }
7 public static void main(String[] args) throws Exception {
8 HelloNative.greeting();
9 }
10 }
11 /* 输出结果如下:
12 Hello, Native World!
13 */

    2.    带有数值类型参数和返回值的本地方法:
    先介绍一下Java中原始类型和C语言(JNI)中原始类型之间的对照关系,以及他们所占有的字节数量:
    Java语言        C语言            字节数量
    boolean         jboolean         1
    byte             jbyte             1
    char             jchar             2
    short            jshort            2
    int               jint              4
    long             jlong             8
    float            jfloat             4
    double          jdouble          8
    1)    编写带有native方法的Java代码:

1     package mytest;
2 public class Printf {
3 public static native int print(int width,int precision,double x);
4 static {
5 System.loadLibrary("Printf");
6 }
7 }

    2)    编写与native方法对应的C语言代码:(记着先执行命令行命令javah mytest.Printf来生成mytest_Printf.h头文件)

 1     #include "mytest_Printf.h"
2 #include <stdio.h>
3 JNIEXPORT jint JNICALL Java_mytest_Printf_print(JNIEnv* env, jclass cl
4 ,jint width, jint precision, jdouble x)
5 {
6 char fmt[30];
7 jint ret;
8 sprintf(fmt,"%%%d.%df",width,precision);
9 ret = printf(fmt,x);
10 fflush(stdout);
11 return ret;
12 }

    3)    编写调用native方法的Java测试代码:

 1     package mytest;
2 public class MyTest {
3 public static void main(String[] args) throws Exception {
4 int count = Printf.print(8,4,3.14);
5 count += Printf.print(8,4,count);
6 System.out.println();
7 for (int i = 0; i < count; ++i) {
8 System.out.print("-");
9 }
10 System.out.println();
11 }
12 }
13 /* 输出结果如下:
14 3.1400 8.0000
15 ----------------
16 */

    3.    带有字符串参数的本地方法:
    JNI中提供了一组C语言的函数用于帮助在Java和C之间传递字符串数据,下面的代码会给出详细的注释说明。
    1)    编写带有native方法的Java代码:

1     public class Printf {
2 public static native int print(String format,double x);
3 static {
4 System.loadLibrary("Printf");
5 }
6 }

    2)    编写与native方法对应的C语言代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5 JNIEXPORT jstring JNICALL Java_Printf_print
6 (JNIEnv* env, jclass cl, jstring format, jdouble x)
7 {
8 //1. 这里通过C语言调用JNI本地函数时,需要将参数env解引用之后在调用。
9 //env是指向所有JNI本地函数指针表的指针。
10 //2. 返回"改良UTF-8"编码的字符串的指针,或者当不能构建字符数组时返回NULL。
11 //直到RelaseStringUTFChars函数调用前,该指针一致有效。
12 //3. 由于Java针对字符串使用了对象池的技术,因此这里一定不能修改返回的const char*
13 const char* cformat = env->GetStringUTFChars(format,NULL);
14 //4. 获取参数中jstring的长度(GetStringUTFLength)
15 char* cret = (char*)calloc(env->GetStringUTFLength(format) + 20,1);
16 sprintf(cret,cformat,x);
17 //5. 根据"改良UTF-8"字节序列,返回一个新的Java对象,或者当字符串构造时,返回NULL。
18 jstring ret = env->NewStringUTF(cret);
19 free(cret);
20 //6. 这里需要手工调用ReleaseStringUTFChars方法,以便Java的垃圾收集器可以回收该资源。
21 env->ReleaseStringUTFChars(format,cformat);
22 return ret;
23 }

    3)    编写调用native方法的Java测试代码:

 1     public class MyTest {
2 public static void main(String[] args) throws Exception {
3 double price = 44.95;
4 double tax = 7.75;
5 double amountDue = price * (1 + tax / 100);
6 String s = Printf.print("Amount due = %8.2f", amountDue);
7 System.out.println(s);
8 }
9 }
10 /* 输出结果如下:
11 Amount due = 48.43
12 */

    4.    带有中文的字符串参数的本地方法:
    1)    编写带有native方法的Java代码: 该代码和上面示例中的代码完全相同。
    2)    编写与native方法对应的C语言代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5 #include <wchar.h>
6
7 JNIEXPORT jstring JNICALL Java_Printf_print
8 (JNIEnv* env, jclass cl, jstring format, jdouble x)
9 {
10 //1. 当传递的字符串中包含中文等unicode字符时,推荐使用GetStringChars这一组函数,
11 //当只是包含ASCII的时候,推荐使用上面例子中的GetStringUTFChars那一组函数。
12 //2. 返回Unicode编码的字符串的指针,或者当不能构建字符数组时返回NULL。
13 //直到RelaseStringChars函数调用前,该指针一致有效。
14 //3. 由于Java针对字符串使用了对象池的技术,因此这里一定不能修改返回的const jchar*
15 const jchar* wformat = env->GetStringChars(format,NULL);
16 jchar* wret = (jchar*)calloc(env->GetStringLength(format) + 20,sizeof(jchar));
17 swprintf((wchar_t*)wret,(const wchar_t*)wformat,x);
18 size_t length = wcslen((wchar_t*)wret);
19 //4. 这里需要用JNI提供的NewString方法将本地的unicode字符串构造成jstring。
20 //和Java进行字符串交互时只能使用jstring。
21 jstring ret = env->NewString(wret,length);
22 free(wret);
23 //5. 这里需要手工调用ReleaseStringChars方法,以便Java的垃圾收集器可以回收该资源。
24 env->ReleaseStringChars(format,wformat);
25 return ret;
26 }

    3)    编写调用native方法的Java测试代码:

 1     public class MyTest {
2 public static void main(String[] args) throws Exception {
3 double price = 44.95;
4 double tax = 7.75;
5 double amountDue = price * (1 + tax / 100);
6 //注意这里的参数字符串中含有unicode字符。
7 String s = Printf.print("Amount due 你好= %8.2f", amountDue);
8 System.out.println(s);
9 }
10 }

    5.    编写更为复杂的字符串参数的本地方法:
    这个示例代码和上面代码中的主要不同源自C语言部分,本示例使用了JNI提供的GetStringUTFRegion和GetStringRegion,通过预置空间的方式将参数jstring中的字符串数据copy到该预置空间中,因此本例的Java部分代码和上例中Java代码完全相同,这里仅仅给出C语言部分的代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5 #include <wchar.h>
6
7 JNIEXPORT jstring JNICALL Java_Printf_print
8 (JNIEnv* env, jclass cl, jstring format, jdouble x)
9 {
10 size_t size = env->GetStringLength(format);
11 //1. 这里要求预置的空间大于size * 2.
12 wchar_t wformat[128];
13 memset(wformat,0,sizeof(wformat));
14
15 //2. 通过将jstring中数据直接copy到预置的空间中,则不需要再调用ReleaseStringChars
16 //的方法告诉JVM的垃圾收集器,与此同时该预置的空间在copy之后可以做任何读写操作,
17 //而不像之前用到的GetStringChars和GetStringUTFChars返回的字符串,必须是只读的。
18 //3. 和GetStringChars与GetStringUTFChars的对应关系雷同,JNI还提供了GetStringUTFRegion
19 //与GetStringRegion对应,用于处理仅包含ASCII字符的字符串传递。
20 env->GetStringRegion(format,0,size,(jchar*)wformat);
21 jchar* wret = (jchar*)calloc(size + 20,sizeof(jchar));
22 swprintf((wchar_t*)wret,(const wchar_t*)wformat,x);
23 size_t length = wcslen((wchar_t*)wret);
24 jstring ret = env->NewString(wret,length);
25 free(wret);
26 return ret;
27 }

    在VC中调试C语言的DLL:
    就像开篇中提到的,跨语言开发势必会给后期的程序调试带来一定的麻烦,特别是在需要调试本地库代码的时候,这时我们有多个选择来调试我们的程序,如输出日志,基于IDE的Debug等,很明显后者会是我们的首选。下面就将介绍一下如何在IDE中调试本地库的代码,我们这里只是以VC为例,至于Linux下的gcc,其方法基本相同。
    1)    我们现在假设调用Java本地方法的测试代码已经可以执行,也不会抛出UnsatisfiedLinkError的异常,只是在程序运行的过程中native方法出现逻辑错误,或是一些指针操作的错误。
    2)    这里我使用的是Visual Studio 2005,如果打算利用VC的IDE调试本地库代码,需要在VC中进行一些必要的配置,以便Java的程序能够拉起该DLL。
    3)    鼠标右键点击"解决方案资源管理器"窗口中的工程名称,如"Print",选择弹出菜单中的"属性"条目,弹出工程的属性页。
    4)    点击右侧的"调试",然后在左侧的"命令"中填入C:\Program Files\Java\jdk1.6.0_23\bin\java,在"命令参数"中填入"MyTest"(结合上面的示例),最后在"工作目录"中填入"DLL工程路径\debug",如"D:\HelloNative\debug"。
    5)    将eclipse工程路径下bin文件夹(如:D:\Programs\Java Programs\Workspace\MyTest\bin)的类文件拷贝到DLL工程路径\debug下(如:D:\HelloNative\debug)。
    6)    由于从java.exe启动,单步调试是汇编代码,所以要在调试的函数中设置断点,运行到断点处再单步调试。
    6.    访问域:
    这个代码示例和前面的示例将会有两点主要差异,一是native方法不在是static方法,而是普通的对象方法,再者就是C语言中的代码将会利用JNI提供的C接口访问Java对象中域字段的信息。
    1)    含有native方法的Java代码:

 1     public class Employee {
2 public Employee(String n,double s) {
3 name = n;
4 salary = s;
5 }
6 public native void raiseSalary(double byPercent);
7 public void print() {
8 System.out.println(name + " " + salary);
9 }
10 private String name;
11 private double salary;
12 static {
13 System.loadLibrary("Employee");
14 }
15 }

    2)  实现本地方法的C语言代码:

 1     #include "Employee.h"
2 #include <stdio.h>
3
4 //1. 由于该示例的native方法为非静态non-static域方法,而是普通的域方法。
5 //该对应函数的第二个参数也由静态方法中jclass改为jobject,可以将其视为this。
6 JNIEXPORT void JNICALL Java_Employee_raiseSalary
7 (JNIEnv* env, jobject this_obj, jdouble byPercent)
8 {
9 //2.获取this对象的Class对象,如Java代码中的getClass()方法。
10 jclass class_Employee = env->GetObjectClass(this_obj);
11 //3.获取域字段的结构体,使用方式和Java中通过反射获取Class的域字段如出一辙。
12 //最后一个参数"D",表示域字段的类型,D表示double,后面会给出详细的列表。
13 jfieldID id_salary = env->GetFieldID(class_Employee,"salary","D");
14 //4.JNI中为了保证JVM的安全,不允许C语言中的代码直接访问Java类中的数据,
15 //这样可以避免暴露JVM中对象布局的细节,而是提供了一组JNI接口函数访问域字段。
16 //这里由于salary是double类型,如果是int类型的域字段可以调用GetIntField函数
17 //获得,不同的域字段类型以此类推。
18 jdouble salary = env->GetDoubleField(this_obj,id_salary);
19 salary *= 2 + byPercent /100;
20 //5. 设置域字段和获取域字段的调用机制相同。对于静态域字段的方法可以调用
21 //env->SetStaticXxxField(jclass,fieldID,value)这里的Xxx表示类型,如本例的Double。
22 env->SetDoubleField(this_obj,id_salary,salary);
23 }

    需要注意的是jclass对象class_Employee会随着本次native方法的结束而被回收,因此不能直接保留该变量到某全局变量或类的成员变量,以便下次再调用时不用再获取该对象。如果没有特殊的处理,在每次进入native的开始部分均需要调用env->GetObjectclass()JNI方法获取该变量。如果确实希望避免这种重复的调用,可使用下面的技巧予以规避:

1     static jclass class_X = 0;
2 static jfieldID id_a;
3 ...
4 if (class_X == 0) {
5 jclass cx = env->GetObjectClass(obj);
6 class_X = env->NewGlobalRef(cx);
7 id_a = env->GetFieldID(env,cls,"a","...");
8 }

    然而需要注意的是,在结束对类的使用时,务必调用:env->DeleteGlobalRef(class_X)来通知JVM释放该资源。
    3)    含有main函数的调用测试代码:

 1     public class MyTest {
2 public static void main(String[] args) throws Exception {
3 Employee[] staff = new Employee[3];
4 staff[0] = new Employee("Harry Hacker",35000);
5 staff[1] = new Employee("Carl Cracker",75000);
6 staff[2] = new Employee("Tony Tester",38000);
7 for (Employee e : staff)
8 e.raiseSalary(5);
9 for (Employee e : staff)
10 e.print();
11 }
12 }
13 /* 输出结果如下:
14 Harry Hacker 71750.0
15 Carl Cracker 153750.0
16 Tony Tester 77900.0
17 */

    7.    编码签名:
    为了访问实例域和调用Java编程语言中定义的方法,你必须学习"编入"数据类型的名称和方法签名的规则,见如下编码方案:
    B       byte
    C       char
    D       double //如下例中env->GetFieldID(class_Employee,"salary","D");
    F       float
    I        int
    J        long
    S       short
    V       void
    Z       boolean
    Lclassname;    类的类型
    如果描述数组类型,需要使用[,如果二维数组[[,以此类推。如:
    [Ljava/lang/String;     //String一维数组
    [[F        //float[][]二维数组
    要建立一个方法的完整签名,需要把括号内的参数类型都列出来,然后列出返回值类型,如:
    (II)I     //一个接收两个整型参数并返回一个整数的方法编码。
    上例中的print方法的签名是:(Ljava/lang/String;)V //表示该方法接收一个字符串String参数,返回值为void。
    注意,在L表达式结尾处的分号是类型表达式的终止符,而不是参数之间的分隔符,如上例中Employee的构造函数:
    Employee(java.lang.String,double,java.util.Date); 具有如下签名:
    (Ljava/lang/String;DLjava/util/Date;)V
    可以看到在D和Ljava/util/Date;之间没有分隔符。另外要注意在这个编码方案中,必须用/代替.来分隔包名和类名。结尾的V表示返回类型为void。即使对Java的构造器没有指定返回类型,也需要将V添加到JVM签名中。
    8.    调用Java非静态方法:
    Java中可以通过JNI调用native的C语言方法,同样JNI也提供了方法可以让C语言中的方法调用Java的方法,见下例:
    1)    包含native方法的Java类代码:

1     import java.io.PrintWriter;
2 public class Printf {
3 public static native void fprint(PrintWriter out,String format,double x);
4 static {
5 System.loadLibrary("Printf");
6 }
7 }

    2)    实现本地方法的C语言代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5
6 JNIEXPORT void JNICALL Java_Printf_fprint
7 (JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
8 {
9 const char* cformat = env->GetStringUTFChars(format,NULL);
10 char* cstr = (char*)calloc(strlen(cformat) + 20,sizeof(char));
11 sprintf(cstr,cformat,x);
12 jstring str = env->NewStringUTF(cstr);
13 free(cstr);
14 env->ReleaseStringUTFChars(format,cformat);
15
16 jclass class_PrintWriter = env->GetObjectClass(out);
17 //对应于void PrintWrite.print(String)方法
18 jmethodID id_print = env->GetMethodID(class_PrintWriter,"print","(Ljava/lang/String;)V");
19 //这里Void是CallXxxMethod一组方法中的一个,表示调用函数的返回值类型。
20 //因此该方法的返回值也是void,如果调用CallIntMethod,其返回值将为jint。
21 env->CallVoidMethod(out,id_print,str);
22 }

    3)    含有main函数的调用测试代码:

 1     import java.io.PrintWriter;
2 public class MyTest {
3 public static void main(String[] args) throws Exception {
4 double price = 44.95;
5 double tax = 7.75;
6 double amountDue = price * (1 + tax / 100);
7 PrintWriter out = new PrintWriter(System.out);
8 Printf.fprint(out,"Amount due = %8.2f\n", amountDue);
9 out.flush();
10 }
11 }

    9.    调用Java静态方法:
    这里我们可以直接修改上例中的C语言本地方法,在其中加入一些code,执行静态方法System.getProperty("java.class.path"),并将结果打印到控制台。

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5
6 JNIEXPORT void JNICALL Java_Printf_fprint
7 (JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
8 {
9 const char* cformat = env->GetStringUTFChars(format,NULL);
10 char* cstr = (char*)calloc(strlen(cformat) + 20,sizeof(char));
11 sprintf(cstr,cformat,x);
12 jstring str = env->NewStringUTF(cstr);
13 free(cstr);
14 env->ReleaseStringUTFChars(format,cformat);
15
16 jclass class_PrintWriter = env->GetObjectClass(out);
17 //对应于void PrintWrite.print(String)方法
18 jmethodID id_print = env->GetMethodID(class_PrintWriter,"print","(Ljava/lang/String;)V");
19 //这里Void是CallXxxMethod一组方法中的一个,表示调用函数的返回值类型。
20 //因此该方法的返回值也是void,如果调用CallIntMethod,其返回值将为jint。
21 env->CallVoidMethod(out,id_print,str);
22
23 //访问静态方法System.getProperty的代码如下:
24 //1. 由于没有System的实例存在,因此需要使用FindClass方法,而不是上面使用的
25 //GetObjectClass()
26 jclass class_System = env->FindClass("java/lang/System");
27 //2. String System.getProperty(String) 为该静态方法的Java原型
28 //由于是静态方法,因此JNI方法名有别于上面的GetMethodID。
29 jmethodID id_getProperty = env->GetStaticMethodID(class_System,"getProperty",
30 "(Ljava/lang/String;)Ljava/lang/String;");
31 //3. 注意CallStaticObjectMethod中Static同样表示调用的是static方法,Object表示
32 //返回的是Object,这里String就是Object。
33 jobject obj_ret = env->CallStaticObjectMethod(class_System,id_getProperty
34 ,env->NewStringUTF("java.class.path"));
35 //4. 从jobject到jstring的向下转型。
36 jstring str_ret = (jstring)obj_ret;
37 const char* outStr = env->GetStringUTFChars(str_ret,NULL);
38 printf("The java.class.path is %s.\n",outStr);
39 env->ReleaseStringUTFChars(str_ret,outStr);
40 }

    10.    在C本地方法中构造Java中的对象:
    我们为了方便起见,继续复用上面的Java代码,而只是修改C语言的本地方法,见如下代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5 JNIEXPORT void JNICALL Java_Printf_fprint
6 (JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
7 {
8 const char* cformat = env->GetStringUTFChars(format,NULL);
9 char* cstr = (char*)calloc(strlen(cformat) + 20,sizeof(char));
10 sprintf(cstr,cformat,x);
11 jstring str = env->NewStringUTF(cstr);
12 free(cstr);
13 env->ReleaseStringUTFChars(format,cformat);
14
15 jclass class_PrintWriter = env->GetObjectClass(out);
16 //对应于void PrintWrite.print(String)方法
17 jmethodID id_print = env->GetMethodID(class_PrintWriter,"print","(Ljava/lang/String;)V");
18 //这里Void是CallXxxMethod一组方法中的一个,表示调用函数的返回值类型。
19 //因此该方法的返回值也是void,如果调用CallIntMethod,其返回值将为jint。
20 env->CallVoidMethod(out,id_print,str);
21
22 //我们这里实例化的是FileOutputStream对象
23 const char filename[] = "D:/test.txt";
24 jstring str_filename = env->NewStringUTF(filename);
25 jclass class_FileOutputStream = env->FindClass("java/io/FileOutputStream");
26 //获取构造函数时,方法名定义为<init>
27 jmethodID id_FileOutputStream = env->GetMethodID(class_FileOutputStream,"<init>"
28 ,"(Ljava/lang/String;)V");
29 jobject obj_Stream = env->NewObject(class_FileOutputStream,id_FileOutputStream,str_filename);
30 printf("Over\n");
31 }

    基于本例中实例化的对象,如果打算写出数据到文件,则需要继续获取write方法的jmethod ID,之后还要获取close方法的jmethod ID,因此在使用时还是比较麻烦的。
    11.    访问数组元素:
    先给出Java数组类型和C数组类型之间的对应关系:
    Java数组类型            C数组类型(JNI)
    boolean[]                 jbooleanArray
    byte[]                     jbyteArray
    char[]                     jcharArray
    int[]                        jintArray
    short[]                    jshortArray
    long[]                     jlongArray
    float[]                     jfloatArray
    double[]                  jdoubleArray
    Object[]                  jobjectArray
    JNI提供了以下几个C接口函数用于访问和操作Java中的数组数据:
    GetArrayLength()获取数组的长度;
    GetObjectArrayElement()和SetObjectArrayElement访问和设置对象数组中的元素,这样的访问方式,如果需要进行矩阵运算的高密度计算工作时,效率会比较低下,因此JNI中提供了另外一组接口方法用于直接获取数组的首地址,就像在C语言代码中操作数组一样,但是该组方法只能应用于原始数据类型,如int,short,float等。
    GetXxxArrayElements和ReleaseXxxArrayElements,这里的Xxx表示不同的原始数据类型。需要注意的是,所有对数组数据的修改只有在调用ReleaseXxxArrayElements方法后才能保证所作的改变在原始Java数组中得到反映。如下例中的部分C接口(JNI)代码:
    1)    包含native方法的Java类代码:

1     public class UpdateArray {
2 public static native void update(double[] d);
3 static {
4 System.loadLibrary("UpdateArray");
5 }
6 }

    2)    实现本地方法的C语言代码:

 1     #include "UpdateArray.h"
2 JNIEXPORT void JNICALL Java_UpdateArray_update
3 (JNIEnv* env, jclass cl, jdoubleArray da)
4 {
5 double sacleFactor = 2.0;
6 double* a = env->GetDoubleArrayElements(da,NULL);
7 int length = env->GetArrayLength(da);
8 for (int i = 0; i < length; ++i) {
9 a[i] = a[i] * sacleFactor;
10 }
11 env->ReleaseDoubleArrayElements(da,a,0);
12 }

    3)    含有main函数的调用测试代码:

 1     public class MyTest {
2 public static void main(String[] args) throws Exception {
3 double[] dd = new double[10];
4 for (int i = 0; i < dd.length; ++i) {
5 dd[i] = i;
6 }
7 System.out.println("The Array before updated is ");
8 for (double d : dd)
9 System.out.print(d + " ");
10 System.out.println();
11 UpdateArray.update(dd);
12 System.out.println("The Array after updated is " + dd);
13 for (double d : dd)
14 System.out.print(d + " ");
15 }
16 }
17 /* 输出结果如下:
18 The Array before updated is
19 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0
20 The Array after updated is [D@13f3045
21 0.0 2.0 4.0 6.0 8.0 10.0 12.0 14.0 16.0 18.0
22 */

    12.    异常错误处理:
    在Java编程语言中,可以通过异常中断当前正在执行的操作,并退出当前操作所在的函数。然而当我们使用native方法的时候,由于是C语言并不支持异常,这样便需要JNI提供一组操作Java异常的方法,以便在二者之间可以共享异常信息。需要注意的是,即便C++中支持异常,也无法将C++中的异常和Java中的直接关联在一起,更无法在两者之间相互转换。这里我们还是通过代码示例的方式,并结合关键的注释来了解这些JNI函数的用法和机制。
    1)    包含native方法的Java类代码:

1     import java.io.PrintWriter;
2 public class Printf {
3 public static native void fprint(PrintWriter ps,String format,double x);
4 static {
5 System.loadLibrary("Printf");
6 }
7 }

    2)    实现本地方法的C语言代码:

 1     #include "Printf.h"
2 #include <stdlib.h>
3 #include <string.h>
4 #include <float.h>
5 JNIEXPORT void JNICALL Java_Printf_fprint
6 (JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
7 {
8 //这里是通过ThrowNew方法生成一个异常对象,这里FindClass查找的是
9 //运行时异常NullPointerException,需要注意的是,对于C语言的代码,
10 //在调用ThrowNew之后,并不会在C代码内部抛出异常,因此在调用ThrowNew后,
11 //其后面的代码仍然可以继续执行。然而由于异常标识一种错误的状态,因此
12 //在通常情况下,我们都会在ThrowNew的后面释放一些资源,之后立刻调用
13 //return退出当前的native方法。
14 //在native方法退出后,其调用的ThrowNew将会发挥作用,JVM会将抛出的
15 //异常返给native方法的最近调用者(Java代码)。
16 if (format == NULL) {
17 env->ThrowNew(env->FindClass("java/lang/NullPointerException"),
18 "Printf.fprint: format is NULL");
19 return;
20 }
21 const char* cformat = env->GetStringUTFChars(format,NULL);
22 char* cstr = (char*)calloc(strlen(cformat) + 20,sizeof(char));
23 if (cstr == NULL) {
24 env->ThrowNew(env->FindClass("java/lang/OutOfMemoryError")
25 ,"Printf.fprint: malloc failed");
26 return;
27 }
28 sprintf(cstr,cformat,x);
29 env->ReleaseStringUTFChars(format,cformat);
30 //调用PrintWriter.print(char)方法。
31 jclass class_PrintWriter = env->GetObjectClass(out);
32 jmethodID id_print = env->GetMethodID(class_PrintWriter,"print","(C)V");
33 //ExceptionOccurred将返回Java中抛出的异常,如果为NULL则表示没有异常出现。
34 //如果不希望知道具体的异常,可以调用jboolean ExceptionCheck()方法来判断
35 //是否异常发生了。如果C语言的代码在获悉异常的类型后,可以自行处理该异常,
36 //那么在处理之后,需要调用JNI函数ExceptionClear()来清除JVM传递过来的异常。
37 for (int i = 0; cstr[i] != 0 && !env->ExceptionOccurred(); ++i) {
38 env->CallVoidMethod(out,id_print,cstr[i]);
39 }
40 free(cstr);
41 }

    3)    含有main函数的调用测试代码:

 1     import java.io.PrintWriter;
2 public class MyTest {
3 public static void main(String[] args) throws Exception {
4 double price = 44.95;
5 double tax = 7.75;
6 double amountDue = price * (1 + tax / 100);
7 PrintWriter out = new PrintWriter(System.out);
8 //This call will throw an exception
9 Printf.fprint(out, "Amount due = %%8.2f\n", amountDue);
10 out.flush();
11 }
12 }
posted @ 2011-10-20 10:34  OrangeAdmin  阅读(2345)  评论(0编辑  收藏  举报