- Download tutorial
C++ and Java are two mainstream languages, with their strengths and weaknesses, and plenty of interesting portable code around. So why not enjoy from the best of both worlds?
The Java Native Interface (JNI) is a standard to integrate in a portable way C++ and Java code. It works in both directions: you can call a C++ library from Java or you can call Java components from C++. In this tutorial, I'll explain the second approach.
JNI is frequently used by Java developers to call some tiny portions of C++ code when ultra-high performance is required. But JNI is more than just that. It also allows you to embed existing Java components into your C++ developed software.
In this article, you'll learn how to use JNI, raw JNI and only JNI to achieve such an integration. No third party wrapper will be used.
Once you've read this tutorial, you'll no longer regret that some cutting edge software components are first developed for Java and only later available for C++. You'll no longer write complex file based or network based interface to link Java and C++ code when tight and time critical interrelation is required. You'll seemlessly integrate both worlds.
For this tutorial, you need to have:
. Note that the JDK installation sets up a JRE automatically.You must add
to the PATH
. This is something you must do unless you are allowed to copy the Java Virtual Machine dynamic library (JVM.dll) into the path of your executable.
For convenience, you should ensure that the JDK tools in
are included in the PATH
: you then can easily compile your Java code.
For each C++ projects in this tutorial, you must add the directories
and
to the include directories of your compiler. Note that the win32 directory Is platform dependent.
You also shall add
With MSVC2103, you do this by right clicking on the project, to display its properties (see screenshot).
The ZIP file contains an MSVC2013 solution with all the 7 examples of this tutorial. Download Article-JNI-1.zip
Before using JNI in your C++ code, you have to load and initialize the Java Virtual Machine (JVM). The following code shows you how to do this:
#includeint main() { Using namespace std; JavaVM *jvm; // Pointer to the JVM (Java Virtual Machine) JNIEnv *env; // Pointer to native interface //================== prepare loading of Java VM ============================ JavaVMInitArgs vm_args; // Initialization arguments JavaVMOption* options = new JavaVMOption[1]; // JVM invocation options options[0].optionString = "-Djava.class.path=."; // where to find java .class vm_args.version = JNI_VERSION_1_6; // minimum Java version vm_args.nOptions = 1; // number of options vm_args.options = options; vm_args.ignoreUnrecognized = false; // invalid options make the JVM init fail //=============== load and initialize Java VM and JNI interface ============= jint rc = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); // YES !! delete options; // we then no longer need the initialisation options. if (rc != JNI_OK) { // TO DO: error processing... cin.get(); exit(EXIT_FAILURE); } //=============== Display JVM version ======================================= cout << "JVM load succeeded: Version "; jint ver = env->GetVersion(); cout << ((ver>>16)&0x0f) << "."<<(ver&0x0f) << endl; // TO DO: add the code that will use JVM <============ (see next steps) jvm->DestroyJavaVM(); cin.get(); }
This code displays the version of the JVM. The example 1
code is enriched with some error processing that should help you to solve any unexpected problems.
If you don't get an error message, but your program gets interrupted abruptly, it'll most probably be that jvm.dll could not be found in the path (see prerequisites above).
Let's write the simplest Java method we could imagine to call: a simple static
method, taking no argument and returning nothing. In Java, everything is embedded in a class. So, we'll write the following code in the file MyTest.java:
public class MyTest { private static int magic_counter=777; public static void mymain() { // <=== We will call this System.out.println("Hello, World in java from mymain"); System.out.println(magic_counter); } }
We compile this code from the command line:
javac MyTest.java
We then check that there was no error and that the file MyTest.class was successfully generated. By the way, I won't tell it but we'll proceed this way for all subsequent examples.
Now, we are ready to enrich our previous C++ code:
... jclass cls2 = env->FindClass("MyTest"); // try to find the class if(cls2 == nullptr) { cerr << "ERROR: class not found !"; } else { // if class found, continue cout << "Class MyTest found" << endl; jmethodID mid = env->GetStaticMethodID(cls2, "mymain", "()V"); // find method if(mid == nullptr) cerr << "ERROR: method void mymain() not found !" << endl; else { env->CallStaticVoidMethod(cls2, mid); // call method cout << endl; } }
FindClass()
, which acts as a class loader. It will search for an appropriate .class file in the list of directories that was provided at JVM initialization. If the Java class is included in a package, you shall provide its full name.GetStaticMethod()
, which shall find the right method in the class. The last parameter of this function is the most difficult one: the method signature. In case of any mismatch here, the method won't be found. "()"
means a function with no parameter and "V"
that the return type is void
.static
method is independent of any object. So we can then call the method with CallStaticVoidMethod()
.JNI passes and returns object of a basic Java type such as int
, long
, double
by value. This is very easy to process. You just have to use the corresponding JNI native type jint
, jlong
, jdouble
and so on.
So let's look at example 3. The Java class is enriched with the following method:
Class MyTest { ... public static int mymain2(int n) { // <== add this new function for (int i=0; i
Calling this function from C++ is very similar to the previous example:
//... we already have the class jmethodID mid2 = env->GetStaticMethodID(cls2, "mymain2", "(I)I"); if(mid2 == nullptr) { cerr << "ERROR: method it main2(int) not found !" << endl; } else { env->CallStaticVoidMethod(cls2, mid2, (jint)5); cout << endl; }
GetStaticMethodID()
is now
"(I)"
, saying that it's a function with one integer argument, followed by
"I"
, i.e., returning an integer. If you want to experiment with other types, have a look at the full signature reference documented in Oracle's JNI specifications.
As soon as you work with a function taking or returning objects that are not of a fundamental type, the object is passed by reference. So let's take the example of a call the Java main()
function, which looks like:
class MyTest { ... public static void main (String[] args) { // test in java //… some code here. } }
Calling this function from C++ is a little bit more complex. First, the signature of the method: arrays are noted with "["
in the JNI signature parameter. Not built in types are indicated with an "L"
followed by the full class name, followed by a semicolumn. As the function returns void
, the signature is hence: "([Ljava/lang/String;)V"
. Yes ! Now, we can retrieve the method:
//... we still have the class from the previous examples jmethodID mid3 = env->GetStaticMethodID(cls2, "main", "([Ljava/lang/String;)V"); if(mid3 == nullptr) { cerr << "ERROR: method not found !" << endl; }
To call the method, we first need to build a Java array, as well as for the string
s to populate it. We do this in the following way:
else { jobjectArray arr = env->NewObjectArray(5, // constructs java array of 5 env->FindClass("java/lang/String"), // Strings env->NewStringUTF("str")); // each initialized with value "str" env->SetObjectArrayElement( arr, 1, env->NewStringUTF("MYOWNSTRING")); // change an element env->CallStaticVoidMethod(cls2, mid3, arr); // call the method with the arr as argument. env->DeleteLocalRef(arr); // release the object }
The important point to understand here is that the Java objects are created by the JVM. So the JVM is responsible to free the memory when it is no longer used. As soon as you no longer need an object, you should hence call DeleteLocalRef()
to tell the JVM that you don't need it anymore. If you don't, memory will leak (see the explanations in this StackOverflow question).
Until now, we've kept things simple: we've called only static
Java methods. These are independent of the object. But this is not the most natural way to go in object oriented programming. So there are big chances that some day you will have to create an object and call the methods for the object.
Let's enrich our Java class with a constructor and a simple method, in example 5:
Class MyTest { ... private int uid; // private data of the object: it's ID public MyTest() { // constructor uid = magic_counter++ * 2; } public void showId() { // simple method that shows the id of the object System.out.println(uid); } }
From C++, you can then create a MyTest
object, by finding and invoking a constructor:
jmethodID ctor = env->GetMethodID(cls2, "", "()V"); // FIND AN OBJECT CONSTRUCTOR if(ctor == nullptr) { cerr << "ERROR: constructor not found !" << endl; } else { cout << "Object succesfully constructed !"< NewObject(cls2, ctor);
If the object is successfully constructed, we can then search for the method we want to call, and invoke it for the object:
if (myo) { jmethodID show = env->GetMethodID(cls2, "showId", "()V"); if(show == nullptr) cerr << "No showId method !!" << endl; else env->CallVoidMethod(myo, show); } }
So, now you know how to launch the JVM, run static
methods, create objects and invoke their methods. You are in full control of any Java component that you would like to integrate with our C++ code.
There's however a last thing that we need for having a full picture...
In your Java code, you could perhaps need to call back C++ functions. This is done with Java native methods. Here a final enhancement of our Java example:
MyTest { ... public native void doTest(); // to be supplied in C++ trhough JNI public void showId() { // replace the previous version of example 5 System.out.println(uid); doTest(); // <==== invoke the native method } }
The native function is declared in Java, but has to be defined and registered in C++ before the Java object is created.
Here's how such a callback function would be declared in C++:
void doTestCPP(JNIEnv*e, jobject o) { std::cout << "C++callback activated" << std::endl; jfieldID f_uid = e->GetFieldID(e->GetObjectClass(o), "uid", "I"); if (f_uid) std::cout << "UID data member: " << e->GetIntField(o, f_uid) << std::endl; else std::cout << "UID not found" << std::endl; }
By the way, as you see, we can easily access object variables using GetFieldId()
.
To register the native function mapping, we use the following code snippet:
JNINativeMethod methods[] { { "doTest", "()V", (void *)&doTestCPP } }; // mapping table if(env->RegisterNatives(cls2, methods, 1) < 0) { // register it if(env->ExceptionOccurred()) // verify if it's ok cerr << " OOOOOPS: exception when registreing naives" << endl; else cerr << " ERROR: problem when registreing naives" << endl; }
Now, you can call again the method showId()
, as in the previous example. But the new version will call doTest()
which will call from Java our new C++ callback.
We can now organize a bidirectional integration C++ to Java and back. You have learnt the essentual JNI surviving techniques. Up to you to play with this new knowledge. Here, you have the full reference of the JNI functions.
While you could now imagine any kind of integration, you should be aware of some performance constraints. JNI means some minimal overhead.
I've written a small benchmark calling the same very small Java function either from Java or from C++. It is provided in example 7.
On my core i7, the results are the following:
Java called from Java: 14 nanoseconds/iteration Java called from C++: 23 nanoseconds/iteration C++ 100% native: 2 nanoseconds/iteration
Each C++ to Java call through JNI has an overhead of 9 to 10 ns. For small functions such as in this benchmark, this overhead is an overkill. So this kind of integration should not be considered for high frequency, low latency function calls. But many JNI applications are about integrating high level Java components or interfaces. In this case, the JNI overhead is negligible compared to the tremendous benefit of the easy integration.
A last point of interest to keep in mind is the difficulty of memory management with Java objects. Memory could leak if Java objects are created in C++ and the C++ variable referring to it goes out of scope. This is manageable for small demos like here. But for the sake of reliability in more complex software, a C++ wrapper implementing RAII should really be considered for Java objects.