【詳解】JNI(Java Native Interface)
前言:
一提到JNI,多數程式設計者會下意識地感受到一種無法言喻的恐懼。它給人的第一感覺就是"難",因為它不是單純地在JVM環境內操作Java程式碼,而是跳出虛擬機器與其他程式語言進行互動。
你可能至今還沒聽說過這個技術,但是如果你是一個原始碼愛好者,或者有翻閱過JDK的一些原始碼,那你一定有接觸過native方法。你是否因為查閱原始碼直到native方法戛然而止,但又由於它的空方法體,而對底層原理不知所以? 本文就帶讓你瞭解JNI。並通過一些案例來自己實現JNI的互動。
什麼是JNI?
JNI 全稱 Java Native Interface。Java本地方法介面,它是Java語言允許Java程式碼與C、C++程式碼互動的標準機制。維基百科是這樣解釋的:“當應用無法完全用Java程式語言實現的時候,(例如,標準Java類庫不支援的特定平臺特性或者程式庫時),JNI使得程式設計者能夠編寫native方法來處理這種情況”。這就意味著,在一個Java應用程式中,我們可以使用我們需要的C++類庫,並且直接與Java程式碼互動,而且在可以被呼叫的C++程式內,反過來呼叫Java方法(回撥函式)。
JNI的優點:
(1)JNI使得一些"過程"無需在Java中實現。例如,硬體敏感的,或者直接與作業系統API關聯的命令。
(2)由於使用底層的庫,如圖形,計算,各種型別的渲染等等,可以提高應用的執行效能。
(3)已經有大量的庫已經被實現,程式設計者可直接使用,不用再自行編寫。這裡的庫指的是用其他程式語言實現的程式庫,例如IO流或者執行緒等底層與OS互動的操作都是由C/C++實現的。
具體實現原理
互動模式如圖
要從Java呼叫C++函式,你需要進行以下操作:
1. 在Java類中建立一個native方法,此方法被本類其他方法呼叫
2. 建立一個頭檔案,可以利用javah命令生成。
在標頭檔案中定義它的簽名,如下所示:
介面規範:
JNIEXPORT <返回型別> JNICALL Java_<包名>_<類名>_<方法名>(JNIEnv*, <原物件引用>,<引數1>..<引數n>)
- extern "C" 只被C++編譯器識別,標明此方法利用C的函式命名協議來編譯。
- JNIEXPORT 是JNI必要的修飾符。
- 資料型別帶有"j"字首的:jdouble,jobject..等是Java物件或型別在C++中的對映
- JNIEnv* 指向JNI 環境,可以利用其呼叫所有JNI函式
- jobject 引用當前Java物件
3. 建立一個原始檔,實現標頭檔案中定義的介面。實現內容就是Java程式碼呼叫的C/C++程式碼。
4. 編譯標頭檔案和原始檔生成C/C++動態連結庫 .so/.dll 檔案
5. 此native方法所在類,載入動態連結庫。因為載入連結庫要在執行native方法之前,所以此載入過程一般放在靜態初始化塊內執行。
或
總結一下,從Java程式碼中呼叫C/C++程式碼的流程:
(1)建立一個有native標識的方法,並且從其他Java方法呼叫它
(2)Java編譯器生成位元組碼
(3)C/C++ 編譯器生成動態庫 .so檔案(Linux)或 .dll檔案(Windows)
(4)執行程式,執行位元組碼
(5)執行到loadLibary或load呼叫的時候,新增一個 .so檔案到這個程序中
(6)執行到native方法的時候,通過方法簽名,在已開啟的.so檔案中進行搜尋。
(7)如果連結庫內有對應方法,就會被執行,否則程式崩潰
注:由於windows沒找到生成動態連結庫的工具,又不想安裝C/C++開發環境,故以下案例都在以CentOS為作業系統的虛擬機器內執行
案例一:從Java呼叫C++程式碼輸出Hello World
此案例所有生成的所有檔案如下:
(1)建立JNI資料夾,建立Java檔案如下:
這裡,我們定義了一個native方法,是個空方法體,我們在主函式內對其進行呼叫。
注:這裡使用的是System.load從絕對路徑引用動態連結庫,當然也可以使用loadLibrary方法,其是從java.library.path對應路徑下搜尋對應名稱的庫檔案並載入。
(2)編譯HelloJNI.java檔案,生成類檔案
(3)利用JDK提供的JNI命令工具,javah生成 .h標頭檔案。
注:發現Linux環境下,javah居然不能從當前資料夾掃描到類檔案,需要指定類路徑 其中 -cp 就是-classpath
以下是利用javah生成的標頭檔案。
(4)建立HelloJNI.c檔案,編寫實現體
(5)利用gcc生成動態連結庫,注意我們這裡有引用到jni.h這個標頭檔案,此檔案由JDK提供,另外jni.h還引用了jni_md.h這個檔案。必須引入這兩個標頭檔案,才能通過編譯。
兩個檔案的所在地,本人JDK的安裝路徑在/usr/java下,每個人可能都不一樣。
在gcc命令內通過指定( -I 路徑 )引入庫所在的目錄,利用前面前面的標頭檔案和原始檔編譯成動態連結庫 hello.so
(6)執行java程式
由圖可知,我們成功呼叫了C的程式碼
未完待續...
以上只介紹了簡單的無參函式呼叫,接下來,還有
Java有參呼叫C++程式碼,
C++程式碼返回值給Java方法,
C++程式碼獲取native方法所在物件的欄位值
C++程式碼呼叫Java的其他函式(回撥)