The time always comes when a programmer must jump the gap between the Java world contained within the Java Virtual Machine (JVM) and the world of the low-level applications programming interface (API) through which hardware services are accessed. Usually, the Java Native Interface (JNI) is the preferred way to access non-Java routines, but its use is often seen as arcane and mysterious. This need not be the case; the JNI can be accessed through comparatively straightforward and effective techniques.
Basically, the JNI allows Java code that runs within a Java Virtual Machine (JVM) to operate with applications and libraries written in other languages, such as C, C++, and assembly. In addition, the Java Invocation API allows the inclusion of the JVM into native applications and allows those native applications to access the JVM. The JNI is used to write native methods to handle those situations when an application cannot be written entirely in Java.
Native methods accessible from the JNI could be useful in the following situations:
- The standard Java class library may not support the platform-dependent features needed by an application .
- It may be deemed more effective to implement a small section of time-critical code in a lower-level programming language, such as assembly, and then have a Java application invoke the section directly.
- A library or application written in another programming language may already be available and it is considered unwise to rewrite it in Java.
Native methods may represent legacy applications or they may be written explicitly to solve a problem that is best handled outside of the Java programming environment.
The JNI framework lets a native method use Java objects in the same way that Java code uses these objects. Essentially, a native method can create Java objects, including arrays and strings, and then inspect and use these objects to perform its tasks. It can also inspect and use objects created by Java application code and can even update Java objects that it created or that were passed to it. These updated objects are available to the Java application. Thus, both the native language side and the Java side of an application can create, update, and access Java objects and then share these objects between them
Native methods can also easily call Java methods. Often there will already exist a library of Java classes and methods which support a particular problem domain. A native method does not need to "re-invent the wheel" to deliver functionality already incorporated in existing Java methods. It uses the JNI framework, can call the existing Java method, pass it the required parameters, and get the results back when the method completes.
Writing native methods for Java programs is a multi-step process:
- Begin by writing the Java program. Create a Java class that declares the native method; this class contains the declaration or signature for the native method. In this example, it also includes a main method which calls the native method.
- Compile the Java class that declares the native method and the main method.
- Generate a header file for the native method using the native interface flag. Once you've generated the header file, you have the formal signature for your native method.
- Write the implementation of the native method in the programming language of your choice, such as C or C++.
- Compile the header and implementation files into a shared library file.
- Run the Java program.
All methods must be declared, whether written in Java or in native code, within a class on the Java side. When a method is implemented in a language other than Java, the keyword native is required as part of the method's definition within the Java class. The native keyword signals to the Java compiler that the function is a native language function.
The implementation of the native function, as it appears in the header file, accepts two parameters even though, in its definition on the Java side, it accepts no parameters. The JNI requires every native method to have these two parameters.
The first parameter for every native method is a JNIEnv interface pointer. It is through this pointer that your native code accesses parameters and objects passed to it from the Java application. The second parameter is jobject, which references the current object itself.
In a sense, the jobject parameter is the "this" variable in Java. For a native instance method, the jobject argument is a reference to the current instance of the object. For native class methods, this argument would be a reference to the method's Java class.
Mapping between Java and native types is a progressive, step-by-step process: First, get a handle to the class to which the object belongs, then get a handle to the field within an instance of that class, then get the value of the field in a type-specific manner. Once a field ID is obtained it could be used subsequently without having to be fetched again.
In addition to instant methods, the same pattern applies to class methods except that the instance pointer obj will then indicate the class as a whole rather than a particular object which is an instance of the class. Additionally, a similarly named set of methods exist to get and set static values, such as GetStaticIntField() and SetStaticIntField().
The JNI performs a symbolic lookup based on the method's name and type signature. This ensures that the same native method will work even after new methods have been added to the corresponding Java class. The Java disassembly tool, javap, helps eliminate the mistakes that can occur when deriving method signatures by hand. This tool can be used to print out member variables and method signatures for any class.
For character representations, the method name is the Java method name in UTF-8 form. Enclosing the word init within angle brackets () specifies the constructor of a class. The angle brackets are required because there may be, and often is, a method called init().
In order to invoke a Java method from within JNI code, a method ID must be obtained, similar to what was done when getting a field ID for an attribute. A method ID is valid only for as long as the class from which it is derived is not unloaded. If the method being invoked has no arguments, that is all we need.
Invoking a method where one or more arguments are passed gets a bit more complex. In particular, there are two variations on invoking a Java method with parameters. The first of these uses the va_list structure familiar to C systems programmers. The second uses the jvalue union.
The JNI provides a complete interface for invoking routines outside of the JVM, especially important to embedded systems programming in Java. Additionally, it allows routines outside of the JVM to invoke both class and instance methods inside the JVM.
While initially formidable in appearance, the JNI is actually straightforward, if a little busy. By following simple examples, the embedded systems programmer can manage to get into and out of the JVM whenever necessary.