This article mainly introduces the data transfer of JNI, that is, how Java transfers objects to C++; and how C++ encapsulates data into objects required by Java.
1. Basic data types
It is very simple and direct to transfer the basic types of java. A type like jxxx has been defined in the local system, for example: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar 和 jboolean
corresponding to int, byte, short, long, float, double, char 和 boolean
the basic types of java respectively.
Java JNI program: 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));
}
}
This JNI program loads myjni.dll(windows)
the library or libmyjni.so(类UNIX)
libraries. And declare a native
method , this method accepts two int
types of parameters, and returns a return value of double
type , this value is int
the average of the two types of numbers. mian
The method calls average
the function .
Next, we compile the above java code into TestJNIPrimitive.class
, and then generate
C/C++ header file TestJNIPrimitive.h:
> javac TestJNIPrimitive.java
> javah TestJNIPrimitive // Output is TestJNIPrimitive.h
C implementation: The TestJNIPrimitive.c
header TestJNIPrimitive.h
file contains a function declaration:
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
It can be seen that here jint
and jdouble
represent respectively int
and double
.
jni.h
( windows
Above win32/jni_mh.h
) The header file contains the definition of these data types, and one more jsize
definition:
// 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;
Interestingly, jint
it corresponds to the C long
type (at least 32bit), not the C int
type (at least 16bit). Therefore, it is important to use jint
instead of in C code . int
At the same time, CygWin
the __int64 type is not supported.
The implementation of TestJNIPrimitive.c is as follows :
#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;
}
Then, we compile the code into a shared library:
// 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
Finally, we run this java code:
java TestJNIPrimitive
C++ implements the TestJNIPrimitive.cpp code as follows:
#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;
}
Use g++ to compile the above code:
// 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. Pass the string
Java JNI program: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);
}
}
The above code declares a native
function sayHello
that accepts a java String
and returns a Java string
, main
the method calls sayHello
the function .
Then, we compile the above code and generate C/C++ header files:
> javac TestJNIString.java
> javah TestJNIString
C code implementation:
The header file above TestJNIString.c TestJNIString.h
declares such a function:
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);
JNI defines jstring
types that correspond to java's String
types. The last parameter in the above statement jstring
is String
the parameter from the Java code, and the return value is also a jstring
type .
Passing a string is more complicated than passing a primitive type, because java's String
is an object, while C's string
is an array NULL
at the end . char
Therefore, we need to convert the Java String
object into a C string representation: char *
.
As we mentioned earlier, the JNI environment pointer JNIEnv *
has defined a very rich interface function for us to handle data conversion:
- Call
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
to convert JNI'sjstring
to C'schar *
- Call
jstring NewStringUTF(JNIEnv*, char*)
to convert C'schar *
to JNI'sjstring
Therefore, the basic process of our C program is as follows:
- Use
GetStringUTFChars()
the function tojstring
convert tochar *
- Then perform the required data processing
- Use
NewStringUTF()
the function tochar *
convert tojstring
, and return
#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);
}
Compile the above code into a shared library:
// MinGW GCC under Windows
> gcc -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.c
Finally, run the code:
> java TestJNIString
In C, the received string is: Hello from Java
Enter a String: test
In Java, the returned string is: test
The string conversion function in JNI
We have shown two functions above, now we will comprehensively sort out the functions provided by JNI. JNI supports Unicode (16bit characters) and UTF-8 (using 1~3 byte encoding) conversion. Generally speaking, we should use UTF-8 encoding in C/C++.
The JNI system provides the following functions for string processing (a total of two groups, UTF8 and 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()
The function can be jstring
converted to char *
, and this function will return NULL
if the content allocation of the system fails. Therefore, it is good practice to check whether the return of this function is correct NULL
. The third parameter is isCopy
that this parameter is a in-out
parameter, and what is passed in is a pointer, and the content of the pointer will be modified when the function ends. If the content is JNI_TRUE
yes, then it means that the returned data is jstring
a copy of the data, otherwise, if JNI_FALSE
yes, it means that the returned string is directly pointing to that String
object instance. In this case, the native code should not arbitrarily modify string
the content in the code, because the modification will modify the code in Java. The JNI system will try to ensure that the return is a direct reference, if not, then return a copy. Usually, we rarely care about modifying these , so we generally pass to parameters string
here .NULL
isCopy
It must be noted that when you no longer need the GetStringUTFChars
returned string, you must remember to call ReleaseStringUTFChars()
the function to release the memory resource! Otherwise there will be a memory leak! And the upper level java GC
can't be done either!
In addition, in GetStringUTFChars
and ReleaseStringUTFChars
cannot block!
NewStringUTF()
Functions are char *
available from strings jstring
.
C++ implementation: 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());
}
Compile the above code with 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
It should be noted that in C++, string
the function call syntax of local classes is different. In C++, we use env->
to call instead of (env*)->
. At the same time, this parameter is not needed in the C++ function JNIEnv*
.
3. Pass an array of primitive types
JNI code: 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 language implementation: TestJNIPrimitiveArray.c
header file TestJNIPrimitiveArray.h
contains the following function declarations:
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_average (JNIEnv *, jobject, jintArray);
In Java, array
refers to a type, similar to a class. There are 9 types of java array
, 8 basic types of arrays and an array of objects. JNI defines corresponding arrays for the basic types of java: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
, and there are also object-oriented ones jobjectArray
.
Similarly, you need to convert between JNI array and Native array. The JNI system has provided us with a series of interface functions:
- Use
jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)
willjintarray
convert to C'sjint[]
- Use
jintArray NewIntArray(JNIEnv *env, jsize len)
the function to allocate alen
byte-sized space, and then usevoid SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)
the functionjint[]
to copy the data in it tojintArray
it.
There are a total of 8 pairs of functions similar to the above, corresponding to the 8 basic data types of java.
Therefore, native programs need to:
- Accept JNI array from java and convert to native array
- Perform required data manipulation
- Convert the data that needs to be returned into
jni
an array, and then return it.
The following is the implementation of the C codeTestJNIPrimitiveArray.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;
}
The array function of the JNI basic type The array( ) function
of the JNI basic type is as follows: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);
Similarly get
, release
there cannot be an always block between the function and the function.
4. Access Java object variables and callback Java methods
Access the variable
JNI program of the Java object instance: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);
}
}
This class contains two private
instance variables, an int and a String object. Then we call the local function in main modifyInstanceVariable
to modify these two variables.
C code implementation: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);
}
In order to access variables in an object, we need to:
- Call
GetObjectClass()
to obtain the class reference of the target object - To access a variable obtained from the class reference obtained above
Field ID
, you need to provide the name of the variable, and the variable's descriptor (also known as the signature). For the java class, the descriptor is in this form: "Lfully-qualified-name;" (note that there is an English half-width semicolon at the end), where the package name dot is replaced with a slash (/), such as java's Stirng The class descriptor is "Ljava/lang/String;". For basic types, I stands for int, B stands for byte, S stands for short, J stands for long, F stands for float, D stands for double, C stands for char, and Z stands for boolean. For array, use left square brackets "[" to indicate, such as "[Ljava/lang/Object;" means Object array, "[I" means int type array. - Based on the above obtained
Field ID
, useGetObjectField()
orGet_primitive-type_Field()
function to parse out the data we want - Use
SetObjectField()
the orSet_primitive-type_Field()
function to modify variables
The functions used to access instance variables in JNI are:
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.
Accessing static variables in a class
Accessing static variables in a class is similar to accessing ordinary instance variables above, except that the functions we use here are GetStaticFieldID()
, Get|SetStaticObjectField()
, Get|SetStatic_Primitive-type_Field()
.
JNI program: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 language implementation: 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);
}
static
The functions used to access variables in the class in JNI are as follows:
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.
Calling back ordinary and static methods of instances
You can native
call back ordinary or static methods in java in your code. The following is an example:
JNI program: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();
}
}
A native function is declared in this class nativeMethod()
, and this function is called in the main method. nativeMethod()
This function calls back various methods defined in this class.
C language implementation: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);
}
To be able to call back methods on an instance, we need:
GetObjectClass()
Get the class object of this instance through the function- From the class object obtained above, call
GetMethodID()
the function to obtain the Method ID, which represents the abstraction of a method in the instance. You need to provide the name and signature information of this method. The signature rules are similar to variables. The format of the signature is this:(parameters)return-type
. If we really feel that the signature of jni is not easy to remember, we can use the tool javap provided by JDK to obtain the signatures of all methods in a certain class, use the -s option to print the signature, and -p to display private members :
> 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;
.......
From the above output we can clearly see the signature of each method in the class.
- Based on the Method ID we obtained above, we can call
_Primitive-type_Method()
orCallVoidMethod()
orCallObjectMethod()
to call this method. If a method requires parameters, just follow the parameters. - If you want to call a static method, use
GetMethodID(), CallStatic_Primitive-type_Method(), CallStaticVoidMethod()
orCallStaticObjectMethod()
.
All functions used to call back instance and static methods in JNI (two types, common and 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.
The parent class instance method overridden by the callback
JNI provides a series of functions CallNonvirtual_Type_Method()
such to call the method of the parent class instance:
- First get the Method ID, use
GetMethodID()
- Based on the Method ID obtained above, the corresponding method is invoked by calling
CallNonvirtual_Type_Method()
the function , and given in parametersobject
, parent class and parameter list.
Functions used in JNI to access parent class methods:
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);
To create Object and Object arrays you can construct and
in native code , by calling and functions , and then tell them to return to java code.jobject
jobjectarray
NewObject()
newObjectArray()
Calling back a Java constructor to create a new java object
Calling back a constructor is similar to calling back other methods. First, init
get the Method ID by using it as the method name and V as the return value, and then NewObject()
build a java class object through the function.
JNI program: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));
}
}
This class declares a getIntegerObject
native method, which accepts an int data, and then creates an object of type Integer in the native code, and its value is this value.
C code implementation: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;
}
The functions used to create objects (jobject) in JNI are:
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 (object) array
JNI program: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]);
}
}
For simplicity, this class declares a native method, which accepts an array of Integer type, and then calculates the sum and average of the numbers in the array in the native code, and then returns the two numbers in the form of Double array
.
C code implementation: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;
}
Unlike an array of primitive data types, you need to use Get|SetObjectArrayElement()
a function to process each element.
JNI provides array(jobjectArray)
the following functions to create objects:
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. Local and global references
Managing references is key to writing efficient programs. FindClass(), GetMethodID()
For example, we will often use a jclass, jmethodID and jfieldID in the local code GetFieldID()
. In fact, these variables should only be obtained for the first time, and then used directly, instead of obtaining them every time, which can improve the efficiency of program execution.
JNI divides object references in native code into two types: local and global references:
- Native references are created in native code and are freed when the function exits or returns. Its valid scope is only inside the native function. You can also call
DeleteLocalRef()
to explicitly invalidate a local reference so that it can be reclaimed during garbage collection. Object references passed as parameters to local functions are local references, and all java objects (jobjects) returned from JNI functions are local references. - Global references will remain until the programmer calls
DeleteGlobalRef()
free to manually free them, you can useNewGlobalRef()
the function to create a global reference from a local reference.
Below we give an example.
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));
}
}
The JNI program above declares two native
functions , both of which create and return java.lang.Integer
objects. In the C code implementation, we need to java.lang.Integer
get the class reference, and then we find the constructor from it method ID
, and then call the constructor. However, we hope to cacheclass
the references we get so that we don't have to get them again the next time we use them. Here's our C code, which we hope will work (but it doesn't!!!):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);
}
In the above program, we call to FindClass()
obtain java.lang.Integer
the class reference, and then save it in a global static variable. However, on the second call the reference is invalidated (it's not NULL
). This is because FindClass()
the local class reference is returned, and once getInteger
the function returns, the local reference becomes invalid.
To fix this, we need to create a global reference from the local reference and then assign it to the global static
variable:
// 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);
}
Note that jmethodID
and jfieldID
are not jobject
, so they cannot create a global reference! !