使用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文件,有興趣的可以研究一下.但是我用的時候不太順手,有很多規則限制,像自己寫程序時可以隨意定義返回值,隨意轉換類型,用這個包的話這些都是不可能的了,所以我的項目還沒開始就把它拋棄了.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章