August 2023
The JNI is Javas compatibility layer with C. It follows a strict syntax to blindly call the (ABI-compatible) functions. It has however arcane syntax and conventions.
The entry point (at least for React Native modules) is the cpp-adapter
file. This file receives a jobject thiz
reference, which maps to the Java/Kotlin instance/class that has created it. You can use this reference to call the methods to the class/instance.
Calling the methods however is a bit tedious, since you have to first reference the method via signature using a weird string syntax, then convert any type of C++ integral type to the Java equivalent. Perform the conversion, call, handle any exceptions, etc.
Here is one example
void set(const char* key, const char* value, bool withBiometrics)
{
JNIEnv *jniEnv = GetJniEnv();
java_class = jniEnv->GetObjectClass(java_object);
jmethodID mid = jniEnv->GetMethodID(java_class, "setItem", "(Ljava/lang/String;Ljava/lang/String;Z)V");
jstring jKey = string2jstring(jniEnv, key);
jstring jVal = string2jstring(jniEnv, value);
jvalue params[3];
params[0].l = jKey;
params[1].l = jVal;
params[2].z = withBiometrics;
jniEnv->CallVoidMethodA(java_object, mid, params);
jthrowable exObj = jniEnv->ExceptionOccurred();
if(exObj) {
jniEnv->ExceptionClear();
jclass clazz = jniEnv->GetObjectClass(exObj);
jmethodID getMessage = jniEnv->GetMethodID(clazz,
"toString",
"()Ljava/lang/String;");
jstring message = (jstring)jniEnv->CallObjectMethod(exObj, getMessage);
const char *mstr = jniEnv->GetStringUTFChars(message, NULL);
throw std::runtime_error(std::string(mstr));
}
}
Here is a convenience method to convert from a CString to a Java String
jstring string2jstring(JNIEnv *env, const char *str)
{
return (*env).NewStringUTF(str);
}
When declaring a Java JNI function you need to declare the types with single characters that will be mapped to C types. The conversion is as follows:
Z = boolean B = byte C = char I = short J = long F = float D = double
Non scalar types need to be declared by their corresponding package:
// Will be passed to the C side as a JString
Ljava/lang/String;
C++ code needs to be within a extern "C"
to prevent name mangling by the compiler and allow the JNI to call the function blindly.
JNIEXPORT
ensures function is visible on the symbols table
JNICALL
ensures function uses the correct calling convention. On Android JNICALL has a different value based on the architecture where it is running.