Interacting with Java Classes and Methods using C++ JNI
The Java Native Interface (JNI) provides a bridge for C++ code to interact with Java components in Android development. This process involves locating the class definition, retrieving method identifiers, instantiating objects if necessary, and finally executing the target logic.
Java Side Definition
Consider a Java class that contains both an instance-level action and a static utility function:
package com.example.utils;
public class LoggerService {
public void logAction(String message) {
System.out.println("Log: " + message);
}
public static int computeHash(int input) {
return input ^ 0x5F3759DF;
}
}
Implementation in C++
To interact with the Java layer, the C++ code must use the JNIEnv pointer to navigate the Java Virtual Machine (JVM).
1. Locating the Java Class
The FindClass function uses the fully qualified name (with slashes instead of dots) to locate the class metadata.
jclass serviceClass = env->FindClass("com/example/utils/LoggerService");
if (serviceClass == nullptr) {
return JNI_ERR;
}
2. Reoslving Method Identifiers
Method IDs are handles used to call specific functions. JNI distinguishes between instance methods and static methods. The method signature must match the Java descriptor exactly.
// Instance method signature: (Ljava/lang/String;)V (takes a String, returns void)
jmethodID logId = env->GetMethodID(serviceClass, "logAction", "(Ljava/lang/String;)V");
// Static method signature: (I)I (takes an int, returns an int)
jmethodID hashId = env->GetStaticMethodID(serviceClass, "computeHash", "(I)I");
3. Object Instantiation
There are two primary ways to create a Java object from C++:
- NewObject: Allocates memory and invokes a constructor immediately.
- AllocObject: Allocates memory without running a constructor (initialization must be handled manually later if needed).
// Retrieve the default constructor ID
jmethodID constructor = env->GetMethodID(serviceClass, "<init>", "()V");
// Option 1: Instantiate via constructor
jobject serviceObj = env->NewObject(serviceClass, constructor);
// Option 2: Allocate memory only
// jobject serviceObj = env->AllocObject(serviceClass);
4. Executing Methods and Exception Handling
When calling methods, ensure that native C++ types are converted to JNI types (e.g., std::string to jstring).
// Invoking the instance method
jstring logMsg = env->NewStringUTF("Native call initiated");
env->CallVoidMethod(serviceObj, logId, logMsg);
// Checking for Java exceptions after the call
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
// Invoking the static method directly via the class reference
jint result = env->CallStaticIntMethod(serviceClass, hashId, 1024);
Integrated Native Function Example
The following logic demonstrates a complete cycle of class resolution and method invocation within a native JNI function.
extern "C" JNIEXPORT jint JNICALL
Java_com_example_app_MainActivity_executeNativeLogic(JNIEnv *env, jobject thiz) {
// 1. Resolve Class
jclass targetClass = env->FindClass("com/example/utils/LoggerService");
if (!targetClass) return -1;
// 2. Resolve Method IDs
jmethodID midLog = env->GetMethodID(targetClass, "logAction", "(Ljava/lang/String;)V");
jmethodID midHash = env->GetStaticMethodID(targetClass, "computeHash", "(I)I");
// 3. Instantiate
jmethodID init = env->GetMethodID(targetClass, "<init>", "()V");
jobject instance = env->NewObject(targetClass, init);
// 4. Perform Invocations
jstring msg = env->NewStringUTF("Operation Successful");
env->CallVoidMethod(instance, midLog, msg);
if (env->ExceptionCheck()) {
env->ExceptionClear();
}
jint hashedValue = env->CallStaticIntMethod(targetClass, midHash, 500);
return hashedValue;
}