For a small research project I need to invoke Java from C++. More precisely, in the C++ code I need to create an object by calling a constructor and then invoke some methods. Surprisingly, I was not able to find a lot of information on-line. There are a number of tutorials to call C/C++ from Java, but not much for the other direction and very little for Java 8 on Mac. This is a very quick summary of what I’ve done, taken mainly from http://www.ibm.com/developerworks/java/tutorials/j-jni/j-jni.html and modified appropriately. Let’s start from a simple Java class, something like this:
package com.example.simple; import java.util.List; import java.util.ArrayList; public class SimpleJNITest { List values; public SimpleJNITest() { values = new ArrayList(); } public void addValue(String v) { values.add(v); } public int getSize() { return values.size(); } public void printValues() { for (String v: values) { System.out.println("Value: " + v); } } |
This is nothing special: a simple Java class with a constructors and a few methods to add values to a list, get the list size, and print the values of the list to standard output. Save this file to SimpleJNITest.java in the directory com/example/simple (I assume you are working at the root of this directory). Now, let’s suppose you need to do the following from a C++ file:
- Create a new object of type SimpleJNITest.
- Add some values to this object.
- Retrieve the number of values currently stored.
- Invoke the printValues method to print values to screen.
The solution is the following (comments in the code. Make sure you read all the comments carefully before asking questions!).
/* Remember to include this */ #include <jni.h> #include <cstring> /* This is a simple main file to show how to call Java from C++ */ int main() { /* The following is a list of objects provided by JNI. The names should be self-explanatory... */ JavaVMOption options[1]; // A list of options to build a JVM from C++ JNIEnv *env; JavaVM *jvm; JavaVMInitArgs vm_args; // Arguments for the JVM (see below) // This will be used to reference the Java class SimpleJNITest jclass cls; // This will be used to reference the constructor of the class jmethodID constructor; // This will be used to reference the object created by calling // the constructor of the class above: jobject simpleJNITestInstance; // You may need to change this. This is relative to the location // of the C++ executable options[0].optionString = "-Djava.class.path=.:./"; // Setting the arguments to create a JVM... memset(&vm_args, 0, sizeof(vm_args)); vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; long status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); if (status != JNI_ERR) { // If there was no error, let's create a reference to the class. // Make sure that this is in the class path specified above in the // options array cls = env->FindClass("com/example/simple/SimpleJNITest"); if(cls !=0) { printf("Class found \n"); // As above, if there was no error... /* Let's build a reference to the constructor. This is done using the GetMethodID of env. This method takes (1) a reference to the class (cls) (2) the name of the method. For a constructor, this is <init>; for standard methods this would be the actual name of the method (see below). (3) the signature of the method (its internal representation, to be precise). To get the signature the quickest way is to use javap. Just type: javap -s -p com/example/simple/SimpleJNITest.class And you will get the signatures of all the methods, including the constructor (check the "descriptor" field). In the case of the constructor, the signature is "()V", which means that the constructor does not take arguments and has no return value. See below for other examples. */ constructor = env->GetMethodID(cls, "<init>", "()V"); if(constructor !=0 ) { printf("Constructor found \n"); /* If there was no error, let's create an instance of the SimpleTestJNI by calling its constructor */ jobject simpleJNITestInstance = env->NewObject(cls, constructor); if ( simpleJNITestInstance != 0 ) { /* If there was no error, let's call some methods of the newly created instance */ printf("Instance created \n"); // First of all, we create two Strings to be passed // as argument to the addValue method. jstring jstr1 = env->NewStringUTF("First string"); jstring jstr2 = env->NewStringUTF("Second string"); /* Then, we create a reference to the addValue method. This is very similar to the reference to the constructor above. As above, you can get the signature of the addValue method with javap. In this case, the method takes a String as input and does not return a value (V is for void) */ jmethodID addValue = env->GetMethodID(cls, "addValue", "(Ljava/lang/String;)V"); /* Finally, let's call the method twice, with two different arguments. (it would probably be a good idea to check for errors here... This is left as a simple exercise to the student ;-). */ env->CallObjectMethod(simpleJNITestInstance , addValue, jstr1); env->CallObjectMethod(simpleJNITestInstance , addValue, jstr1); /* Let's now call another Java method: printValues should print on screen the content of the List of values in the object. The pattern is identical to the one above, but no arguments are passed. */ jmethodID printValues = env->GetMethodID(cls, "printValues", "()V"); env->CallObjectMethod(simpleJNITestInstance , printValues); /* Finally, let's extract an int value from a method call. We need to invoke CallIntMethod and we are going to use getSize of simpleJNITestInstance. Notice the signature: the method returns an int and it does not take arguments */ jint listSize; jmethodID getSize = env->GetMethodID(cls, "getSize", "()I"); listSize = env->CallIntMethod(simpleJNITestInstance , getSize); printf("The size of the Java list is: %d\n", listSize); } } else { printf("I could not create constructor\n"); } } jvm->DestroyJavaVM(); printf("All done, bye bye!\n"); return 0; } else return -1; } |
The code in itself is not too difficult. The main problem is getting the correct location of the various header files and libraries… To make the whole thing run, save the code above in a file (call it test.cpp) and do the following:
- Compile the Java class: this is easy, just type
javac com/example/simple/SimpleJNITest.java
- Compile the C++ file: this is slightly more complicated because you need to specify various paths. The command on my machine (Mac OSX Mavericks, Java 1.8, gcc) is the following:
g++ -o test \ -I/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include \ -I/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/darwin \ -L/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/server/ \ test.cpp \ -ljvm
You can remove the backslash at the end of each line if you write the command on a single line. The two -I directives tell where to find jni.h and jni_md.h; the -L (and -l) directive are used by the linker to find libjvm.dylib. If the compilation is successful, you should get an executable called test.
- Before running test, you need to set LD_LIBRARY_PATH with the following instruction:
export LD_LIBRARY_PATH=/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/server/
- From the same terminal where you have executed the command above, just type ./test.
Et voila, job done, if everything is OK you should see something like this:
$ ./test Class found Constructor found Instance created Value: First string Value: First string The size of the Java list is: 2 All done, bye bye!