An introduction to the Java Native Interface
by
, April 6th, 2011 at 03:20 PM (8565 Views)
The Java Native Interface is a way for users to run native code (i.e. code not JIT-ed/interpreted by a JVM). These are typically written in C/C++ (I'm not positive if this is a requirement, but they're definitely the most common languages used with JNI).
In this post, I will give a brief introduction to using JNI, when to use JNI, and some mistakes that could happen when using JNI. For the purposes of this simple tip, a focus is given on Java invoking native code, however the JNI also allows native code to create and use the JVM.
Setting up the Java side
In order to use JNI, Java provides the native keyword. This declares that a particular method should not be implemented in Java, but rather should be looked for in a native library. Note that native methods can be declared with all the standard method modifiers (public, protected, private, static, and final), with the exception that native methods cannot be declared as abstract.
Note that even though a native method cannot be declared as abstract (which obviously makes no sense), native methods can be overridden with either native or non-native methods, as well as non-native methods can be overridden with native methods.
Now that you have the native method declarations setup, you must tell Java where to look for the libraries to run these native methods from. This is done using the LoadLibrary method. Typically, this is done using a static initializer block. This allows the library to "automatically" be loaded when the class is loaded, rather than having to call a method which will load the required libraries.
A few things to note: Even though it's typical to load a library in a static initializer block, it's not required. The only requirement is that the library be loaded before a native method in that library is called. Also, there is a lack of an extension on the loaded library. The JVM will automatically append the correct extension based off of the running OS. This is to make Java as cross-platform as possible, even though native libraries must be compiled for each different platforms. For more information on this, see the further considerations section.
Calling a JNI method is no different from calling a regular method.
Setting up the native side
The first step is to create the appropriate header. Fortunately, Java provides a tool call javah which will generate the required header to include in your C/C++ project. The javah tool requires you to create the native library from compiled java classes (.class files). Note that various IDE's may have internal or external tools for generating header files, however here I'm going to stick to the command-line. Note that this is the command for Windows, the command for other OS's are similar in every way except for where to find javah (do a search if you don't know how to run a program from the command-line in your OS).
"%JAVAHOME%"\bin\javah tips.JNITest
JAVAHOME points to the base folder of the JDK installation.
A brief message on packages:
As with other Java command-line tools, you should set the classpath to the base folder. Depending on the tool, this is either the bin folder, or the src folder. In the above example, say my folder structure was like this:
- Project_Folder
- bin
- tips
- JNITest.class
- src
- tips
- JNITest.java
So, for this structure, the classpath should be ..\Project_Folder\bin\. Note that the current directory is the default classpath.
The javah tool then looks for classes as if you were importing a class (In the above example, it would be javah tips.JNITest)
The generated header files are put in the base bin folder.
A quick inspection of the generated file:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class tips_JNITest */ #ifndef _Included_tips_JNITest #define _Included_tips_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class: tips_JNITest * Method: printHelloWorld * Signature: (Ljava/lang/String;IZ)V */ JNIEXPORT void JNICALL Java_tips_JNITest_printHelloWorld (JNIEnv *, jclass, jstring, jint, jboolean); /* * Class: tips_JNITest * Method: getName * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_tips_JNITest_getName (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
It's not the most important thing to know exactly what this header file does, but there are a few things that should be noted.
This includes the JNI header file which lets the C++ compiler know how to compile and deal with Java objects, as well as define the calling convention and export the library symbols. It's found at "%JAVAHOME%"\include. This path should be added to your compiler's include path. Note that sometimes there may be a secondary folder inside the include path (for example, in windows there's a win32 folder). If your compiler does not recursively search these folders, you must explicitly add them to the include path or else the build will fail.#include <jni.h>
JNIEXPORT void JNICALL Java_tips_JNITest_printHelloWorld (JNIEnv *, jclass, jstring, jint, jboolean);
This is a declaration of our method. The rather verbose name is there to allow the JVM to find the correct method when it loads the library. The JNIEnv parameter is always included. This is a variable which allows the native library to access/modify the JVM and the runtime environment. The second parameter is either the class which this method belongs to, or it's the object this method is being called from. It differs between the two depending on if the method was declared static or instanced. The remaining parameters are the arguments passed to the method.
As with most C/C++ formatting styles, it's usually desired that the implementation and the declarations be in separate files.
So create a cpp source file and include this generated header, then you can go ahead and implement your methods as you wish./* DO NOT EDIT THIS FILE - it is machine generated */
#include "tips_JNITest.h" #include <iostream> #include <string> /* * Class: tips_JNITest * Method: printHelloWorld * Signature: (Ljava/lang/String;IZ)V */ JNIEXPORT void JNICALL Java_tips_JNITest_printHelloWorld (JNIEnv *env, jclass c, jstring name, jint age, jboolean ask_kindly) { jsize name_length = env->GetStringUTFLength(name); const char* name_c_str = env->GetStringUTFChars(name, false); if(ask_kindly) { std::cout << "hello " << name_c_str << ". I have been happy to have known you for " << age << " years." << std::endl; } else { std::cout << "hello " << name_c_str << ". These last " << age << " years have been miserable because of you." << std::endl; } } /* * Class: tips_JNITest * Method: getName * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_tips_JNITest_getName (JNIEnv *env, jclass c) { std::cout << "what is your name? "; std::string name; std::getline(std::cin, name); return env->NewStringUTF(name.c_str()); }
Working with Java variables in the native code
Working with Java variables passed to a native method is an interesting prospect. Primitive values are simply typedef'd, so you can use these as you normally would in C/C++. However, if you open up the declaration of some basic classes, you'll notice that the declarations are completely empty!
This is because the parameters passed (for example, jstring) are just addresses of the underlying object in the JVM. It's difficult to work with this data securely, so it's recommend to not touch this. Instead, you can modify these variables using the JNIEnv variable provided.
std::cout << "what is your name? "; std::string name; std::getline(std::cin, name); return env->NewStringUTF(name.c_str());
Notice how the underlying question is asked using C++ mechanisms, and then the results are returned using the JNIEnv object to create the appropriate java.lang.String object. A similar method is used in the first method to extract the string characters from a java.lang.String and put it into a std::string which is easy to work with in C++.
The last step is compiling your project as a dynamic library, and then placing it somewhere that the JVM can find.
Additional Considerations
While Java may be fairly platform-independent, the compiled native libraries aren't. So how do we deal with this to make our program as platform-independent as possible? Obviously, the native library has to be re-compiled (and possible re-implemented) to deal with the differences between platforms and computer architectures. However, the main problem is getting Java to determine what library to load.
Java will automatically try to load files with the correct extension, but this will only partially solve the OS problem (Linux/Solaris both use the .so extension), and completely ignores the chip architecture.
A good way to fix this is to name your libraries appropriately, then on runtime have Java determine what os it's running on and the system architecture, then loading the appropriate libraries. This wasn't my idea, but rather was presented here. The code provided below does vary slightly, but the same idea was used.
static { String os = System.getProperty("os.name").toLowerCase(); String arch = System.getProperty("os.arch").toLowerCase(); String lib_os_name = null; String lib_arch_name = null; if (os.contains("window")) { lib_os_name = "win"; } else if (os.contains("mac")) { lib_os_name = "mac"; } else if (os.contains("linux")) { lib_os_name = "linux"; } else if (os.contains("sun") || os.contains("solaris")) { lib_os_name = "solaris"; } else { // don't know what type of library we have throw new RuntimeException("Error! Unknown OS, can't load appropriate library"); } if (arch.contains("ppc")) { if (arch.contains("64")) { lib_arch_name = "ppc64"; } else { lib_arch_name = "ppc32"; } } else if (arch.contains("64")) { lib_arch_name = "x64"; } else if (arch.contains("sparc")) { lib_arch_name = "sparc"; } else if (arch.contains("86") | arch.contains("32")) { lib_arch_name = "x86"; } else { throw new RuntimeException("Error! Unknown architechture, can't load appropriate library"); } System.loadLibrary("JNITestLibrary_" + lib_os_name + "_" + lib_arch_name); }
Note that the above code doesn't cover every OS or computer architecture. Feel free to add/remove any items you don't wish to support with your application.