多语言编程之 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
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().
然后我们编译这个类:
> javac HelloJNI.java
2.1.2 生成 C/C++ 头文件
使用 javah 创建头文件:
> javah HelloJNI
会得到如下 .h 文件 HelloJNI.h
/* 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:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
这个方法名是由javah转换来的,转换规则为:
Java_{package_and_classname}_{function_name}(JNI arguments)
包名中的 点 会被转换为 下划线
函数中带有两个参数(虽然我们在本例中并没有使用这两个参数)
- JNIEnv*: 指向 JNI environment ,提供对 JNI 方法的访问
- jobject: 指向 this Java 对象
2.1.3 C 代码的实现
HelloJNI.c
#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 文件:
#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 检查成果
> 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,在文件中添加函数的声明:
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv*, jobject, jint, jint);
这里用到了 jint 。我们可以在 jni.h 和 jni_md.h 中找到各原始类型的定义:
// 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 中实现上一步声明的函数:
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv* env, jobject obj, jint a , jint b)
{
return (a + b);
}
3.1.3 编辑 Java 文件
在Java中声明 native 方法并调用:
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 程序,结果如下:
Hello World! the result of 3 add 4 is: 7
3.2 传递 String 参数
3.2.1 示例
Java 代码
private native String sayHi(String name);
C 函数声明
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring);
JNI 使用 jstring 来表示 Java String. 最后一个参数 jstring 是 Java 传递到 native 的 String 参数
C 函数实现
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) 提供了转换的方法:
// 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 操作函数
// 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 代码
private native double[] sumAndAvg(int[] nums);
JNI C 函数声明
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray);
JNI C 函数实现
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)操作函数如下:
// 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
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
/* 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
#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 参考