JNI - krishnaramb/cplusplus GitHub Wiki

Introduction

  • The very important documentation of java https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

  • The Java Native Interface (JNI) is a Java layer that allows Java code running in the Java Virtual Machine (JVM) to call and be called by native applications and libraries written in other languages, such as C, C++, and assembly.

  • Developers use the JNI to write native methods to handle situations when an application cannot be written entirely in Java, such as when the standard Java class library does not support the platform-dependent features or program library

JNI Basics

  • JNI defines the following JNI types in the native system 👊 🍀 🌺 that correspond to Java types:

    • Java Primitives: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean for Java Primitive of int, byte, short, long, float, double, char and boolean, respectively
    • Java Reference Types:
      • jobject 👊 for java.lang.Object. It also defines the following sub-type
      • jclass for java.lang.Class
      • jstring for java.lang.String.
      • jthrowable for java.lang.Throwable
      • jarray for Java array. Java array is a reference type with eight primitive array and one Object array. Hence, there are eight array of primitives jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray and jbooleanArray; and one object array jobjectArray
  • The native functions receives argument in the above JNI types and returns a value in the JNI type (such as jstring, jintArray) 🌺

  • However, native functions operate on their own native types (such as C-string, C's int[]). Hence, there is a need to convert (or transform) between JNI types and the native types.

  • The native programs:

    • Receive the arguments in JNI type (passed over by the Java program).
    • For reference JNI type, convert or copy the arguments to local native types, e.g., jstring to a C-string, jintArray to C's int[], and so on. Primitive JNI types such as jint and jdouble do not need conversion and can be operated directly.
    • Perform its operations, in local native type.
    • Create the returned object in JNI type, and copy the result into the returned object.
    • Return.
  • The most confusing and challenging task in JNI programming is the conversion (or transformation) between JNI reference types (such as jstring, jobject, jintArray, jobjectArray) and native types (C-string, int[]). The JNI Environment interface provides many functions to do the conversion.

  • JNI is a C interface, which is not object-oriented. It does not really pass the objects.

Creating Objects and Object Arrays

  • You can construct jobject and jobjectArray inside the native code, via NewObject() and newObjectArray() functions, and pass them back to the Java program.

Callback the Constructor to Create a New Java Object in the Native Code

  • Callback the constructor is similar to calling back method. First, get the Method ID of the constructor by passing "" as the method name and "V" as the return-type. You can then use methods like NewObject() to call the constructor to create a new java object.

Local and global references

  • The class references, field IDs, and method IDs are guaranteed valid until the class is unloaded. Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android.

  • Note however that the jclass is a class reference and must be protected with a call to 'NewGlobalRef'

  • Every argument passed to a native method, and almost every object returned by a JNI function is a "local reference". This means that it's valid for the duration of the current native method in the current thread. Even if the object itself continues to live on after the native method returns, the reference is not valid.

  • This applies to all sub-classes of jobject, including jclass, jstring, and jarray. (The runtime will warn you about most reference mis-uses when extended JNI checks are enabled.

  • The only way to get non-local references is via the functions NewGlobalRef and NewWeakGlobalRef.

  • If you want to hold on to a reference for a longer period, you must use a "global" reference. The NewGlobalRef function takes the local reference as an argument and returns a global one. The global reference is guaranteed to be valid until you call DeleteGlobalRef.

  • This pattern is commonly used when caching a jclass returned from FindClass, e.g.:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
  • why do we need to delete the local ref. look here

JavaVM and JNIEnv

  • The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one

  • The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument

  • The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads. 👊 👊

  • If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread's JNIEnv 🙏 . (Assuming it has one; see AttachCurrentThread below.)

threads

  • All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start()), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create() or std::thread can be attached using the AttachCurrentThread() or AttachCurrentThreadAsDaemon() functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls. 👊 👊

  • The JNI interface pointer (JNIEnv) is valid only in the current thread.

  • Should another thread need to access the Java VM, it must first call AttachCurrentThread() to attach itself to the VM and obtain a JNI interface pointer. 👊

  • Once attached to the VM, a native thread works just like an ordinary Java thread running inside a native method.

  • The native thread remains attached to the VM until it calls DetachCurrentThread() to detach itself.

  • The attached thread should have enough stack space to perform a reasonable amount of work. The allocation of stack space per thread is operating system-specific. For example, using pthreads, the stack size can be specified in the pthread_attr_t argument to pthread_create

  • Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" ThreadGroup, 👊 making it visible to the debugger. Calling AttachCurrentThread() on an already-attached thread is a no-op.

  • why do we need to call AttachCurrentThread() is explained HERE

Passing Arguments and Result between Java & Native Programs

  • Passing Primitives
  • Passing Java primitives is straight forward. A jxxx type is defined in the native system, i.e,. jint, jbyte, jshort, jlong, jfloat, jdouble, jchar and jboolean for each of the Java's primitives int, byte, short, long, float, double, char and boolean, respectively.

How the JNI works

  • In the JNI, native functions are implemented in a separate .c or .cpp file. (C++ provides a slightly cleaner interface with the JNI.)

  • When the JVM invokes the function, it passes a JNIEnv pointer, a jobject pointer, and any Java arguments declared by the Java method🍀

  • A JNI function may look like this:

    JNIEXPORT void JNICALL Java_ClassName_MethodName
      (JNIEnv *env, jobjectobj)
    
    {
        //Method native implemenation
    
    }
  • The env pointer is a structure that contains the interface to the JVM. It includes all of the functions necessary to interact with the JVM and to work with Java objects.

  • Example JNI functions are converting native arrays to and from Java arrays, converting native strings to and from Java strings, instantiating objects, throwing exceptions, etc

  • To be more formal, native code accesses JVM features by calling JNI functions, which are available through an interfacepointer (this is a pointer to a pointer) 💘. This pointer points to an array of pointers, each of which points to an interface function 👊

  • Every interface function is at a predefined offset inside the array. Native methods receive the JNI interface pointer as an argument 💓

  • The JVM is guaranteed to pass the same interface pointer to a native method when it makes multiple calls to the native method from the same Java thread

  • However, a native method can be called from different Java threads, and therefore may receive different JNI interface pointers.

  • Native methods are loaded with the System.loadLibrary method

  • In the following example, the class initialization method loads a platform-specific native library in which the native method f is defined

    packagepkg;  
    
    class Cls {
         native double f(inti, String s);
         static {
             System.loadLibrary("pkg_Cls");
         }
    }
  • The argument to System.loadLibraryis a library name chosen arbitrarily by the programmer. The system follows a standard, platform-specific approach to convert the library name to a native library name

    • For example, a Solaris system converts the name pkg_Cls to libpkg_Cls.so, while a Win32 system converts the same pkg_Cls name to pkg_Cls.dll
  • Dynamic linkers resolve entries based on their names. A native method name is concatenated from components, which include: the prefix Java_, a mangled fully-qualified class name, and a mangled method name.

Data type mapping

  • Primitive types, such as integers, characters, and so on, are copied between Java and native code. Arbitrary Java objects, on the other hand, are passed by reference.

  • This table below shows the mapping of types between Java and native code. These types are interchangeable.

  • You can use jint where you normally use an int, and vice-versa without any typecasting required.

  • However, mapping between Java Strings and arrays to native strings and arrays is different. If you use a jstring where a char * would be, your code could crash the JVM.

  • This is an example of how you should work correctly with strings:

JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobjectobj, jstringjavaString)

{
    //Get the native string from Java string
    const char *nativeString = env->GetStringUTFChars(env,javaString, 0);
    printf("%s", nativeString);
    env->ReleaseStringUTFChars(env,javaString, nativeString);

}
  • You always manipulate Java objects using the interface pointer env 👊
⚠️ **GitHub.com Fallback** ⚠️