1. 程式人生 > >java 呼叫c++動態連結庫

java 呼叫c++動態連結庫

        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程式碼
package com.chnic.jni;    
   
public class SayHellotoCPP {    
        
    public SayHellotoCPP(){    
     }    
    public native void sayHello(String name);    
}   

      一般的第一個程式總是HelloWorld。今天換換口味,把world換成一個名字。我的native本地方法有一個String的引數。會傳遞一個name到後臺去。本地方法已經完成,現在來介紹下javah這個方法,接下來就要用javah方法來生成一個相對應的.h標頭檔案。

      javah是一個專門為JNI生成標頭檔案的一個命令。CMD開啟控制檯之後輸入javah回車就能看到javah的一些引數。在這裡就不多介紹 我們要用的是 -jni這個引數,這個引數也是預設的引數,他會生成一個JNI式的.h標頭檔案。在控制檯進入到工程的根目錄,也就是HelloFore這個目錄,然後輸入命令。

Java程式碼       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程式碼
/* 
* Class:      com_chnic_jni_SayHellotoCPP 
* Method:     sayHello 
* Signature: (Ljava/lang/String;)V 
*/   
JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello    
   (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程式碼
#include <iostream.h>    
#include "com_chnic_jni_SayHellotoCPP.h"    
   
   
JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello    
   (JNIEnv* env, jobject obj, jstring name)    
{    
    const char* pname = env->GetStringUTFChars(name, NULL);    
     cout << "Hello, " << pname << endl;    
}   

      因為我們生成的那個標頭檔案是在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 (這樣好像不行,最後放到jdk/bin目錄下才     成功)

      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程式碼
package com.chnic.jni;    
   
public class SayHellotoCPP {    
        
    static{    
         System.loadLibrary("HelloEnd");    
     }    
    public SayHellotoCPP(){    
     }    
    public native void sayHello(String name);    
        
}   

      這樣我們的程式碼就能認識並載入這個動態連結庫檔案了。萬事俱備,只欠測試程式碼了,接下來編寫測試程式碼。

     Java程式碼
   SayHellotoCPP shp = new SayHellotoCPP();    
   shp.sayHello("World");   

      我們不讓他直接Hello,World。我們把World傳進去,執行程式碼。發現控制檯打印出來Hello, World這句話。就此一個最簡單的JNI程式已經開發完成。也許有朋友會對CPP程式碼裡的

Cpp程式碼

        constchar* pname = env->GetStringUTFChars(name, NULL);  

這句有疑問,這個GetStringUTFChars就是JNI給developer提供的API,我們以後再講。在這裡不得不多句嘴。

  1. 因為JNI有一個Native這個特點,一點有專案用了JNI,也就說明這個專案基本不能跨平臺了。
  2. JNI呼叫是相當慢的,在實際使用的之前一定要先想明白是否有這個必要。
  3. 因為C++和C這樣的語言非常靈活,一不小心就容易出錯,比如我剛剛的程式碼就沒有寫析構字串釋放記憶體,對於java developer來說因為有了GC 垃圾回收機制,所以大多數人沒有寫解構函式這樣的概念。所以JNI也會增加程式中的風險,增大程式的不穩定性。