티스토리 뷰

Developer/Java, Kotlin

[JNI] 예제

데브포유 2015. 1. 20. 11:37
반응형

JNI Overview

In this module, we’ll explore the following topics:

  • JNI Components

  • JNI By Example

  • Native Method Arguments

  • Primitive Type Mapping

  • Reference Type Mapping

  • Global and Local References

  • Using Strings

  • Arrays

  • Reflection

  • Registering Native Methods On Load

  • Exceptions

JNI: What it is and why you’d care

JNI is an interface that allows Java to interact with code written in another language

Motivation for JNI:

  • Access low-level OS-specific features (e.g. ioctl, poll, etc.)
  • Reuse existing/legacy code written in C/C++ from Java
  • Improve the performance by writing CPU-bound code natively - but modern just-in-time (JIT) compilers make this a moot point
NoteJNI code is not portable!
Note

JNI can also be used to invoke Java code from within natively-written applications - such as those written in C/C++.

In fact, the java command-line utility is an example of one such application, that launches Java code in a Java Virtual Machine.

JNI Components

  • javah - JDK tool that builds C-style header files from a given Java class that includes native methods

    • Adapts Java method signatures to native function prototypes

  • jni.h - C/C+ header file included with the JDK that maps Java types to their native counterparts

    • javah automatically includes this file in the application header files

JNI By Example

  1. We start by creating a Java class with one or more native methods

    src/com/marakana/jniexamples/Hello.java:
    package com.marakana.jniexamples;
    
    public class Hello {
    
        public static native void sayHi(String who, int times); // 1
    
        static {
            System.loadLibrary("hello"); // 2
        }
    
        public static void main(String[] args) {
            sayHi(args[0], Integer.parseInt(args[1])); // 3
        }
    }
    1The method sayHi(String, int) will be implemented in C/C+ in separate files, which will be compiled into a shared library.
    2Load the shared library by its logical name. The actual name is system-dependent: libhello.so (on Linux/Unix), hello.dll (on Windows), and libhello.jnilib (Mac OSX).
    3Here we call our native method as a regular Java method.
  2. Compile the Java code

    $ mkdir -p bin
    $ javac -d bin/ src/com/marakana/jniexamples/Hello.java
  3. Using the javah tool, we generate the C header file from the compiledcom.marakana.jniexamples.Hello class:

    $ mkdir -p jni
    $ javah -jni -classpath bin -d jni com.marakana.jniexamples.Hello
  4. Observe the generated C header file:

    jni/com_marakana_jniexamples_Hello.h:
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_marakana_jniexamples_Hello */
    
    #ifndef _Included_com_marakana_jniexamples_Hello
    #define _Included_com_marakana_jniexamples_Hello
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_marakana_jniexamples_Hello
     * Method:    sayHi
     * Signature: (Ljava/lang/String;I)V
     */
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi
      (JNIEnv *, jclass, jstring, jint);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    NoteMethod names resolve to C functions based on a pre-defined naming strategy: the prefix Java_, followed by a mangled fully-qualified class name, followed by an underscore ("_") separator, followed by a mangled method name. For overloaded native methods, two underscores ("__") followed by the mangled argument signature.
  5. Provide the C implementation:

    jni/com_marakana_jniexamples_Hello.c:
    #include "com_marakana_jniexamples_Hello.h"
    
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi
      (JNIEnv *env, jclass clazz, jstring who, jint times) {
      const char *name = (*env)->GetStringUTFChars(env, who, NULL);
      if (name != NULL) {
        jint i;
        for (i = 0; i < times; i++) {
          printf("Hello %s\n", name);
        }
        (*env)->ReleaseStringUTFChars(env, who, name);
      }
    }
    NoteMost of the time, we cannot just use Java data types directly in C. For example, we have to convertjava.lang.String to char * before we can effectively use it in C.
    NoteThis code assumes: #define NULL ((void *) 0)
  6. Compile the shared library

    $ mkdir -p libs
    $ gcc -o libs/libhello.jnilib -lc -shared \
        -I/System/Library/Frameworks/JavaVM.framework/Headers \
        jni/com_marakana_jniexamples_Hello.c
    $ file libs/libhello.jnilib
    libs/libhello.jnilib: Mach-O 64-bit dynamically linked shared library x86_64
    NoteOn Unix/Linux, compile as:
    gcc -o libs/libhello.so -lc -shared -fPIC -I$JAVA_HOME/include jni/com_marakana_jniexamples_Hello.c
  7. Run our code

    $ java -Djava.library.path=libs -classpath bin com.marakana.jniexamples.Hello Student 5
    Hello Student
    Hello Student
    Hello Student
    Hello Student
    Hello Student
    NoteInstead of specifying -Djava.library.path=libs, we could have preceded our javacommand with export LD_LIBRARY_PATH=libs.
    TipCommon mistakes resulting in java.lang.UnsatisfiedLinkError usually come from incorrect naming of the shared library (O/S-dependent), the library not being in the search path, or wrong library being loaded by Java code.

Native Method Arguments

  • A C/C++ implementation of a native Java method accepts the following function arguments:

    • JNIEnv *env, an interface pointer (pointer to a pointer) to a function table, where each entry in the table is a pointer to a function that enables Java-to-C/C++ integration (e.g. type conversion)

    • The second argument varies depending on whether the native method is a static method or an instance (i.e. non-static) method:

      • In case of instance methods, the second argument is a jobject obj, which is a reference to the object on which the method is invoked

      • In case of static methods, the second is a jclass clazz, which is a reference to the class in which the method is defined

    • The remaining arguments correspond to regular Java method arguments (subject to the mapping described below)

  • The native function can pass its result back to Java via the return value (or return void)

Primitive Type Mapping

  • Java primitives have well-known size and are signed by default

  • C/C++ primitives vary in size, depending on the platform (e.g. the size of the long depends on the size of the word)

  • To provide inter-operability with C/C++, jni.h defines the following mappings:

    Java Language TypeNative TypeDescriptiontypedef (C99)typedef(otherwise)

    boolean

    jboolean

    unsigned 8 bits

    uint8_t

    unsigned char

    byte

    jbyte

    signed 8 bits

    int8_t

    signed char

    char

    jchar

    unsigned 16 bits

    uint16_t

    unsigned short

    short

    jshort

    signed 16 bits

    int16_t

    short

    int

    jint

    signed 32 bits

    int32_t

    int

    long

    jlong

    signed 64 bits

    int64_t

    long long

    float

    jfloat

    32-bit IEEE 754

    float

    float

    double

    jdouble

    64-bit IEEE 754

    double

    double

    void

    void

    N/A

    N/A

    N/A

    N/A

    jsize

    used to describe cardinal indices and sizes

    jint

    jint

  • Because booleans are treated as unsigned bytes, the following definitions are also useful

    Java Boolean ValueNative Boolean TypeDefinition

    false

    JNI_FALSE

    #define JNI_FALSE 0

    true

    JNI_TRUE

    #define JNI_TRUE 1

  • Primitive data types are always copied between Java and native code

Reference Type Mapping

  • The JNI includes a number of pre-defined reference types (passed as opaque references in C) that correspond to different kinds of Java object types:

    Java Language TypeNative Typetypedef in C

    java.lang.Object

    jobject

    void*

    java.lang.Class

    jclass

    jobject

    java.lang.Throwable

    jthrowable

    jobject

    java.lang.String

    jstring

    jobject

    java.lang.ref.WeakReference

    jweak

    jobject

    N/A

    jarray

    jobject

    java.lang.Object[]

    jobjectArray

    jarray

    boolean[]

    jbooleanArray

    jarray

    byte[]

    jbyteArray

    jarray

    char[]

    jcharArray

    jarray

    short[]

    jshortArray

    jarray

    int[]

    jintArray

    jarray

    long[]

    jlongArray

    jarray

    float[]

    jfloatArray

    jarray

    double[]

    jdoubleArray

    jarray

    Note
    A note about reference types in C++

    These reference types in C++ are defined as proper classes:

    class _jobject {};
    class _jclass : public _jobject {};
    class _jstring : public _jobject {};
    class _jarray : public _jobject {};
    class _jobjectArray : public _jarray {};
    class _jbooleanArray : public _jarray {};
    class _jbyteArray : public _jarray {};
    class _jcharArray : public _jarray {};
    class _jshortArray : public _jarray {};
    class _jintArray : public _jarray {};
    class _jlongArray : public _jarray {};
    class _jfloatArray : public _jarray {};
    class _jdoubleArray : public _jarray {};
    class _jthrowable : public _jobject {};
    
    typedef _jobject*       jobject;
    typedef _jclass*        jclass;
    typedef _jstring*       jstring;
    typedef _jarray*        jarray;
    typedef _jobjectArray*  jobjectArray;
    typedef _jbooleanArray* jbooleanArray;
    typedef _jbyteArray*    jbyteArray;
    typedef _jcharArray*    jcharArray;
    typedef _jshortArray*   jshortArray;
    typedef _jintArray*     jintArray;
    typedef _jlongArray*    jlongArray;
    typedef _jfloatArray*   jfloatArray;
    typedef _jdoubleArray*  jdoubleArray;
    typedef _jthrowable*    jthrowable;
    typedef _jobject*       jweak;
    Tip
    A tip about NULL in Android NDK

    Android’s native development kit (NDK) does not define NULL in its jni.h, so the following definition can be useful when working with pointers and reference types:

    #define NULL ((void *) 0)
    WarningOpaque references are C pointer types that refer to internal data structures in the JVM. It is an error to try to dereference opaque references and try to use them directly.

Global and Local References

  • Unlike primitives, arbitrary Java objects are always passed by reference

  • The VM must keep track of all objects that have been passed to the native code, so that these objects are not freed by the GC (i.e. these references are considered object roots while in use)

    • Consequently, the native code must have a way to tell the VM that it no longer needs references to objects

    • Additionally, the GC must be able to relocate an object referred to by native code (in the case of a copying GC), which means that these references must not be dereferenced by the native code

    • We will access the fields/methods on these references via JNI accessor functions

  • All objects passed to native code and returned from JNI functions are considered local references

    • Automatically "freed" after the native function call returns

      • The VM creates a table of references before every function call, and frees it when the call completes

      • Consequently, the native function calls are more expensive than regular method calls

    • Also possible to explicitly free using:

      void DeleteLocalRef(JNIEnv *env, jobject localRef);
      NoteIn practice, we only really want to use DeleteLocalRef(JNIEnv *, jobject) if we know we are not going to need the reference in the rest of the function body, say before we start executing code that may take a long time. This allows the referenced memory to be freed by GC, assuming nobody else is using it.
    • Only valid in the thread in which they are created

    • If we end up using more than 16 local references (default) within a native function scope, we can ask the VM for more:

      jint EnsureLocalCapacity(JNIEnv *env, jint capacity);
      jint PushLocalFrame(JNIEnv *env, jint capacity);
      jobject PopLocalFrame(JNIEnv *env, jobject result);
  • To hold on to an object beyond a single native function call, we can convert a local reference to a global reference

    jobject NewGlobalRef(JNIEnv *env, jobject obj);
    NoteNow we could save the result of NewGlobalRef(JNIEnv *, jobect) to a global/static variable and use it later, in another funciton call.
    • Becomes our responsibility to explicitly delete the reference when no longer in use:

      void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
    • Also supported are the weak global references

      jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
      void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
  • JNI accessor functions do not make a distinction between the different reference types

Using Strings

  • In C, a string is a pointer to a \0-terminated character array where each character is one byte

  • In Java, an instance of java.lang.String is an immutable object which wraps a character array, which itself is an object (i.e. it knows its length so it is not \0-terminated), and each character is represented in UTF16 as two bytes

  • String Operations are provided by JNIEnv:

    /* On Unicode (UTF-16) Characters */
    jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
    jsize GetStringLength(JNIEnv *env, jstring string);
    const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
    void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
    void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
    
    /* On (modified) UTF-8 Characters */
    jstring NewStringUTF(JNIEnv *env, const char *bytes);
    jsize GetStringUTFLength(JNIEnv *env, jstring string);
    const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
    void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
    void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
    Note
    A note about GetString[UTF]Chars(…) functions

    The pointer resulting from GetString[UTF]Chars(…) is valid until ReleaseString[UTF]Chars(…) is called.

    If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned string is a direct pointer to the characters in the original java.lang.String instance, which is then pinned in memory. The native code must ensure not to modify the contents of the returned string, otherwise, it would be modifying the private data of the immutable java.lang.String object!

    Regardless of isCopy, we have to call ReleaseString[UTF]Chars(…) when we are done using the character array, either to free the memory (when *isCopy == JNI_TRUE) or to un-pin the original string in memory (when*isCopy == JNI_FALSE).

    Note
    A note about modified UTF-8 strings:

    JNI’s UTF string functions (that work with char *) return/assume \0-terminated character arrays that are encoded as UTF-8 character sequences, except that if the string contains a \u0000 character, it is represented by a pair of bytes 0xc0 0x80 (1100000010000000) instead of 0x00. When working with regular ASCII characters, each character is represented by exactly one byte.

  • Examples:

    • What NOT to do:

      #include <stdio.h>
      #include "com_marakana_jniexamples_HelloName.h"
      
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) {
          printf("Hello %s", name); /* 1  */
      }
      1This example would not work (would likely crash the VM) since the jstring type represents strings in the Java virtual machine. This is different from the C string type (char *).
    • Convert Java string to C string:

      #include <stdio.h>
      #include "com_marakana_jniexamples_HelloName.h"
      
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name){
          const char *cName = (*env)->GetStringUTFChars(env, name, NULL); /* 1 */
          if (cName == NULL) {
              return; /* OutOfMemoryError already thrown */
          } else {
              printf("Hello %s\n", cName);
              (*env)->ReleaseStringUTFChars(env, name, cName); /* 2 */
          }
      }
      1This returns a pointer to an array of bytes representing the string in modified UTF-8 encoding; or NULL if we ran out of memory, in which case java.lang.OutOfMemoryError would also be thrown (upon returning back to the Java runtime).
      2Calling ReleaseStringUTFChars indicates that the native method no longer needs the UTF-8 string returned by GetStringUTFChars, thus the memory taken by the UTF-8 string can be freed. Failure to do this would result in a memory leak, which could ultimately lead to memory exhaustion.
    • Convert C string to Java string:

      #include <stdio.h>
      #include "com_marakana_jniexamples_GetName.h"
      
      JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) {
          char buf[20];
          fgets(buf, sizeof(buf), stdin);
          return (*env)->NewStringUTF(env, buf);
      }
    • Print each character of a Java string

      #include <stdio.h>
      #include "com_marakana_jniexamples_HelloName.h"
      
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) {
          char buf[4];
          jint i;
          jsize len = (*env)->GetStringUTFLength(env, name);
          fputs("Hello ", stdout);
          for (i = 0; i < len; i++) {
             (*env)->GetStringUTFRegion(env, name, i, 1, buf);
             putc(buf[0], stdout); /* assumes ASCII */
          }
          putc('\n', stdout);
      }

Arrays

  • As with Strings, Java arrays are different than C arrays; the former are objects (which know their length) whereas the latter are just pointers to memory addresses

    • So, int[] in Java is not the same as jint[] in C/C++

    • Instead, int[] in Java, becomes jintArray in C/C++

  • JNI provides functions for

    • Creating arrays:

      jobjectArray  NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
      jbooleanArray NewBooleanArray(JNIEnv *env, jsize length);
      jbyteArray    NewByteArray(JNIEnv *env, jsize length);
      jcharArray    NewCharArray(JNIEnv *env, jsize length);
      jshortArray   NewShortArray(JNIEnv *env, jsize length);
      jintArray     NewIntArray(JNIEnv *env, jsize length);
      jlongArray    NewLongArray(JNIEnv *env, jsize length);
      jfloatArray   NewFloatArray(JNIEnv *env, jsize length);
      jdoubleArray  NewDoubleArray(JNIEnv *env, jsize length);
    • Getting the length of an array

      jsize GetArrayLength(JNIEnv *env, jarray array);
    • Getting/Setting individual elements of object arrays

      jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
      void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
    • Converting Java arrays of primitives to C/C++ arrays:

      jboolean* GetBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *isCopy);
      jbyte*    GetByteArrayElements(JNIEnv *env, jbyteArray array, jboolean *isCopy);
      jchar*    GetCharArrayElements(JNIEnv *env, jcharArray array, jboolean *isCopy);
      jshort*   GetShortArrayElements(JNIEnv *env, jshortArray array, jboolean *isCopy);
      jint*     GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
      jlong*    GetLongArrayElements(JNIEnv *env, jlongArray array, jboolean *isCopy);
      jfloat*   GetFloatArrayElements(JNIEnv *env, jfloatArray array, jboolean *isCopy);
      jdouble*  GetDoubleArrayElements(JNIEnv *env, jdoubleArray array, jboolean *isCopy);
      
      void      ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode);
      void      ReleaseByteArrayElements(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode);
      void      ReleaseCharArrayElements(JNIEnv *env, jcharArray array, jchar *elems, jint mode);
      void      ReleaseShortArrayElements(JNIEnv *env, jshortArray array, jshort *elems, jint mode);
      void      ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode);
      void      ReleaseLongArrayElements(JNIEnv *env, jlongArray array, jlong *elems, jint mode);
      void      ReleaseFloatArrayElements(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode);
      void      ReleaseDoubleArrayElements(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode);
  • Copying Java primitive arrays to and from C/C++ arrays:

    void GetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf);
    void GetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf);
    void GetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf);
    void GetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf);
    void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf);
    void GetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf);
    void GetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf);
    void GetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf);
    
    void SetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf);
    void SetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf);
    void SetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf);
    void SetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf);
    void SetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf);
    void SetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf);
    void SetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf);
    void SetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf);
  • For example, to sum the contents of a Java int array in C, we could:

    JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) {
        jint *cArray = (*env)->GetIntArrayElements(env, array, NULL);
        if (cArray == NULL) {
            return 0;
        } else {
            jini len = (*env)->GetArrayLength(env, array);
            jint i;
            jint result = 0;
            for (i = 0; i < len; i++) {
                result += cArray[i];
            }
            (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT);
            return result;
        }
    }
  • Alternatively, we could also extract the individual elements:

    JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) {
        jint buf[1];
        jini len = (*env)->GetArrayLength(env, array);
        jint i;
        jint result = 0;
        for (i = 0; i < len; i++) {
            (*env)->GetIntArrayRegion(env, array, i, 1, buf);
            result += buf[0];
        }
        return result;
    }
  • To do the opposite, copy a C array to Java array, we would do:

    JNIEXPORT jintArray JNICALL Java_com_marakana_jniexamples_Foo_getData(JNIEnv *env, jclass class) {
        jint cArray[10];
        jsize len = sizeof(cArray);
        jintArray jArray = (*env)->NewIntArray(env, len);
        if (jArray != NULL) {
            jint i;
            /* "get" the data */
            for (i = 0; i < len; i++) {
                cArray[i] = i;
            }
            (*env)->SetIntArrayRegion(env, jArray, 0, len, cArray);
        }
        return jArray;
    }
  • To avoid any chance of memory copying, we could also use direct memory buffers (java.nio.ByteBuffer):

    jobject NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity);
    void* GetDirectBufferAddress(JNIEnv* env, jobject buf);
    jlong GetDirectBufferCapacity(JNIEnv* env, jobject buf);
    • For example

      In Java
      public class Foo {
          public static void main(String[] args) {ByteBuffer buf = ByteBuffer.allocateDirect(1024);
              // populate buf
              processData(buf);}
      
          public native static void processData(ByteBuffer buf);
      }
      In C
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processData(JNIEnv *env, jclass clazz, jobject buf) {
          char *cBuf = (*env)->GetDirectBufferAddress(env, buf);
          /* process cBuf from 0 to (*env)->GetDirectBufferCapacity(env, buf) */
      }
Note
A note about memory

The pointer resulting from GetTypeArrayElements(…) is valid untilReleaseTypeArrayElements(…) is called (unless mode == JNI_COMMIT).

If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned array is a direct pointer to the elements of the Java array, which is then pinned in memory.

Regardless of isCopy, we have to call ReleaseTypeArrayElements(…, int mode) when we are done using the native array, either to un-pin the Java array in memory when *isCopy == JNI_FALSE, or, when *isCopy == JNI_TRUE, to:

  • copy the native array over the Java array and free it (when mode == 0)

  • copy the native array over the Java array but not free it (when mode == JNI_COMMIT)
    This option assumes that we will call ReleaseTypeArrayElements(…, JNI_ABORT) at some later point

  • leave the Java array intact and free the native array (when mode == JNI_ABORT)

JNI also supports a critical version of these functions:

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

The function GetPrimitiveArrayCritical(…) is similar to GetTypeArrayElements(…), except that the VM is more likely to return the pointer to the primitive array (i.e. *isCopy == JNI_FALSE is more likely). However, this comes with some caveats. The native code betweenGetPrimitiveArrayCritical(…) and ReleasePrimitiveArrayCritical(…) must not call other JNI functions, or make any system calls that may cause the current thread to block and wait for another Java thread. This is because the VM may temporarily suspend the garbage collection - in case it does not support memory pinning.

Reflection

  • JNI supports getting a reference to a Java class in native code:

    jclass FindClass(JNIEnv *env, const char *name);
    • The name argument is a fully-qualified classname (with / as the package separators)

      /* load the java.lang.String class */
      (*env)->FindClass(env, "java/lang/String");
    • For arrays, we use [Lclassname; format

      /* load the java.lang.String[] class */
      (*env)->FindClass(env, "[Ljava/lang/String;");
  • Given a reference to a class, we can find out its super class, as well as whether it is assignable from another class:

    jclass GetSuperclass(JNIEnv *env, jclass clazz);
    jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1, jclass clazz2);
  • Given a reference to an object, we can find out its class, as well as whether it is an instance of another class:

    jclass GetObjectClass(JNIEnv *env, jobject obj);
    jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
  • Once we have a class, JNI offers functions to lookup, get/set fields and invoke methods of that class - based on the following field and method signatures:

    Type SignatureJava Type

    Z

    boolean

    B

    byte

    C

    char

    S

    short

    I

    int

    J

    long

    F

    float

    D

    double

    Lfully-qualified-class;

    fully-qualified-class

    [type

    type[]

    (arg-types)ret-type

    method type

  • For fields, JNI offers functions to:

    • Lookup a field ID based on its name/signature:

      jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
      jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    • Get/set the value of a field based on the field ID:

      jobject     (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
      jboolean    (*GetStaticBooleanField)(JNIEnv*, jclass, jfieldID);
      jbyte       (*GetStaticByteField)(JNIEnv*, jclass, jfieldID);
      jchar       (*GetStaticCharField)(JNIEnv*, jclass, jfieldID);
      jshort      (*GetStaticShortField)(JNIEnv*, jclass, jfieldID);
      jint        (*GetStaticIntField)(JNIEnv*, jclass, jfieldID);
      jlong       (*GetStaticLongField)(JNIEnv*, jclass, jfieldID);
      jfloat      (*GetStaticFloatField)(JNIEnv*, jclass, jfieldID);
      jdouble     (*GetStaticDoubleField)(JNIEnv*, jclass, jfieldID);
      
      void        (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject);
      void        (*SetStaticBooleanField)(JNIEnv*, jclass, jfieldID, jboolean);
      void        (*SetStaticByteField)(JNIEnv*, jclass, jfieldID, jbyte);
      void        (*SetStaticCharField)(JNIEnv*, jclass, jfieldID, jchar);
      void        (*SetStaticShortField)(JNIEnv*, jclass, jfieldID, jshort);
      void        (*SetStaticIntField)(JNIEnv*, jclass, jfieldID, jint);
      void        (*SetStaticLongField)(JNIEnv*, jclass, jfieldID, jlong);
      void        (*SetStaticFloatField)(JNIEnv*, jclass, jfieldID, jfloat);
      void        (*SetStaticDoubleField)(JNIEnv*, jclass, jfieldID, jdouble);
      
      jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);
      jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
      jbyte       (*GetByteField)(JNIEnv*, jobject, jfieldID);
      jchar       (*GetCharField)(JNIEnv*, jobject, jfieldID);
      jshort      (*GetShortField)(JNIEnv*, jobject, jfieldID);
      jint        (*GetIntField)(JNIEnv*, jobject, jfieldID);
      jlong       (*GetLongField)(JNIEnv*, jobject, jfieldID);
      jfloat      (*GetFloatField)(JNIEnv*, jobject, jfieldID);
      jdouble     (*GetDoubleField)(JNIEnv*, jobject, jfieldID);
      
      void        (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
      void        (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean);
      void        (*SetByteField)(JNIEnv*, jobject, jfieldID, jbyte);
      void        (*SetCharField)(JNIEnv*, jobject, jfieldID, jchar);
      void        (*SetShortField)(JNIEnv*, jobject, jfieldID, jshort);
      void        (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);
      void        (*SetLongField)(JNIEnv*, jobject, jfieldID, jlong);
      void        (*SetFloatField)(JNIEnv*, jobject, jfieldID, jfloat);
      void        (*SetDoubleField)(JNIEnv*, jobject, jfieldID, jdouble);
    • For example:

      In Java
      public class Foo {
          private String bar;public native void processBar();
      }
      In C
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) {
          /* Same as object.getClass() */
          jclass clazz = (*env)->GetObjectClass(env, object);
          if (clazz != NULL) { /* cannot be null in this case */
              /* Same as clazz.getField("bar") */
              jfieldID field = (*env)->GetFieldID(env, clazz, "bar", "Ljava/lang/String;");
              if (field != NULL) { /* make sure we got the field */
                  /* Same as field.get(object) */
                  jstring jString = (*env)->GetObjectField(env, object, field);
                  if (jString != NULL) {
                      /* Convert the value to a C (UTF-8) string */
                      const char *cString = (*env)->GetStringUTFChars(env, jString, NULL);
                      if (cString == NULL) {
                          return; /* Out of memory */
                      }
                      printf("Value of \"bar\" before the change: \"%s\"\n", cString);
                      (*env)->ReleaseStringUTFChars(env, jString, cString);
                  }
                  /* Create a new String */
                  jString = (*env)->NewStringUTF(env, "Bar2");
                  if (jString != NULL) { /* make sure we are not out of memory */
                      /* Same as field.set(object, jString) */
                      (*env)->SetObjectField(env, object, field, jString);
                  }
              }
          }
      }
  • For methods, JNI offers functions to:

    • Lookup a method ID based on its name and signature:

      jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
      jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    • Invoke a method based on the method ID:

      jobject     (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
      jboolean    (*CallStaticBooleanMethod)(JNIEnv*, jclass, jmethodID, ...);
      jbyte       (*CallStaticByteMethod)(JNIEnv*, jclass, jmethodID, ...);
      jchar       (*CallStaticCharMethod)(JNIEnv*, jclass, jmethodID, ...);
      jshort      (*CallStaticShortMethod)(JNIEnv*, jclass, jmethodID, ...);
      jint        (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...);
      jlong       (*CallStaticLongMethod)(JNIEnv*, jclass, jmethodID, ...);
      jfloat      (*CallStaticFloatMethod)(JNIEnv*, jclass, jmethodID, ...);
      jdouble     (*CallStaticDoubleMethod)(JNIEnv*, jclass, jmethodID, ...);
      void        (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);
      
      jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
      jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
      jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
      jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
      jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
      jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
      jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
      jfloat      (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...);
      jdouble     (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...);
      void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
      Note

      JNI also offers Call…(…) functions that take args in a form of va_list as well as an array of jvalue-s:

      <Type>     (*Call<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list);
      <Type>     (*CallStatic<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list);
      <Type>     (*Call<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
      <Type>     (*CallStatic<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    • For example:

      In Java
      public class Foo {
          private String bar;
          public void setBar(String bar) {
              this.bar = bar;
          }public native void processBar();
      }
      In C
      JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) {
          /* Same as object.getClass() */
          jclass clazz = (*env)->GetObjectClass(env, object);
          if (clazz != NULL) {
              /* Same as clazz.getMethod("setBar", String.class) - assuming non-static */
              jmethodID method = (*env)->GetMethodID(env, clazz, "setBar", "(Ljava/lang/String;)V");
              if (method != NULL) { /* make sure we found the method */
                  /* Create a new Java String */
                  jstring jString = (*env)->NewStringUTF(env, "Bar2");
                  if (jString != null) {
                      /* Same as method.invoke(object, jString) */
                      (*env)->CallVoidMethod(env, object, method, jString);
                  }
              }
          }
      }

Registering Native Methods On Load

  • JNI supports JNI_OnLoad function, which if exported by the library, will automatically be called when the library is loaded by System.loadLibrary(String)

    jint JNI_OnLoad(JavaVM *vm, void *reserved);
  • When used with JNI’s RegisterNatives, we can pre-bind native methods as soon as our library is first loaded, and consequently, we no longer need to use javah:

    #include <jni.h>
    
    static void sayHi(JNIEnv *env, jclass clazz, jstring who, jint times) {
      const char *name = (*env)->GetStringUTFChars(env, who, NULL);
      if (name != NULL) {
        jint i;
        for (i = 0; i < times; i++) {
          printf("Hello %s\n", name);
        }
        (*env)->ReleaseStringUTFChars(env, who, name);
      }
    }
    
    static JNINativeMethod method_table[] = {
      { "sayHi", "(Ljava/lang/String;I)V", (void *) sayHi }
    };
    
    static int method_table_size = sizeof(method_table) / sizeof(method_table[0]);
    
    jint JNI_OnLoad(JavaVM* vm, void* reserved) {
      JNIEnv* env;
      if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
      } else {
        jclass clazz = (*env)->FindClass(env, "com/marakana/jniexamples/Hello");
        if (clazz) {
          jint ret = (*env)->RegisterNatives(env, clazz, method_table, method_table_size);
          (*env)->DeleteLocalRef(env, clazz);
          return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR;
        } else {
          return JNI_ERR;
        }
      }
    }
    
    NoteNotice that we JNI function that will be bound to the native Java method is declared as static. It won’t even be exported to the symbol table. It does not need to be because RegisterNatives will bind it via a function pointer to the Java method.
  • JNI supports JNI_OnUnload, but since this is only invoked when the class-loader that loaded the library is GC’ed, it’s not commonly used

Exceptions

  • JNI permits us to throw exceptions from native code

    • Assuming we have a Throwable object:

      jint Throw(JNIEnv *env, jthrowable obj);
    • Assuming we have a Throwable class, and that it has a constructor that takes a string message:

      jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
    • For example:

      static void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) {
          jclass clazz = (*env)->FindClass(env, name);
          if (clazz != NULL) {
              (*env)->ThrowNew(env, clazz, message);
              (*env)->DeleteLocalRef(env, clazz);
          }
      }if (invalidArgument == TRUE) {
          ThrowExceptionByClassName(env, "java/lang/IllegalArgumentException", "This argument is not valid!");
      }
  • Throwing an exception from native code just registers the throwable (as an exception pointer) with the current thread - it does not interrupt the current flow of the code!

    • The pending exception gets "thrown" upon returning to the Java code

    • While an exception is pending, we can only call the following JNI functions:

      • DeleteGlobalRef

      • DeleteLocalRef

      • DeleteWeakGlobalRef

      • ExceptionCheck

      • ExceptionClear

      • ExceptionDescribe

      • ExceptionOccurred

      • MonitorExit

      • PopLocalFrame

      • PushLocalFrame

      • Release<PrimitiveType>ArrayElements

      • ReleasePrimitiveArrayCritical

      • ReleaseStringChars

      • ReleaseStringCritical

      • ReleaseStringUTFChars

  • Calling methods on Java objects (e.g. via CallObjectMethod) can fail with an exception, but since exceptions don’t automatically abort our function calls, we must explicitly check for existence of pending exceptions (e.g. with ExceptionCheck)

  • Native code can "catch" an exception by calling ExceptionCheck(…) or ExceptionOccurred(…), print its stack trace (to stderr) with ExceptionDescribe(…), and "handle" it (i.e. clear it) withExceptionClear(…)

    jthrowable ExceptionOccurred(JNIEnv *env);  /* NULL if no exception is currently being thrown */
    jboolean ExceptionCheck(JNIEnv *env);
    void ExceptionDescribe(JNIEnv *env);
    void ExceptionClear(JNIEnv *env);
    • To get to the message of a throwable object we would do something like:

      (*env)->CallObjectMethod(env,); /* this can throw an exception */
      
      if ((*env)->ExceptionCheck(env)) {
          jthrowable throwable = (*env)->ExceptionOccurred(env);
          (*env)->ExceptionDescribe(env); /* optionally dump the stack trace */
          (*env)->ExceptionClear(env); /* mark the exception as "handled" */
          jclazz clazz = (*env)->GetObjectClass(env, throwable);
          jmethodID getMessageMethod = (*env)->GetMethodID(env, clazz, "getMessage", "()Ljava/lang/String;");
          jstring message = (*env)->CallObjectMethod(env, throwable, getMessageMethod);
          const char *cMessage = (*env)->GetStringUTFChars(env, message, NULL);
          if (cMessage) {
              printf("ERROR: %s\n", cMessage);
              (*env)->ReleaseStringUTFChars(env, message, cMessage);
          }
          (*env)->DeleteLocalRef(env, clazz);
      }


반응형