Java JNI的使用基础
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