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 type | JNI type | C/C++ equivalent | Signature | Description |
---|---|---|---|---|
boolean | jboolean | unsigned char | Z | unsigned 8 bits |
byte | jbyte | signed char | B | signed 8 bits |
char | jchar | unsigned short | C | unsigned 16 bits |
double | jdouble | double | D | 64 bits |
float | jfloat | float | F | 32 bits |
int | jint | long | I | signed 32 bits |
long | jlong | long long __int64 | J | signed 64 bits |
short | jshort | short | S | signed 16 bits |
void | – | – | V | – |
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
- Download and install MinGW-64. Be sure to set “Architecture” to “x86_64” during installation for 64-bit instructions.
- Download the latest zip of MSYS.
- Extract MSYS to the MinGW install directory, e.g.,
C:\Program Files\mingw-w64
. - Add the bin for MinGW and MSYS to the environment path:
- Start -> right-click Computer -> Properties -> Advanced system settings -> Advanced tab -> Environment Variables
- Add the absolute paths to the Path variable under “User variables for (username)” with ‘;’ separators
- 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;
- If NetBeans is open, you must restart it to include the new Path variables.
- Open NetBeans.
- Tools -> Options -> C/C++ -> Build Tools.
- Add.
- Set Base Directory to, e.g.,
C:\Program Files\mingw-w64\x86_64-5.2.0-posix-seh-rt_v4-rev0\mingw64\bin
. - Check that Tool Collection Family is GNU MinGW and set Tool Collection Name to MinGW64.
- 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
. - Set make to, e.g.,
C:\Program Files\mingw-w64\MSYS-20111123\msys\bin\make.exe
. - Set as default.
JNI Configuration
- Open NetBeans if it is not already open.
- Create a new C/C++ Dynamic Library named JNINative.
- Set C/C++ properties in JNINative:
- Right-click JNINative -> properties.
- C++ Compiler -> Change Configuration to <All Configurations>.
- Include Directories -> Press the […] button.
- Add the path to your JDK include and win32 directories (absolute paths). In this example, jdk1.8.0_51:
C:/Program Files/Java/jdk1.8.0_51/include
C:/Program Files/Java/jdk1.8.0_51/include/win32
- Set Linker -> Output to
../JNI/libJNINative.${CND_DLIB_EXT}
- Setup Code Assistant:
- Tools -> Options -> C/C++ -> Code Assistance
- Ensure the correct Tool Collection is selected
- C++ Compiler -> Include Directories -> Add
C:/Program Files/Java/jdk1.8.0_51/include
C:/Program Files/Java/jdk1.8.0_51/include/win32
- Create a new Java Applications named JNI and add your code (see below for an example).
- Once your code is complete, create the header file:
- Clean and build the Java project (a must since it runs on
class
instead ofjava
files) command prompt> cd <NetBeans directory>\JNI\build\classes
command prompt> "C:\Program Files\Java\jdk1.8.0_51\bin\javah.exe" -o ..\..\..\JNINative\JNINative.h -jni jni.JNI
- Clean and build the Java project (a must since it runs on
- Add the header file to JNINative:
- The first time: right-click Source Files -> Add Existing Item…, select JNINative.h
- If a new header file was generated, you might have to right-click JNINative.h -> Refresh
- Add a new source file to JNINative and add your code (see below for an example):
- Right-click Source Files -> new C++ source file…
- name: JNINative
- Once your code is complete, clean and build the C/C++ project.
- Clean and build the Java project.
- 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++