1 引言
1.1 简介
在实际项目中,很多时候是以多种语言混合开发的。常用的模式是使用 C/C++ 做底层支撑,然后上层辅之以高级语言。 JNI可以使 native 代码与 Java 对象交互,而不会像 Java 代码中的功能那样受到诸多限制, 往往充作 Java 与 C/C++/Assembly 语言的粘合剂。
JNI 明确分开了 Java 代码与 native 代码的执行,为两者的通信定义了一套清晰的API.避免 native 代码对 JVM 的直接内存引用。
2 JNI 入门
2.1 JNI 调用 C
2.1.1 java 中的 native 方法声明
HelloJNI.java
1 2 3 4 5 6 7 8 9 10 11 public class HelloJNI { static { System.loadLibrary("hello" ); } private native void sayHello () ; public static void main (String[] args ) { new HelloJNI ().sayHello(); } }
当HelloJNI类加载的时候,静态初始化调用 System.loadLibrary() 来加载本地库 hello.dll(或libhello.so )。如果不能加载 hello 库,会抛出 UnsatisfiedLinkError 异常。
然后我们使用 native 关键字声明了 native 方法 sayHello() ,这表明该方法是使用其它语言实现的。
最后 main() 方法实例化了 HelloJNI 并调用了 sayHello().
然后我们编译这个类:
2.1.2 生成 C/C++ 头文件
使用 javah 创建头文件:
会得到如下 .h 文件 HelloJNI.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <jni.h> #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" {#endif JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv *, jobject) ; #ifdef __cplusplus } #endif #endif
这份自动生成的头文件里声明一个 C 函数 Java_HelloJNI_sayHello:
1 JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv *, jobject) ;
这个方法名是由javah转换来的,转换规则为:
1 Java_{package_and_classname}_{function_name}(JNI arguments)
包名中的 点 会被转换为 下划线
函数中带有两个参数(虽然我们在本例中并没有使用这两个参数)
2.1.3 C 代码的实现
HelloJNI.c
1 2 3 4 5 6 7 8 #include <jni.h> #include <stdio.h> #include "HelloJNI.h" JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv * env, jobject obj) { printf ("Hello World!" ); }
这个实现将在控制台打印万能的 “Hello World!”
文件中包含了 jni.h 头文件。这个头文件由 java 提供,位于 %JAVA_HOME%/include 路径下(Windows下还需要引用 %JAVA_HOME%/include/win32).
2.1.4 编译 C 库
接下来,编译这个 c 文件:
1 2 3 4 5 6 > gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c > cl /c /I%JAVA_HOME%/include /I%JAVA_HOME%/include/win32 HelloJNI.c > link HelloJNI.obj /DLL /OUT:hello.dll
2.1.5 检查成果
1 2 3 > java HelloJNI > java -Djava.library.path=. HelloJNI
3 在 Java 和 Native 间传递参数
3.1 传递原始类型参数
Java 原始参数可以在 Java 和 Native 单直接传递,只不过这些类型在JNI中的名称不一样。JNI 定义了 jxxx 类型来对应相应的原始类型。如 jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分别对应 Java 中的 int, byte, short, long, float, double, char, boolean. 下例中,将演示如何传递 Java 原始参数。这一次,我们编辑的顺序将从 native 到 Java。
3.1.1 编辑 C 头文件
打开 HelloJNI.h,在文件中添加函数的声明:
1 JNIEXPORT jint JNICALL Java_HelloJNI_add (JNIEnv*, jobject, jint, jint) ;
这里用到了 jint 。我们可以在 jni.h 和 jni_md.h 中找到各原始类型的定义:
1 2 3 4 5 6 7 8 9 10 11 12 typedef long jint;typedef __int64 jlong;typedef signed char jbyte;typedef unsigned char jboolean;typedef unsigned short jchar;typedef short jshort;typedef float jfloat;typedef double jdouble;typedef jint jsize;
3.1.2 实现函数
在 HelloJNI.c 中实现上一步声明的函数:
1 2 3 4 JNIEXPORT jint JNICALL Java_HelloJNI_add (JNIEnv* env, jobject obj, jint a , jint b) { return (a + b); }
3.1.3 编辑 Java 文件
在Java中声明 native 方法并调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class HelloJNI { static { System.loadLibrary("hello" ); } private native void sayHello () ; private native int add (int a, int b) ; public static void main (String[] args ) { HelloJNI hj = new HelloJNI (); hj.sayHello(); System.out.println("the result of 3 add 4 is: " + hj.add(3 ,4 )); } }
3.1.4 编译并执行
像上一章那样,编译 HelloJNI.c 和 HelloJNI.java 并执行 Java 程序,结果如下:
1 2 Hello World! the result of 3 add 4 is: 7
3.2 传递 String 参数
3.2.1 示例
Java 代码
1 private native String sayHi (String name) ;
C 函数声明
1 JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi (JNIEnv *, jobject, jstring) ;
JNI 使用 jstring 来表示 Java String. 最后一个参数 jstring 是 Java 传递到 native 的 String 参数
C 函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi (JNIEnv * env, jobject obj , jstring str) { const char * pcsz = (*env)->GetStringUTFChars(env, str, NULL ); if (NULL == pcsz) return NULL ; char szMsg[256 ]; sprintf (szMsg,"say hi to %s from C native" ,pcsz); (*env)->ReleaseStringUTFChars(env, str, pcsz); return (*env)->NewStringUTF(env, szMsg); }
一般使用 GetStringUTFChars() 将接收到的 jstring 转换为 C-string ; 使用 NewStringUTF 将 C-string 转换为 jstring 返回给 Java
Java String 和 C-string 不同,是一个复杂的引用类型,在传递过程中需要将字符串在这两种类型间转换。JNIEnv*(JNI Enviroment) 提供了转换的方法:
1 2 3 4 5 const char * GetStringUTFChars (JNIEnv*, jstring, jboolean*) jstring NewStringUTF (JNIEnv*, char *)
GetStringUTFChars() 函数将从第二个参数 jstring 参数中返回 C-string(char*)。如果有内存分配错误,则返回NULL. 函数的第三个参数 isCopy 可以设置为 JNI_TRUE 或 JNI_FALSE . 如果是 JNI_TRUE, 则返回Java String 的副本的 C-string. 如果是 JNI_FALSE,则直接返回 Java String 参数的 C-string(char*) 指针,此时不要改变返回值的内容。
ReleaseStringUTFChars() 函数将用来释放 GetStringUTFChars() 的返回值。只要 GetStringUTFChars() 的返回值不再被使用时,它应该被立即释放以便垃圾回收器介入处理。
NewStringUTF() 用来从 C-string 创建 jstring.
3.2.2 JNI String 操作函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const char * GetStringUTFChars (JNIEnv *env, jstring string , jboolean *isCopy) ; void ReleaseStringUTFChars (JNIEnv *env, jstring string , const char *utf) ; jstring NewStringUTF (JNIEnv *env, const char *bytes) ; jsize GetStringUTFLength (JNIEnv *env, jstring string ) ; void GetStringUTFRegion (JNIEnv *env, jstring str, jsize start, jsize length, char *buf) ; const jchar * GetStringChars (JNIEnv *env, jstring string , jboolean *isCopy) ; void ReleaseStringChars (JNIEnv *env, jstring string , const jchar *chars) ; jstring NewString (JNIEnv *env, const jchar *unicodeChars, jsize length) ; jsize GetStringLength (JNIEnv *env, jstring string ) ; void GetStringRegion (JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf) ;
更多的 JNI 函数可参考这里
3.3 传递原始类型数组
3.3.1 示例
Java 代码
1 private native double [] sumAndAvg(int [] nums);
JNI C 函数声明
1 JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg (JNIEnv *, jobject, jintArray) ;
JNI C 函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg (JNIEnv *env, jobject obj, jintArray nums) { jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL ); if (pcArray == NULL ) return NULL ; jsize len = (*env)->GetArrayLength(env,nums); jint sum = 0 ; for (int i = 0 ; i < len; i++) { sum += pcArray[i]; } jdouble avg = (jdouble)sum / len; (*env)->ReleaseIntArrayElements(env,nums, pcArray, 0 ); jdouble outArray[] = {sum,avg}; jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2 ); if (outArray == NULL ) return NULL ; (*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2 , outArray); return outJNIArray; }
3.3.2 JNI 原始类型数组操作函数
JNI 原始数据类型的数组(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray and jbooleanArray)操作函数如下:
1 2 3 4 5 6 7 8 9 10 NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array , jboolean *isCopy); void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array , NativeType *elems, jint mode);void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array , jsize start, jsize length, NativeType *buffer);void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array , jsize start, jsize length, const NativeType *buffer);ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length); void * GetPrimitiveArrayCritical (JNIEnv *env, jarray array , jboolean *isCopy) ;void ReleasePrimitiveArrayCritical (JNIEnv *env, jarray array , void *carray, jint mode) ;
GetArrayElements() 从 Java 数组创建 C 数组
Get|SetArrayRegion()将从 Java 数组和 C 数组中来回拷贝数据
NewArray() 分配新的 Java 数组内存
GetPrimitiveArrayCritical() 得到原始数据类型内容的指针,可能使得垃圾回收器不回收此内存,直到 ReleasePrimitiveArrayCritical() 调用
4 附录
4.1 本文源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class HelloJNI { static { System.loadLibrary("hello" ); } private native void sayHello () ; private native int add (int a, int b) ; private native String sayHi (String name) ; private native double [] sumAndAvg(int [] nums); public static void main (String[] args ) { HelloJNI hj = new HelloJNI (); hj.sayHello(); System.out.println("the result of 3 add 4 is: " + hj.add(3 ,4 )); System.out.println("say hi to wandoer: " + hj.sayHi("wandoer.com" )); int [] nums = new int []{3 ,4 }; double [] dArr = hj.sumAndAvg(nums); System.out.println("the sum and avg of 3 and 4 is :" + dArr[0 ] + " and " + dArr[1 ]); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <jni.h> #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" {#endif JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv *, jobject) ; JNIEXPORT jint JNICALL Java_HelloJNI_add (JNIEnv *, jobject, jint, jint) ; JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi (JNIEnv *, jobject, jstring) ; JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg (JNIEnv *, jobject, jintArray) ; #ifdef __cplusplus } #endif #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <jni.h> #include <stdio.h> #include "HelloJNI.h" JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv * env, jobject obj) { printf ("Hello World!\n" ); } JNIEXPORT jint JNICALL Java_HelloJNI_add (JNIEnv* env, jobject obj, jint a , jint b) { return (a + b); } JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi (JNIEnv * env, jobject obj , jstring str) { const char * pcsz = (*env)->GetStringUTFChars(env, str, NULL ); if (NULL == pcsz) return NULL ; char szMsg[256 ]; sprintf (szMsg,"say hi to %s from C native" ,pcsz); (*env)->ReleaseStringUTFChars(env, str, pcsz); return (*env)->NewStringUTF(env, szMsg); } JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg (JNIEnv *env, jobject obj, jintArray nums) { jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL ); if (pcArray == NULL ) return NULL ; jsize len = (*env)->GetArrayLength(env,nums); jint sum = 0 ; for (int i = 0 ; i < len; i++) { sum += pcArray[i]; } jdouble avg = (jdouble)sum / len; (*env)->ReleaseIntArrayElements(env,nums, pcArray, 0 ); jdouble outArray[] = {sum,avg}; jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2 ); if (outArray == NULL ) return NULL ; (*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2 , outArray); return outJNIArray; }
4.2 参考