Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VisualC++

Calling Java from C++ with JNI

4.74/5 (30 votes)
19 May 2015Zlib8 min read 243.1K   3.9K  
A tutorial to master the Java Native Interface with C++

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.

[SCREENSHOT]

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:

C++
#include <jni.h>

int 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).

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:

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:

Java
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:

C++
...
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;
    }
}

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:

Java
Class MyTest {
    ...
    public static int mymain2(int n) {   // <== add this new function
        for (int i=0; i<n; i++)  {    
            System.out.print (i);
            System.out.println("Hello, World !");
        }
        return n*2;                    // return twice the param
    }
}

Calling this function from C++ is very similar to the previous example:

C++
//... 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;
}

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:

Java
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:

C++
//... 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 strings to populate it. We do this in the following way:

C++
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).

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:

Java
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:

C++
jmethodID ctor = env->GetMethodID(cls2, "<init>", "()V");  // FIND AN OBJECT CONSTRUCTOR
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:

C++
    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:

Java
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++:

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:

C++
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.

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
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

License

This article, along with any associated source code and files, is licensed under The zlib/libpng License