Java Native Interface
Parameters#
Parameter | Details |
---|---|
JNIEnv | Pointer to the JNI environment |
jobject | The object which invoked the non-static native method |
jclass | The class which invoked the static native method |
## Remarks# | |
Setting up JNI requires both a Java and a native compiler. Depending on the IDE and OS, there is some setting up required. A guide for Eclipse can be found here. A full tutorial can be found here. |
These are the steps for setting up the Java-C++ linkage on windows:
- Compile the Java source files (
.java
) into classes (.class
) usingjavac
. - Create header (
.h
) files from the Java classes containingnative
methods usingjavah
. These files “instruct” the native code which methods it is responsible for implementing. - Include the header files (
#include
) in the C++ source files (.cpp
) implementing thenative
methods. - Compile the C++ source files and create a library (
.dll
). This library contains the native code implementation. - Specify the library path (
-Djava.library.path
) and load it in the Java source file (System.loadLibrary(...)
).
Callbacks (Calling Java methods from native code) requires to specify a method descriptor. If the descriptor is incorrect, a runtime error occurs. Because of this, it is helpful to have the descriptors made for us, this can be done with javap -s
.
Calling C++ methods from Java
Static and member methods in Java can be marked as native to indicate that their implementation is to be found in a shared library file. Upon execution of a native method, the JVM looks for a corresponding function in loaded libraries (see Loading native libraries), using a simple name mangling scheme, performs argument conversion and stack setup, then hands over control to native code.
Java code
/*** com/example/jni/JNIJava.java **/
package com.example.jni;
public class JNIJava {
static {
System.loadLibrary("libJNI_CPP");
}
// Obviously, native methods may not have a body defined in Java
public native void printString(String name);
public static native double average(int[] nums);
public static void main(final String[] args) {
JNIJava jniJava = new JNIJava();
jniJava.printString("Invoked C++ 'printString' from Java");
double d = average(new int[]{1, 2, 3, 4, 7});
System.out.println("Got result from C++ 'average': " + d);
}
}
C++ code
Header files containing native function declarations should be generated using the javah
tool on target classes. Running the following command at the build directory :
javah -o com_example_jni_JNIJava.hpp com.example.jni.JNIJava
… produces the following header file (comments stripped for brevity) :
// com_example_jni_JNIJava.hpp
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h> // The JNI API declarations
#ifndef _Included_com_example_jni_JNIJava
#define _Included_com_example_jni_JNIJava
#ifdef __cplusplus
extern "C" { // This is absolutely required if using a C++ compiler
#endif
JNIEXPORT void JNICALL Java_com_example_jni_JNIJava_printString
(JNIEnv *, jobject, jstring);
JNIEXPORT jdouble JNICALL Java_com_example_jni_JNIJava_average
(JNIEnv *, jclass, jintArray);
#ifdef __cplusplus
}
#endif
#endif
Here is an example implementation :
// com_example_jni_JNIJava.cpp
#include <iostream>
#include "com_example_jni_JNIJava.hpp"
using namespace std;
JNIEXPORT void JNICALL Java_com_example_jni_JNIJava_printString(JNIEnv *env, jobject jthis, jstring string) {
const char *stringInC = env->GetStringUTFChars(string, NULL);
if (NULL == stringInC)
return;
cout << stringInC << endl;
env->ReleaseStringUTFChars(string, stringInC);
}
JNIEXPORT jdouble JNICALL Java_com_example_jni_JNIJava_average(JNIEnv *env, jclass jthis, jintArray intArray) {
jint *intArrayInC = env->GetIntArrayElements(intArray, NULL);
if (NULL == intArrayInC)
return -1;
jsize length = env->GetArrayLength(intArray);
int sum = 0;
for (int i = 0; i < length; i++) {
sum += intArrayInC[i];
}
env->ReleaseIntArrayElements(intArray, intArrayInC, 0);
return (double) sum / length;
}
Output
Running the example class above yields the following output :
Invoked C++ ‘printString’ from Java
Got result from C++ ‘average’: 3.4
Calling Java methods from C++ (callback)
Calling a Java method from native code is a two-step process :
- obtain a method pointer with the
GetMethodID
JNI function, using the method name and descriptor ; - call one of the
Call*Method
functions listed here.
Java code
/*** com.example.jni.JNIJavaCallback.java ***/
package com.example.jni;
public class JNIJavaCallback {
static {
System.loadLibrary("libJNI_CPP");
}
public static void main(String[] args) {
new JNIJavaCallback().callback();
}
public native void callback();
public static void printNum(int i) {
System.out.println("Got int from C++: " + i);
}
public void printFloat(float i) {
System.out.println("Got float from C++: " + i);
}
}
C++ code
// com_example_jni_JNICppCallback.cpp
#include <iostream>
#include "com_example_jni_JNIJavaCallback.h"
using namespace std;
JNIEXPORT void JNICALL Java_com_example_jni_JNIJavaCallback_callback(JNIEnv *env, jobject jthis) {
jclass thisClass = env->GetObjectClass(jthis);
jmethodID printFloat = env->GetMethodID(thisClass, "printFloat", "(F)V");
if (NULL == printFloat)
return;
env->CallVoidMethod(jthis, printFloat, 5.221);
jmethodID staticPrintInt = env->GetStaticMethodID(thisClass, "printNum", "(I)V");
if (NULL == staticPrintInt)
return;
env->CallVoidMethod(jthis, staticPrintInt, 17);
}
Output
Got float from C++: 5.221
Got int from C++: 17
Getting the descriptor
Descriptors (or internal type signatures) are obtained using the javap program on the compiled .class
file. Here is the output of javap -p -s com.example.jni.JNIJavaCallback
:
Compiled from "JNIJavaCallback.java"
public class com.example.jni.JNIJavaCallback {
static {};
descriptor: ()V
public com.example.jni.JNIJavaCallback();
descriptor: ()V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
public native void callback();
descriptor: ()V
public static void printNum(int);
descriptor: (I)V // <---- Needed
public void printFloat(float);
descriptor: (F)V // <---- Needed
}
Loading native libraries
The common idiom for loading shared library files in Java is the following :
public class ClassWithNativeMethods {
static {
System.loadLibrary("Example");
}
public native void someNativeMethod(String arg);
...
Calls to System.loadLibrary
are almost always static so as to occur during class loading, ensuring that no native method can execute before the shared library has been loaded. However the following is possible :
public class ClassWithNativeMethods {
// Call this before using any native method
public static void prepareNativeMethods() {
System.loadLibrary("Example");
}
...
This allows to defer shared library loading until necessary, but requires extra care to avoid java.lang.UnsatisfiedLinkError
s.
Target file lookup
Shared library files are searched for in the paths defined by the java.library.path
system property, which can be overriden using the -Djava.library.path=
JVM argument at runtime :
java -Djava.library.path=path/to/lib/:path/to/other/lib MainClassWithNativeMethods
Watch out for system path separators : for example, Windows uses ;
instead of :
.
Note that System.loadLibrary
resolves library filenames in a platform-dependent manner : the code snippet above expects a file named libExample.so
on Linux, and Example.dll
on Windows.
An alternative to System.loadLibrary
is System.load(String)
, which takes the full path to a shared library file, circumventing the java.library.path
lookup :
public class ClassWithNativeMethods {
static {
System.load("/path/to/lib/libExample.so");
}
...