JNI编程笔记二

在上一篇文章中着重介绍了JNI编程的书写步骤,借助于所建议的操作步骤,我们实现了一个简单的JNI调用示例。今天继续JNI介绍,重点包括JNIEnv在C/C++中的实现方式,使用关键字JNIEXPORT、JNICALL以及extern "C"语法的目的,然后是JNI函数名称的命名规范,最后是JNI中的数据类型,包括基本数据类和引用数据类型。

JNIEnv简绍

在对JNIEnv分析之前,这里先介绍一下C++中struct的一些其它特性。这部分内容也很重要,下文分析C和C++中JNIEnv实现的差异时就是基于该特性。

C++ struct

我们知道在C++中引入了面向对象的特性。类的引入就是为了面向对象而设计的,在类中我们可以封装各种属性或者方法,而且可以对函数进行重载的设计。有关重载有一点需要强调一下,所谓的C++支持重载,其实是建立在类特性基础之上的,如果脱离了类,再谈重载,则无意义。怎么理解上面这句话呢,就是说,重载的函数一定是定义在类中的,如果你定义两个同名的函数,参数个数不同,但是却定义在了类的外部,这时候编译器会直接报错,无法通过编译。

在C++基础知识部分,介绍了类的相关部分,我们知道类中函数的声明和实现是可以相分离的,并且可以使用关键字this代表一个指向当前对象的指针。其实结构体struct也有和类相似的特征,在C++中结构体也可以将函数的声明和实现分离,并且在结构体内部也可以使用this指针指向一个当前对象。

如下是一个很简单的使用结构体struct的使用示例。

struct student
{
	int num;
	char *name;
	float score;

	float func(){
		return this->score;
	};
	void printMsg();
};

void student::printMsg(){
	cout << this->name<< "\n" << endl;
}

void main(){

	student stu;
	stu.score = 10.5;
	stu.name = "username";
	printf("%f \n", stu.func());

	stu.printMsg();

	printf("%d \n", 100);
}

JNIEnv在C/C++的实现

由于C++是完全兼容C语言的实现,所以JNI的函数并不是C语言实现一份,然后C++语言再实现一份,而是C语言实现后,直接在C++中调用C的实现。 我们抽取一个常用的典型函数NewStringUTF(),通过源码分析一下它在C和C++实现的差异。

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif

struct JNINativeInterface_ {
	jstring (JNICALL *NewStringUTF)(JNIEnv *env, const char *utf);
	//...
}

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
	jstring NewStringUTF(const char *utf) {
        return functions->NewStringUTF(this,utf);
    }
	//...
}

通过上面源码可以知道,在C语言中JNIEnv是一个JNINativeInterface_类型的指针变量,在结构体JNINativeInterface_中声明了各种JNI函数。如果想要调用JNINativeInterface_中的方法,我们只需要直接使用类似JNIEnv->NewStringUTF()的方式即可。

在C++中JNIEnv是一个JNIEnv_结构体类型的变量,JNIEnv_结构体中定义了一个JNINativeInterface_类型的指针变量functions,然后在C++中通过functions指针变量调用C中函数,如functions->NewStringUTF(this,utf)。在C语言中定义的NewStringUTF()函数,第一个入参是JNIEnv类型的指针变量,由于在C++中JNIEnv是一个结构体变量,那么this就是一个指向当前对象的指针变量,在这里就直接传入了this指针。

所以在C或者C++中就有了如下不同的调用方式:

JNIEXPORT jstring JNICALL Java_com_sunny_demo_MainTest_getStringFromC
(JNIEnv *env, jclass jcls){
	return (*env)->NewStringUTF(env,"I'm from C");
}

//C++实现
JNIEXPORT jstring JNICALL Java_com_sunny_demo_MainTest_getStringFromCPlusPlus
(JNIEnv * env, jobject jobj){
	return env->NewStringUTF("I'm from C ++");
}

JNIEXPORT和JNICALL

JNIEXPORT和JNICALL在jni.h中是两个宏定义,也可以说是两个符号常量。在不同的操作系统上面它们两个的定义也有所不同。

//window
#define JNIEXPORT __declspec(dllexport)
#define JNICALL __stdcall

//linux
#define JNIEXPORT
#define JNICALL

//android
#define JNIEXPORT  __attribute__ ((visibility ("default")))
#define JNICALL __NDK_FPABI__

Windows中编译.dll动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec(dllexport)标识,表示将该函数导出后,在外部可以调用。而Linux中导出.so动态库则没有这种限制,主要还是由于在不同操作系统上各自的编译器所产生的可执行文件格式不一样。

有时候会将JNIEXPORT定义为__attribute__ ((visibility ("default"))),visibility用于设置动态链接库中函数的可见性,如果将变量或函数设置为hidden,表示该符号仅在本so中可见,在其他库中则不可见,除非强制声明为其它值,如这里声明为了default值。

上面可以看出在Linux中JNIEXPORT是一个空定义,所以在Linux生成的头文件中JNIEXPORT关键字是可以省略的,但是为了使我们开发的库兼容不同操作系统平台,所以开发中不建议省略该标记。

JNICALL在windows中的值为__stdcall,在Android中的值为__NDK_FPABI__,该关键字主要用于约束函数入栈顺序和堆栈清理的规则。

JNIEXPORT和JNICALL多数情况下我们不需要手写,因为可以通过javah命令自动生成的,所以也不需要刻意去记这两个宏用法,只需要知道它们在使用上面是一套JNI的语法结构,是一种特定的写法格式即可。

extern "C"

JNI之C语言上篇中我们介绍了extern "C",由于C语言不支持extern "C"语法,为了使生成的头文件同时支持C和C++语言,这里使用的C++的宏"__cplusplus"来判断是不是C++编译器。

我们知道C++语言支持多态,C++在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。

JNI函数名定义规范

对于JNI编程来说,JNI函数跟Java类方法的名称之间是有一一对应关系的,因此需要遵循一定的命名规范,如下:

  1. 前缀:Java_
  2. 类的全限定名,用下划线进行分隔:com_sunny_demo_MainTest
  3. 方法名:getStringFromC
  4. JNI函数指定第一个参数:JNIEnv *
  5. JNI函数指定第二个参数:jobject
  6. 实际Java参数:jstring, jint ....
  7. 返回值的参数: jstring, jint ....

所以对于在Java类com.sunny.demo.MainTest中的一个方法:

public native static String getStringFromC();

对应的JNI层的方法是:

JNIEXPORT jstring JNICALL Java_com_sunny_demo_MainTest_getStringFromC
(JNIEnv *env, jclass jcls);

上述命名规范其实是JNI静态注册中采取的方式,当然了,我们也可以利用函数注册的方法,这种方式称之为动态注册方式。将Java层的方法名跟JNI层的方法名的对应关系保存起来,注册到JVM或者DVM中,这时候就不需要上面对应关系的命名规范了,详细可以参看这篇博文Android JNI 函数注册的两种方式(静态注册/动态注册)

JNI数据类型

由于Java与C/C++的数据类型不匹配,所以需要定义一种中间数据类型来完成它们之间映射类型的转换。

下面给出了数据类型的对应表,包括基本数据类型和引用数据类型。

基本数据类型对照表

Java类型 JNI类型 描述
boolean Jboolean 无符号8位
byte Jbyte 无符号8位
char Jchar 无符号16位
short Jshort 有符号16位
int Jint 有符号32位
long Jlong 有符号64位
float Jfloat 有符号32位
double Jdouble 有符号64位

引用数据类型对照表

Java引用类型 JNI类型
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
All objects jobject
java.lang.Class jclass
java.lang.String jstring
Object[] jobjectArray
java.lang.Throwable jthrowable

引用类型继承关系

小结

在本文中,我们对JNIEnv在C/C++中的的定义和实现进行了简单的对比介绍,对JNIEXPORT和JNICALL关键字在不同操作系统上的定义进行了分析,介绍了使用extern "C"的目的,然后给出了静态注册JNI函数的命名规范,最后罗列了JNI的数据类型。

JNI数据类型需要重点掌握的部分,特别是引用类型,因为在JNI中基本数据类型可以自动相互转化,而引用类型必须借助JNI函数进行显示转化。

后续我们介绍几种常见的使用场景,Java层调动Native层比较好理解,也容易实现。但是Native层调用Java层代码,相对复杂,包括如何访问Java层字段、方法和构造方法,如何抛出异常等等,这些后续都会通过具体的示例一一介绍。

参考资料

Android jni/ndk编程一:jni初级认识与实战体验

NI/NDK开发指南(二)——JVM查找java native方法的规则

评论(1)

  • yimipuzi.com

    Here is an peanuts currency estimable because victory. yimipuzi.com http://bit.ly/2NJu6tP

您确定要删除吗?删除之后不可恢复