第一章    JNI概述




一、Java 代码访问本地代码


  Java 是跨平台的语言,但是有时候需要调用本地代码(如由 C/C++ 编写的本地代码),为了满足这种需要,Sun公司提供了 JNI 技术, JNI 是 Java 平台的一个功能强大的接口,这个 JNI 接口提供了 Java 与操作系统本地代码互相调用的功能。但是如果一个应用程序使用了 JNI ,那么它就失去了 Java 应用的跨平台特性了,因为 native 方法是平台相关的。这样,当使用了 JNI 的 Java 应用要移植到别的平台上时,就必须重写其 native 代码。


  首先让我们通过一个简单的 HelloWorld 程序来学习一下 JNI 的不要实现步骤:


(1)在 Java 类中声明一个 native 方法


  使用 eclipse 新建一个 java 工程,工程名为 JavaNativeJni ,然后在其 src 文件夹下新建一个 java 类 NativeTest.java 包名为 cn.org.lion.jniTest


(2)使用 Javah 命令生成包含 native 方法定义的 C/C++ 头文件


  在命令行窗口中切换到 eclipse 为 NativeTest 类生成的 class 所在的 bin 目录 ,即 G:\jworkspace\JavaNativeJni\bin (我的 eclipse 的工作空间为 G:\jworkspace),然后执行 javah cn.org.lion.jniTest.NativeTest ,这样就会在该工程的 bin 目录下生成了对应的头文件 cn_org_lion_jniTest_NativeTest.h ,这个头文件的内容如下


/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_org_lion_jniTest_NativeTest */
#ifndef _Included_cn_org_lion_jniTest_NativeTest
#define _Included_cn_org_lion_jniTest_NativeTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: cn_org_lion_jniTest_NativeTest
 * Method: HelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_cn_org_lion_jniTest_NativeTest_HelloWorld
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif




(3)按照生成的 C/C++ 头文件来编写 C/C++ 源文件


jni.h)是 jdk 中的头文件,我机器的 jdk 目录为 D:\mysoftInstall\jdk1.7.0_10\include ,所以我得到这个目录下把 jni.h 这个头文件复制到这个 win32 项目的目录下,而 jni.h 中包含的头文件 jni_md.h 也是 jdk 里面的头文件,它在D:\mysoftInstall\jdk1.7.0_10\include\win32 目录下,所以这个文件也要复制到当前的 win32 项目目录下。这些头文件都复制过来后,就可以编写 .cpp 源文件了,在源文件夹下新建一个 .cpp 文件,名字任意(我取名为 source.cpp),然后在该 cpp 文件中实现在cn_org_lion_jniTest_NativeTest.h 头文件中定义的函数,最终我的 source.cpp 文件内容如下


#include <iostream>
#include "cn_org_lion_jniTest_NativeTest.h"
using namespace std;
JNIEXPORT void JNICALL Java_cn_org_lion_jniTest_NativeTest_HelloWorld(JNIEnv *env, jobject object)
{
 cout << "Hello, Lion !" << endl;
}




编写好后就可编译该项目了,编译后会在该项目的 Debug 目录下生成一个 NativeTest.dll 动态链接库,这就是我们想要的库。



(4)把动态链接库存的路径(我的路径为 G:\VisualStudio2010\NativeTest\NativeTest\Debug;)添加到系统环境变量 Path 变量中,这样 Java 应用才能够找到这个动态链接库。(此时要重启一下 eclipse 这样,因为 eclipse 每次启动时都会读取一次环境变量,修改环境变量后,要让它重新读取新设置的环境变量,就得重启它)我们就可以在 eclipse 的 Java 工程中使用这个 NativeTest.dll 动态链接库了,下面是 NativeTest.java 类的内容。


package cn.org.lion.jniTest;
public class NativeTest {
 
 public native void HelloWorld();
 
 public static void main(String[] args)
 {
  System.loadLibrary("NativeTest"); //加载动态链接库,括号内写上动态链接库的名称
  NativeTest test = new NativeTest();
  test.HelloWorld(); //调用库中提供的函数
 }
}




Hello, Lion ! 



  上面的四个步骤可简单概括如下


(1)在 Java 类中声明一个 native 方法


(2)使用 Javah 命令生成包含 native 方法声明的 C/C++ 头文件


(3)根据生成的 C/C++ 头文件来编写 C/C++ 源文件


(4)把 DLL 文件加入到 PATH 环境变量上


(5)在 Java 类中加载 DLL ,然后调用声明的 native 方法


  其实在这整个过程中,VisualStudio 的作用不过是辅助生成我们想要的 dll 而已 。



二、本地代码访问Java 代码


    在被调用的 C/C++ 函数中也可以反过来访问 Java 类,由 javah 工具生成的 C/C++ 头文件函数声明如下


JNIEXPORT void JNICALL Java_cn_org_lion_jniTest_NativeTest_HelloWorld(JNIEnv *env, jobject object)
{
 cout << "Hello, Lion !" << endl;
}


JNIEnv  实际上代表了 Java 环境,通过这个  JNIEnv * 指针,就可以对 Java 端的代码进行调用(如创建 Java 类的对象、调用 Java 对象的方法、获取 Java 对象的属性等), JNIEnv  的指针会被 JNI 传入到本地方法的实现函数中来,可通过它对 Java 端的代码进行操作。 JNIEnv  类中有很多函数可以调用,如比较常用的有:


NewObject/NewString/New<TYPE>Array


Get/Set<TYPE>Field


Get/SetStatic<TYPE>Field


Call<TYPE>Method/CallStatic<TYPE>Method



Java 中的类型和 C/C++ 中的类型的映射关系


    Java类型    

            本地类型             

    JNI中定义的别名    

int

long

jint

long

_int64

jlong

byte

signed char

jbyte

boolean

unsigned char

jboolean

char

unsigned short

jchar

short

short

jshort

float

float

jfloat

double

double

jdouble

Object

_jobject*

jobject

这些我们可以在 jni.h 和 jni_md.h 头文件中找到依据,如果 jni.h 中有如下定义

typedef unsigned char jboolean;


typedef unsigned short jchar;


typedef short jshort;


typedef float jfloat;


typedef double jdouble;


看到这些,我们就不难理解上表中的映射是怎么回事了。



  为了能够在 C/C++ 中使用 Java 类, jni.h 头文件中专门定义了 jclass 类型来表示 java 中的 Class 类,在 jni.h 中定义为:typedef _jclass *jclass;


 jclass 




jclass FindClass(const char* clsName);


jclass GetObjectClass(jobject obj);


jclass GetSuperClass(jclass obj);



FindClass 会在 classpath 系统环境变量中下寻找对应的类,调用这个函数时要传入完整的类名,注意包与包之间要用 “/”分隔,如 jclass cls_string = env->FindClass("java/lang/String");



  访问 Java 类中的属性与方法


  在 C/C++ 本地代码中访问 Java 端的代码时,一个常见的应用就是获取类的属性和调用类的方法,为了在 C/C++ 中表示属性和方法,jni.h 头文件中定义了 jfieldID、jmethodID 类来分别代表 Java 端的属性和方法。我们在访问或设置 Java 属性的时候,首先要先在本地代码取得代表该 Java 属性的 fieldID,然后才能在本地代码进行 Java 属性的操作。同样的,我们要调用 Java 端的方法时,也是需要取得代表该方法的 jmethodID 才能进行 Java 方法调用。使用 JNIEnv 的


GetFieldID/GetMethodID


GetStaticFieldID/GetStaticMethodID


就可以取得相应的 jfieldID 和 jmethodID,在 jni.h 中,这几个函数声明或定义如下



jfieldID GetFieldID(jclass clazz, const char *name,


                        const char *sig) {


        return functions->GetFieldID(this,clazz,name,sig);


    }




jfieldID GetStaticFieldID(jclass clazz, const char *name,


                              const char *sig) {


        return functions->GetStaticFieldID(this,clazz,name,sig);


    }




jmethodID GetMethodID(jclass clazz, const char *name,


                          const char *sig) {


        return functions->GetMethodID(this,clazz,name,sig);


    }




jmethodID GetStaticMethodID(jclass clazz, const char *name,


                                const char *sig) {


        return functions->GetStaticMethodID(this,clazz,name,sig);


    }




 GetMethodID 也能够取得构造函数的 jmethodID ,创建一个 Java 对象时可以调用指定的构造方法,如


sig


  如对于这样的一个类


package cn.org.lion;
public class HelloNative
{
    public void goodness(int temp)
    {
        // do something...
    }
    public void goodness(double temp)
    {
        // do something...
    }
}




如果在 C/C++ 中,要取到这个类中重载的 goodness() 方法的 jmethodID,我们该怎么办呢?正如我们所知道的,肯定要先获得这个类的引用,所以我们可这样写


jclass clazz_test = env->FindClass("cn/org/lion/HelloNative");
 
sig
 
 
 
 
jmethodID id_function = env->GetMethodID(clazz_test, "goodness", "(I)V");   // 或
 
jmethodID id_function = env->GetMethodID(clazz_test, "goodness", "(D)V");



属性/方法的类型的,那么 "()V" 括号中该写什么呢?看一下面这个映射表你就知道了


类型

相应的签名

boolean

Z

byte

B

char

C

short

S

int

I

long

J

float

F

double

D

void

V

object







L用/分隔包的完整类名: Ljava/lang/String;







Array








[签名 [I [Ljava/lang/Object;








Method








(参数1类型签名 参数2类型签名···)返回值类型签名 









注意:Object后面一定要有分号 “;”,多个对象参数中间也要以 分号“;”来分隔,如


方法

相应的签名








void  f1() 















()V















int  f2( int ,  long )















(IJ)I















boolean f3( int [])















([I)B















double  f4(String,  int )















(Ljava/lang/String;I)D















void  f5( int , String [],  char )















(I[Ljava/lang/String;C)V











  下面我们再通过一个例子来看看如何通过 JNI 实现在本地 C/C++ 代码中获取并操作 Java 中的属性和方法,首先先定义一个 Java 类,并在其中声明一个本地方法 public native void HelloWorld(); 整个类的源码如下

package cn.org.lion.jniTest;
import java.util.Date;
public class NativeTest {
 
 public String str = "JustForTest";
 public int num = 168;
 
 public int test_method(int i, Date date, int[] array)
 {
  System.out.println("test_method()");
  return 0;
 }
 
 public native void HelloWorld();
 
 public static void main(String[] args)
 {
  System.loadLibrary("NativeTest");
  NativeTest test = new NativeTest();
  test.HelloWorld();
 }
}



  接着用 C/C++代码来实现这个 Java 类中声明的本地方法  HelloWorld () ,并在这个方法中获取 Java 类中的属性和方法,并进行一些简单的操作,源码如下



#include <iostream>
#include "cn_org_lion_jniTest_NativeTest.h"
using namespace std;
JNIEXPORT void JNICALL Java_cn_org_lion_jniTest_NativeTest_HelloWorld(JNIEnv *env, jobject object)
{
 cout << "Begin to test !" << endl;
 // 先获取对应的对象
 jclass temp_clazz = env->GetObjectClass(object);
 // 获取对象的属性,并赋值给 jfieldID 类型的变量
 jfieldID str_fieldID = env->GetFieldID(temp_clazz, "str", "Ljava/lang/String;");
 jfieldID num_fieldID = env->GetFieldID(temp_clazz, "num", "I");
 // 获取对象的方法,并赋值给 jmethodID 类型的变量
 jmethodID func_methodID = env->GetMethodID(temp_clazz, "test_method", "(ILjava/util/Date;[I)I");
 // 调用 test_method 方法
 env->CallIntMethod(object, func_methodID, 312L, NULL, NULL);
 // 获取 str 和 num 属性
 jobject str = env->GetObjectField(object, str_fieldID);
 jint num = env->GetIntField(object, num_fieldID);
 cout << "before modify, num = " << num << endl;
 // 修改 num 属性的值
 env->SetIntField(object, num_fieldID, 1110L);
 num = env->GetIntField(object, num_fieldID);
 cout << "After modify, num = " << num << endl;
}





  现在可以编译这个 .cpp 源文件了,得到动态链接库  NativeTest.dll ,然后在上面的 Java 类中就可以加载并使用这个动态链接库了,然后执行这个 Java 程序,该程序打印结果如下


Begin to test !
 
        
test_method()
 
        
before modify, str = 0051CC4C
 
        
before modify, num = 168
 
        
After modify, num = 1110