Introduction
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.
Background
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.
Tutorial
Prerequisites
For this tutorial, you need to have:
- An installed Java Developer's Kit (JDK). Let's call the installation directory
<JDK-DIR>
- An installed Java Runtime environment (JRE). Let's say the installation directory is
<JRE-DIR>
. Note that the JDK installation sets up a JRE automatically. - A working C++ toolchain. My explanation is tailored for MSVC13 on Windows. But as the code is standard, the explanation could easily be adapted for other compilers and operating systems.
You must add <JRE>/bin/server
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 <JDK-DIR>/bin
are included in the PATH
: you then can easily compile your Java code.
Project Setup
For each C++ projects in this tutorial, you must add the directories <JDK-DIR>/include
and <JDK-DIR>/include/win32
to the include directories of your compiler. Note that the win32 directory Is platform dependent.
You also shall add <JDK-DIR>/lib/jvm.lib to the linker input files as additional dependencies).
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
First Example: Load and Initialize the JVM
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:
#include <jni.h>
int main()
{
Using namespace std;
JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption[1]; options[0].optionString = "-Djava.class.path=."; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options;
vm_args.ignoreUnrecognized = false; jint rc = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); delete options; if (rc != JNI_OK) {
cin.get();
exit(EXIT_FAILURE);
}
cout << "JVM load succeeded: Version ";
jint ver = env->GetVersion();
cout << ((ver>>16)&0x0f) << "."<<(ver&0x0f) << endl;
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).
Example 2: Access a Simple Java Static Method
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() {
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"); if(cls2 == nullptr) {
cerr << "ERROR: class not found !";
}
else { cout << "Class MyTest found" << endl;
jmethodID mid = env->GetStaticMethodID(cls2, "mymain", "()V"); if(mid == nullptr)
cerr << "ERROR: method void mymain() not found !" << endl;
else {
env->CallStaticVoidMethod(cls2, mid); cout << endl;
}
}
How does this work?
- We first have to find the right class with
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. - The class is then passed to
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
. - A
static
method is independent of any object. So we can then call the method with CallStaticVoidMethod()
.
Excample 3: Java Static Method Taking Arguments And Returning Values
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) {
for (int i=0; i<n; i++) {
System.out.print (i);
System.out.println("Hello, World !");
}
return n*2;
}
}
Calling this function from C++ is very similar to the previous example:
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;
}
The signature passed to 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.
Example 4: Java Arrays and Objects
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) {
}
}
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:
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, env->FindClass("java/lang/String"), env->NewStringUTF("str")); env->SetObjectArrayElement( arr, 1, env->NewStringUTF("MYOWNSTRING")); env->CallStaticVoidMethod(cls2, mid3, arr); env->DeleteLocalRef(arr); }
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).
Example 5: Objects and Methods
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;
public MyTest() {
uid = magic_counter++ * 2;
}
public void showId() {
System.out.println(uid);
}
}
From C++, you can then create a MyTest
object, by finding and invoking a constructor:
jmethodID ctor = env->GetMethodID(cls2, "<init>", "()V"); if(ctor == nullptr) {
cerr << "ERROR: constructor not found !" << endl;
}
else {
cout << "Object succesfully constructed !"<<endl;
jobject myo = env->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...
Example 6: Callbacks and Instance Variables
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();
public void showId() {
System.out.println(uid);
doTest();
}
}
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 } };
if(env->RegisterNatives(cls2, methods, 1) < 0) { if(env->ExceptionOccurred()) 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.
Points of Interest
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.
History
- 19/5/2015 Initial version