JNI Basics

JNI stands for Java Native Interface (detailed Oracle documentation). It allows Java to execute code written in other languages such as C/C++ and assembly. So, why would you want to do this? While Java is quite comprehensive, it is by design generic. That is, it must operate seamlessly across all supported architectures (and there are a lot). Thus, architecture-specific elements must be dealt with individually and tied in through JNI. I have used JNI in several projects over the years to scale applications and interface more efficiently with (non)volatile memory.

Preliminaries

This will not be a comprehensive look into JNI; just a few simple C/C++ examples to get you acquainted with the process. That being said, there are a couple of points to introduce.

First, Java, JNI, and C/C++ types differ. The table below provides these mappings. For instance, in Java, a char data type supports Unicode characters, and is thus compatible with an unsigned short in C/C++. In terms of JNI data types, char becomes jchar, which is defined to be an unsigned short in jni.h – the C header imported into C/C++ JNI files.

Java typeJNI typeC/C++ equivalentSignatureDescription
booleanjbooleanunsigned charZunsigned 8 bits
bytejbytesigned charBsigned 8 bits
charjcharunsigned shortCunsigned 16 bits
doublejdoubledoubleD64 bits
floatjfloatfloatF32 bits
intjintlongIsigned 32 bits
longjlonglong long __int64Jsigned 64 bits
shortjshortshortSsigned 16 bits
voidV

Second, the external code to be loaded must be in the form of a library. On Windows, this is a dll; o, typically, on Unix systems. Therefore, the C/C++ project must be created as a library instead of application (if using an IDE, for example). Third, methods interfacing with C/C++ must be declared as native. Once the Java program is compiled, a C/C++ header is generated using javah at the command line. The resulting h file is the foundation for all Java to C/C++ method calls. Lastly, it is possible to call Java methods from C/C++.

The following examples use NetBeans IDE 8.1 (“All” feature bundle which includes C/C++) and JDK 1.8u51 on a Windows machine (because it is the most difficult to get to work – go figure). If using a 64-bit version of Windows, follow the instruction in the proceeding section titled “Windows 64-bit Configuration and NetBeans.”

Windows 64-bit Configuration and NetBeans
  1. Download and install MinGW-64. Be sure to set “Architecture” to “x86_64” during installation for 64-bit instructions.
  2. Download the latest zip of MSYS.
  3. Extract MSYS to the MinGW install directory, e.g., C:\Program Files\mingw-w64.
  4. Add the bin for MinGW and MSYS to the environment path:
    1. Start -> right-click Computer -> Properties -> Advanced system settings -> Advanced tab -> Environment Variables
    2. Add the absolute paths to the Path variable under “User variables for (username)” with ‘;’ separators
    3. E.g., C:\Program Files\mingw-w64\x86_64-5.2.0-posix-seh-rt_v4-rev0\mingw64\bin;C:\Program Files\mingw-w64\MSYS-20111123\msys\bin;
  5. If NetBeans is open, you must restart it to include the new Path variables.
  6. Open NetBeans.
  7. Tools -> Options -> C/C++ -> Build Tools.
  8. Add.
  9. Set Base Directory to, e.g., C:\Program Files\mingw-w64\x86_64-5.2.0-posix-seh-rt_v4-rev0\mingw64\bin.
  10. Check that Tool Collection Family is GNU MinGW and set Tool Collection Name to MinGW64.
  11. Set C++ Compiler to, e.g., C:\Program Files\mingw-w64\x86_64-5.2.0-posix-seh-rt_v4-rev0\mingw64\bin\x86_64-w64-mingw32-g++.exe.
  12. Set make to, e.g., C:\Program Files\mingw-w64\MSYS-20111123\msys\bin\make.exe.
  13. Set as default.
JNI Configuration
  1. Open NetBeans if it is not already open.
  2. Create a new C/C++ Dynamic Library named JNINative.
  3. Set C/C++ properties in JNINative:
    1. Right-click JNINative -> properties.
    2. C++ Compiler -> Change Configuration to <All Configurations>.
    3. Include Directories -> Press the […] button.
    4. Add the path to your JDK include and win32 directories (absolute paths). In this example, jdk1.8.0_51:
      1. C:/Program Files/Java/jdk1.8.0_51/include
      2. C:/Program Files/Java/jdk1.8.0_51/include/win32
    5. Set Linker -> Output to ../JNI/libJNINative.${CND_DLIB_EXT}
  4. Setup Code Assistant:
    1. Tools -> Options -> C/C++ -> Code Assistance
    2. Ensure the correct Tool Collection is selected
    3. C++ Compiler -> Include Directories -> Add
    4. C:/Program Files/Java/jdk1.8.0_51/include
    5. C:/Program Files/Java/jdk1.8.0_51/include/win32
  5. Create a new Java Applications named JNI and add your code (see below for an example).
  6. Once your code is complete, create the header file:
    1. Clean and build the Java project (a must since it runs on class instead of java files)
    2. command prompt> cd <NetBeans directory>\JNI\build\classes
    3. command prompt> "C:\Program Files\Java\jdk1.8.0_51\bin\javah.exe" -o ..\..\..\JNINative\JNINative.h -jni jni.JNI
  7. Add the header file to JNINative:
    1. The first time: right-click Source Files -> Add Existing Item…, select JNINative.h
    2. If a new header file was generated, you might have to right-click JNINative.h -> Refresh
  8. Add a new source file to JNINative and add your code (see below for an example):
    1. Right-click Source Files -> new C++ source file…
    2. name: JNINative
  9. Once your code is complete, clean and build the C/C++ project.
  10. Clean and build the Java project.
  11. Run the Java project.
Java Code

The following example provides all the necessary Java elements for JNI to function in bi-direction fashion. That is, it contains native methods to interface with C/C++ and plain methods for C/C++ to call.

Lines 10-16 represent the methods available to C/C++ (defined as native). Lines 19-22 load the dll created by the C/C++ library project (code to follow).

Lines 33-56 illustrate various JNI options. Line 33 calls a JNI method that will ultimately print a message from within C++. A string, number, and sum are retrieved from C/C++ and printed in lines 36-37, 40-41, and 44-47 respectively. Line 50 sends a Java string to C/C++ to be print. Line 53 calls a JNI method that will call the Java method printCalledByJNI(string). Finally, line 56 calls a JNI method that retrieves the Java string on line 74 via getCalledByJNI() and prints it in C/C++.

import java.io.File;
 
/**
 * JNI example class.This calls and is called by C/C++. Note: all Java methods
 * are process before C/C++ regardless of order of execution.
 * @author Ray Hylock
 */
public class JNI {
    // external call
    private native void nativePrint();
    private native String nativeGetString();
    private native int nativeGetInt();
    private native int nativeGetAdd(int a, int b);
    private native void nativePrintString(String string);
    private native void nativeCallJavaPrint();
    private native void nativeGetJavaString();
     
    // load the dll file
    static {
        System.load((new File(System.getProperty("user.dir"), 
                "libJNINative.dll")).toString());
    }
 
    /**
     * Main method.
     * @param args  arguments (none required)
     */
    public static void main(String[] args) {
        // instantiates this object
        JNI jni = new JNI();
         
        // call JNI method that will print a C++ string
        jni.nativePrint();
 
        // get a string from C++ and print it
        String fromNativeString = jni.nativeGetString();
        System.out.println("From java: " + fromNativeString);
 
        // get an integer from C++ and print it
        int fromNativeInt = jni.nativeGetInt();
        System.out.println("From java: " + fromNativeInt + " retrieved from C++");
 
        // add two values in C++, get the value, and print the results
        int a = 2, b = 3;
        int fromNativeAdd = jni.nativeGetAdd(a, b);
        System.out.println("From java: " + a + "+" + b + "=" + fromNativeAdd 
                + " added by C++");
         
        // call JNI method that will print a Java string in C++
        jni.nativePrintString("Hello World from Java");
         
        // call JNI method that will call printCalledByJNI(string)
        jni.nativeCallJavaPrint();
         
        // call JNI method that will call getCalledByJNI() and print the string
        jni.nativeGetJavaString();
    }
     
    /**
     * Prints the string. This method is called from within C++ by
     * the native methods referenced by {@link #nativeCallJavaPrint()}.
     * @param string the string to print
     */
    public void printCalledByJNI(String string){
        System.out.println("From Java: " + string);
    }
     
    /**
     * Returns the string. This method is called from within C++ by
     * the native methods referenced by {@link #nativeGetJavaString()}.
     * @return a string
     */
    public String getCalledByJNI(){
        return "Hello World from Java, retrieved by C++";
    }
JNINative.h

Once the code above is complete, the header file can be created following step 6 in the JNI Configuration section above. The output is as follows.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_JNI */
 
#ifndef _Included_jni_JNI
#define _Included_jni_JNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     jni_JNI
 * Method:    nativePrint
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativePrint
  (JNIEnv *, jobject);
 
/*
 * Class:     jni_JNI
 * Method:    nativeGetString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_JNI_nativeGetString
  (JNIEnv *, jobject);
 
/*
 * Class:     jni_JNI
 * Method:    nativeGetInt
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_jni_JNI_nativeGetInt
  (JNIEnv *, jobject);
 
/*
 * Class:     jni_JNI
 * Method:    nativeGetAdd
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_jni_JNI_nativeGetAdd
  (JNIEnv *, jobject, jint, jint);
 
/*
 * Class:     jni_JNI
 * Method:    nativePrintString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativePrintString
  (JNIEnv *, jobject, jstring);
 
/*
 * Class:     jni_JNI
 * Method:    nativeCallJavaPrint
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativeCallJavaPrint
  (JNIEnv *, jobject);
 
/*
 * Class:     jni_JNI
 * Method:    nativeGetJavaString
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativeGetJavaString
  (JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif
JNINative.cpp

Now it is time to write the C++ class JNINative.cpp. Following steps 7 and 8 in the JNI Configuration section above, add the header and create the cpp file. The method names that will interface with JNI must match the prototypes in JNINative.h. The methods are fairly self-explanatory, but there are a few particulars to cover.

The first point of exposition is conveting C++ strings to their Java equivalents – lines 24-26 illustrate this process. The string to pass from C++ is stored as a char[]. That array is then converted to a jstring using the JNI environment method NewStringUTF(char[]), which is then returned to Java.

The second is the reverse of the previous item. Lines 61-63 show the conversion from a JNI jstring (which is simply a String in the Java code) to a C++ const char* object. This next portion is particular important – the conversion must be “released” after the value is used to remove the pointer reference.

The final two methods have C++ to Java calls. Both must obtain the class objects and method ids for their respective calls (lines 74-79 and 95-100 respectively). Notice the method requires a JNI signature and not simply a data type. For instance, on line 78, the printCalledByJNI method requires a String as a parameter and returns void. The string is represented by the object signature letter L and the full import path to the object, which is java/lang/String terminated by a semicolon – thus, Ljava/lang/String;. Void is simply V. It is possible to send arrays as well by substituting L with [.

The difference between the two lies in the final few lines of each. For Java_jni_JNI_nativeCallJavaPrint lines 82-84 create a new jstring using the JNI environment method NewStringUTF, calls the void returning method, and deletes the string. Java_jni_JNI_nativeGetJavaString retrieves the String from Java using an object return call (CallObjectMethod), converts that to its C++ equivalent, prints the message, and, finally, releases the jstring reference.

#include <string>
#include <jni.h>
#include <stdio.h>
#include "JNINative.h"
 
using namespace std;
 
/**
 * Prints the message, "From C++: Hello World from C++".
 * @param env   JNI environment
 * @param obj   class object
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativePrint(JNIEnv *env, jobject obj) {
    printf("From C++: Hello World from C++\n");
}
 
/**
 * Returns a C++ {@code char[]} as a {code jstring} to java.
 * @param env   JNI environment
 * @param obj   class object
 * @return      a C++ {@code char[]} as a Java {code jstring}
 */
JNIEXPORT jstring JNICALL Java_jni_JNI_nativeGetString(JNIEnv *env, jobject obj) {
    char str[] = "Hello World from C++";
    jstring jstrBuf = (env)->NewStringUTF(str); // conver to jstring
    return jstrBuf;
}
 
/**
 * Returns the number 1 to Java.
 * @param env   JNI environment
 * @param obj   class object
 * @return      the number 1
 */
JNIEXPORT jint JNICALL Java_jni_JNI_nativeGetInt(JNIEnv *env, jobject obj) {
    int a = 1;
    return a;
}
 
/**
 * Adds {@code a} to {@code b} and returns the sum to Java.
 * @param env   JNI environment
 * @param obj   class object
 * @param a     first number
 * @param b     second number
 * @return      the sum to Java
 */
JNIEXPORT jint JNICALL Java_jni_JNI_nativeGetAdd(JNIEnv *env, jobject obj, jint a, jint b){
    int add = a + b;
    return add;
}
 
/**
 * Prints a string send by Java.
 * @param env   JNI environment
 * @param obj   class object
 * @param str   the string to print
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativePrintString(JNIEnv *env, jobject obj, jstring str){
    // convert from jstring to C++ equivalent
    const char* t = env->GetStringUTFChars(str, NULL);
    printf("From C++: %s\n", t);
    env->ReleaseStringUTFChars(str, t); // release the variable
}
 
/**
 * Calls the Java method {@code printCalledByJNI(java.lang.String)}, which 
 * prints, in Java, the C++ string sent.
 * @param env   JNI environment
 * @param obj   class object
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativeCallJavaPrint(JNIEnv *env, jobject obj) {
    // get the java class
    jclass cls = env->GetObjectClass(obj);
    if(cls == 0) printf("java class not found");
     
    // get the java method
    jmethodID mid = env->GetMethodID(cls, "printCalledByJNI", "(Ljava/lang/String;)V");
    if(mid == 0) printf("java method printCalledByJNI not found");
     
    // create the string to print, execute, and clear string
    jstring jstr = env->NewStringUTF("Method called by C++");
    env->CallVoidMethod(obj, mid, jstr);
    env->DeleteLocalRef(jstr);
}
 
/**
 * Retrieves a String from Java via the Java method {@code getCalledByJNI()}
 * and prints the value.
 * @param env   JNI environment
 * @param obj   class object
 */
JNIEXPORT void JNICALL Java_jni_JNI_nativeGetJavaString(JNIEnv *env, jobject obj) {
    // get the java class
    jclass cls = env->GetObjectClass(obj);
    if(cls == 0) printf("java class not found");
     
    // get the java method
    jmethodID mid = env->GetMethodID(cls, "getCalledByJNI", "()Ljava/lang/String;");
    if(mid == 0) printf("java method getCalledByJNI not found");
     
    // get the string from java
    jstring jstr = (jstring)env->CallObjectMethod(obj, mid);
     
    // convert and print
    const char* t = env->GetStringUTFChars(jstr, NULL);
    printf("From C++: %s\n", t);
    env->ReleaseStringUTFChars(jstr, t);
}
Output from JNI

The following is the output from the JNI.java class:

From java: Hello World from C++
From java: 1 retrieved from C++
From java: 2+3=5 added by C++
From Java: Method called by C++
From C++: Hello World from C++
From C++: Hello World from Java
From C++: Hello World from Java, retrieved by C++