JNI编程示例

了解了JNI的基本原理后,再结合具体的示例来学习下如何进行JNI编程。以下为转载网友的内容,稍作了一些整理。

开发环境: j2sdk1.4.2_15 + eclipse 3.2 + VC++ 6.0

在eclipse里建立一个HelloFore的Java工程,然后编写下面的代码。

Java代码 
  1. package com.chnic.jni;  
  2.   
  3. public class SayHellotoCPP {  
  4.       
  5.     public SayHellotoCPP(){  
  6.     }  
  7.     public native void sayHello(String name);  
  8. }   

该native本地方法有一个String类型的参数,会传递一个name到后台去。本地方法完成后,现在介绍下javah命令。

 javah是一个专门为JNI生成头文件的命令。CMD打开控制台之后输入javah回车就能看到javah的一些参数。我们要用的是 -jni这个参数,这个参数也是默认的参数,他会生成一个JNI式的.h头文件。在控制台进入到工程的根目录,也就是HelloFore这个目录,然后输入命令。

Java代码 
  1. javah -jni com.chnic.jni.SayHellotoCPP  

 命令执行完之后在工程的根目录就会发现com_chnic_jni_SayHellotoCPP.h 这个头文件。需要说明的是:在执行javah的时候,要输入完整的包名+类名,否则在以后的测试调用过程中会发生java.lang.UnsatisfiedLinkError这个异常

 

到这里java部分算是基本完成了,接下来我们来编写后端的C++代码。打开VC++首先新建一个Win32 Dynamic-Link library工程,之后选择An empty DLL project空工程。在这里我C++的工程是HelloEnd,把刚刚生成的那个头文件拷贝到这个工程的根目录里。随便用什么文本编辑器打开这个头文件,发现有一个如下的方法签名。

Cpp代码 
  1. /* 
  2.  * Class:     com_chnic_jni_SayHellotoCPP 
  3.  * Method:    sayHello 
  4.  * Signature: (Ljava/lang/String;)V 
  5.  */  
  6. JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello  
  7.   (JNIEnv *, jobject, jstring);  

 仔细观察一下这个方法,在注释上标注类名、方法名、签名(Signature),至于这个签名是做什么用的,我们以后再说。在这里最重要的是 Java_com_chnic_jni_SayHellotoCPP_sayHello这个方法签名。在Java端我们执行 sayHello(String name)这个方法之后,JVM就会帮我们唤醒在DLL里的Java_com_chnic_jni_SayHellotoCPP_sayHello这个方法。因此我们新建一个C++ source file来实现这个方法。

Cpp代码 
  1. #include <iostream.h>  
  2. #include "com_chnic_jni_SayHellotoCPP.h"  
  3.   
  4.   
  5. JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello   
  6.   (JNIEnv* env, jobject obj, jstring name)  
  7. {  
  8.     const char* pname = env->GetStringUTFChars(name, NULL);  
  9.     cout << "Hello, " << pname << endl;  
  10. }  

因为我们生成的那个头文件是在C++工程的根目录而不是在环境目录,所以我们要把尖括号改成双引号,至于VC++的环境目录可以在 Tools->Options->Directories里设置。F7编译工程发现缺少jni.h这个头文件,这个头文件可以在%JAVA_HOME%/include目录下找到。把这个文件拷贝到C++工程目录,继续编译发现还是找不到。原来是因为在我们刚刚生成的那个头文件里,jni.h这个文件是被 #include <jni.h>引用进来的,因此我们把尖括号改成双引号#include "jni.h",继续编译发现少了jni_md.h文件,接着在%JAVA_HOME%/include/win32下面找到那个头文件,放入到工程根目录,F7编译成功。在Debug目录里会发现生成了HelloEnd.dll这个文件。

 

这个时候后端的C++代码也已经完成,接下来的任务就是怎么把他们连接在一起了。要让前端的java程序“认识并找到”这个动态链接库,就必须把这个DLL放在windows path环境变量下面。有两种方法可以做到:

  1. 把这个DLL放到windows下面的sysytem32文件夹下面,这个是windows默认的path
  2. 复制你工程的Debug目录(我这里是C:/Program Files/Microsoft Visual Studio/MyProjects/HelloEnd/Debug这个目录),把这个目录配置到User variable的Path下面。重启eclipse,让eclipse在启动的时候重新读取这个path变量。

 比较起来,第二种方法比较灵活,在开发的时候不用来回copy dll文件了,节省了很多工作量,所以在开发的时候推荐用第二种方法。在这里我们使用的也是第二种,eclipse重启之后打开 SayHellotoCPP这个类。其实我们上面做的那些只是让JVM能找到那些DLL文件,接下来我们要让我们自己的java代码“认识”这个动态链接库。加入System.loadLibrary("HelloEnd");这句到静态初始化块里。

Java代码 
  1. package com.chnic.jni;  
  2.   
  3. public class SayHellotoCPP {  
  4.       
  5.     static{  
  6.         System.loadLibrary("HelloEnd");  
  7.     }  
  8.     public SayHellotoCPP(){  
  9.     }  
  10.     public native void sayHello(String name);  
  11.       
  12. }  

 这样我们的代码就能认识并加载这个动态链接库文件了。万事俱备,只欠测试代码了,接下来编写测试代码。

Java代码 
  1. SayHellotoCPP shp = new SayHellotoCPP();  
  2. shp.sayHello("World");  

 我们不让他直接Hello,World。我们把World传进去,执行代码。发现控制台打印出来Hello, World这句话。就此一个最简单的JNI程序已经开发完成。

Cpp代码 
  1. const char* pname = env->GetStringUTFChars(name, NULL);   

 也许有朋友会对上面这句cpp代码有疑问,这个GetStringUTFChars就是JNI提供的API,我们稍后再讲。

 

现在来介绍下JNI里的数据类型。在C++里,编译器会根据所处的平台来为一些基本的数据类型分配长度,因此也就造成了平台不一致性。而这个问题在因为有JVM的缘故,在Java中是不存在的, 其基本数据类型在所有平台下的长度都是相同的,比如int的宽度永远都是32位。基于这方面的原因,java和c++的基本数据类型就需要实现一些mapping以保持一致性。下面的表可以概括:

    Java类型                本地类型                 JNI中定义的别名    
intlongjint
long_int64jlong
bytesigned charjbyte
booleanunsigned charjboolean
charunsigned shortjchar
shortshortjshort
floatfloatjfloat
doubledoublejdouble
Object_jobject*jobject

 上面的表格是我在网上搜的,放上来给大家对比一下。对于每一种映射的数据类型,JNI的设计者其实已经帮我们取好了相应的别名以方便记忆。如果想了解一些更加细致的信息,可以去看一些jni.h这个头文件,各种数据类型的定义以及别名就被定义在这个文件中。

 了解了JNI中的数据类型,下面就来看这次的例子。这次我们用Java来实现一个前端的market(以下就用Foreground代替),用CPP来实现一个后端factory(以下用backend代替)。我们首先还是来编写包含本地方法的java类。

Java代码 
  1. package com.chnic.service;  
  2.   
  3. import com.chnic.bean.Order;  
  4.   
  5. public class Business {  
  6.     static{  
  7.         System.loadLibrary("FruitFactory");  
  8.     }  
  9.       
  10.     public Business(){  
  11.           
  12.     }  
  13.       
  14.     public native double getPrice(String name);  
  15.     public native Order getOrder(String name, int amount);  
  16.     public native Order getRamdomOrder();  
  17.     public native void analyzeOrder(Order order);  
  18.       
  19.     public void notification(){  
  20.         System.out.println("Got a notification.");  
  21.     }  
  22.       
  23.     public static void notificationByStatic(){  
  24.         System.out.println("Got a notification in a static method.");  
  25.     }  
  26. }  

 这个类里面包含4个本地方法,一个静态初始化块加载将要生成的dll文件。剩下的方法都是很普通的java方法,等会在backend中回调这些方法。这个类需要一个名为Order的JavaBean。

Java代码 
  1. package com.chnic.bean;  
  2.   
  3. public class Order {  
  4.       
  5.     private String name = "Fruit";  
  6.     private double price;  
  7.     private int amount = 30;  
  8.       
  9.     public Order(){  
  10.           
  11.     }  
  12.   
  13.     public int getAmount() {  
  14.         return amount;  
  15.     }  
  16.    
  17.     public void setAmount(int amount) {  
  18.         this.amount = amount;  
  19.     }  
  20.   
  21.     public String getName() {  
  22.         return name;  
  23.     }  
  24.   
  25.     public void setName(String name) {  
  26.         this.name = name;  
  27.     }  
  28.   
  29.     public double getPrice() {  
  30.         return price;  
  31.     }  
  32.   
  33.     public void setPrice(double price) {  
  34.         this.price = price;  
  35.     }  
  36. }  

 JavaBean中,我们为两个私有属性赋值,方便后面的例子演示。到此为止除了测试代码之外的Java端的代码就全部搞定了。接下来进行生成.h 头文件、建立C++工程的工作,在这里就一笔带过,不熟悉的朋友请回头看。在工程里我们新建一个名为Factory的C++ source file 文件,去实现那些native方法。具体的代码如下。

Cpp代码 
  1. #include <iostream.h>  
  2. #include <string.h>  
  3. #include "com_chnic_service_Business.h"  
  4.   
  5. jobject getInstance(JNIEnv* env, jclass obj_class);  
  6.   
  7. JNIEXPORT jdouble JNICALL Java_com_chnic_service_Business_getPrice(JNIEnv* env,   
  8.                                                                    jobject obj,   
  9.                                                                    jstring name)  
  10. {  
  11.     const char* pname = env->GetStringUTFChars(name, NULL);  
  12.     cout << "Before release: "  << pname << endl;  
  13.   
  14.     if (strcmp(pname, "Apple") == 0)  
  15.     {  
  16.         env->ReleaseStringUTFChars(name, pname);  
  17.         cout << "After release: " << pname << endl;  
  18.         return 1.2;  
  19.     }   
  20.     else  
  21.     {  
  22.         env->ReleaseStringUTFChars(name, pname);  
  23.         cout << "After release: " << pname << endl;  
  24.         return 2.1;  
  25.     }     
  26. }  
  27.   
  28.   
  29. JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getOrder(JNIEnv* env,   
  30.                                                                    jobject obj,   
  31.                                                                    jstring name,   
  32.                                                                    jint amount)  
  33. {  
  34.     jclass order_class = env->FindClass("com/chnic/bean/Order");  
  35.     jobject order = getInstance(env, order_class);  
  36.       
  37.     jmethodID setName_method = env->GetMethodID(order_class, "setName""(Ljava/lang/String;)V");  
  38.     env->CallVoidMethod(order, setName_method, name);  
  39.   
  40.     jmethodID setAmount_method = env->GetMethodID(order_class, "setAmount""(I)V");  
  41.     env->CallVoidMethod(order, setAmount_method, amount);  
  42.   
  43.     return order;  
  44. }  
  45.   
  46. JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getRamdomOrder(JNIEnv* env,   
  47.                                                                          jobject obj)  
  48. {  
  49.     jclass business_class = env->GetObjectClass(obj);  
  50.     jobject business_obj = getInstance(env, business_class);  
  51.   
  52.     jmethodID notification_method = env->GetMethodID(business_class, "notification""()V");  
  53.     env->CallVoidMethod(obj, notification_method);  
  54.   
  55.     jclass order_class = env->FindClass("com/chnic/bean/Order");  
  56.     jobject order = getInstance(env, order_class);  
  57.     jfieldID amount_field = env->GetFieldID(order_class, "amount""I");  
  58.     jint amount = env->GetIntField(order, amount_field);  
  59.     cout << "amount: " << amount << endl;  
  60.     return order;  
  61. }  
  62.   
  63.   
  64. JNIEXPORT void JNICALL Java_com_chnic_service_Business_analyzeOrder (JNIEnv* env,   
  65.                                                                      jclass cls,   
  66.                                                                      jobject obj)  
  67. {  
  68.     jclass order_class = env->GetObjectClass(obj);  
  69.     jmethodID getName_method = env->GetMethodID(order_class, "getName""()Ljava/lang/String;");  
  70.     jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method));  
  71.     const char* pname = env->GetStringUTFChars(name_str, NULL);  
  72.   
  73.     cout << "Name in Java_com_chnic_service_Business_analyzeOrder: " << pname << endl;  
  74.     jmethodID notification_method_static = env->GetStaticMethodID(cls, "notificationByStatic""()V");  
  75.     env->CallStaticVoidMethod(cls, notification_method_static);  
  76.   
  77. }  
  78.   
  79. jobject getInstance(JNIEnv* env, jclass obj_class)  
  80. {  
  81.     jmethodID construction_id = env->GetMethodID(obj_class, "<init>""()V");  
  82.     jobject obj = env->NewObject(obj_class, construction_id);  
  83.     return obj;  
  84. }  

 可以看到,在我Java中的四个本地方法在这里全部被实现。接下来针对这四个方法解释一下JNI相关的API的使用方法。先从第一个方法讲起吧:

 1.getPrice(String name)

 这个方法是从foreground传递一个类型为string的参数到backend,然后backend判断返回相应的价格。在cpp的代码中,我们用GetStringUTFChars这个方法把传来的jstring变成一个UTF-8编码的char型字符串。jstring的实际类型是 jobject。

GetStringUTFChars方法包含两个参数,第一个参数是你要处理的jstring对象,第二个参数表示是否需要在内存中生成一个副本对象。将 jstring转换成为一个const char*之后,我们用string.h中的strcmp函数来比较这两个字符串:如果传来的字符串是“Apple”,我们返回1.2;反之返回 2.1。在这里还要多说一下ReleaseStringUTFChars这个函数,从字面上不难理解,就是释放内存用的。有点像cpp里的析构函数,只不过Sun帮我们已经封装好了。由于在JVM中有GC这个东东,所以多数java coder并没有写析构的习惯,不过在JNI里是必须的了,否则容易造成内存泄露。我们在release之前和之后分别打出这个字符串来看一下效果。

 粗略的解释完一些API之后,我们编写测试代码。

 Java代码 

  1. Business b = new Business();          
  2. System.out.println(b.getPrice("Apple"));  

 运行这段测试代码,控制台上打出

 Before release: Apple

After release: ??
1.2

 

在release之前打印出来的是我们“需要”的Apple,release之后就成了乱码了。由于传递的是Apple,所以得到1.2。测试成功。

 

2. getOrder(String name, int amount)

 在foreground中可以通过这个方法让backend返回一个你“指定”的Order。所谓“指定”,其实也就是指方法里的两个参数:name和amout,在cpp的代码中,会根据传递的两个参数来构造一个Order。

Java代码 
  1. jclass order_class = env->FindClass("com/chnic/bean/Order");   

 是不是觉得这句代码似曾相识?没错,这句代码很像我们java里写的Class.forName(className)反射的代码。其实在这里 FindClass的作用和上面的forName是类似的。只不过在forName中要用完整的类名,但是在这里必须用"/"来代替“.”。这个方法会返回一个jclass的对象,其实也就是我们在Java中说的类对象。

Java代码 
  1. jmethodID construction_id = env->GetMethodID(obj_class, "<init>""()V");  
  2. jobject obj = env->NewObject(obj_class, construction_id);  

 拿到"类对象"之后,按照Java RTTI的逻辑我们接下来就要唤醒那个类对象的构造函数了。在JNI中,包括构造函数在内的所有方法都被看成Method。每个method都有一个特定的ID,我们通过GetMethodID这个方法就可以拿到我们想要的某一个java 方法的ID。GetMethodID需要传三个参数,第一个很显然是jclass,第二个参数是java方法名,第三个参数是方法签名。

在这里有必要单独来讲一讲这个方法签名,为什么要用这个东东呢?我们知道,在Java里方法是可以被重载的,比如我一个类里有public void a(int arg)和public void a(String arg)这两个方法,在这里用方法名来区分方法显然就是行不通的了。方法签名包括两部分:参数类型和返回值类型。具体的格式:(参数1类型签名;参数2类型签名)返回值类型签名。下面是java类型和签名类型的一个对照表:

    Java类型     对应的签名
booleanZ
byteB
charC
shortS
intI
longL
floatF
doubleD
voidV
ObjectL用/分割包的完整类名;  Ljava/lang/String;
Array[签名       [I       [Ljava/lang/String;

 其实除了自己对照手写之外,JDK也提供了一个很好用的生成签名的工具javap。cmd进入控制台到你要生成签名的那个类的目录下(以 Order类为例),敲入: javap -s -private Order。所有方法签名都会被输出,关于javap的一些参数可以在控制台下面输入 javap -help查看。

回到我们刚刚的getMethodID这个方法上。因为是调用构造函数JNI规定调用构造函数的时候传递的方法名应该为<init>,通过javap查看我们要的那个无参的构造函数的方法签名是()V。得到方法签名,最后我们调用NewObject方法来生成一个新的对象。

拿到对象之后我们开始为对象jobject填充数值,还是首先拿到setXXX方法的Method ID,之后调用Call<Type>Method来调用java方法。这里的<Type>所指的是方法的返回类型,我们刚刚调用的是set方法的返回值是void,因此这里的方法也就是CallVoidMethod,这个方法的参数除了前两个要传入jobject和 jmethodID之外还要传入要调用的那个方法的参数,而且要顺序必须一致,这点和Java的反射一模一样,在这里就不多解释。

终于介绍完了第二个方法,下来就是测试代码测试。

Java代码 
  1. Order o = b.getOrder("Watermelom"100);  
  2. System.out.println("java: " + o.getName());  
  3. System.out.println("java: " + o.getAmount());  

 控制台打出

 java: Watermelom

java: 100

 

就此,我们完成了第二个方法的测试。


3.getRandomOrder()

 这个方法会从backend得到一个随机的Order对象,然后再调用java中相应的通知方法来通知 foreground。getRandomOrder方法没有参数,但是所对应的C++方法里却有两个参数,一定有人会不解。其实细心的朋友一定会发现,JNI里所有对应Java方法的C++ 方法都会比Java方法多两个参数,第一个参数是我们很熟悉的JNI Env*指针,第二个参数有时是jobject,有时是个jclass。针对这第二个参数在这里有必要多废话两句。

其实第二个参数传递的是包含了native方法的对象或者类对象。我们知道非静态的方法是属于某一个对象的,而静态方法是属于类对象的,所以静态方法可以被所有对象共享。有这个对象/类对象,我们就可以很方便的操作包含了native方法的对象的一些函数了。因为getRandomOrder不是静态的,所以C++相对应的参数中传递来的是一个jobject对象。

Cpp代码 
  1. jclass business_class = env->GetObjectClass(obj);  

 这一句不难理解,GetObjectClass方法可以得到一个对象的类对象,这句有点像Java中的Object.class。不熟悉的朋友建议再去看一下Java反射机制。接下来的几句C++代码应该在之前的方法1和方法2中都解释过。backend端会发一个“消息”给 foreground,之后new一个新的Order类出来。接下来的三句有必要再废话一下。

Cpp代码 
  1. jfieldID amount_field = env->GetFieldID(order_class, "amount""I");  
  2. jint amount = env->GetIntField(order, amount_field);  
  3. cout << "amount: " << amount << endl;  

 之前我为Order这个Javabean的amount的属性设置了一个初始值为30,其实就是为了在这里演示如何在C++中拿一个Java对象的属性,拿的方法和我们之前说过的调用Java方法的程序差不多,也要先拿到一个jfieldID,之后调用Get<type>Field方法来取得某一个对象中的某一个属性的数值,最后cout把他打印出来。我们编写测试代码来看一下最终效果。

Java代码 
  1. Business b = new Business();                   
  2. Order o2 = b.getRamdomOrder();  
  3. System.out.println(o2.getName());  

运行上述的测试代码之后,控制台上打出了

 Got a notification.

amount: 30
Fruit

和我们想要的结果是一样的,测试成功。

 

4.analyzeOrder(Order order)

 这是一个静态方法,foreground会通过这个方法传一个Order的对象到backend去,然后再由CPP端进行“analyze”。在这里我们取出来传递过来的Order对象的name属性,然后打印到控制台上。因为这个方法是静态static方法,所以相对应的C++方法中的第二个参数也变成了jclass对象,也就是Business.class这个类对象。第三个参数是一个jobject对象,很明显就是我们传递过来的order对象。

 前5句代码应该不难理解,就是调用getName这个方法,然后打印出来。因为JNI的API中并没有提供CallStringMethod这个方法,所以我们用CallObjectMethod这个方法来取得name这个字符串(String很明显也是一个Object),然后再转型成为 jstring。也就是下面这句代码。

Cpp代码 
  1. jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method));  

取到了name这个字符串之后cout打印出来,之后调用Business这个类对象中的静态方法notificationByStatic来通知 foreground。调用的流程以及方法和非静态都是一样的,只不过注意JNI中调用静态方法的API所传递的一个参数是一个jclass而非 jobject(这个也不难理解,因为静态方法是属于class类对象的)

 还是编写测试代码测试这个方法

Java代码 
  1. Business b = new Business();          
  2. Order o = b.getOrder("Watermelom"100);  
  3. Business.analyzeOrder(o);  
 控制台上打印出

 Name in Java_com_chnic_service_Business_analyzeOrder: Watermelom

Got a notification in a static method.

 第一句是C++中cout打印出来的,第二句则是Java中的静态方法打印出来的,和我们想要的结果是一致的。


本文参考部分内容参考:

http://blog.youkuaiyun.com/skywalker256/article/details/4677644

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值