使用JNI技术实现Java和C/C++互调(C/C++调Java)

    上一篇博客里我们讲了怎样通过JNI和动态链接库实现从Java调C/C++,今天我们来讲怎么在C/C++中获取Java的数据类型以及调用Java的方法。

    接着上一篇所讲,我们通过在Java代码中声明native方法,再用javah工具生成头文件,然后编写头文件中的函数,实现功能,再编译成库文件,又Java虚拟机在运行时动态加载和调用,那么这次的重点就是如何编写实现函数。

    在这之前,我先讲一下关于Java的native方法在编译成函数时的命名重整规则。熟悉C++朋友可能知道,C++支持函数重载,函数名可能是相同的,但参数不同,或者是类内函数,或者是名字空间内函数,有或者兼而有之。这就是通过命名重整做到得,系统在搜索一个符号(函数)的地址时,无法区分两个名字相同的符号,而在C语言中,函数名字就是其符号,这就导致了C语言无法重载相同名字的函数。而C++通过一套重整规则,把名字空间路径,类名,返回值,函数名,参数,常量值等都作为命名的一部分,这就形成了一个函数独有的函数签名,也就有了唯一的标示。虽然因为编译器对这套规则没有统一的标准而引发很多问题,但却能为我们今天要讲的Java方法的命名带来一些启发。下面我们来看一段Java代码:

public class Hello {
	public native static void hello();
	public native static void hello(String s);
	public native void hello(int i);
	public native String hello(char c);
}
    在这个类中,我分别定义了几个函数名相同,但函数签名不同的方法。接下来我们生成对应的头文件:

#include 
/* Header for class Hello */

#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Hello
 * Method:    hello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Hello_hello__
  (JNIEnv *, jclass);

/*
 * Class:     Hello
 * Method:    hello
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_Hello_hello__Ljava_lang_String_2
  (JNIEnv *, jclass, jstring);

/*
 * Class:     Hello
 * Method:    hello
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_Hello_hello__I
  (JNIEnv *, jobject, jint);

/*
 * Class:     Hello
 * Method:    hello
 * Signature: (C)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Hello_hello__C
  (JNIEnv *, jobject, jchar);

#ifdef __cplusplus
}
#endif
#endif

    在注释中可以看到函数对应的类名,方法名和参数返回值。可以看出Java函数是以Java_为前缀,类名(包含包路径),函数名,参数(类名或者类型名),重载限定符号__作为函数名。这里要注意没有返回值,因为返回值本质上不影响函数签名,所以在重整的时候没有纳入。这里我想说的是,JVM实际上就是根据这个命名去搜索需要调用的本地函数,所以我们编写的库中尽量不要包含相同名字的库,也不要使用相近的命名方法,以防编译错误或者运行时的不正常。这算是我们和JVM的一个约定。

    下面我们继续看上面头文件的代码,可以看到每个函数的第一个参数都是一个名为JNIEnv的类型,这个参数实际上就是调用本地函数的虚拟机线程的环境信息,也是JNI技术中底层代码调用Java方法的关键接口,我们在编写JNI时最常用到的接口。但是现在我们先跳过它,晚点再讲。可以看到第二个参数有jobject或者是jclass,从名字我们可以看出来,jobject就是一个具体的Java对象,而jclass是一个Java类的对象,就有点像Java中的Class类(即类的类),为什么会有两种不同的参数,其实看看Java代码就知道了,jobject的函数是类内函数(成员函数),jclass的函数是静态函数,静态函数是对整个类而言,而成员函数是对类的一个具体对象作用的。这有点像C++中的成员函数中隐式的传进了一个this指针,而静态函数没有。而之后的参数就是真正的函数的形参对应的实参。可以看到一些像jint,jchar,jstring的类型。下面我们先打开jni.h来看看这些类型是怎么定义的,先看看基本类型:

typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;

typedef jint            jsize;

    这几行代码我就不解释了,但可能眼尖的朋友会提出一个问题,为什么没有jint和jlong?这就要从操作系统的数据模型说起,对于不同位数的CPU,我们通常会安装等于或小于其位数的操作系统,而我们的数据模型就是由这些操作系统定义的。其中用的最多的就是LP和LLP类型。为什么jni头文件没有直接对int和long(或者说long long)定义的原因就在此。对于不同的数据类型,int有可能是16位,32位,甚至64位的,而long有可能是32位或64位的。这就导致了不一致性,Java为了避免这种状况,把这些不一致的地方分在了jni_md.h这个头文件中,让我们来看看:

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

    上面的代码是我的64位Linux上的代码,因为这个类型的系统int类型确定是32位,所以没有分开定义,但是jlong却不一样,有的系统中long是32位(LLP64,比如Windows和一些类Unix系统),所以jlong就会被定义为long long类型(或者__int64,在Windows系统上),反之如果是LP64的就会被定义为long类型。

    在回到jni.h这个文件中,我们下面来看一下类的定义:

#ifdef __cplusplus

class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};

typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;

    这里最上面有一个宏,指的是这块是C++代码,因为在C中没有类和继承的概念,所以一这块内容只在C++代码中存在,下面我们来看看C的版本:

#else

struct _jobject;

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

#endif

    熟悉C/C++编程的朋友应该知道这两者是等价的。不过看到这里应该又很多人有一个疑问,为什么jobject,jclass和其它定义类都是空类,那么我们在这头拿到的不都是指向一个空类对象的指针吗?是的,所以在C/C++中是无法直接访问这些对象的,只能直接访问基本类型(jint,jchar等)。那我们要如何来访问Java对象,其实,在JVM调用本底方法时,就在一张本地程序注册表中把函数调用相关的参数变量对象和全局变量注册进去了,这张表不需要用户自己调用,只要使用之前提到的JNIEnv指针去掉用就可以了,下面我们来看看这个指针的实现:

struct JNINativeInterface_;

struct JNIEnv_;

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

struct JNINativeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);

    jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);
    ......
};

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus

    jint GetVersion() {
        return functions->GetVersion(this);
    }
    jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
                       jsize len) {
        return functions->DefineClass(this, name, loader, buf, len);
    }
    ......
};


    上面的代码很清晰的表达了JNIEnv的作用,就是作为Java层和本地库层的一个通信渠道,我们所需要的变量都可以通过它来获取,关于JNIEnv的使用我只提一点,就是在C中是

    (*env)->xxx(xxx);

    而在C++中则是:

    env->xxx(xxx);

    这个差异在上面的代码中已经揭示,C++中的JNIEnv是JNIEnv_的类型定义,所以可以直接使用指针操作,而C中是JNINativeInterface的指针,所以要先解引用再使用。关于怎么去调用JNIEnv可以参考官方文档,请记住传进来的对象不能直接使用,因为他们只是一个标示,要获得对象成员属性还得依靠JNIEnv,这点是在JNI编程中一定要牢记并且要养成习惯的。

    今天就先讲到这里,更多关于JNI的信息可以参考官方文档或者头文件,甚至感兴趣的朋友可以去下载jdk的源码来阅读。只要记住一点,JNI是用来作为接口的,一般会在其他函数或者库中实现功能。

你可能感兴趣的:(C++,Java)