1. 程式人生 > >JNI簡易入門

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的步驟如下:

  1. 編寫.java檔案,需要使用本地語言實現的方法,新增native關鍵字,並且不需要實現(不在.java檔案中實現)。
  2. 使用javac工具,編譯.java檔案,生成.class檔案。
  3. 使用javah工具,生成.h標頭檔案。
  4. 新建與.h檔案對應的.cpp檔案,實現.h檔案中的函式。
  5. 編譯.cpp檔案 ,生成動態連結庫檔案(Windows下為dll檔案,Linux下位so檔案)
  6. 聯合.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就可以了)

同樣按照上述的步驟。

  1. idea新建Java工程

    新建一個空的Java工程,命名為JNI_Demo。

    在src目錄下,新建一個HelloWorld.java,檔案內容和之前相同。

       

  2. 編譯Java工程

    點選執行按鈕,編譯並允許,會有以下輸出:

    丟擲異常是正常的,我們還沒有建立動態連結庫檔案,自然找不到。

    執行的目的是檢視JVM系統引數user.dir的值,動態連結庫生成後要儲存在這個路徑。

       

  3. 使用javah工具生成標頭檔案

    右鍵工程檢視中的HelloWorld.java檔案,選擇"Open in Terminal",在開啟的命令列視窗使用javah工具,生成HelloWorld.h檔案。

    成功後,可以在工程檢視中看到HelloWorld.h檔案。

       

  4. 使用VS新建動態連結庫工程

    右鍵工程檢視中的HelloWorld.h檔案,選擇"Show in Explorer",檢視該檔案的儲存位置。

    使用VS,在該位置建立一個動態連結庫工程,同樣命名為HelloWorld:

       

    右鍵工程檢視的"Header Files"資料夾,選擇add-Existing Item,新增HelloWorld.h檔案到工程中。

       

    編寫工程中的HelloWorld.cpp檔案,內容與之前的類似。

       

  5. 編譯動態連結庫

    編譯前,需要對工程進行配置 。

    首先選擇動態庫生成的位數,與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檔案:

       

  6. 執行測試

    回到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完全手冊"。