The Java Native Interface (JNI) is a Java layer that allows
Java code running in the Java Virtual Machine (JVM) to call and be called by native applications and
libraries written in other languages, such as C, C++, and assembly. Developers
use the JNI to write native methods to handle situations when an application
cannot be written entirely in Java, such as when the standard Java class
library does not support the platform-dependent features or program library. It
is also used to modify an existing application, written in another programming
language, to be accessible to Java applications.

Many of the
standard library classes depend on the JNI to provide functionality to the
developer and the user, e.g., I/O file reading and sound capabilities. The
inclusion of performance- and platform-sensitive API implementations in the
standard library allows all Java applications to access this functionality in a
safe and platform-independent manner. Before resorting to using the JNI, developers should make
sure the functionality is not already provided in the standard libraries. In
this introductory article, I will cover how the JNI works and how native types
are mapped to Java types and classes.

How the JNI works

In the JNI,
native functions are implemented in a separate .c or .cpp
file. (C++ provides a slightly cleaner interface with the JNI.) When the JVM
invokes the function, it passes a JNIEnv pointer, a jobject pointer, and any Java arguments declared by the
Java method. A JNI function may look like this:

JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobjectobj)
{
    //Method native implemenation
}

The env pointer is a
structure that contains the interface to the JVM. It includes all of the
functions necessary to interact with the JVM and to work with Java objects.
Example JNI functions are converting native arrays to and from Java arrays,
converting native strings to and from Java strings, instantiating objects,
throwing exceptions, etc. Basically, you can use JNIEnv to do anything that Java does,
albeit with considerably less ease.

To be more
formal, native code accesses JVM features by calling JNI functions, which are
available through an interfacepointer (this is a pointer to a pointer).
This pointer points to an array of
pointers, each of which points to an interface function. Every interface
function is at a predefined offset inside the array. Native methods receive the
JNI interface pointer as an argument. The JVM is guaranteed to pass the same interface
pointer to a native method when it makes multiple calls to the native method
from the same Java thread. However, a native method can be called from
different Java threads, and therefore may receive different JNI interface
pointers.

Native methods are loaded with the System.loadLibrary
method. In the following example, the class initialization method loads a
platform-specific native library in which the native method f is defined:

packagepkg;  
class Cls {
     native double f(inti, String s);
     static {
         System.loadLibrary(pkg_Cls");
     }
}

The argument
to System.loadLibrary is a library name chosen arbitrarily by the
programmer. The system follows a standard, platform-specific approach to
convert the library name to a native library name. For example, a
Solaris system converts the name pkg_Cls to libpkg_Cls.so, while a Win32 system converts the same pkg_Cls name to pkg_Cls.dll.

Get developer tips in your inbox

Delivered each Thursday, our free Java newsletter provides insight and hands-on tips you need to unlock the full potential of this programming language.

Automatically sign up today!

Dynamic linkers
resolve entries based on their names. A native method name is concatenated from
components, which include: the prefix Java_,
a mangled fully-qualified class name, and a mangled method name.

Note: Microsoft’s implementation of a JVM has a
similar mechanism for calling native Windows code from Java called the Raw
Native Interface (RNI).

Data type mapping

Primitive types,
such as integers, characters, and so on, are copied between Java and native
code. Arbitrary Java objects, on the other hand, are passed by reference.

This table shows the mapping of types between Java and native code. These types are interchangeable. You can use jint where you normally use an int,
and vice-versa without any typecasting required. However, mapping between Java
Strings and arrays to native strings and arrays is different. If you use a jstring
where a char * would be, your code could crash the JVM. This is an example of
how you should work correctly with strings:

JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobjectobj, jstringjavaString)
{
    //Get the native string from Java string
    const char *nativeString = env->GetStringUTFChars(env,javaString, 0);
    printf("%s", nativeString);
    env->ReleaseStringUTFChars(env,javaString, nativeString);
}

You always
manipulate Java objects using the interface pointer env.

Conclusion

It is not particularly easy to implement the JNI in your application. However, its performance
and its ability to preserve legacy code and add more functionality to your Java
app may outweigh its challenges. For more detailed information, visit the JNI home page.

Peter V. Mikhalenko is a Sun certified professional who works for Deutsche Bank as business consultant.