Tips:
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
如上面的Tips所讲,JNI 技术在与硬件、操作系统进行交互时是非常有用的,这章主要实现使用 JNI 技术来访问 C 代码,下面以代码开始讲解,先是一段 java 代码,里面访问一个 C 实现的 hello 函数来实现打印,这个过程可以分为三个步骤:
1. load –> 使用 System.loadLibrary 来加载库
2. map –> 即映射 java 的 hello 函数到 C 的 hello 函数里
3. call –> 调用在 C 里面实现的功能函数
代码如下:
public class JNIDemo {
static {
/* 静态块构造时只执行一次,这里去加载 C 库 */
/* 1. load */
System.loadLibrary("native");
}
/* 加了 static 表明这个方法可以直接调用,否则需要先实例化 */
/* 声明的这个 "native" 方法是在本地语言实现的,即C或C++ */
public native static void hello();
public static void main(String args[]) {
/* 2. map java hello <--> c c_hello*/
/* map 在C代码里面实现 */
/* 3. call */
hello();//调用一个C实现的 hello 方法
}
}
接下来是C代码:
#include <stdio.h>
#include <jni.h> /* 这里需要加载jni.h这个文件,提供一些jni实现的函数调用 */
/* env是运行环境,cls是类对象,这两个参数必须有,第三个参数开始才是传递进来的第一个参数 */
void c_hello(JNIEnv *env, jobject cls)
{
printf("Hello world\n");
}
/* 下面这个结构体在 jni.h 定义 */
#if 0
typedef struct {
char *name; /* Java里调用的函数名 */
char *signature; /* JNI字段描述符, 用来表示Java里调用的函数的参数和返回值类型 */
void *fnPtr; /* C语言实现的本地函数指针 */
} JNINativeMethod;
#endif
/* 根据上面的结构体进行定义 */
static const JNINativeMethod methods[] = {
{"hello", "()V", (void *)c_hello},
/* 这里还可以注册多个函数 */
};
/* 如果java调用了 System.loadLibrary 函数就会先调用C里面实现的这个方法 */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
jclass cls;
/* 获得一个JAVA运行环境,运行环境的版本:JNI_VERSION_1_4 */
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_4)) {
return JNI_ERR; /* JNI version not supported */
}
/* C语言需要映射到java程序JNIDemo类里面的hello程序,所以要先找类 */
cls = (*env)->FindClass(env, "JNIDemo");
if (cls == NULL) {
return JNI_ERR;
}
/* 开始建立联系 */
/* 2. map java hello <--> c c_hello*/
/* 把这个方法注册到 env 环境里面的 JNIDemo 这个类里 */
/* 第四个参数表示有多少项这个方法 */
if((*env)->RegisterNatives(env, cls, methods, 1) < 0)
return JNI_ERR;
return JNI_VERSION_1_4;
}
按照上面的步骤后,一个简单的 JNI 映射程序就建立完成,下面讲解一下代码的编译指令:
- javac JNIDemo.java
- gcc -shared -o libnative.so native.c -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -fPIC
- export LD_LIBRARY_PATH=.
- java JNIDemo
通过上面步骤的编译后原来的C代码会生成一个 so 库,它需要用到一些头文件所以要到指定目录下去查找这个头文件即上面的include,如果编译时发现缺少某个头文件需要包含进来的话只要把对应报错找不到的头文件copy到include下就好了,so库文件的查找路径定在了(export LD_LIBRARY_PATH=.)即当前目录,然后编译后既可实现 java 调用 c 里面的 c_hello 打印程序,这里先简单说明一下上面所谓的字段描述符,”()V” 表示的是函数没有形参,返回值为 void(V),那么如果是有参数的情况下,可以参考下面这张图进行填充字段描述符。
以及 java 方法传递参数时和 JNI 函数里面参数声明的对应关系图:
这里的字段描述符如果一开始不熟悉的话其实也不需要经常查表,可以使用取巧的方式来获得字段描述符和JNI函数的定义形式,可以使用 java 虚拟机提供的工具 “javah”,具体实现如下:
- javah -jni JNIDemo
后面是类名,这样会在目录下生成一个 JNIDemo.h 文件,里面会有字段描述符合 JNI 函数的函数定义形式。
上面的例子函数没带参数也没返回值,那么下面以几个例子来说明下,带参数和返回值该如何实现,首先看下如果形参传递一个整型变量并且返回一个整型变量的实现。
JAVA代码:
public class JNIDemo {
static {
System.loadLibrary("native");
}
public native int hello(int m);
public static void main(String args[]) {
/* 不使用static就要实例化对象 */
JNIDemo d = new JNIDemo();
System.out.println(d.hello(123));
}
}
C代码:
#include <stdio.h>
#include <jni.h>
/* 函数定义发生了变化,具体变化参照上面的表格即可 */
jint c_hello(JNIEnv *env, jobject cls, jint m)
{
/* 可以直接打印和返回 */
printf("Hello world val = %d\n",m);
return 100;
}
#if 0
typedef struct {
char *name; /* Java里调用的函数名 */
char *signature; /* JNI字段描述符, 用来表示Java里调用的函数的参数和返回值类型 */
void *fnPtr; /* C语言实现的本地函数指针 */
} JNINativeMethod;
#endif
/* 这里字段描述符发生了变化,参照上面的表格即可 */
static const JNINativeMethod methods[] = {
{"hello", "(I)I", (void *)c_hello},
};
/* OnLoad 函数没有变化 */
JNIEXPORT jint JNICALL
/* 如果java调用了loadLibrary函数就会先调用C里面实现的这个方法 */
JNI_OnLoad(JavaVM *jvm, void *reserved)/* System.loadLibrary */
{
JNIEnv *env;
jclass cls;
/* 获得一个JAVA运行环境,运行环境的版本:JNI_VERSION_1_4 */
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_4)) {
return JNI_ERR; /* JNI version not supported */
}
/* C语言需要映射到java程序JNIDemo类里面的hello程序,所以要先找类 */
cls = (*env)->FindClass(env, "JNIDemo");
if (cls == NULL) {
return JNI_ERR;
}
/* 开始建立联系 */
/* 2. map java hello <--> c c_hello*/
/* 把这个方法注册到env环境里面的JNIDemo类里 */
if((*env)->RegisterNatives(env, cls, methods, 1) < 0)/* 第四个参数表示有多少项这个方法 */
return JNI_ERR;
return JNI_VERSION_1_4;
}
下面再看看传递String类型该如何实现。
JAVA代码:
public class JNIDemo {
static {
System.loadLibrary("native");
}
/* 传递一个 String 类型返回一个 String 类型 */
public native String hello(String str);
public static void main(String args[]) {
JNIDemo d = new JNIDemo();
System.out.println(d.hello("this is java"));
}
}
C代码:
#include <stdio.h>
#include <jni.h>
/* 传递进字符串改变就比较大了,不能直接打印出字符串
一般需要做什么改变具体可以看jni手册,里面都有详细
介绍如何使用这些这些env提供的函数以及功能,下面就直接调用了 */
jstring c_hello(JNIEnv *env, jobject cls, jstring str)
{
/* 不能直接这样使用,错误 */
//printf("Hello world val = %sn",str);
//retrn "return from c";
/* 字符串的传递需要借用运行环境里面的某些函数 */
const jbyte *cstr;
cstr = (*env)->GetStringUTFChars(env, str, NULL);//转化
if (cstr == NULL) {
/* 转化后有可能使用到分配内存的函数,有可能会失败 */
return NULL; /* OutOfMemoryError already thrown */
}
/* 打印传递进来的字符串 */
printf("Get string from java :%s\n", cstr);
/* 释放上面分配的内存空间 */
(*env)->ReleaseStringUTFChars(env, str, cstr);
return (*env)->NewStringUTF(env, "return from C");
}
/* 这里的字段描述符可以看上面的表格或者使用 javah 这个工具查看 */
static const JNINativeMethod methods[] = {
{"hello", "(Ljava/lang/String;)Ljava/lang/String;", (void *)c_hello},
};
/* JNI_OnLoad 这个函数没有变化这里就不贴出来了 */
最后再看看传递数组以及返回一个数组该怎么实现。
JAVA代码:
public class JNIDemo {
static {
System.loadLibrary("native");
}
/* 传递数组返回数组 */
public native int[] hello(int[] a);
public static void main(String args[]) {
/* 不使用static就要实例化对象 */
JNIDemo d = new JNIDemo();
int [] a = {1, 2, 3};
int [] b = null;
int i;
b = d.hello(a);
for(i=0; i<b.length; i++)
System.out.println(b[i]);//打印返回的数组
}
}
C代码:
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
jintArray c_hello(JNIEnv *env, jobject cls, jintArray arr)//1.从java里面传入的int值,JNI定为jint
{
jint *carr;
jint *oarr;
jintArray rarr;
jint i, n = 0;
/* 把数组里面的全部元素返回给carr */
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL) {
return 0; /* exception occurred */
}
/* 获取传递进来的数组长度 */
n = (*env)->GetArrayLength(env,arr);
/* 分配一个一样的数组长度 */
oarr = malloc(sizeof(jint) * n);
if(oarr == NULL)
{
/* 分配失败 */
(*env)->ReleaseIntArrayElements(env, arr, oarr, 0);
return 0;
}
for(i=0; i<n; i++)
{
/* 数组倒过来存放 */
oarr[i] = carr[n-1-i];
}
/* 释放内存 */
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
/* 构造 jintArray */
rarr = (*env)->NewIntArray(env, n);//n为这个数组有多少个参数
if(rarr == NULL)
{
return 0;
}
/* 设置rarr这个数组,从0开始设置到n,数据来源于oarr */
(*env)->SetIntArrayRegion(env, rarr, 0, n, oarr);
free(oarr);
/* 构造的IntArray返回给java */
return rarr;
}
/* javah -jni JNIDemo */
static const JNINativeMethod methods[] = {
{"hello", "([I)[I", (void *)c_hello},
};
/* JNI_OnLoad 这个函数没有变化这里就不贴出来了 */
最后以思维导图做总结: