阅读 59

JNI一文详解

1. 准备工作

  • Ubuntu更换阿里云软件源

  • 在Ubuntu上安装gcc和g++

  • 在Ubuntu上安装OpenJDK

    sudo apt install default—jdk
    复制代码

    设置JAVA_HOME环境变量:找到OpenJDK的安装路径(e.g. /usr/lib/jvm/java—11—openjdk—amd64)

    sudo vim /etc/profile
    复制代码

    在文件末尾添加

    export JAVA_HOME=/usr/lib/jvm/java—11—openjdk—amd64
    export PATH=$PATH:$JAVA_HOME/bin
    复制代码

    检查是否配置成功

    echo $JAVA_HOME
    echo $PATH
    复制代码

2. JNI简介

全称Java Native Interface,主要用于在Java代码中调用非Java代码,以此绕过Java的内存管理和解决Java的性能问题。

3. Getting Started

3.1 JNI with C

先看一下Java调用C的简单示例:

public class HelloJNI {  // Save as HelloJNI.java
   static {
      System.loadLibrary("hello"); // Load native library hello.dll (Windows) or libhello.so (Unixes)
                                   //  at runtime
                                   // This library contains a native method called sayHello()
   }
 
   // Declare an instance native method sayHello() which receives no parameter and returns void
   private native void sayHello();
 
   // Test Driver
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // Create an instance and invoke the native method
   }
}
复制代码

关键在于System.loadLibrary("hello");这行代码,Java通过这行代码调用动态链接库(Windows中的后缀名是.dll,Unix系OS中是.so),在运行时Java文件时可以通过设置虚拟机参数—Djava.library.path=/path/to/lib将本地库引入。

通过下面的代码来编译Java代码并生成对应的头文件:

javac —h . HelloJNI.java
复制代码

生成的头文件代码如下:

/* DO NOT EDIT THIS FILE — it is machine generated */
#include <jni.h>
/* Header—class HelloJNI */
 
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif
复制代码

头文件中定义了一个函数:

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
复制代码

依照惯例函数的命名规则是Java_{package_and_classname}_{function_name}(JNI_arguments)。

函数的两个参数的含义分别是:

  • JNIEnv*: reference to JNI environment, which lets you access all the JNI functions.
  • jobject: reference to "this" Java object.

extern "C"只被C++编译器识别,用于提示编译器这些函数应当使用C的函数命名协议来编译,C和C++的函数命名协议不同,C++支持重载。

下面的代码是要被调用的C文件:

// Save as "HelloJNI.c"
#include <jni.h>        // JNI header provided by JDK
#include <stdio.h>      // C Standard IO Header
#include "HelloJNI.h"   // Generated
 
// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}
复制代码

jni.h是包含在JDK中的,不同OS平台的存放位置不同。

下面来编译和运行:

$ gcc —fPIC —I"$JAVA_HOME/include" —I"$JAVA_HOME/include/linux" —shared —o libhello.so HelloJNI.c
复制代码
$ java —Djava.library.path=. HelloJNI
复制代码

3.2 JNI with C++

将上一个模块中c文件替换成cpp文件:

// Save as "HelloJNI.cpp"
#include <jni.h>       // JNI header provided by JDK
#include <iostream>    // C++ standard IO header
#include "HelloJNI.h"  // Generated
using namespace std;

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
	cout << "Hello World from C++!" << endl;
   return;
}
复制代码

编译:

$ g++ —fPIC —I"$JAVA_HOME/include" —I"$JAVA_HOME/include/linux" —shared —o libhello.so HelloJNI.cpp
复制代码

运行:

java —Djava.library.path=. HelloJNI
复制代码

4. JNI 基础

JNI中定义了一下两类数据类型和Java中的类型来对应:

  • 基础类型:

    jint—int jbyte—byte jshort—short jlong—long jfloat—float jdouble—double jchar—char jboolean—boolean

  • 引用类型:

    jobject—java.lang.Object

    jclass—java.lang.Class

    jstringjava.lang.String

    jthrowablejava.lang.Throwable

    jarray—Java array jarray包括八种基础类型(jintArray,jbyteArray,jshortArray,jlongArray,jfloatArray,jdoubleArray,jcharArrayandjbooleanArray)和一种引用类型(jobjectArray)

native程序要做的事是:

  1. 通过上面的JNI类型获取函数参数,这些参数都是从Java层传递过来的。
  2. 同样,JNI类型和native类型也有对应关系,例如:jstring对应c的string类型,jintArray对应c的int[]类型,jni中的基础类型像jint这种不需要转换,可以直接操作。
  3. 执行操作。
  4. 创建一个jni类型的返回对象,然后把返回值复制到这个对象中。
  5. 返回。

最困难的一步就是jni类型和native类型的转换,jni为此提供了很多的转换接口。

jni本质上是一个c的接口,它并不会真正传递对象。

5. 在Java和native之间传递参数和返回结果

5.1 传递基础类型

jni和java基础类型不需要转换,八种jni基础类型和java基础类型一一对应。

jni和c基础类型需要转换,在"jni.h" 和"win32\jni_mh.h"中可以查看类型定义的声明:

// In "win\jni_mh.h" - machine header which is machine dependent
typedef long            jint;
typedef __int64         jlong;
typedef signed char     jbyte;
 
// In "jni.h"
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;
typedef jint            jsize;
复制代码

需要注意的是:jint对应着c中的long(至少32位),而不是int(可以为16位)。

TestJNIPrimitive.java:
public class TestJNIPrimitive {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Declare a native method average() that receives two ints and return a double containing the average
   private native double average(int n1, int n2);
 
   // Test Driver
   public static void main(String args[]) {
      System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
   }
}
复制代码
TestJNIPrimitive.c:
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"
 
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
          (JNIEnv *env, jobject thisObj, jint n1, jint n2) {
   jdouble result;
   printf("In C, the numbers are %d and %d\n", n1, n2);
   result = ((jdouble)n1 + n2) / 2.0;
   // jint is mapped to int, jdouble is mapped to double
   return result;
}
复制代码
TestJNIPrimitive.cpp:
#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;
 
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
          (JNIEnv *env, jobject obj, jint n1, jint n2) {
   jdouble result;
   cout << "In C++, the numbers are " << n1 << " and " << n2 << endl;
   result = ((jdouble)n1 + n2) / 2.0;
   // jint is mapped to int, jdouble is mapped to double
   return result;
}
复制代码

5.2 传递String类型

头文件中的函数定义:

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);
复制代码

在jni和c之间传递string类型比基本类型更加复杂,需要在jni类型jstring和c类型char*之间做转换。

针对这种转换,jni提供了两个函数:

  • jstring—>char*

    const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
    复制代码
  • char*—>jstring

    jstring NewStringUTF(JNIEnv*, char*)
    复制代码

示例代码:

TestJNIString.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"
 
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
   // Step 1: Convert the JNI String (jstring) into C-String (char*)
   const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
   if (NULL == inCStr) return NULL;
 
   // Step 2: Perform its intended operations
   printf("In C, the received string is: %s\n", inCStr);
   (*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr);  // release resources
 
   // Prompt user for a C-string
   char outCStr[128];
   printf("Enter a String: ");
   scanf("%s", outCStr);    // not more than 127 characters
 
   // Step 3: Convert the C-string (char*) into JNI String (jstring) and return
   return (*env)->NewStringUTF(env, outCStr);
}
复制代码

jni支持Unicode (16-bit characters) 和 UTF-8 (encoded in 1-3 bytes) 两种类型的string,UTF-8类似于c中的char array(从本质而言,C语言把字符串字面量作为字符数组来处理。当C语言编译器在程序中遇到长度为n的字符串字面量时,它会为字符串字面量分配长度为n+1的内存空间。这块内存空间将用来存储字符串字面量中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是一个所有位都为0的字节,因此用转义序列\0来表示,空字符的码值为0),用于C/C++。

// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
   // Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
   // Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
   // Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
   // Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding 
   // and place the result in the given buffer buf.
  
// Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
   // Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
   // Constructs a new java.lang.String object from an array of Unicode characters.
jsize GetStringLength(JNIEnv *env, jstring string);
   // Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
   // Copies len number of Unicode characters beginning at offset start to the given buffer buf
复制代码

函数GetStringUTFChars()用于从一个java的jstring类型生成c的char*类型:

// Returns a pointer to an array of bytes representing the string in modified UTF-8       	 	encoding.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
复制代码

其中的第三个参数(类型为jboolean*), isCopy的值为JNI_TRUE的时候说明函数的返回值是原始的String实例的复制,为JNI_FALSE的时候则是指向原始实例的指针(这种情况下native层不可以修改字符串的内容)。一般来说jni优先返回一个指针,其次是返回一个复制实例。但是,我们很少修改基础字符串,所以第三个参数经常传递NULL指针。

ReleaseStringUTFChars()和GetStringUTFChars()必须成对出现,用来释放内存和引用,使对象可以被GC回收。

函数NewStringUTF()通过c的string创建一个新的jstring。

JDK1.2中引入了GetStringUTFRegion()可以用来替代GetStringUTFChars(),isCopy不再需要,因为这个函数直接将jstring复制到了c的char array中。

JDK 1.2中还引入了函数Get/ReleaseStringCritical(),这两个函数成对出现,且中间不能被阻塞。它的功能和GetStringUTFChars()类似,一般返回指针,如果失败的话,会返回一个复制实例。

示例代码:

TestJNIString.cpp
#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;
 
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
   // Step 1: Convert the JNI String (jstring) into C-String (char*)
   const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
   if (NULL == inCStr) return NULL;
 
   // Step 2: Perform its intended operations
   cout << "In C++, the received string is: " << inCStr << endl;
   env->ReleaseStringUTFChars(inJNIStr, inCStr);  // release resources
 
   // Prompt user for a C++ string
   string outCppStr;
   cout << "Enter a String: ";
   cin >> outCppStr;
 
   // Step 3: Convert the C++ string to C-string, then to JNI String (jstring) and return
   return env->NewStringUTF(outCppStr.c_str());
}
复制代码

注意c++和c的string函数语法不通,在c++中,我们可以用 "env->"代替"(env*)->",此外,在c++函数中我们可以省略JNIEnv* 参数。c++支持string类,也支持c中的char array。

5.3 传递基础类型数组

TestJNIPrimitiveArray.java
public class TestJNIPrimitiveArray {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Declare a native method sumAndAverage() that receives an int[] and
   //  return a double[2] array with [0] as sum and [1] as average
   private native double[] sumAndAverage(int[] numbers);
 
   // Test Driver
   public static void main(String args[]) {
      int[] numbers = {22, 33, 33};
      double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);
      System.out.println("In Java, the average is " + results[1]);
   }
}
复制代码

生成的头文件中的函数定义如下:

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage (JNIEnv *, jobject, jintArray);
复制代码

在java中,数组是引用类型。有九种数组类型,包括8个基础类型和一个引用类型。jni分别对应地定义了8种基础类型数组,jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray,对应java中的8种基础类型:int, byte, short, long, float, double, char, boolean。jobjectArray对应引用类型数组。

和前面介绍的基础类型类似,需要在java类型和jni类型之间进行转换:

jni的jintArray—>c的jint[]

jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)
复制代码

c的jint[]—>jni的jintArray

Step1 : 分配内存

jintArray NewIntArray(JNIEnv *env, jsize len)
复制代码

Step2 : 从 jint[] 复制内容到 jintArray

void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)
复制代码

上述的函数有八种,对应8个基础类型。

native代码需要做的事:

  1. 把jni数组转换成c的数组
  2. 执行操作
  3. 把c的数组转换成jni的数组,然后返回
TestJNIPrimitiveArray.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"
 
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
          (JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
   // Step 1: Convert the incoming JNI jintarray to C's jint[]
   jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
   if (NULL == inCArray) return NULL;
   jsize length = (*env)->GetArrayLength(env, inJNIArray);
 
   // Step 2: Perform its intended operations
   jint sum = 0;
   int i;
   for (i = 0; i < length; i++) {
      sum += inCArray[i];
   }
   jdouble average = (jdouble)sum / length;
   (*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources
 
   jdouble outCArray[] = {sum, average};
 
   // Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
   jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);  // allocate
   if (NULL == outJNIArray) return NULL;
   (*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray);  // copy
   return outJNIArray;
}
复制代码
jni基础类型数组函数
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
复制代码

GET|Release<*PrimitiveType*>ArrayElements() 用于从java的jxxxArray创建c的array jxxx[]

GET|Set<*PrimitiveType*>ArrayRegion() 用于将一个jxxxArray复制到预分配的c的 jxxx[],或者从c的 jxxx[]复制到jxxxArray中。

The New<PrimitiveType>Array() 用于给 jxxxArray 分配一个给定的大小,然后可以用Set<*PrimitiveType*>ArrayRegion() 从一个jxxx[]中获取内容填充 jxxxArray

Get|ReleasePrimitiveArrayCritical()成对出现,中间不允许被阻塞。

6. 获取java类的成员变量/方法和类静态变量/方法

6.1 获取类的成员变量

  1. 通过 GetObjectClass()获取对象的类的引用。

  2. 获取成员ID GetFieldID()。需要提供变量名和成员描述。格式如下:"L<fully-qualified-name>;"。用/代替.。例如:String - "Ljava/lang/String;"。

    对于基础类型:

    "I" for int,

    "B" for byte,

    "S" for short,

    "J" for long,

    "F" for float,

    "D" for double,

    "C" for char,

    "Z" for boolean

    对于数组:

    要加一个前缀"[",例如 Object array-"[Ljava/lang/Object;",int array-"[I"

  3. 基于 Field ID, 通过 GetObjectField() or Get<primitive-type>Field() 获取成员变量。

  4. 使用 SetObjectField() or Set<primitive-type>Field() 修改成员变量。

示例代码:

TestJNIInstanceVariable.java
public class TestJNIInstanceVariable {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Instance variables
   private int number = 88;
   private String message = "Hello from Java";
 
   // Declare a native method that modifies the instance variables
   private native void modifyInstanceVariable();
 
   // Test Driver   
   public static void main(String args[]) {
      TestJNIInstanceVariable test = new TestJNIInstanceVariable();
      test.modifyInstanceVariable();
      System.out.println("In Java, int is " + test.number);
      System.out.println("In Java, String is " + test.message);
   }
}
复制代码
TestJNIInstanceVariable.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIInstanceVariable.h"
 
JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
          (JNIEnv *env, jobject thisObj) {
   // Get a reference to this object's class
   jclass thisClass = (*env)->GetObjectClass(env, thisObj);
 
   // int
   // Get the Field ID of the instance variables "number"
   jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
   if (NULL == fidNumber) return;
 
   // Get the int given the Field ID
   jint number = (*env)->GetIntField(env, thisObj, fidNumber);
   printf("In C, the int is %d\n", number);
 
   // Change the variable
   number = 99;
   (*env)->SetIntField(env, thisObj, fidNumber, number);
 
   // Get the Field ID of the instance variables "message"
   jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
   if (NULL == fidMessage) return;
 
   // String
   // Get the object given the Field ID
   jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
 
   // Create a C-string with the JNI String
   const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
   if (NULL == cStr) return;
 
   printf("In C, the string is %s\n", cStr);
   (*env)->ReleaseStringUTFChars(env, message, cStr);
 
   // Create a new C-string and assign to the JNI string
   message = (*env)->NewStringUTF(env, "Hello from C");
   if (NULL == message) return;
 
   // modify the instance variables
   (*env)->SetObjectField(env, thisObj, fidMessage, message);
}
复制代码

获取成员变量的jni方法:

jclass GetObjectClass(JNIEnv *env, jobject obj);
   // Returns the class of an object.
   
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for an instance variable of a class.
 
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
  // Get/Set the value of an instance variable of an object
  // <type> includes each of the eight primitive types plus Object.
复制代码

6.2 获取类静态变量

获取静态变量和实例变量类似,只是在调用的函数上有所差别,GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic<Primitive-type>Field()。

TestJNIStaticVariable.java
public class TestJNIStaticVariable {
   static {
      System.loadLibrary("myjni"); // nyjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Static variables
   private static double number = 55.66;
 
   // Declare a native method that modifies the static variable
   private native void modifyStaticVariable();
 
   // Test Driver
   public static void main(String args[]) {
      TestJNIStaticVariable test = new TestJNIStaticVariable();
      test.modifyStaticVariable();
      System.out.println("In Java, the double is " + number);
   }
}
复制代码
TestJNIStaticVariable.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIStaticVariable.h"
 
JNIEXPORT void JNICALL Java_TestJNIStaticVariable_modifyStaticVariable
          (JNIEnv *env, jobject thisObj) {
   // Get a reference to this object's class
   jclass cls = (*env)->GetObjectClass(env, thisObj);
 
   // Read the int static variable and modify its value
   jfieldID fidNumber = (*env)->GetStaticFieldID(env, cls, "number", "D");
   if (NULL == fidNumber) return;
   jdouble number = (*env)->GetStaticDoubleField(env, cls, fidNumber);
   printf("In C, the double is %f\n", number);
   number = 77.88;
   (*env)->SetStaticDoubleField(env, cls, fidNumber, number);
}
复制代码

获取静态变量的jni函数:

jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for a static variable of a class.
 
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
  // Get/Set the value of a static variable of a class.
  // <type> includes each of the eight primitive types plus Object.
复制代码

6.3. 调用Java类的成员方法和类静态方法

调用成员方法的步骤:

step1:通过 GetObjectClass()获取对象的类的引用。

step2:GetMethodID()获取Method ID,提供方法名和签名,签名的格式是 "(parameters)return-type"。可通过javap utility (Class File Disassembler) with -s (print signature) and -p (show private members)这个命令来列出java类的方法签名:

> javap --help
> javap -s -p TestJNICallBackMethod
  .......
  private void callback();
    Signature: ()V
 
  private void callback(java.lang.String);
    Signature: (Ljava/lang/String;)V
 
  private double callbackAverage(int, int);
    Signature: (II)D
 
  private static java.lang.String callbackStatic();
    Signature: ()Ljava/lang/String;
  .......
复制代码

step3:基于Method ID,调用CallMethod()orCallVoidMethod()orCallObjectMethod(),返回值类型分别是<Primitive-type>, void and Object,对于有参数的方法要提供参数。

示例代码:

TestJNICallBackMethod.java
public class TestJNICallBackMethod {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Declare a native method that calls back the Java methods below
   private native void nativeMethod();
 
   // To be called back by the native code
   private void callback() {
      System.out.println("In Java");
   }
 
   private void callback(String message) {
      System.out.println("In Java with " + message);
   }
 
   private double callbackAverage(int n1, int n2) {
      return ((double)n1 + n2) / 2.0;
   }
 
   // Static method to be called back
   private static String callbackStatic() {
      return "From static Java method";
   }

   // Test Driver 
   public static void main(String args[]) {
      new TestJNICallBackMethod().nativeMethod();
   }
}
复制代码
TestJNICallBackMethod.c
#include <jni.h>
#include <stdio.h>
#include "TestJNICallBackMethod.h"
 
JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod
          (JNIEnv *env, jobject thisObj) {
 
   // Get a class reference for this object
   jclass thisClass = (*env)->GetObjectClass(env, thisObj);
 
   // Get the Method ID for method "callback", which takes no arg and return void
   jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
   if (NULL == midCallBack) return;
   printf("In C, call back Java's callback()\n");
   // Call back the method (which returns void), baed on the Method ID
   (*env)->CallVoidMethod(env, thisObj, midCallBack);
 
   jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass,
                               "callback", "(Ljava/lang/String;)V");
   if (NULL == midCallBackStr) return;
   printf("In C, call back Java's called(String)\n");
   jstring message = (*env)->NewStringUTF(env, "Hello from C");
   (*env)->CallVoidMethod(env, thisObj, midCallBackStr, message);
 
   jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass,
                                  "callbackAverage", "(II)D");
   if (NULL == midCallBackAverage) return;
           jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3);
   printf("In C, the average is %f\n", average);
 
   jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass,
                                 "callbackStatic", "()Ljava/lang/String;");
   if (NULL == midCallBackStatic) return;
   jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic);
   const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL);
   if (NULL == resultCStr) return;
   printf("In C, the returned string is %s\n", resultCStr);
   (*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr);
}
复制代码

如果要调用静态方法,使用GetStaticMethodID(), CallStaticMethod(),CallStaticVoidMethod() or CallStaticObjectMethod()。

调用成员和静态方法的jni函数:

jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.
   
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.
复制代码

6.4. 调用父类的实例方法

CallNonvirtualMethod(),类似于java中的super.methodName()。

具体步骤:

step1:GetMethodID()

step2:CallNonvirtualMethod()

jni函数如下:

NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
复制代码

7. 创建java对象和对象数组

在native中调用NewObject()andnewObjectArray(),构建jobjectandjobjectArray,然后返回到java中。

7.1 在native中调用java的构造方法创建java对象

调用构造方法和调用实例方法类似:

step1: 获取Method ID method name:"<init>" return-type:"V"

step2: 调用NewObject()创建java对象

示例代码:

TestJavaConstructor.java
public class TestJNIConstructor {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // Native method that calls back the constructor and return the constructed object.
   // Return an Integer object with the given int.
   private native Integer getIntegerObject(int number);
 
   public static void main(String args[]) {
      TestJNIConstructor obj = new TestJNIConstructor();
      System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
   }
}
复制代码
TestJavaConstructor.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIConstructor.h"
 
JNIEXPORT jobject JNICALL Java_TestJNIConstructor_getIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   // Get a class reference for java.lang.Integer
   jclass cls = (*env)->FindClass(env, "java/lang/Integer");
 
   // Get the Method ID of the constructor which takes an int
   jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(I)V");
   if (NULL == midInit) return NULL;
   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = (*env)->NewObject(env, cls, midInit, number);
 
   // Try running the toString() on this newly create object
   jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
   if (NULL == midToString) return NULL;
   jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString);
   const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL);
   printf("In C: the number is %s\n", resultCStr);

   //May need to call releaseStringUTFChars() before return
   return newObj;
}
复制代码

用于创建对象的jni函数:

jclass FindClass(JNIEnv *env, const char *name);
 
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
   // Constructs a new Java object. The method ID indicates which constructor method to invoke
 
jobject AllocObject(JNIEnv *env, jclass cls);
  // Allocates a new Java object without invoking any of the constructors for the object.
复制代码

7.2 对象数组

不同于基础类型的数组可以批量处理所有元素,对象数组需要使用Get|SetObjectArrayElement()处理每个元素。

示例代码:创建一个Integer数组,计算总和和平均值,返回Double数组。

TestJNIObjectArray.java
import java.util.ArrayList;
 
public class TestJNIObjectArray {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
   // Native method that receives an Integer[] and
   //  returns a Double[2] with [0] as sum and [1] as average
   private native Double[] sumAndAverage(Integer[] numbers);
 
   public static void main(String args[]) {
      Integer[] numbers = {11, 22, 32};  // auto-box
      Double[] results = new TestJNIObjectArray().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);  // auto-unbox
      System.out.println("In Java, the average is " + results[1]);
   }
}
复制代码
TestJNIObjectArray.c
#include <jni.h>
#include <stdio.h>
#include "TestJNIObjectArray.h"
 
JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage
          (JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
   // Get a class reference for java.lang.Integer
   jclass classInteger = (*env)->FindClass(env, "java/lang/Integer");
   // Use Integer.intValue() to retrieve the int
   jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I");
   if (NULL == midIntValue) return NULL;
 
   // Get the value of each Integer object in the array
   jsize length = (*env)->GetArrayLength(env, inJNIArray);
   jint sum = 0;
   int i;
   for (i = 0; i < length; i++) {
      jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i);
      if (NULL == objInteger) return NULL;
      jint value = (*env)->CallIntMethod(env, objInteger, midIntValue);
      sum += value;
   }
   double average = (double)sum / length;
   printf("In C, the sum is %d\n", sum);
   printf("In C, the average is %f\n", average);
 
   // Get a class reference for java.lang.Double
   jclass classDouble = (*env)->FindClass(env, "java/lang/Double");
 
   // Allocate a jobjectArray of 2 java.lang.Double
   jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL);
 
   // Construct 2 Double objects by calling the constructor
   jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V");
   if (NULL == midDoubleInit) return NULL;
   jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum);
   jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average);
   // Set to the jobjectArray
   (*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum);
   (*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve);
 
   return outJNIArray;
}
复制代码

jni中用于创建和操作对象数组的函数:

jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
   // Constructs a new array holding objects in class elementClass.
   // All elements are initially set to initialElement.
 
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
   // Returns an element of an Object array.
 
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
   // Sets an element of an Object array.
复制代码

8. jni中的局部引用和全局引用

对于追求效率的程序而言,管理引用非常关键,有助于节省开销,提高可重用性。

jni把对象引用(对于jobject的引用)分为两种:局部和全局引用。

  1. 局部引用在native方法内创建,一旦方法结束就被释放。它的生命周期和方法相同。你也可以调用DeleteLocalRef()主动释放,使对象可以被回收。对象都是作为局部引用被传递给native方法的。所有的jni函数返回的对象都是局部引用,通过CallStaticObjectMethod()获取的jobject也是局部引用
  2. 全局引用会一直存在知道通过DeleteGlobalRef()被主动释放,可以用一个局部引用通过NewGlobalRef()来创建全局引用。

示例代码:在java中定义两个native方法,都用于创建Integer对象。

public class TestJNIReference {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // A native method that returns a java.lang.Integer with the given int.
   private native Integer getIntegerObject(int number);
 
   // Another native method that also returns a java.lang.Integer with the given int.
   private native Integer anotherGetIntegerObject(int number);
 
   public static void main(String args[]) {
      TestJNIReference test = new TestJNIReference();
      System.out.println(test.getIntegerObject(1));
      System.out.println(test.getIntegerObject(2));
      System.out.println(test.anotherGetIntegerObject(11));
      System.out.println(test.anotherGetIntegerObject(12));
      System.out.println(test.getIntegerObject(3));
      System.out.println(test.anotherGetIntegerObject(13));
   }
}
复制代码

在c代码中我们要通过FindClass()获取class java.lang.Integer的引用,然后获取构造方法的method ID。但是我们想复用class ref 和method ID。

下面看一个错误的使用情况:

#include <jni.h>
#include <stdio.h>
#include "TestJNIReference.h"
 
// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
 
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
 
   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      classInteger = (*env)->FindClass(env, "java/lang/Integer");
   }
   if (NULL == classInteger) return NULL;
 
   // Get the Method ID of the Integer's constructor if missing
   if (NULL == midIntegerInit) {
      printf("Get Method ID for java.lang.Integer's constructor\n");
      midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
   }
   if (NULL == midIntegerInit) return NULL;
 
   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
   printf("In C, constructed java.lang.Integer with number %d\n", number);
   return newObj;
}
 
JNIEXPORT jobject JNICALL Java_TestJNIReference_getIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}
 
JNIEXPORT jobject JNICALL Java_TestJNIReference_anotherGetIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}
复制代码

在第二次调用时,java.lang.Integer的引用失效了(并且不是NULL),这是因为FindClass()返回的是一个局部引用,方法一旦结束就失效了。

为了解决这个问题,我们需要创建一个全局引用:

   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      // FindClass returns a local reference
      jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
      // Create a global reference from the local reference
      classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
      // No longer need the local reference, free it!
      (*env)->DeleteLocalRef(env, classIntegerLocal);
   }
复制代码

需要注意的是jmethodID和jfieldID不是jobject,无法创建全局引用。

9. jni的常见错误

错误信息:SEVERE: java.lang.UnsatisfiedLinkError: no xxx in java.library.path

原因:使用了第三方的本地库

结局方法:将本地库的路径添加到"java.library.path",使用如下命令(以JOGL为例):

> java -Djava.library.path=d:\bin\jogl2.0\lib myjoglapp
复制代码

源码地址:关注下面的公众号回复“JNI一文详解”可获得源码地址。

微信公众号 长夜西风

个人网站 www.cmder.info/