JNI是Java與C、C++、Objective-C、Objective-C++等靜態編譯語言以及匯編語言相交互的接口。盡管目前而言,Java提供了諸多運行時性能較高的運行時庫,但是在很多方面,尤其是高性能計算領域,Java提供的高效庫還不是很多,因此我們可以通過JNI接口將我們用靜態語言以及匯編編譯連接為動態庫後給Java應用程序加載調用。
首先,Java為不同的操作系統平台提供了各自相適應的運行時環境以及根據不同的編譯器提供了JNI頭文件。JNI頭文件一般由兩個組成:jni_md.h提供了依賴於平台的頭文件;jni.h提供了jni所需要的接口聲明以及各種類型的定義。這兩個頭文件都可以在JDK的include中找到。我們在創建一個JNI動態庫的工程時應該將工程的輸出目標設置為動態連接庫(瘟抖死下為.dll,Unix-like系統下為.so,OS X下為.dylib)。我們在創建工程時可以將這兩個頭文件導入到工程中。
然後我們可以看以下代碼:
然後我們可以看以下代碼:
#include <stdio.h>
#include "jni.h"
JNIEXPORT jint JNICALL Java_MyJNI_myTest(JNIEnv *env, jobject obj, jintArray dstArray, jintArray srcArray)
{
jboolean isCopy = 0;
jint* csrcArray = (*(*env)->GetIntArrayElements)(env, srcArray, &isCopy);
jsize dstSize = (*(*env)->GetArrayLength)(env, dstArray);
jsize srcSize = (*(*env)->GetArrayLength)(env, srcArray);
jsize length = dstSize >= srcSize? dstSize : srcSize;
printf("The length is: %u\n", length);
printf("Is copy available? %d\n", isCopy);
printf("The sum of source array is: %d", csrcArray[0] + csrcArray[1]);
jint dstBuffer[32];
for(jsize i = 0; i < length; i++)
dstBuffer[i] = csrcArray[i] + i + 100;
(*(*env)->SetIntArrayRegion)(env, dstArray, 0, length, dstBuffer);
return 100;
}
以上代碼提供了一個導出Java方法,Java_後面的名字表示在Java應用端的類名,類名後面的_所跟的名字是該類的方法名(該方法可以是成員方法也可以是類方法)。一個標准的JNI方法應該提供兩個形參,一個是JNIEnv*,另一個是jobject。env提供了Java運行時環境的句柄,後面調用各種Java運行時方法都需要傳這個參數。jobject是指向該對象的指針,相當於Java應用端中的this。因此如果你所定義的這個JNI方法在Java應用端是一個類方法,那麼這個參數即被忽略。
以上方法實現的功能是將第二個數組的每個元素加上100再加其索引後的值相應賦值給第一個目的數組元素。這邊假定兩個數組的元素個數最多為32。最後返回一個int類型的值100。
然後,我們可以看Java端相應的代碼:
class MyJNI {
native static int myTest(int[] dstArray, int[] srcArray);
}
public class Test {
static {
System.loadLibrary("MyJNI");
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] dstArray = new int[2];
int[] srcArray = {100, 600};
System.out.println("The value is: " + MyJNI.myTest(dstArray, srcArray));
System.out.println("The sum is: " + (dstArray[0] + dstArray[1]));
}
}
這裡調用System.loadLibrary方法來加載動態連接庫。
在使用Linux環境時必須注意,在運行前必須設置LD_LIBRARY_PATH環境變量以指定動態庫所在的路徑,比如:
LD_LIBRARY_PATH='/usr/home/java_test'
export LD_LIBRARY_PATH
如果使用Eclipse開發環境的話,我們可以在當前項目的Run Configuration以及Debug Configuration中來設置此環境變量。方便起見,我們在設置路徑時可以使用右側的variable按鈕,選擇project_loc。這個內建變量指定了當前項目的系統絕對路徑。然後把動態庫導入到當前項目的根目錄下即可。
最後,我們舉一個更為完整的例子。這個例子中,我們在JNI代碼中將加入對匯編語言的一起編譯鏈接,做成.so文件。這裡要注意的是,由於匯編器不支持-fPIC選項,因此要與匯編文件一起鏈接的話就不能帶-fPIC的編譯選項了,否則連接就會失敗。
我們先看Java代碼:
package test;
public class Main {
static {
System.loadLibrary(("ctest"));
}
native static int myJNITest();
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("The answer is: " + myJNITest());
}
}
這裡加入了一個package——test,因此,對於JNI的native函數myJNITest而言,其前綴就需要把包名加上,變為——
Java_test_Main_myJNITest。
我們下面就看JNI部分。我們這裡可以再建立一個JNI的工程,方便後面使用shell進行編譯。首先,在此工程裡創建一個C源文件,文件名可以隨便命名,.c作為後綴即可,這裡用a.c:
#include <jni.h>
extern int __attribute__((fastcall)) asmTest(void);
JNIEXPORT jint JNICALL Java_test_Main_myJNITest(JNIEnv *env, jobject obj)
{
return 100 + asmTest();
}
這裡的asmTest函數就定義在一個匯編文件裡,下面將會看到。這裡使用fastcall調用規則,使得參數以及返回值都能在寄存器中,這樣可以方便匯編函數對實參的獲取。當然,在64位應用環境下就不需要fastcall了。因為64位環境下,x86-64遵循的是System-V的函數調用約定,具體可參考——http://www.cocoachina.com/bbs/read.php?tid-66986.html
下面看看匯編源文件(asmtest.s):
.text
.align 2
.globl asmTest
asmTest:
mov $15, %eax
ret
完成了C源代碼以及匯編代碼之後,我們將寫一個簡單的shell文件把它們分別編譯,然後再連接成一個so動態共享庫文件。
gcc -Wall -c -I/home/zenny_chen/MyPrograms/eclipse/jdk/include -I/home/zenny_chen/MyPrograms/eclipse/jdk/include/linux a.c
gcc -Wall -c asmtest.s
gcc -shared -z noexecstack -o libctest.so a.o asmtest.o
由於jni.h在 jdk/include 下,而jni_md.h則是在 jdk/include/linux 下(其它操作系統則是其它操作系統的名稱),因此,這裡要把兩個頭文件包含路徑都加上。另外,最後的-z noexecstack要加,因為Java會對棧進行檢查,如果沒有此連接選項,Java在調用此函數時就會報warning(比較煩人,呼呼~)。最後,連接生成的庫名就是libctest.so,這個文件名必須與Java中loadLibrary的庫名一致。
Oracle官方對JNI的說明文檔(基於Java8):http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html