JNI

《JNI详解》

JNI其实是Java Native Interface的简称,也就是java本地接口。它提供了若干的API实现了和Java和其他语言的通信(主要是C&C++)。也许不少人觉得Java已经足够强大,为什么要需要JNI这种东西呢?我们知道Java是一种平台无关性的语言,平台对于上层的java代码来说是透明的,所以在多数时间我们是不需要JNI的,但是假如你遇到了如下的三种情况之一呢?

 

  1. 你的Java代码,需要得到一个文件的属性。但是你找遍了JDK帮助文档也找不到相关的API。
  2. 在本地还有一个别的系统,不过他不是Java语言实现的,这个时候你的老板要求你把两套系统整合到一起。
  3. 你的Java代码中需要用到某种算法,不过算法是用C实现并封装在动态链接库文件(DLL)当中的。

对于上述的三种情况,如果没有JNI的话,那就会变得异常棘手了。就算找到解决方案了,也是费时费力。其实说到底还是会增加开发和维护的成本。

 

说了那么多一通废话,现在进入正题。看过JDK源代码的人肯定会注意到在源码里有很多标记成native的方法。这些个方法只有方法签名但是没有方法体。其实这些naive方法就是我们说的 java native interface。他提供了一个调用(invoke)的接口,然后用C或者C++去实现。我们首先来编写这个“桥梁”.我自己的开发环境是 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. }  
 

 一般的第一个程序总是HelloWorld。今天换换口味,把world换成一个名字。我的native本地方法有一个String的参数。会传递一个name到后台去。本地方法已经完成,现在来介绍下javah这个方法,接下来就要用javah方法来生成一个相对应的.h头文件。

 

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++代码。(用C也可以,只不过cout比printf用起来更快些,所以这里俺偷下懒用C++)打开VC++首先新建一个Win32 Dynamic-Link library工程,之后选择An empty DLL project空工程。在这里我C++的工程是HelloEnd,把刚刚生成的那个头文件拷贝到这个工程的根目录里。随便用什么文本编辑器打开这个头文件,发现有一个如下的方法签名。

 
[cpp] view plaincopy
 
  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] view plaincopy
 
  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] view plaincopy
 
  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] view plaincopy
 
  1. SayHellotoCPP shp = new SayHellotoCPP();  
  2. shp.sayHello("World");  

 

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

 
[cpp] view plaincopy
 
  1. const char* pname = env->GetStringUTFChars(name, NULL);  

 

 这句有疑问,这个GetStringUTFChars就是JNI给developer提供的API,我们以后再讲。在这里不得不多句嘴。

  1. 因为JNI有一个Native这个特点,一点有项目用了JNI,也就说明这个项目基本不能跨平台了。
  2. JNI调用是相当慢的,在实际使用的之前一定要先想明白是否有这个必要。
  3. 因为C++和C这样的语言非常灵活,一不小心就容易出错,比如我刚刚的代码就没有写析构字符串释放内存,对于java developer来说因为有了GC 垃圾回收机制,所以大多数人没有写析构函数这样的概念。所以JNI也会增加程序中的风险,增大程序的不稳定性。

  

其实在Java代码中,除了对本地方法标注native关键字和加上要加载动态链接库之外,JNI基本上是对上层coder透明的,上层coder调用那些本地方法的时候并不知道这个方法的方法体究竟是在哪里,这个道理就像我们用JDK所提供的API一样。所以在Java中使用JNI还是很简单的,相比之下在C++中调用java,就比前者要复杂的多了。

 

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

 

    Java类型                 本地类型                  JNI中定义的别名    
int long jint
long _int64 jlong
byte signed char jbyte
boolean unsigned char jboolean
char unsigned short jchar
short short jshort
float float jfloat
double double jdouble
Object _jobject* jobject

 

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

posted @ 2014-10-08 20:02  小天_y  阅读(330)  评论(0编辑  收藏  举报