【Android -- JNI 和 NDK】Java 和 C/C++ 之间传递参数和返回值

在这里插入图片描述

本文主要介绍 JNI 的数据传递上,即 Java 如何传递对象给 C++; 而 C++ 又如何将数据封装成 Java 所需的对象。

1. 基本数据类型

传递 java 的基本类型是非常简单而直接的,一个 jxxx 之类的类型已经定义在本地系统中了,比如:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar 和 jboolean分别对应java的int, byte, short, long, float, double, char 和 boolean基本类型。

Java JNI 程序: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));
   }
}

这个 JNI 程序加载了 myjni.dll(windows)库或者 libmyjni.so(类UNIX)库。并且声明了一个 native 方法,这个方法接受两个int类型的参数,并且返回一个 double 类型的返回值,这个值是两个int型数的平均值。mian方法调用了 average 函数。

下面,我们将上面的 java 代码编译成TestJNIPrimitive.class,进而生成

C/C++ 头文件 TestJNIPrimitive.h:

> javac TestJNIPrimitive.java
> javah TestJNIPrimitive       // Output is TestJNIPrimitive.h

C实现:TestJNIPrimitive.c
头文件 TestJNIPrimitive.h 中包含了一个函数声明:

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);

可以看到,这里的 jintjdouble 分别表示 java 中的 intdouble
jni.hwindows 上是 win32/jni_mh.h)头文件包含了这些数据类型的定义,同时多了一个jsize的定义:

// 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 类型(至少是 32bit 的),而不是 C 的 int 类型(至少是16bit的)。于是,在 C 代码中要使用jint而不是int是很重要的。同时,CygWin 不支持__int64类型。
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;
}

然后,我们编译代码成一个共享库:

// MinGW GCC under Windows
> set JAVA_HOME={jdk-installed-directory}
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.c

最后,我们运行这个 java 代码:

java TestJNIPrimitive

C++ 实现 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;
}

使用 g++ 来编译上面的代码:

// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.cpp

2. 传递字符串

Java JNI 程序:TestJNIString.java

public class TestJNIString {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
   // Native method that receives a Java String and return a Java String
   private native String sayHello(String msg);

   public static void main(String args[]) {
      String result = new TestJNIString().sayHello("Hello from Java");
      System.out.println("In Java, the returned string is: " + result);
   }
}

上面的代码声明了一个 native 函数 sayHello,这个函数接受一个 java 的 String,然后返回一个 Java stringmain 方法调用了 sayHello 函数。

然后,我们编译上面的代码,并且生成 C/C++ 的头文件:

> javac TestJNIString.java
> javah TestJNIString

C代码实现:TestJNIString.c
上面的头文件 TestJNIString.h声明了这样的一个函数:

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);

JNI 定义了 jstring 类型应对 java 的 String 类型。上面声明中的最后一个参数 jstring 就是来自 Java 代码中的 String 参数,同时,返回值也是一个 jstring 类型。

传递一个字符串比传递基本类型要复杂的多,因为java的String是一个对象,而C的string是一个NULL结尾的char数组。因此,我们需要将Java的String对象转换成C的字符串表示形式:char *

前面我们提到,JNI环境指针JNIEnv *已经为我们定义了非常丰富的接口函数用来处理数据的转换:

  1. 调用const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)来将JNI的jstring转换成C的char *
  2. 调用jstring NewStringUTF(JNIEnv*, char*)来将C的char *转换成JNI的jstring

因此我们的C程序基本过程如下:

  1. 使用 GetStringUTFChars() 函数来将 jstring 转换成 char *
  2. 然后进行需要的数据处理
  3. 使用 NewStringUTF() 函数来将 char * 转换成 jstring,并且返回
#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 == inCSt) 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);
}

将上面的代码编译成共享库:

// MinGW GCC under Windows
> gcc -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.c

最后,运行代码:

> java TestJNIString
In C, the received string is: Hello from Java
Enter a String: test
In Java, the returned string is: test

JNI 中的 string 转换函数
上面我们展示了两个函数,现在我们全面梳理下JNI为我们提供的函数。JNI支持Unicode(16bit字符)和UTF-8(使用1~3字节的编码)转化。一般而言,我们应该在C/C++中使用UTF-8的编码方式。
JNI系统提供了如下关于字符串处理的函数(一共两组,UTF8和Unicode):

// 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() 函数可以将 jstring 转成 char *,这个函数会返回NULL,如果系统的内容分配失败的话。因此,好的做法是检查这个函数的返回是不是NULL。第三个参数是isCopy,这个参数是一个in-out参数,传进去的是一个指针,函数结束的时候指针的内容会被修改。如果内容是JNI_TRUE的话,那么代表返回的数据是jstring数据的一个拷贝,反之,如果是JNI_FALSE的话,就说明返回的字符串就是直接指向那个String对象实例的。在这种情况下,本地代码不应该随意修改string中的内容,因为修改会代码Java中的修改。JNI系统会尽量保证返回的是直接引用,如果不能的话,那就返回一个拷贝。通常,我们很少关心修改这些string ,因此我们这里一般传递NULLisCopy参数。

必须要注意的是,当你不在需要 GetStringUTFChars 返回的字符串的时候,一定记得调用ReleaseStringUTFChars()函数来将内存资源释放!否则会内存泄露!并且上层java中的GC也不能进行!

另外,在GetStringUTFCharsReleaseStringUTFChars不能block!
NewStringUTF()函数可以从char *字符串得到jstring

C++实现: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());
}

使用g++编译上面的代码:

// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.cpp

需要注意的是,在C++中,本地string类的函数调用语法不一样。在C++中,我们使用env->来调用,而不是(env*)->。同时,在C++函数中不需要JNIEnv*这个参数了。

3. 传递基本类型的数组

JNI 代码: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]);
   }
}

C语言实现:TestJNIPrimitiveArray.c
头文件 TestJNIPrimitiveArray.h 包含以下函数声明:

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_average (JNIEnv *, jobject, jintArray);

在Java中,array是指一种类型,类似于类。一共有9种java的array,8个基本类型的array和一个object的array。JNI针对java的基本类型都定义了相应的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray,并且也有面向object的jobjectArray

同样地,你需要在JNI array和Native array之间进行转换,JNI系统已经为我们提供了一系列的接口函数:

  1. 使用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)jintarray转换成C的jint[]
  2. 使用jintArray NewIntArray(JNIEnv *env, jsize len)函数来分配一个len字节大小的空间,然后再使用void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)函数将jint[]中的数据拷贝到jintArray中去。

一共有8对类似上面的函数,分别对应java的8个基本数据类型。
因此,native程序需要:

  1. 接受来自java的JNI array,然后转换成本地array
  2. 进行需要的数据操作
  3. 将需要返回的数据转换成jni的array,然后返回
    下面是C代码实现的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基本类型的array函数
JNI基本类型的array(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray 和 jbooleanArray)函数如下:

// 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 函数之间也不能always block。

4. 访问Java对象变量和回调Java方法

访问Java对象实例的变量
JNI程序: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);
   }
}

这个类包含了两个private实例变量,一个 int,一个 String 对象。然后我们在 main 中调用本地函数modifyInstanceVariable来修改这两个变量。

C代码实现: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);
}

为了访问对象中的变量,我们需要:

  1. 调用GetObjectClass()获得目标对象的类引用
  2. 从上面获得的类引用中获得Field ID来访问变量,你需要提供这个变量的名字,变量的描述符(也称为签名)。对于java类而言,描述符是这样的形式:“Lfully-qualified-name;”(注意最后有一个英文半角分号),其中的包名点号换成斜杠(/),比如java的Stirng类的描述符就是“Ljava/lang/String;”。对于基本类型而言,I代表int,B代表byte,S代表short,J代表long,F代表float,D代表double,C代表char,Z代表boolean。对于array而言,使用左中括号”[“来表示,比如“[Ljava/lang/Object;”表示Object的array,“[I”表示int型的array。
  3. 基于上面获得的Field ID,使用GetObjectField() 或者 Get_primitive-type_Field()函数来从中解析出我们想要的数据
  4. 使用SetObjectField() 或者 Set_primitive-type_Field()函数来修改变量

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.

访问类中的static变量
访问类中的static变量类似于上面访问普通的实例变量,只是我们这里使用的函数是GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic_Primitive-type_Field()
JNI 程序: 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);
   }
}

C语言实现:C Implementation - 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中用来访问类中的static变量的函数如下:

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.

回调实例的普通和static方法
你可以在native代码中回调java中的普通或者static的方法。下面是实例:
JNI程序: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();
   }
}

这个类中声明了一个native函数nativeMethod(),并且在main方法中调用了这个函数。nativeMethod()这个函数会回调这个类中定义的各种方法。
C语言实现: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);
}

为了能够回调实例中的方法,我们需要:

  1. 通过GetObjectClass()函数获得这个实例的类对象
  2. 从上面获得类对象中,调用GetMethodID()函数来获得Method ID,Method ID表示了实例中的某个方法的抽象。你需要提供这个方法的名字和签名信息,签名规则和变量类似。签名的格式是这样的:(parameters)return-type。如果我们实在觉得jni的签名不好记忆的话,我们可以是用JDK为我们提供的工具javap来获得某个class类中的所有方法的签名,使用-s选项表示打印签名,-p表示显示private成员:
> 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;
  .......

从上面的输出我们可以清楚地看到类中每一个方法的签名。

  1. 基于上面我们获得的Method ID,我们可以调用_Primitive-type_Method() 或者 CallVoidMethod() 或者 CallObjectMethod()来调用这个方法。如果某个方法需要参数的话,就在后面跟上参数即可。
  2. 如果想要调用一个static方法的话,使用GetMethodID(), CallStatic_Primitive-type_Method(), CallStaticVoidMethod() 或者 CallStaticObjectMethod()
    JNI中用来回调实例和static方法的所有函数(两类,普通的和static的):
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.

回调复写的父类实例方法
JNI提供了一系列的形如 CallNonvirtual_Type_Method()之类的函数来调用父类实例的方法:

  1. 首先获得Method ID,使用GetMethodID()
  2. 基于上获得的Method ID,通过调用 CallNonvirtual_Type_Method()函数来调用相应的方法,并且在参数中给出object,父类和参数列表。

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);

创建 Object 和 Object arrays
你可以在native代码中构造jobjectjobjectarray,通过调用NewObject()newObjectArray()函数,然后讲它们返回给java代码。

回调Java构造器来创建一个新的java对象
回调一个构造器和回调其他的方法是类似的,首先通过init作为方法名,V作为返回值来获得Method ID,然后通过NewObject()函数来构建一个java类对象。

JNI程序: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));
   }
}

这个类声明了一个getIntegerObject的native方法,这个方法接受一个int的数据,然后在native代码中创建一个Integer类型的对象,其中的值就是这个值。

C代码实现: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 runnning 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);

   return newObj;
}

JNI中用于创建对象(jobject)的函数有:

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.

对象(object)的array
JNI程序: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]);
   }
}

为了简单起见,这个类声明了一个native方法,这个方法接受一个Integer类型的array,然后在本地代码中计算这个array中的数的和与平均数,然后讲这两个数以Double array的形式返回。
C代码实现: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;
}

不像基本数据类型的array那样,你需要使用Get|SetObjectArrayElement()函数来处理每一个元素。
JNI提供了创建对象array(jobjectArray)的函数如下:

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.

5. 本地和全局引用

管理引用是编写高效程序的关键。比如,我们会在本地代码中经常使用FindClass(), GetMethodID(), GetFieldID()来会的一个jclass,jmethodID和jfieldID。其实这些变量应该只是在第一次的时候获取,之后直接使用就可以了,而不用每次都去获取一遍,这样可以提高程序执行效率。
JNI讲本地代码中的对象引用分为了两种类型: 本地和全局引用:

  1. 本地引用是在本地代码中创建的,并且当函数退出或者返回的时候就被free了。它的有效范围只是这个native函数的内部。你也可以调用DeleteLocalRef()来显式地将某个本地引用作废,这样可以让垃圾回收时能够将这部分回收。作为参数传递到本地函数中的对象引用是本地引用,所有的从JNI函数返回的java对象(jobject)都是本地引用。
  2. 全局引用会保留直到程序员调用DeleteGlobalRef()手动free掉他们,你可以使用NewGlobalRef()函数从本地引用创建一个全局引用。
    下面我们给出一个例子。
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));
   }
}

上面的 JNI 程序声明了两个 native函数,这两个都创建并且返回java.lang.Integer对象。在 C 代码实现中,我们需要获得 java.lang.Integer的类引用,然后我们从中找到构造器的method ID,然后调用构造器。然而,我们希望,将我们获得的class引用和 Method ID 缓存起来,这样我们下次在使用的时候就不用再次去获取了。
下面是我们的 C 代码,我们希望这样可以 OK(然而事实是不行!!!):

#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);
}

在上面的程序中,我们调用 FindClass() 获得了 java.lang.Integer 类引用,然后把它保存在一个全局静态的变量中。然而,在第二次调用中这个引用却无效了(并不是NULL)。这是因为FindClass()返回的是本地类引用,一旦当getInteger函数返回的时候,这个局部引用就失效了。
为了解决这个问题,我们需要从局部引用中创建一个全局引用,然后再赋值给全局static变量:

// 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);
}

需要注意的是,jmethodIDjfieldID 并不是 jobject,因此他们不能创建一个全局引用!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/28472.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

认识ASP.NET MVC的5种AuthorizationFilter

一、IAuthorizationFilter 所有的AuthorizationFilter实现了接口IAuthorizationFilter。如下面的代码片断所示&#xff0c;IAuthorizationFilter定义了一个OnAuthorization方法用于实现授权的操作。作为该方法的参数filterContext是一个表示授权上下文的AuthorizationContext对…

FasterTransformer 004 open_attention.h forward

initialize forward() https://github1s.com/NVIDIA/FasterTransformer/blob/v1.0/fastertransformer/cuda/open_attention.h#L149-L217 使用cuBLAS库执行矩阵乘法运算&#xff0c;并对cublasGemmEx&#xff08;&#xff09;进行三个单独的调用。这些操作包括将属性核与输入张…

【社区图书馆】《看漫画学Python:有趣、有料、好玩、好用(全彩修订版)》

背景 Python是一门既简单又强大的编程语言&#xff0c;被广泛应用于数据分析、大数据、网络爬虫、自动化运维、科学计算和人工智能等领域。Python也越来越重要&#xff0c;成为国家计算机等级考试科目&#xff0c;某些中小学也开设了Python编程课程。本书秉承有趣、有料、好玩…

SpringCloud服务注册与发现组件Eureka(五)

Eureka github 地址&#xff1a; https://github.com/Netflix/eureka Eureka简介 Eureka是Netflix开发的服务发现框架&#xff0c;本身是一个基于REST的服务&#xff0c;主要用于定位运行在AWS域中的中间层服务&#xff0c;以达到负载均衡和中间层服务故障转移的目的。Spring…

【Android -- JNI 和 NDK】JNI 基础知识以及如何使用

JNI 基础知识 我们来系统梳理一下JNI中涉及的基本知识。 JNI定义了以下数据类型&#xff0c;这些类型和Java中的数据类型是一致的&#xff1a; Java原始类型&#xff1a;jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean这些分别对应这 java 的int, byte, shor…

css 包含块

你不知道的 CSS 之包含块 一说到 CSS 盒模型&#xff0c;这是很多小伙伴耳熟能详的知识&#xff0c;甚至有的小伙伴还能说出 border-box 和 content-box 这两种盒模型的区别。 但是一说到 CSS 包含块&#xff0c;有的小伙伴就懵圈了&#xff0c;什么是包含块&#xff1f;好像…

微服务springcloud 02 创建项目中的三个service子系统,springcloud中注册中心Eureka介绍和把三个系统注册到Eureka中

item service项目 01.使用springboot创建项目 02.选择依懒项在这里插入代码片 spring web 03.添加sp01-commons依赖 在pom.xml文件中 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" x…

【色度学】光学基础

1. 光的本质 &#xff08;1&#xff09;波长不同的可见光&#xff0c;引起人眼的颜色感觉不同。 &#xff08;2&#xff09;人们观察到的颜色是物体和特有色光相结合的结果&#xff0c;而不是物体产生颜色的结果。 2. 光度量 【ISP】光的能量与颜色&#xff08;1&#xff0…

NIO 基础

3. 文件编程 non-blocking io 非阻塞 IO 1.1 Channel & Buffer channel 类似于 stream&#xff0c;它就是读写数据的双向通道&#xff0c;可以从 channel 将数据读入 buffer&#xff0c;也可以将 buffer 的数据写入 channel&#xff0c;而之前的 stream 要么是输入&#…

统信UOS V20 安装mysql5.7.42详细教程

1 安装包准备 到mysql官网可以看到最新的是8.0.33&#xff0c;想下载其他版本的点击 Looking for previous GA versions?Select Operating System: 选择如下版本的mysql 安装包 2 安装 2.1 上传文件至服务器 下载后通过远程将安装包上传至服务器&#xff0c;我这里将安装…

Seesion会话超时时间测试-业务安全测试实操(3)

Seesion会话超时时间测试, Cookie仿冒测试, 密文比对认证测试 本地加密传输测试-业务安全测试实操(2)_luozhonghua2000的博客-CSDN博客 测试原理和方法 在用户成功登录系统获得Session认证会话后,该Session认证会话应具有生命周期,即用户在成功登录系统后,如果在固定时间内…

两个链表相加

描述 假设链表中每一个节点的值都在 0 - 9 之间&#xff0c;那么链表整体就可以代表一个整数。 给定两个这种链表&#xff0c;请生成代表两个整数相加值的结果链表。 数据范围&#xff1a;0≤n,m≤1000000&#xff0c;链表任意值 0≤val≤9 要求&#xff1a;空间复杂度 O(n)…

Triton教程 -- 利用Triton部署你自己的模型

Triton教程—利用Triton部署你自己的模型 给定一个经过训练的模型&#xff0c;我如何使用 Triton 推理服务器以最佳配置大规模部署它&#xff1f; 本文档旨在帮助回答这个问题。 对于那些喜欢高级概述的人&#xff0c;下面是大多数用例的通用流程。 对于那些希望直接进入的人…

Windows Server AD域控服务器升级/迁移(AD域控的五大角色转移)

Windows Server AD域控服务器升级/迁移&#xff08;AD域控的五大角色转移&#xff09; 新域控服务器安装配置域控服务器&#xff0c;加入现有域域控角色迁移到新域控服务器原域控服务器降级退域 本文主要介绍在现有域环境下如何进行域控服务器的迁移/升级操作。对于域结构的网络…

抖音seo矩阵系统源码|需求文档编译说明(一)

抖音seo矩阵系统文章目录技术囊括 ①产品原型 ②需求文档 ③产品流程图 ④部署方式说明 ⑤完整源码 ⑥源码编译方式说明 ⑦三方框架和SDK使用情况说明和代码位置 ⑧平台操作文档 ⑨程序架构文档 短视频矩阵系统源码开发锦囊囊括前言一、短视频账号矩阵系统开发者必备能力语言&…

深度相机介绍

一、什么是深度相机 &#xff08;五&#xff09;深度相机&#xff1a;结构光、TOF、双目相机 - 知乎 传统的RGB彩色普通相机称为2D相机&#xff0c;只能拍摄相机视角内的物体&#xff0c;没有物体到相机的距离信息&#xff0c;只能凭感觉感知物体的远近&#xff0c;没有明确的数…

基于SpringBoot+vue的简历系统设计和实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

seatunnel入门案例,集群模式

目录 安装部署 解压 环境变量 安装plugin 添加资源jar包 SEATUNNEL 配置文件 env&#xff1a;环境设置 source&#xff1a;数据源设置 sink&#xff1a;数据去向设置 transform: 数据转换设置 运行方式 seatunnel 引擎(zeta) 本地模式 集群模式 安装部署 解压 tar…

深入浅出Node.js中的node_modules

文章目录 1. 什么是node_modulesnode_modules是什么npm包管理器和node_modules的关系 2. 如何安装和使用node_modulesnpm安装和使用node_modules的基本命令package.json文件的作用和结构npm包版本号的含义及如何管理包版本 3. 如何发布自己的npm包npm包的结构和规范如何将自己的…

基于SpringBoot+微信小程序的医院预约叫号小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 该项目是基于uniappWe…