一步一步学习JNI(二)

时间:2022-10-21 08:19:37

       在一步一步学习JNI(一) 文章中我们对JNI进行了初步的入门,并且也给出了一个例子,并且在各个平台怎么生成动态链接库做了讲解。在这一篇中我们进行更深入的学习。

函数原型

       我们在来看一下前面的样例代码。

package com.sunny;

public class Hello {

static{
//System.loadLibrary("Hello");
System.load("/Users/doc/Jni/jniHello.jnilib");
}

public static native int getSum(int a, int b);

public static void main(String[] args) {
// 虚拟机扫描加载的lib路径
System.out.println(System.getProperty("java.library.path"));
int sum = getSum(2, 5);
System.out.println(sum);
}

}

       在上面的代码中我们声明了public static native int getSum(int a, int b);这样的一个函数,这里我们使用了native关键字,表示我们声明的是本地方法,该关键字是告诉java编译器,该函数在java代码中只是声明,具体的是有c/c++来进行实现。在运行的时候运行本地方法。

       并且我们在也在静态代码块中加载了so包,当java编译器执行的时候,他就开始执行本地方法。我们在一步一步学习JNI(一)看到,如果没有加载so包或者函数找不到都会抛出如下的异常。

Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library: /Users/doc/Jni/jniHello.jnilib
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1827)
at java.lang.Runtime.load0(Runtime.java:809)
at java.lang.System.load(System.java:1086)
at com.sunny.Hello.<clinit>(Hello.java:7)

       那么,Java虚拟机在加载本地运行库时,如果把java代码中的本地方法与库中的方法映射到一起的呐?这里就是使用了函数原型,当生成了函数原型,java虚拟机就会根据相应的规则将两个方法映射到一起。

       那我们来看看函数原型。在前一篇文章中我们用javah生成了如下的代码。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_sunny_Hello */

#ifndef _Included_com_sunny_Hello
#define _Included_com_sunny_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_sunny_Hello
* Method: getSum
* Signature: (II)I
*/

JNIEXPORT jint JNICALL Java_com_sunny_Hello_getSum
(JNIEnv *, jclass, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

       我们来看看上面的代码,这里的代码不要随便修改,函数原型是在Java类中指定了native的方法基础上生成,我们可以对比的来看看两个函数。

//java
public static native int getSum(int a, int b);

//native
JNIEXPORT jint JNICALL Java_com_sunny_Hello_getSum
(JNIEnv *, jclass, jint, jint);

       由于只有一个函数,因此很多共性就不能体现了,这里我们定义两个native函数,并且生成他的函数原型。java代码如下:

public class Hello{

public static native String getName();

public native void setName(String name);
}

       生成的.h文件如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Hello */

#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Hello
* Method: getName
* Signature: ()Ljava/lang/String;
*/

JNIEXPORT jstring JNICALL Java_Hello_getName
(JNIEnv *, jclass);

/*
* Class: Hello
* Method: setName
* Signature: (Ljava/lang/String;)V
*/

JNIEXPORT void JNICALL Java_Hello_setName
(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

       这里我们有两个函数,其中一个是静态函数,一个为非静态函数,再加上getSum,我们来看看这个三个函数的共同点。

       三个函数都有JNIEXPORT,JNICALL两个关键字,这表示会被JNI调用,所有的native函数都有这个两个关键字,从上面我们也可以看到,三个函数都有,其实这两个关键字都是宏定义,它们被定义字jni_md.h 中,我们来看看Linux下的定义:

#ifndef _JAVASOFT_JNI_MD_H_ 
#define _JAVASOFT_JNI_MD_H_

#define JNIEXPORT
#define JNIIMPORT
#define JNICALL

typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif

typedef signed char jbyte;

#endif

       这里可以看到在Linux下是定义是空定义,所以在Linux也可以不加这两个关键字。但是在其他平台下就必须要加。

       我们来继续分析,可以看到生成的函数名都是JAVA_类名_方法名,并且所有函数都有JNIEnv *这个参数,但是可以发现第二个参数是不一样的,有的是jobject,有的是jclass,这其实是根据函数是否是静态来生成的,静态函数属于类,所有参数是jclass的,而非静态函数属于实例的,传递的是一个实例,参数为jobect。剩下的就是各个函数各自的参数。最后就是返回值,返回值处于JNIEXPORT, JNICALL中间。但是我们看到所有的参数都加了一个j,跟我们传入的参数不一样了。这里我继续来看看函数类型。

数据类型

本地类型

       java是一种与平台无关的语言,数据类型在任何平台下都占用相同的大小,但是在JNI编程中,Java程序与c/c++程序中经常进行数据交换,因此必须要消除两个数据类型的差异,所以JNI提供了一套与java数据类型相对应的java本地类型,这样本地语言就可以使用java数据类型。我来看看他们的映射关系。

Java类型 Java本地类型 大小
byte jbtye 1
short jshort 2
int jint 4
long jlong 8
float jfloat 4
double jdouble 8
char jchar 2
boolean jboolean 1
void void

       以上的本地类型都定义在JAVA_HOME/include/jni.h与JAVA_HOME/include/platform/jni_md.h中。

引用类型

       同时Java本地类型也提供了另外三种类型,分别对应于Java类,对象与字符串三种引用的数据类型,在前面我们已经见过类,与对象的数据类型了。

Java引用类型 Java本地类型
jclass
对象 jobject
String jstring

       我们在看看官方给出引用类型,对应关系如下:

一步一步学习JNI(二)

       有兴趣的同学可以去官方文档看看全部的类型。链接地址

jvalue类型

       jvalue类型是一个union(联合),在JNI中将基本数据类型与引用类型定义在一个联合类型中,表示用jvalue定义的变量,可以存储任意JNI类型的数据,声明如下:

typedef union jvalue { 
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;

总结

       这里主要讲解了函数原型,这样可以保证java编译器在运行本地库是能够找到对应的方法,但是还有另外一种方式可以生成映射关系那就是注册本地函数,后面会讲到这种方式,其次就是讲解了函数类型的对应关系。更多的东西可以查看官方文档