Java LanguageJava Native Interface

Parameters

ParameterDetails
JNIEnvPointer to the JNI environment
jobjectThe object which invoked the non-static native method
jclassThe class which invoked the static native method

Remarks

Setting up JNI requires both a Java and a native compiler. Depending on the IDE and OS, there is some setting up required. A guide for Eclipse can be found here. A full tutorial can be found here.

These are the steps for setting up the Java-C++ linkage on windows:

  • Compile the Java source files (.java) into classes (.class) using javac.
  • Create header (.h) files from the Java classes containing native methods using javah. These files "instruct" the native code which methods it is responsible for implementing.
  • Include the header files (#include) in the C++ source files (.cpp) implementing the native methods.
  • Compile the C++ source files and create a library (.dll). This library contains the native code implementation.
  • Specify the library path (-Djava.library.path) and load it in the Java source file (System.loadLibrary(...)).

Callbacks (Calling Java methods from native code) requires to specify a method descriptor. If the descriptor is incorrect, a runtime error occurs. Because of this, it is helpful to have the descriptors made for us, this can be done with javap -s.

Calling C++ methods from Java

Static and member methods in Java can be marked as native to indicate that their implementation is to be found in a shared library file. Upon execution of a native method, the JVM looks for a corresponding function in loaded libraries (see Loading native libraries), using a simple name mangling scheme, performs argument conversion and stack setup, then hands over control to native code.

Java code

/*** com/example/jni/JNIJava.java **/

package com.example.jni;

public class JNIJava {
    static {
        System.loadLibrary("libJNI_CPP");
    }

    // Obviously, native methods may not have a body defined in Java
    public native void printString(String name);
    public static native double average(int[] nums);

    public static void main(final String[] args) {
        JNIJava jniJava = new JNIJava();
        jniJava.printString("Invoked C++ 'printString' from Java");

        double d = average(new int[]{1, 2, 3, 4, 7});
        System.out.println("Got result from C++ 'average': " + d);
    }
}

C++ code

Header files containing native function declarations should be generated using the javah tool on target classes. Running the following command at the build directory :

javah -o com_example_jni_JNIJava.hpp com.example.jni.JNIJava

... produces the following header file (comments stripped for brevity) :

// com_example_jni_JNIJava.hpp

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h> // The JNI API declarations

#ifndef _Included_com_example_jni_JNIJava
#define _Included_com_example_jni_JNIJava
#ifdef __cplusplus
extern "C" { // This is absolutely required if using a C++ compiler
#endif

JNIEXPORT void JNICALL Java_com_example_jni_JNIJava_printString
  (JNIEnv *, jobject, jstring);

JNIEXPORT jdouble JNICALL Java_com_example_jni_JNIJava_average
  (JNIEnv *, jclass, jintArray);

#ifdef __cplusplus
}
#endif
#endif

Here is an example implementation :

// com_example_jni_JNIJava.cpp

#include <iostream>
#include "com_example_jni_JNIJava.hpp"

using namespace std;

JNIEXPORT void JNICALL Java_com_example_jni_JNIJava_printString(JNIEnv *env, jobject jthis, jstring string) {
    const char *stringInC = env->GetStringUTFChars(string, NULL);
    if (NULL == stringInC)
        return;
    cout << stringInC << endl;
    env->ReleaseStringUTFChars(string, stringInC);
}

JNIEXPORT jdouble JNICALL Java_com_example_jni_JNIJava_average(JNIEnv *env, jclass jthis, jintArray intArray) {
    jint *intArrayInC = env->GetIntArrayElements(intArray, NULL);
    if (NULL == intArrayInC)
        return -1;
    jsize length = env->GetArrayLength(intArray);
    int sum = 0;
    for (int i = 0; i < length; i++) {
        sum += intArrayInC[i];
    }
    env->ReleaseIntArrayElements(intArray, intArrayInC, 0);
    return (double) sum / length;
}

Output

Running the example class above yields the following output :

Invoked C++ 'printString' from Java
Got result from C++ 'average': 3.4

Calling Java methods from C++ (callback)

Calling a Java method from native code is a two-step process :

  1. obtain a method pointer with the GetMethodID JNI function, using the method name and descriptor ;
  2. call one of the Call*Method functions listed here.

Java code

/*** com.example.jni.JNIJavaCallback.java ***/

package com.example.jni;

public class JNIJavaCallback {
    static {
        System.loadLibrary("libJNI_CPP");
    }
    
    public static void main(String[] args) {
        new JNIJavaCallback().callback();
    }

    public native void callback(); 

    public static void printNum(int i) {
        System.out.println("Got int from C++: " + i);
    }
    
    public void printFloat(float i) {
        System.out.println("Got float from C++: " + i);
    }
}

C++ code

// com_example_jni_JNICppCallback.cpp

#include <iostream>
#include "com_example_jni_JNIJavaCallback.h"

using namespace std;

JNIEXPORT void JNICALL Java_com_example_jni_JNIJavaCallback_callback(JNIEnv *env, jobject jthis) {
    jclass thisClass = env->GetObjectClass(jthis);

    jmethodID printFloat = env->GetMethodID(thisClass, "printFloat", "(F)V");
    if (NULL == printFloat)
        return;
    env->CallVoidMethod(jthis, printFloat, 5.221);

    jmethodID staticPrintInt = env->GetStaticMethodID(thisClass, "printNum", "(I)V");
    if (NULL == staticPrintInt)
        return;
    env->CallVoidMethod(jthis, staticPrintInt, 17);
}

Output

Got float from C++: 5.221
Got int from C++: 17

Getting the descriptor

Descriptors (or internal type signatures) are obtained using the javap program on the compiled .class file. Here is the output of javap -p -s com.example.jni.JNIJavaCallback :

Compiled from "JNIJavaCallback.java"
public class com.example.jni.JNIJavaCallback {
  static {};
    descriptor: ()V

  public com.example.jni.JNIJavaCallback();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  public native void callback();
    descriptor: ()V

  public static void printNum(int);
    descriptor: (I)V  // <---- Needed

  public void printFloat(float);
    descriptor: (F)V  // <---- Needed
}

Loading native libraries

The common idiom for loading shared library files in Java is the following :

public class ClassWithNativeMethods {
    static {
        System.loadLibrary("Example");
    }

    public native void someNativeMethod(String arg);
    ...

Calls to System.loadLibrary are almost always static so as to occur during class loading, ensuring that no native method can execute before the shared library has been loaded. However the following is possible :

public class ClassWithNativeMethods {
    // Call this before using any native method
    public static void prepareNativeMethods() {
        System.loadLibrary("Example");
    }

    ...

This allows to defer shared library loading until necessary, but requires extra care to avoid java.lang.UnsatisfiedLinkErrors.

Target file lookup

Shared library files are searched for in the paths defined by the java.library.path system property, which can be overriden using the -Djava.library.path= JVM argument at runtime :

java -Djava.library.path=path/to/lib/:path/to/other/lib MainClassWithNativeMethods

Watch out for system path separators : for example, Windows uses ; instead of :.

Note that System.loadLibrary resolves library filenames in a platform-dependent manner : the code snippet above expects a file named libExample.so on Linux, and Example.dll on Windows.

An alternative to System.loadLibrary is System.load(String), which takes the full path to a shared library file, circumventing the java.library.path lookup :

public class ClassWithNativeMethods {
    static {
        System.load("/path/to/lib/libExample.so");
    }

    ...