java使用native关键字声明本地方法,和声明通常的方法没有什么区别,只有两点不同:一是必须有native关键字,二是以分号结束,没有{},因为在类中并没有实现,这和声明抽象方法类似。本地方法一般是c/c++实现。下面以一个简单的例子来实现一个类,这个类调用本地方法。
public class JNIDemo { public native String input(String prompt); public native int sum(int a, int b); public native int sum(int[] a); static { System.load("/home/fgp/program/java/jni/libutil.so"); /* 以下方法,需要设置lib库的路径 */ //System.loadLibrary("util"); } public static void main(String[] args) { JNIDemo jni = new JNIDemo(); String name = jni.input("Type your name:"); System.out.println("Your name is " + name); System.out.println(jni.sum(1, 2)); System.out.println(jni.sum(new int[]{1, 2, 3, 4, 5})); } }
我们声明了三个方法,同时使用了方法重载。这个三个方法均在main方法中被调用。
使用javac命令编译:
javac JNIDemo.java
此时如果运行java JNIDemo,则会出现异常,因为我们还没有实现本地方法。
在实现本地方法之前,最好先自动生成c头文件,使用javah:
javah -jni JNIDemo
此时会自动产生c头文件,
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class JNIDemo */ #ifndef _Included_JNIDemo #define _Included_JNIDemo #ifdef __cplusplus extern "C" { #endif /* * Class: JNIDemo * Method: input * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_JNIDemo_input (JNIEnv *, jobject, jstring); /* * Class: JNIDemo * Method: sum * Signature: (II)I */ JNIEXPORT jint JNICALL Java_JNIDemo_sum__II (JNIEnv *, jobject, jint, jint); /* * Class: JNIDemo * Method: sum * Signature: ([I)I */ JNIEXPORT jint JNICALL Java_JNIDemo_sum___3I (JNIEnv *, jobject, jintArray); #ifdef __cplusplus } #endif #endif
初次看起来与我们平常的头文件有点不同,这就是java与本地方法通信的协议,或者说规范。比如函数名必须是Java_ClassName_MethodName__Sigature(JNIEnv *, jobject, arg...)。Sigature主要解决c没有实现重载的问题。函数形参JNIEnv* 和jobject 是必须的,分别表示JNI接口指针和对象本身(类似c++ 的this指针)。
我们不难看出,java的原始数据类型与c类型一一对应,本地方法可以直接访问,java中int对应jint,float对应jfloat等。而非原始类型,则直接对应jobject,比如java中的Object对应jobject,当然为了减少程序出错,引入了jobject的子类,比如jstring,jarray,jintArray等等。
这样以上的头文件也就不难理解了。
下面实现本地方法,注意本地方法实现时需要和头文件一致。
先从input方法说起。
#include <stdio.h> #include "JNIDemo.h" JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { }
首先jstring并不是c中常规字符串(char *),而是自己重新封装了下,
因此直接调用printf("%s", prompt)是错误的。必须调用JNI的函数GetStringUTFChars把它转化成c字符串。同样的c字符串(char *)可以调用newStringUTF函数转化成jstring。以下就简单多了。实现如下:
JNIEXPORT jstring JNICALL Java_Prompt_input(JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const char *str = (*env)->GetStringUTFChars(env, prompt, 0); printf("%s\n", str); (*env)->ReleaseStringUTFChars(env, prompt, str); scanf("%s", buf); return (*env)->NewStringUTF(env, buf); }
注意不再使用的jstring,需要手动调用ReleaseXX释放内存。
以下两个方法实现也不难理解,最后的完整版本为:
#include <stdio.h> #include "JNIDemo.h" JNIEXPORT jstring JNICALL Java_JNIDemo_input(JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const char *str = (*env)->GetStringUTFChars(env, prompt, 0); printf("%s\n", str); /* 必须调用释放函数,否则内存泄漏 */ (*env)->ReleaseStringUTFChars(env, prompt, str); scanf("%s", buf); return (*env)->NewStringUTF(env, buf); } JNIEXPORT jint JNICALL Java_JNIDemo_sum__II(JNIEnv *env, jobject obj, jint a, jint b) { return a + b; } JNIEXPORT jint JNICALL Java_JNIDemo_sum___3I(JNIEnv *env, jobject obj, jintArray array) { int i; int sum = 0; jsize len = (*env)->GetArrayLength(env, array); jint *a = (*env)->GetIntArrayElements(env, array, 0); for (i = 0; i < len; i++) sum += a[i]; (*env)->ReleaseIntArrayElements(env, array, a, 0); return sum; }
最后编译c文件为动态链接库(windows为dll文件,unix为libXX.so文件),
#!/bin/bash gcc -I/usr/lib/jvm/java-7-openjdk-i386/include/ -I/usr/lib/jvm/java-7-openjdk-i386/include/linux/ JNIDemo.c -shared -o libutil.so
然后运行java JNIDemo,输出结果为:Type your name:
Mary Your name is Mary 3 15
另外注意System.load()必须使用绝对路径,否则会出错,如果你的动态链接库已在环境变量中可以直接调用System.loadLibrary("util");
如果出现java.lang.UnsatisfiedLinkError,则可能路径不对,jvm没有找到对应的动态链接库。
参考:http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/index.html