多语言编程之 C and Java:JNI 30 分钟入门
目录
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"); //运行时加载本地库 hello.dll(Windows)或libhello.so(Unixes) 或 libhello.dylib(MacOX) } private native void sayHello(); //声明 native 方法 public static void main(String[] args ){ new HelloJNI().sayHello(); //调用本地方法 } } |
当HelloJNI类加载的时候,静态初始化调用 System.loadLibrary() 来加载本地库 hello.dll(或libhello.so)。如果不能加载 hello 库,会抛出 UnsatisfiedLinkError 异常。
然后我们使用 native 关键字声明了 native 方法 sayHello() ,这表明该方法是使用其它语言实现的。
最后 main() 方法实例化了 HelloJNI 并调用了 sayHello().
然后我们编译这个类:
1 |
> javac HelloJNI.java |
2.1.2 生成 C/C++ 头文件
使用 javah 创建头文件:
1 |
> javah HelloJNI |
会得到如下 .h 文件 HelloJNI.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: HelloJNI * Method: sayHello * Signature: ()V */ 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) |
包名中的 点 会被转换为 下划线
函数中带有两个参数(虽然我们在本例中并没有使用这两个参数)
- JNIEnv*: 指向 JNI environment ,提供对 JNI 方法的访问
- jobject: 指向 this Java 对象
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 |
#Windows gcc > gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c #Windows VS cl.exe link.exe > 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 #or > 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 |
// In "jni_md.h" typedef long jint; typedef __int64 jlong; typedef signed char jbyte; // In "jni.h" 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) { // jstring 转换为 C-string 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); // C-string 转换为 jstring 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 |
// jstring to C-string const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*) // C-string to jstring 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 |
// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII) // Can be mapped to null-terminated char-array C-string const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy); // Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding. void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf); // Informs the VM that the native code no longer needs access to utf. jstring NewStringUTF(JNIEnv *env, const char *bytes); // Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding. jsize GetStringUTFLength(JNIEnv *env, jstring string); // Returns the length in bytes of the modified UTF-8 representation of a string. void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf); // Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding // and place the result in the given buffer buf. // Unicode Strings (16-bit character) const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy); // Returns a pointer to the array of Unicode characters void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars); // Informs the VM that the native code no longer needs access to chars. jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length); // Constructs a new java.lang.String object from an array of Unicode characters. jsize GetStringLength(JNIEnv *env, jstring string); // Returns the length (the count of Unicode characters) of a Java string. void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf); // Copies len number of Unicode characters beginning at offset start to the given buffer 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) { //获取 C 数组 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}; //C 数组转为 Java 数组 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 |
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray // PrimitiveType: int, byte, short, long, float, double, char, boolean // NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 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); |
- Get<PrimitiveType>ArrayElements() 从 Java 数组创建 C 数组
- Get|Set<PrimitiveType>ArrayRegion()将从 Java 数组和 C 数组中来回拷贝数据
- New<PrimitiveType>Array() 分配新的 Java 数组内存
- GetPrimitiveArrayCritical() 得到原始数据类型内容的指针,可能使得垃圾回收器不回收此内存,直到 ReleasePrimitiveArrayCritical() 调用
4 附录
4.1 本文源码
- HelloJNI.java
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]); } } |
- HelloJNI.h
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 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: HelloJNI * Method: sayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject); /* * Class: HelloJNI * Method: add * Signature: (II)I */ JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv *, jobject, jint, jint); /* * Class: HelloJNI * Method: sayHi * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring); /* * Class: HelloJNI * Method: sunAndAvg * Signature: ([I)[D */ JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray); #ifdef __cplusplus } #endif #endif |
- HelloJNI.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 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) { // jstring 转换为 C-string 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); // C-string 转换为 jstring return (*env)->NewStringUTF(env, szMsg); } JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *env, jobject obj, jintArray nums) { //获取 C 数组 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}; //C 数组转为 Java 数组 jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); if(outArray == NULL) return NULL; (*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outArray); return outJNIArray; } |
4.2 参考