JNI-201 — Dynamic Registration

Anshul Saraf
5 min readJun 26, 2023

--

Here we pick up from the last post about JNI, if you missed it — have a look. The java layer calls the JNI method with the native keyword. It needs to register the correspondence between the java layer and the native layer.

The JVM has two ways to find and link native methods with Java code. The first one is to call a native function in a specific way so that the JVM can find it (Static Registration). Another way is to use the JNI RegisterNatives() method.

As the name suggests, RegisterNatives() registers native methods with the class passed as an argument. By using this approach, we can name our C++ functions whatever we want, this comes under Dynamic registration.

Static Registration

This registration method is often used by small projects and is relatively common. This is generally used by the application layer, binding package name + class name + method name. When calling the JNI method, the corresponding function is searched through the class loader.

Since we’ve covered in last blog, we know that The disadvantage of static registration is that when the package name, class name or method name is modified, the jni method name of the native layer must also be modified accordingly. This creates unwanted tech-debts 📉📉.

static registration (visualised)

Dynamic Registration

Dynamic registration JNI is a technique that allows native Java code to be dynamically loaded and registered with the Java Virtual Machine (JVM). This can be useful for systems that need to be able to adapt to changing requirements or that need to be able to add new features without disrupting existing users 📈📈.

Init 🎬

This is not-quite-easy to setup first time, but once done, it’s easy to maintain. There are several reasons why you might want to use dynamic registration:

  • Easier to add new features: With dynamic registration JNI, new native Java code can be dynamically loaded and registered with the JVM without having to modify the system’s source code.
  • Easier to update existing features: If a native Java code needs to be updated, it can simply be unregistered from the JVM and then re-registered with the updated version. This can avoid the need to restart the JVM or to recompile the system’s Java code.
  • More flexible: Dynamic registration JNI allows for more flexibility in the way that native Java code is loaded and registered with the JVM. This can be useful for systems that need to be able to dynamically load native Java code from different sources, such as files, JAR files, or network resources.

Registration ⚙️

To register a native method dynamically, you need to call the RegisterNatives() method of the JNIEnv object. The RegisterNatives() method takes two arguments:

— The first argument is the class object for the Java class that contains the native method.

— The second argument is an array of JNINativeMethod structures. Each JNINativeMethod structure contains the following information:

  1. The name of the native method.

2. The signature of the native method.

3. A pointer to the native method implementation.

type signatures
// Inside Constants.h 

// just a random funcition (0)
static jfloat JniVersion(JNIEnv *env, jobject object) {
return 1.4;
}

// just a random funcition (1)
static jstring OpenCVVersion(JNIEnv *env, jobject object){
return env->NewStringUTF(CV_VERSION);
}

// this is the path of java package for the wrapper class.
static const char *constants = "com/projectdelta/chopper/util/Constants";

static JNINativeMethod constants_methods[] = {
// ------- java-function-name --------- jni-signature -------- cpp-function-ptr--
{"nativeGetJniVersion" , "()F" , (void *) JniVersion},
{"nativeGetOpenCVVersion", "()Ljava/lang/String;", (void *) OpenCVVersion}
};

Then we need to create function with same name on java side

class Constants { // package name: com/projectdelta/chopper/util/Constants
companion object {
const val CHOPPER_LIBS = "chopper"

val coreJniVersion: Float
get() = nativeGetJniVersion()

val openCVVersion: String
get() = nativeGetOpenCVVersion()

// function name should match to JNINativeMethod::name one!!
@[Keep, JvmStatic]
private external fun nativeGetJniVersion(): Float

@[Keep, JvmStatic]
private external fun nativeGetOpenCVVersion(): String
}
}

For starters we will call this RegisterNatives method from JNI_OnLoad

// Inside chopper_jni.h class

typedef union {
JNIEnv *env;
void *venv;
} UnionJNIEnvToVoid;

/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods,
int numMethods) {
jclass clazz;
clazz = env->FindClass(className); // here identifies from reflection -> slow :(
if (clazz == NULL) {
ALOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { // main/imp method
ALOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE; // rip!
}
return JNI_TRUE;
}

/*
* Register native methods for all classes we know about.
* returns: JNI_TRUE on success.
*/
static int registerNatives(JNIEnv *env) {
if (!registerNativeMethods(env,
constants , // included from Constants.h
constants_methods, // included from Constants.h
SIZE(constants_methods))) {
return JNI_FALSE;
} // for constants-class
return JNI_TRUE;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved){

UnionJNIEnvToVoid uenv;
uenv.venv = nullptr;
jint result = -1;
JNIEnv* env;

ALOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed");
goto bail;
}
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) {
ALOGE("ERROR: registerNatives failed");
goto bail; // r.i.p!
}

result = JNI_VERSION_1_4;

bail:
return result;
}

ET voila now we are free of shackles of writing long-long method names.

Disadvantages 🤯

Here are some of the disadvantages of using dynamic registration JNI:

  • Security: Dynamic registration JNI can introduce security risks if it is not used properly. For example, if a malicious native Java code is dynamically loaded and registered with the JVM, it could potentially exploit vulnerabilities in the JVM or in the system’s native code.
  • Performance: Dynamic registration JNI can have a negative impact on performance, especially if it is used to load and register a large number of native Java code modules.
  • Complexity: Dynamic registration JNI can be complex to implement and use. This is because it requires the developer to understand the details of how the JVM loads and registers native Java code.
Random meme to keep the mood light XD

Conclusion 🫡

Overall, dynamic registration JNI can be a useful technique for systems that need to be able to dynamically load and register native Java code. However, it is important to weigh the advantages and disadvantages before using it.

Added a sample project for reference

--

--

Anshul Saraf

Why does `greater<T>` in `priority_queue` builds a `min heap`