JNI簡易入門
JNI簡介
JNI(Java Native Interface)是JDK的一部分,提供了若干API實現了Java和其他語言的通訊(主要是C/C++)。JNI主要用於以下場景:
- 貼近硬體底層的功能,Java無法實現;
- 複用已有的程式(非Java開發);
- 對部分程式碼有較高的效能要求,如矩陣運算、圖形渲染等;
在java的原始碼中,也多處使用了JNI,例如在Thread.java中,底層用於新建執行緒的方法,就是通過JNI,使用本地語言實現的。
Java的特點的跨平臺、可移植性強。但由於執行在JVM中,相對於本地語言來說,效能方面有一定的劣勢。使用JNI可以使Java可以與本地語言互相通訊(Java中可以呼叫本地語言的方法,本地語言也可以呼叫Java的方法),一定程度上提升程式碼的效能,但由於本地語言的引入,又讓Java失去了平臺移植的特性。
使用JNI時,本地語言通常是C/C++,可以看成Java與C/C++的混合開發。需要了解C/C++基本法語言 、標頭檔案、動態連結庫、呼叫約定等概念。
使用JNI的步驟如下:
- 編寫.java檔案,需要使用本地語言實現的方法,新增native關鍵字,並且不需要實現(不在.java檔案中實現)。
- 使用javac工具,編譯.java檔案,生成.class檔案。
- 使用javah工具,生成.h標頭檔案。
- 新建與.h檔案對應的.cpp檔案,實現.h檔案中的函式。
- 編譯.cpp檔案 ,生成動態連結庫檔案(Windows下為dll檔案,Linux下位so檔案)
- 聯合.class檔案,使用java工具執行。
如下圖所示:
《圖片來自百度百科》
第一個JNI程式
下面舉一個例子,在Java中,使用C語言的printf()函式,輸出"Hello World"。根據上面的流程,一步步執行。
1、編寫java檔案新建一個HelloWorld.java檔案,檔案的內容如下:
public class HelloWorld { public native void showHelloWorld();
static { System.out.println(System.getProperty("user.dir")); System.out.println(System.getProperty("java.library.path")); System.loadLibrary("HelloWorld"); }
public static void main(String[] args) { new HelloWorld().showHelloWorld(); } } |
有兩點需要注意:
- showHelloWorld()方法宣告中有native關鍵字,表示該方法使用JNI,使用本地語言實現,在java檔案中,不需要實現也能通過編譯。
- 在靜態區塊中,呼叫System.loadLibrary()方法,載入一個動態連結庫。這個動態連結庫等會由我們生成(在Windows下為dll檔案,在Linux下為so檔案 ),我們只需要指定檔名,JVM會根據當前的作業系統選擇合適的檔案格式。
此外,靜態區塊中還輸出了兩個JVM的系統屬性,後面會詳細說明這兩個屬性。
2、使用javac編譯
和編譯普通的Java程式一樣,編譯前先新建一個out資料夾,使用-d引數編譯。
javac -d ./out HelloWorld.java |
如果沒有錯誤,將會在out資料夾下,生成HelloWorld.class檔案。
3、使用javah生成標頭檔案
javah工具也是JDK的一部分,專門用於JNI開發,根據.java檔案,生成.h檔案。
使用方法與javac類似:
javah HelloWorld |
注意,不要帶有java字尾名。正常的話,會在.java的同級資料夾,生成.h檔案。
本例中,生成的HelloWorld.h的內容如下:
jni.h由JDK提供,是JNI的一部分,該檔案存放在<JAVA_HOME>/include路徑下。
JNIEXPORT和JNICALL都是巨集定義,不同平臺的定義不同,在Windows下的定義如下:
__declspec(dllexport)表示宣告的函式為匯出函式。
__stdcall是呼叫約定的一種,表示函式引數從右至左入棧,呼叫接受後,函式內部完成堆疊清理。
以上定義可以在jni_md.h檔案中找到,該檔案位於<JAVA_HOME>/include/win32。
注意jni.h與jni_md.h所在的路徑,後面編譯動態連結庫時,會用到。
該標頭檔案中聲明瞭一個函式——Java_HelloWorld_showHelloWorld(),該函式就是HelloWorld.java檔案中,使用native宣告的showHelloWorld()方法。native方法在沒有過載的情況下,與原生代碼中的函式對應關係如下:
Java_<包名>_<類名>_<方法名>();
4、編寫cpp檔案,實現標頭檔案中的函式
在HelloWorld.h所在的目錄下,新建HelloWorld.cpp檔案,檔案內容如下:
#include "HelloWorld.h" #include <stdio.h>
JNIEXPORT void JNICALL Java_HelloWorld_showHelloWorld (JNIEnv *, jobject) { printf("Hello World"); } |
注意第一行,包含了javah生成的HelloWorld.h檔案,並對標頭檔案中的函式進行了實現。
5、編譯cpp檔案位動態連結庫
不同平臺的編譯工具五花八門,例如在Window下,可以直接使用cl.exe編譯;或編寫Makefile,使用nmake編譯;又或者編寫vcxproj檔案,使用msbuild編譯。在Linux平臺下,可以使用gcc編譯,也可以編寫Makefile,使用make工具編譯。
為了儘可能保證可移植性,這裡選擇CMake作為編譯工具,讓原生代碼儘可能的支援多平臺。CMake的使用說明可以參考《CMake簡易入門》。
在HelloWorld.cpp所在的目錄下,新建CMakeLists.txt檔案,檔案內容如下:
# 指定cmake的最低版本 cmake_minimum_required(VERSION 2.8)
# 指定專案名 project(HelloWorld)
# 新增標頭檔案 include_directories($ENV{JAVA_HOME}/include $ENV{JAVA_HOME}/include/win32)
# 設定生成目錄 SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR})
# 生成連結庫檔案,三個引數分別是連結庫名、連結庫型別、原始碼檔案 add_library(${PROJECT_NAME} SHARED HelloWorld.cpp) |
第3個命令,指定標頭檔案jni.h與jni_md.h所在的路徑。
第5個命令,表示要編譯生成一個動態連結庫檔案,第一個引數指定動態連結庫檔名。PROJECT_NAME是cmake內建的變數,其值有第2個命令配置為HelloWorld。本例中,需要和第1步中,System.loadLibrary()入參相同。
編寫結束後,開啟VS的命令列視窗。VS的命令列視窗有x86、x64兩種,需要和JDK位數對應。JDK的位數,可以使用命令"java -version"檢視:
開啟VS的命令列後,cd切換到CMakeLists.txt所在的目錄,輸入以下命令,生成Makefile(本例子使用nmake工具編譯):
cmake -G "NMake Makefiles" -B .\out . |
成功執行後,在out目錄下,會生成以下檔案:
之後,使用以下命令(在CMakeLists.txt所在的目錄),編譯生成dll檔案:
cmake --build ./out --target HelloWorld |
或者可以使用cd命令進入out資料夾,之後使用namke命令編譯:
cd out nmake |
無論是那種方法,編譯成功後,均會在out目錄下,生成HelloWorld.dll檔案(還有其他檔案,這裡不關心):
6、使用java工具,執行測試
命令列視窗切換到out目錄,使用以下java工具,執行HelloWorld.class程式:
java HelloWorld |
如果前面步驟沒有問題,會有以下輸出:
先看末尾,紅框中的"Hello World"就是用原生代碼——printf()輸出的結果,這說明Java與C/C++的互動成功了。
再回到HelloWorld.java中,System.loadLibrary()僅根據一個檔名就找到了動態連結庫檔案,這裡引出一個問題,動態連結庫檔案要儲存在哪裡?是不是和本例中,和.class檔案儲存在同級目錄就可以了?
答案是否定的。System.loadLibrary()載入動態連線庫時,會到指定的路徑中的搜尋,這個路徑儲存在JVM系統引數java.library.path中。該引數實際上就是本地系統的環境變數PATH加上當前路徑(注意該引數的末尾,";."表示當前路徑,也就是JVM系統引數user.dir的值)。如果在這些路徑中找不到指定的動態連結庫,就會丟擲以下java.lang.UnsatisfiedLinkError:
使用IDE
但工程畢竟複製時,命令列編譯就會變得畢竟繁瑣。下面使用idea+VS的組合,完成JNI的開發。idea複製java部分的編譯,VS負責C++的編譯。(據說VS 2019支援Java ,那個時候估計只需要VS就可以了)
同樣按照上述的步驟。
-
idea新建Java工程
新建一個空的Java工程,命名為JNI_Demo。
在src目錄下,新建一個HelloWorld.java,檔案內容和之前相同。
-
編譯Java工程
點選執行按鈕,編譯並允許,會有以下輸出:
丟擲異常是正常的,我們還沒有建立動態連結庫檔案,自然找不到。
執行的目的是檢視JVM系統引數user.dir的值,動態連結庫生成後要儲存在這個路徑。
-
使用javah工具生成標頭檔案
右鍵工程檢視中的HelloWorld.java檔案,選擇"Open in Terminal",在開啟的命令列視窗使用javah工具,生成HelloWorld.h檔案。
成功後,可以在工程檢視中看到HelloWorld.h檔案。
-
使用VS新建動態連結庫工程
右鍵工程檢視中的HelloWorld.h檔案,選擇"Show in Explorer",檢視該檔案的儲存位置。
使用VS,在該位置建立一個動態連結庫工程,同樣命名為HelloWorld:
右鍵工程檢視的"Header Files"資料夾,選擇add-Existing Item,新增HelloWorld.h檔案到工程中。
編寫工程中的HelloWorld.cpp檔案,內容與之前的類似。
-
編譯動態連結庫
編譯前,需要對工程進行配置 。
首先選擇動態庫生成的位數,與JDK要相同。
工程檢視中,右鍵HelloWorld工程,選擇"Properties"(或者使用Alt + F7),開啟工程屬性配置視窗。
先配置動態庫檔案生成的位置,目標位置就是第1步中,user.dir的值(也就是java工程的根目錄)。
之後新增標頭檔案路徑,一共需要新增3個路徑:
這3個路徑分別是jni.h、jni_md.h、HelloWorld.h這3個頭檔案所在的目錄。
配置正確,HelloWorld.cpp中的下滑紅線會全部消失。右鍵專案,選擇"Build",開始編譯。
編譯成功的話,在idea的工程檢視,可以看到HelloWorld.dll檔案:
-
執行測試
回到idea,工程檢視中右鍵src/HelloWorld資料夾,選擇"Mark Directory as"-"Exclusion"。不然執行程式會有以下錯誤:
配置結束後,就可以正常執行程式:
除了idea + VS的組合外,還有其他多種組合,沒有固定搭配,選擇合適自己的工具即可。
進階
結束了嗎?不,到這裡為止才剛剛開始。通過JNI我們可以與原生代碼相互通訊,互相通訊包括:
- Java呼叫原生代碼
- 原生代碼呼叫Java方法
- Java向原生代碼傳遞引數
- 原生代碼項Java返回資料
我們目前只完成了第一步,後面的可以參考官方文件:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
中文翻譯可以百度"JNI-API完全手冊"。