Java 本地介面 JNI 使用詳解

曹勝歡發表於2015-08-10

對於java程式設計師來說,java語言的好處和優點,我想不用我說了,大家自然會說出很多一套套的。但雖然我們作為java程式設計師,但我們不得不承認java語言也有一些它本身的缺點。比如在效能、和底層打交道方面都有它的缺點。所以java就提供了一些本地介面,他主要的作用就是提供一個標準的方式讓java程式通過虛擬機器與原生程式碼進行互動,這也就是我們平常常說的java本地介面(JNI——java native Interface)。它使得在 Java 虛擬機器(VM) 內部執行的Java 程式碼能夠與用其它程式語言(如 C、C++ 和組合語言)編寫的應用程式和庫進行互操作。JNI 最重要的好處是它沒有對底層 Java 虛擬機器的實現施加任何限制。因此,Java虛擬機器廠商可以在不影響虛擬機器其它部分的情況下新增對JNI 的支援。程式設計師只需編寫一種版本的本地應用程式或庫,就能夠與所有支援JNI 的Java 虛擬機器協同工作。我們來看一下為什麼要與原生程式碼進行互動:

一:提高應用程式效能。我們知道java對於c/c++、組合語言來說,顯得比較“高階”。其實這裡的高階就是簡化了程式設計師的工作。很多底層的東西都讓java虛擬機器做了。但畢竟相對於直接訪問底層來講,java多了一步虛擬機器的過程,所以在效能上比著這些原生語言稍微有點慢。

二:實現一些與底層相關的功能。Java平臺提供的標準類庫,還有強大的API,雖然能完成大部分功能。但有些和底層硬體打交道的功能在java API提供的類庫中還是無法完成。

三:與已有的使用原生程式碼編寫的程式進行整合。在於作業系統上由c或者c++等原生語言編寫的軟體進行整合的時候,可以用JNI。

JNI 介面函式和指標

平臺相關程式碼是通過呼叫 JNI 函式來訪問Java 虛擬機器功能的。JNI 函式可通過介面指標來獲得。介面指標是指標的指標,它指向一個指標陣列,而指標陣列中的每個元素又指向一個介面函式。每個介面函式都處在陣列的某個預定偏移量中。下圖說明了介面指標的組織結構。

Java本地介面JNI詳解

JNI 介面的組織類似於C++ 虛擬函式表或COM 介面。使用介面表而不使用硬性編入的函式表的好處是使JNI 名字空間與平臺相關程式碼分開。虛擬機器可以很容易地提供多個版本的JNI 函式表。例如,虛擬機器可支援以下兩個JNI 函式表:

1)一個表對非法引數進行全面檢查,適用於除錯程式;

2)另一個表只進行 JNI 規範所要求的最小程度的檢查,因此效率較高。

JNI 介面指標只在當前執行緒中有效。因此,本地方法不能將介面指標從一個執行緒傳遞到另一個執行緒中。實現 JNI 的虛擬機器可將本地執行緒的資料分配和儲存在 JNI 介面指標所指向的區域中。

本地方法將JNI 介面指標當作引數來接受。虛擬機器在從相同的 Java 執行緒中對本地方法進行多次呼叫時,保證傳遞給該本地方法的介面指標是相同的。但是,一個本地方法可被不同的 Java 執行緒所呼叫,因此可以接受不同的 JNI 介面指標。

Java本地介面JNI詳解

1)編寫Java類程式碼

其中,需要JNI實現的方法應當用native關鍵字宣告,在該類中,用System.loadLibrary()方法載入需要的動態連結庫,關鍵程式碼如下:

//Compute.java
public class Compute{
    public native double sqrt(double  params);
    static{
        //呼叫動態連結庫
        System.loadLibrary("compute");
    }
}

2)編譯成位元組程式碼

在這個過程中,由於採用了native關鍵字宣告,Java編譯器會忽視沒有程式碼體的JNI方法部分。

3)生成相關JNI方法的標頭檔案

這個過程的實現一般是通過利用jlavah-jni  * class生成的(-jni可以省略),也可以手工生成該檔案;但是由於 Java 虛擬機器是根據一定的命名規範完成對JNI方法的呼叫,所以手工編寫標頭檔案需要特別小心。

上述檔案產生的標頭檔案部分程式碼如下:

//Compute.h
extern“C”{
JNIEXPORT jdoubleJNICALL Java_Compute_comp(JNI-Env *, jobject, jdoubleArray);

JNI函式名稱分為三部分:首先是Java關鍵字,供Java虛擬機器識別;然後是呼叫者類名稱(全限定的類名,其中用下劃線代替名稱分隔符);最後是對應的方法名稱,各段名稱之間用下劃線分割。

JNI函式的引數也由三部分組成:首先是JNIEnv *,是一個指向JNI執行環境的指標;第二個引數隨本地方法是靜態還是非靜態而有所不同一一非靜態本地方法的第二個引數是對物件的引用,而靜態本地方法的第二個引數是對其Java類的引用;其餘的引數對應通常Java方法的引數,引數型別需要根據一定規則進行對映。

4)編寫相應方法的實現程式碼

在編碼過程中,需要注意變數的長度問題,例如Java的整型變數長度為32位,而C語言為16位,所以要仔細核對變數型別對映表,防止在傳值過程中出現問題。

5)將JNI實現程式碼編譯成動態連結庫

編譯過程是利用C/C++編譯器實現的,在windows平臺上,編譯和連線的結果是動態連結庫DLL檔案。當要使用生成的動態連結庫時,呼叫者類中需要顯式呼叫該連結庫dll檔案。
經過上述處理,基本上完成了一個包含本地化方法的Java類的開發。

附錄:將Jav型別對映到本地C 型別

Java本地介面JNI詳解

為了使用方便,特提供以下定義。

#define JNI_FALSE  0

#define JNI_TRUE   1

jsize 整數型別用於描述主要指數和大小:

typedef jint jsize;

故障排除

當使用 JNI 從Java 程式訪問本機程式碼時,您會遇到許多問題。您會遇到的三個最常見的錯誤是:

1)無法找到動態連結。它所產生的錯誤訊息是:java.lang.UnsatisfiedLinkError。這通常指無法找到共享庫,或者無法找到共享庫內特定的本機方法。

2)無法找到共享庫檔案。當用 System.loadLibrary(String libname) 方法(引數是檔名)裝入庫檔案時,請確保檔名拼寫正確以及沒有指定副檔名。還有,確保庫檔案的位置在類路徑中,從而確保 JVM 可以訪問該庫檔案。

3)無法找到具有指定說明的方法。確保您的 C/C++ 函式實現擁有與標頭檔案中的函式說明相同的說明。

結束語

從 Java 呼叫 C 或 C++ 本機程式碼(雖然不簡單)是 Java 平臺中一種良好整合的功能。雖然 JNI 支援 C 和 C++,但 C++ 介面更清晰一些並且通常比 C 介面更可取。正如您已經看到的,呼叫 C 或 C++ 本機程式碼需要賦予函式特殊的名稱,並建立共享庫檔案。當利用現有程式碼庫時,更改程式碼通常是不可取的。要避免這一點,在C++ 中,通常建立代理程式碼或代理類,它們有專門的 JNI 所需的命名函式。然後,這些函式可以呼叫底層庫函式,這些庫函式的說明和實現保持不變。

相關文章