Java JNI

1、JNI概述

JNI 是 Java Native Interface 的简称

  • Java是跨平台的编程语言,但是在有些时候仍然是有需要调用本地代码(这些代码通常是由 C 与 C++ 编写的)。
  • JNI 是 Java 平台的一个功能强大的接口。这个 JNI接口提供了Java与操作系统本地代码相互调用的功能。

    2、C/C++代码的步骤

    在 Java 类中声明一个native方法

    1. public native void sayHello();

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

    1. javah com.omg.NativeLib

    将生成的C/C++头文件来编写C/C++源文件

    ```c

    include

    include “com_omg_NativeLib.h”

    using namespace std;

JNIEXPORT void JNICALL Java_com_olive_NativeLib_sayHello (JNIEnv *, jobject){ cout << “hello world” << endl; }

  1. <a name="AsWvt"></a>
  2. ### 将C/C++源文件编译成动态链接库(windows对应DLL,Linux对应so)
  3. <a name="LVzrJ"></a>
  4. ### 把dll文件的路径添加到环境变量PATH中
  5. <a name="zOxE1"></a>
  6. ### 在Java类中加载dll文件,然后调用声明的native方法
  7. ```java
  8. System.load("C:\\lib\\NativeCodeJni.dll");
  9. NativeLib nl = new NativeLib();
  10. nl.sayHello();

或者

  1. System.loadLibrary("NativeCodeJni");
  2. NativeLib nl = new NativeLib();
  3. nl.sayHello();

3、本地代码调用Java代码

1. 本地代码访问Java代码

  • 在被调用的C/C++函数中也可以反过来访问Java程序中的方法
  • javah 工具生成的C/C++函数声明中,可以看到有两个参数:

    1. JNIEXPORT void JNICALL Java_com_omg_NativeLib_sayHello (JNIEnv *env, jobject jobj){
    2. cout << "hello world" << endl;
    3. }

    2. JNIEnv 类型

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

  • JNIEnv类中有很多函数可以使用

    1. NewObject / NewString / New<TYPE>Array
    2. Get<TYPE>Field / Set<TYPE>Field
    3. GetStatic<TYPE>Field / SetStatic<TYPE>Field
    4. Call<TYPE>Method / CallStatic<TYPE>Method

    等许许多多的函数

  • jobject 什么的一个类型呢?

不是静态方式时,传入的是当前关联的类对象
静态方法时,传入当前类的Class对象

3. Java的类型在C/C++中的映射关系

Java类型 本地类型 JNI定义的别名
int long jint / jsize
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

4. jclass如何获取

  • 为了能够在C/C++中使用Java类。JNI.h头文件中,专门定义了jclass类型来表示Java中的Class类
  • JNIEnv类中有以下几个函数可以获取jclass:

    1. jclass FindClass(const char* clsName);
    2. jclass GetObjectClass(jobject ojb);
    3. jclass GetSuperClass(jclass obj);

    在Java JDK中也有这几个方法

  • FindClass 将会在classpath环境变量下寻找类。传入完整类名,注意包与包之间是使用 “/ ” ,而不是使用 “.” 来分隔

    1. jclass cls_str = env->FindClass("java/lang/String");

    5. 访问Java类中的属性与方法

  • 在C/C++本地代码中访问Java端的代码,一个常见的场景就是获取类的属性和调用类的方法。为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethdID类型来分别代表Java端的属性和方法

  • 在访问,或者设置Java属性的时候,首先就要先在本地代码中获得代表Java属性的jfieldID;然后才能在本地代码中进行Java的属性操作。同样的,在需要调用Java的方法时,也是需要先获取到代表该方法的jmethodID才能进行Java方法调用

使用JNIEnv的

  1. GetFieldID / GetMethodID
  2. GetStaticFieldID / GetStaticMethodID

来取得相应的jfieldID和jmethodID
来看下方法定义

  1. jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
  2. jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
  3. jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
  4. jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig);
  • GetMethodID也能够获得构造函数的jmethodID。创建一个Java对象时可以调用指定的构造方法,这个将在后面向大家介绍

如:env -> GetMethodID(data_Clazz, "<init>", "()V");

  • 类似Java的反射(Reflect),需要指定类跟属性/方法名来获取相应的jfieldID或者jmethodID。而sign又是什么呢?

    6. Sign是什么?

    例如:TestNative类中有两个重载的方法 ```java package com.omg;

public class TestNative {

  1. public void function( int i ){
  2. System.out.println("Integer: " + i);
  3. }
  4. public void function( double d ){
  5. System.out.println("double: " + d);
  6. }

}

  1. 然后再C/C++代码中调用其中的一个function方法的话;
  2. ```c
  3. //首先取得要调用的方法所在的类
  4. jclass clazz_TestNative = env->FindClass("com/omg/TestNative");
  5. //获取jmethodID之后才能进行调用
  6. jmethodID id_func = env->GetMethodID(clazz_TestNative, "function", ??);

但是到底取得的是 void function(int i) 方法还是 void function(double d) 方法的jmethodID呢?
这就是sign的作用了,他用于指定要取得的属性/方法的类型。
这里的sign如果指定为”(I)V”,则获得的是 void function(int i) 方法的jmethodID;如果指定为”(D)V”,则获得的是 void function(double d) 方法的jmethodID。

7. sign签名

用来表示要取得的属性/方法的类型

类型 签名
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
object L + 用/分隔包的完整类名 + ;
Array [ + 签名 例如,[I、[Ljava/lang/String;
Method (参数1类型签名 参数2类型签名…)返回值类型签名

8. 使用签名获得属性/方法ID的例子(复杂)

  1. import java.util.Date;
  2. public class HelloNative(){
  3. public int property;
  4. public int function(int foo, Date date, int[] arr){
  5. System.out.println("function");
  6. }
  7. public native void test();
  8. }

test本地方法的实现

  1. JNIEXPORT void Java_Hello_test(JNIEnv* env, jboject ojb) {
  2. //因为test不是静态函数,所以传进来的就是调用这个函数的对象
  3. jclass hello_clazz = env->GetObjectClass(obj);
  4. jfieldID filedID_prop = env->GetFieldID(hello_clazz, "property", "I");
  5. jmethodID methodID_func = env->GetMethodID("hello_clazz", "function", "(ILjava/util/Date;[I)I");
  6. env->CallIntMethod(obj, methodID_func, 0L, NULL, NULL); //invoke
  7. }

取得的property是int类型的,所以在签名中传入”I
取得function的ID时
JNI:Java代码调用本地代码 - 图1
所以在最后得到(ILjava/util/Date;[I)I

9. 使用javap命令来产生签名

  • JDK提供了一个工具来查看一个类的申请,其中就可以设置输出每一个方法/属性的签名
  • java -s -p [full Calss Name]

-s 表示输出签名信息
-p-private,输出包括private访问权限的成员信息

4、取得Java属性和设置Java属性值

1. 取得Java属性/设定Java属性值

  • 获得了相应属性的jfieldID之后,就可以使用

    1. Set<TYPE>Field
    2. Get<TYPE>Field
    3. SetStatic<TYPE>Field
    4. GetStatic<TYPT>Field

    等函数来对Java属性进行操作了。

  • 类实例方法

    1. Set/GetBooleanField
    2. Set/GetByteField
    3. Set/GetDoubleField
    4. Set/GetFloatField
    5. Set/GetIntField
    6. Set/GetLongField
    7. Set/GetShortField
    8. Set/GetObjectField
  • 类静态方法

    1. Set/GetStaticBooleanField
    2. Set/GetStaticByteField
    3. Set/GetStaticDoubleField
    4. Set/GetStaticFloatField
    5. Set/GetStaticIntField
    6. Set/GetStaticLongField
    7. Set/GetStaticShortField
    8. Set/GetStaticObjectField
  • 怎样获取数组的属性呢?

可以使用GetObjectField来获取数组类型的属性。不过JNI提供了一系列方法获取数组的属性,后续介绍。

案例

在C/C++中修改Java中的属性

  1. package com.omg;
  2. public class TestNative {
  3. public int number;
  4. public native void sayHello();
  5. }

C++代码

  1. jclass clazz_TestNative = env->GetObjectClass( obj);
  2. jfieldID id_number = env->GetFieldID(clazz_TestNative, "number", "I");
  3. jint number = env->GetIntFiled(obj, id_number);
  4. count << number << endl;
  5. env->SetIntField(obj, id_number, 100L);

2. Java方法调用

  • JNIEnv提供了众多的 Call<TYPE>MethodCallStatic<TYPE>Method,还有CallNonvirtual<TYPE>Method函数。需要通过GetMethodID取得相应方法的jmethodID来传入上述函数的参数中。
  • 调用实例方法的三种形式
    1. Call<TYPE>Method( jobject obj, jmethodID id, ...);
    2. Call<TYPE>MethodV( jobject obj, jmethodID id, va_list lst);
    3. Call<TYPE>MethodA( jobject obj, jmethodID id, jvalue* v);
  1. 第一种是最常用的方式
  2. 第二种是当调用这个函数的时候,有一个指向参数表的va_list变量时使用(不常用)
  3. 第三种是当调用这个函数的时候,有一个指向jvalue或者jvalue数组的指针时使用

调用静态方法的三种形式

  1. CallStatic<TYPE>Method( jclass clazz, jmethodID id, ...);
  2. CallStatic<TYPE>MethodV( jclass clazz, jmethodID id, va_list lst);
  3. CallStatic<TYPE>MethodA( jclass clazz, jmethodID id, jvalue* v);

伪代码
java:

  1. public boolean function(int i, double d, char c){
  2. //......
  3. }

C

  1. env->CallBooleanMethod(obj, id_function, 100L, 1.11, L'3')
  2. jvalue * args = new jvalue[3];
  3. args[0].i = 100L;
  4. args[1].d = 3.44;
  5. args[2].c = L'3';
  6. env->CallBooleanMethod( obj, id_function, args);
  7. delete [] args;

案例

在C/C++中代用Java中的方法
Java代码

  1. package com.omg;
  2. public class TestNative {
  3. public max(double num1, double num2){
  4. return num1 > num2 ? num1 : num2;
  5. }
  6. }

C++代码

  1. jclass clazz_TestNative = env->GetObjectClass( obj );
  2. jmethodID id_max = env->GetMetodID( clazz_TestNative, obj, "(DD)D");
  3. jdouble maxVal = env->GetDoubleMethod(obj, id_max, 2.1, 3.14);
  4. cout << maxVal << endl;

3. CallNonvirtual<TYPE>Method

有如下Java代码

  1. public class Father {
  2. public void function(){
  3. System.out.println("Father function");
  4. }
  5. }
  6. public class Child extends Father {
  7. public void function(){
  8. System.out.println("Child function");
  9. }
  10. }

想想这段Java代码调用的是哪个类的方法?

  1. Father p = new Child();
  2. p.function();

如果想调用父类的function是没有办法的
有如下C++代码

  1. class Father {
  2. public:
  3. void function(){
  4. cout << "Father function" <<endl;
  5. }
  6. }
  7. class Child : public Father {
  8. public:
  9. void function(){
  10. cout << "Child function" <<endl;
  11. }
  12. }

想想这段代码调用的是哪个类的成员函数?(调用的是父类的函数)

  1. Father* p = new Child();
  2. p->function();

C++有虚函数的概念,修改一下父类加一个 virtual

  1. class Father {
  2. public:
  3. virtual void function(){
  4. cout << "Father function" <<endl;
  5. }
  6. }
  7. class Child : public Father {
  8. public:
  9. void function(){
  10. cout << "Child function" <<endl;
  11. }
  12. }

再想想这段代码调用的是哪个类的成员函数?(调用的是子类的函数)

  1. Father* p = new Child();
  2. p->function();

在Java语言中,所有的函数都是虚函数

  • 在JNI中定义了CallNonvirtual<TYPE>Method就能实现子类对象调用父类方法的功能,如果想要调用一个对象的父类的方法,而不是子类的重写(over write)的方法的话,就可以使用CallNonVirtual<TYPE>Method
  • 要使用CallNonVirtual<TYPE>Method,首先要取得父类及要调用的父类的方法的jmethodID。然后传入到这个函数就能通过子类对象调用被重写(over write)的父类的方法

    案例

    ```java

package com.omg;

public class Father { public void function() { System.out.println(“Father function”); } }

package com.omg;

public class Child extends Father {

  1. public void function() {
  2. System.out.println("Child function");
  3. }

}

  1. 测试<br />Java定义成员变量
  2. ```java
  3. public Father p = new Child();

C++代码

  1. jclass clazz_TestNative = env->GetObjectClass( obj );
  2. jfieldID id_p = env->GetFieldID(clazz_TestNative, "p", "Lcom/omg/Father;");
  3. jobject p = env->GetObjectField(obj, id_p);
  4. jclass clazz_Father = env->FindClass( "Lcom/omg/Father;" );
  5. jmethodID id_Father_function = env->GetMethodID(clazz_Father, "function", "()V");
  6. env->CallVoidMethod(p, id_Father_function);
  7. env->CallNonvirtualVoidMethod(p, clazz_Father, id_Father_function);

5、使用JNI弊端

  • 使用了 JNI,那么这个Java Application 将不能跨平台了。如果需要移植到别的平台上,那么需要重新编写native代码
  • Java 是强类型的语言,而 C/C++并不是。因此,在编写JNI时要更小心
  • 总之,必须在构建Java程序的时候,尽量少用本地代码