使用JNI技术实现JAVA程序调用dll
JAVA的跨平台的特性深受java程序员们的喜爱,但正是由于它为了实现跨平台的目的,使得它和本地机器的各种内部联系变得很少,大大约束了它的功能,比如与一些硬件设备通信,往往要花费很大的精力去设计流程编写代码去管理设备端口,而且有一些设备厂商提供的硬件接口已经经过一定的封装和处理,不能直接使用java程序通过端口和设备通信,这种情况下就得考虑使用java程序去调用比较擅长同系统打交道的第三方程序,从1.1版本开始的JDK提供了解决这个问题的技术标准:JNI技术.
JNI是Java Native Interface(Java本地接口)的缩写,本地是相对于java程序来说的,指直接运行在操作系统之上,与操作系统直接交互的程序.从1.1版本的JDK开始,JNI就作为标准平台的一部分发行.在JNI出现的初期是为了Java程序与本地已编译语言,尤其是C和C++的互操作而设计的,后来经过扩展也可以与c和c++之外的语言编写的程序交互,例如Delphi程序.
使用JNI技术固然增强了java程序的性能和功能,但是它也破坏了java的跨平台的优点,影响程序的可移植性和安全性,例如由于其他语言(如C/C++)可能能够随意地分配对象/占用内存,Java的指针安全性得不到保证.但在有些情况下,使用JNI是可以接受的,甚至是必须的,例如上面提到的使用java程序调用硬件厂商提供的类库同设备通信等,目前市场上的许多读卡器设备就是这种情况.在这必须使用JNI的情况下,尽量把所有本地方法都封装在单个类中,这个类调用单个的本地库文件,并保证对于每种目标操作系统,都可以用特定于适当平台的版本替换这个文件,这样使用JNI得到的要比失去的多很多.
现在开始讨论上面提到的问题,一般设备商会提供两种类型的类库文件,windows系统的会包含.dll/.h/.lib文件,而linux系统的会包含.so/.a文件,这里只讨论windows系统下的c/c++编译的dll文件调用方法.
我把设备商提供的dll文件称之为第三方dll文件,之所以说第三方,是因为JNI直接调用的是按它的标准使用c/c++语言编译的dll文件,这个文件是客户程序员按照设备商提供的.h文件中的列出的方法编写的dll文件,我称之为第二方dll文件,真正调用设备商提供的dll文件的其实就是这个第二方dll文件.到这里,解决问题的思路已经产生了,大慨分可以分为三步:
1>编写一个java类,这个类包含的方法是按照设备商提供的.h文件经过变形/转换处理过的,并且必须使用native定义.这个地方需要注意的问题是java程序中定义的方法不必追求和厂商提供的头文件列出的方法清单中的方法具有相同的名字/返回值/参数,因为一些参数类型如指针等在java中没法模拟,只要能保证这个方法能实现原dll文件中的方法提供的功能就行了;
2>按JNI的规则使用c/c++语言编写一个dll程序;
3>按dll调用dll的规则在自己编写的dll程序里面调用厂商提供的dll程序中定义的方法.
我之前为了给一个java项目添加IC卡读写功能,曾经查了很多资料发现查到的资料都是只说到第二步,所以剩下的就只好自己动手研究了.下面结合具体的代码来按这三个步骤分析.
1>假设厂商提供的.h文件中定义了一个我们需要的方法:
__int16 __stdcall readData( HANDLE icdev, __int16 offset, __int16 len, unsigned char *data_buffer );
a.__int16定义了一个不依赖于具体的硬件和软件环境,在任何环境下都占16 bit的整型数据(java中的int类型是32 bit),这个数据类型是vc++中特定的数据类型,所以我自己做的dll也是用的vc++来编译.
b.__stdcall表示这个函数可以被其它程序调用,vc++编译的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式.c/c++语言默认的调用方式是__cdecl,所以在自己做可被java程序调用的dll时一定要加上__stdcall的声明,否则在java程序执行时会报类型不匹配的错误.
c.HANDLE icdev是windows操作系统中的一个概念,属于win32的一种数据类型,代表一个核心对象在某一个进程中的唯一索引,不是指针,在知道这个索引代表的对象类型时可以强制转换成此类型的数据.
这些知识都属于win32编程的范围,更为详细的win32资料可以查阅相关的文档.
这个方法的原始含义是通过设备初始时产生的设备标志号icdev,读取从某字符串在内存空间中的相对超始位置offset开始的共len个字符,并存放到data_buffer指向的无符号字符类型的内存空间中,并返回一个16 bit的整型值来标志这次的读设备是否成功,这里真正需要的是unsigned char *这个指针指向的地址存放的数据,而java中没有指针类型,所以可以考虑定义一个返回字符串类型的java方法,原方法中返回的整型值也可以按经过一定的规则处理按字符串类型传出,由于HANDLE是一个类型于java中的Ojbect类型的数据,可以把它当作int类型处理,这样java程序中的方法定义就已经形成了:
String readData( int icdev, int offset, int len );
声明这个方法的时候要加上native关键字,表明这是一个与本地方法通信的java方法,同时为了安全起见,此文方法要对其它类隐藏,使用private声明,再另外写一个public方法去调用它,同时要在这个类中把本地文件加载进来,最终的代码如下:
package test;
public class LinkDll
{
//从指定地址读数据
private native String readData( int icdev, int offset, int len );
public String readData( int icdev, int offset, int len )
{
return this.readDataTemp( icdev, offset, len );
}
static
{
System.loadLibrary( "TestDll" );//如果执行环境是linux这里加载的是SO文件,如果是windows环境这里加载的是dll文件
}
}
2>使用JDK的javah命令为这个类生成一个包含类中的方法定义的.h文件,可进入到class文件包的根目录下(只要是在classpath参数中的路径即可),使用javah命令的时候要加上包名javah test.LinkDll,命令成功后生成一个名为test_LinkDll.h的头文件.
文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated*/
#include <jni.h>
/* Header for class test_LinkDll */
#ifndef _Included_test_LinkDll #define
Included_test_LinkDll
#ifdef __cplusplus extern "C" { #endif
/*
* Class: test_LinkDll
* Method: readDataTemp
* Signature: (III)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp(JNIEnv *, jobject, jint, jint, jint);
#ifdef __cplusplus } #endif
#endif
可以看出,JNI为了实现和dll文件的通信,已经按它的标准对方法名/参数类型/参数数目作了一定的处理,其中的JNIEnv*/jobjtct这两个参数是每个JNI方法固有的参数,javah命令负责按JNI标准为每个java方法加上这两个参数.JNIEnv是指向类型为JNIEnv_的一个特殊JNI数据结构的指针,当由C++编译器编译时JNIEnv_结构其实被定义为一个类,这个类中定义了很多内嵌函数,通过使用"->"符号,可以很方便使用这些函数,如:
(env)->NewString( jchar* c, jint len )
可以从指针c指向的地址开始读取len个字符封装成一个JString类型的数据.
其中的jchar对应于c/c++中的char,jint对应于c/c++中的len,JString对应于java中的String,通过查看jni.h可以看到这些数据类型其实都是根据java和c/c++中的数据类型对应关系使用typedef关键字重新定义的基本数据类型或结构体.
具体的对应关系如下:
Java类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组
更为详细的资料可以查阅JNI文档.
需要注意的问题:test_LinkDll.h文件包含了jni.h文件;
3>使用vc++ 6.0编写TestDll.dll文件,这个文件名是和java类中loadLibrary的名称一致.
a>使用vc++6.0 新建一个Win32 Dynamic-Link Library的工程文件,工程名指定为TestDll
b>把源代码文件和头文件使用"Add Fiels to Project"菜单加载到工程中,若使用c来编码,源码文件后缀名为.c,若使用c++来编码,源码文件扩展名为.cpp,这个一定要搞清楚,因为对于不同的语言,使用JNIEnv指针的方式是不同的.
c>在这个文件里调用设备商提供的dll文件,设备商一般提供三种文件:dll/lib/h,这里假设分别为A.dll/A.lib/A.h.
这个地方的调用分为动态调用和静态调用静态调用即是只要把被调用的dll文件放到path路径下,然后加载lib链接文件和.h头文件即可直接调用A.dll中的方法:
把设备商提供的A.h文件使用"Add Fiels to Project"菜单加载到这个工程中,同时在源代码文件中要把这个A.h文件使用include包含进来;
然后依次点击"Project->settings"菜单,打开link选项卡,把A.lib添加到"Object/library modules"选项中.
具体的代码如下:
//读出数据,需要注意的是如果是c程序在调用JNI函数时必须在JNIEnv的变量名前加*,如(*env)->xxx,如果是c++程序,则直接使用(env)->xxx
#include<WINDOWS.H>
#include<MALLOC.H>
#include<STDIO.H>
#include<jni.h>
#include "test_LinkDll.h"
#include "A.h"
JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_len )
{
//*************************基本数据声明与定义******************************
HANDLE H_icdev = (HANDLE)ji_icdev;//设备标志符
__int16 i16_len = (__int16)ji_len;//读出的数据长度,值为3,即3个HEX形式的字符
__int16 i16_result;//函数返回值
__int16 i16_coverResult;//字符转换函数的返回值
int i_temp;//用于循环的中间变量
jchar jca_result[3] = { 'e', 'r', 'r' };//当读数据错误时返回此字符串
//无符号字符指针,指向的内存空间用于存放读出的HEX形式的数据字符串
unsigned char* uncp_hex_passward = (unsigned char*)malloc( i16_len );
//无符号字符指针,指向的内存空间存放从HEX形式转换为ASC形式的数据字符串
unsigned char* uncp_asc_passward = (unsigned char*)malloc( i16_len * 2 );
//java char指针,指向的内存空间存放从存放ASC形式数据字符串空间读出的数据字符串
jchar *jcp_data = (jchar*)malloc(i16_len*2+1);
//java String,存放从java char数组生成的String字符串,并返回给调用者
jstring js_data = 0;
//*********读出3个HEX形式的数据字符到uncp_hex_data指定的内存空间**********
i16_result = readData( H_icdev, 6, uncp_hex_data );//这里直接调用的是设备商提供的原型方法.
if ( i16_result != 0 )
{
printf( "读卡错误......\n" );
//这个地方调用JNI定义的方法NewString(jchar*,jint),把jchar字符串转换为JString类型数据,返回到java程序中即是String
return (env)->NewString( jca_result, 3 );
}
printf( "读数据成功......\n" );
//**************HEX形式的数据字符串转换为ASC形式的数据字符串**************
i16_coverResult = hex_asc( uncp_hex_data, uncp_asc_data, 3 );
if ( i16_coverResult != 0 )
{
printf( "字符转换错误!\n" );
return (env)->NewString( jca_result, 3 );
}
//**********ASC char形式的数据字符串转换为jchar形式的数据字符串***********
for ( i_temp = 0; i_temp < i16_len; i_temp++ )
jcp_data[i_temp] = uncp_hex_data[i_temp];
//******************jchar形式的数据字符串转换为java String****************
js_data = (env)->NewString(jcp_data,i16_len);
return js_data;
}
动态调用,不需要lib文件,直接加载A.dll文件,并把其中的文件再次声明,代码如下:
#include<STDIO.H>
#include<WINDOWS.H>
#include "test_LinkDll.h"
//首先声明一个临时方法,这个方法名可以随意定义,但参数同设备商提供的原型方法的参数保持一致.
typedef int ( *readDataTemp )( int, int, int, unsigned char * );//从指定地址读数据
//从指定地址读数据
JNIEXPORT jstring JNICALL Java_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_offset, jint ji_len )
{
int i_temp;
int i_result;
int i_icdev = (int)ji_icdev;
int i_offset = (int)ji_offset;
int i_len = (int)ji_len;
jchar jca_result[5] = { 'e', 'r', 'r' };
unsigned char *uncp_data = (unsigned char*)malloc(i_len);
jchar *jcp_data = (jchar *)malloc(i_len);
jstring js_data = 0;
//HINSTANCE是win32中同HANDLE类似的一种数据类型,意为Handle to an instance,常用来标记App实例,在这个地方首先把A.dll加载到内存空间,以一个App的形式存放,然后取
得它的instance交给dllhandle,以备其它资源使用.
HINSTANCE dllhandle;
dllhandle = LoadLibrary( "A.dll" );
//这个地方首先定义一个已声明过的临时方法,此临时方法相当于一个结构体,它和设备商提供的原型方法具有相同的参数结构,可互相转换
readDataTemp readData;
//使用win32的GetProcAddress方法取得A.dll中定义的名为readData的方法,并把这个方法转换为已被定义好的同结构的临时方法,
//然后在下面的程序中,就可以使用这个临时方法了,使用这个临时方法在这时等同于使用A.dll中的原型方法.
readData = (readDataTemp) GetProcAddress( dllhandle, "readData" );
i_result = (*readData)( i_icdev, i_offset, i_len, uncp_data );
if ( i_result != 0 )
{
printf( "读数据失败......\n" );
return (env)->NewString( jca_result, 3 );
}
for ( i_temp = 0; i_temp < i_len; i_temp++ )
{
jcp_data[i_temp] = uncp_data[i_temp];
}
js_data = (env)->NewString( jcp_data, i_len );
return js_data;
}
4>以上即是一个java程序调用第三方dll文件的完整过程,当然,在整个过程的工作全部完成以后,就可以使用java类LinkDll中的public String radData( int, int, int )方法了,效果同直接使用c/c++调用这个设备商提供的A.dll文件中的readData方法几乎一样.
总结:JNI技术确实是提高了java程序的执行效率,并且扩展了java程序的功能,但它也确确实实破坏了java程序的最重要的优点:平台无关性,所以除非必须(不得不)使用JNI技术,一般还是提倡写100%纯java的程序.根据自己的经验及查阅的一些资料,把可以使用JNI技术的情况罗列如下:
1>需要直接操作物理设备,而没有相关的驱动程序,这时候我们可能需要用C甚至汇编语言来编写该设备的驱动,然后通过JNI调用;
2>涉及大量数学运算的部分,用java会带来些效率上的损失;
3>用java会产生系统难以支付的开销,如需要大量网络链接的场合;
4>存在大量可重用的c/c++代码,通过JNI可以减少开发工作量,避免重复开发.
另外,在利用JNI技术的时候要注意以下几点:
1>由于Java安全机制的限制,不要试图通过Jar文件的方式发布包含本地化方法的Applet到客户端;
2>注意内存管理问题,虽然在本地方法返回Java后将自动释放局部引用,但过多的局部引用将使虚拟机在执行本地方法时耗尽内存;
3>JNI技术不仅可以让java程序调用c/c++代码,也可以让c/c++代码调用java代码.
注:有一个名叫Jawin开源项目实现了直接读取第三方dll文件,不用自己辛苦去手写一个起传值转换作用的dll文件,有兴趣的可以研究一下.但是我用的时候不太顺手,有很多规则限制,像自己写程序时可以随意定义返回值,随意转换类型,用这个包的话这些都是不可能的了,所以我的项目还没开始就把它抛弃了.