Use native methods to expand the Java environment

Learn how to interact with libraries and applications written in other languages

The JNI provides a documented and supported specification that allows programs written in other languages to be called from Java. Calling Java from an application written in another language is often referred to as embedding, and the process requires an understanding of the Invocation API. The JNI also provides a way for applications written in other languages to call Java.

Before we get too deep into our discussion, I'd like to provide some background on how native methods worked prior to the release of the JNI. In short, before JDK 1.1 native methods were difficult to use because:

  • The specification did not interface to the Java virtual machine (VM) at runtime for symbol lookup. It was assumed that symbols would be in an include file and compiled in at compile-time. Unfortunately, this is not the way object environments work with dynamic linking. This approach also created an opportunity for out-of-date source files, and prevented dynamic changes to values at runtime.

  • The interface for embedding was vague.

  • There was no support for pinning (preventing garbage collection and movement of Java objects).

  • Java strings were converted to pointers without support for large character sets, making internationalization impossible.

  • The nomenclature was terribly confusing.

With the introduction of the JNI in JDK 1.1, native methods have become much easier to use. To help you better understand the JNI, we'll review portions of the actual JNI spec to explain why you need to use it and what it allows you to do. The italicized items are direct quotes from the JNI specification, and the regular text is the annotation I've supplied for clarity. Once you know the "why" and "what" of using the JNI, we'll focus on the "how." Note that for brevity's sake I make no attempt to fully explain the JNI specification. If you feel you need a primer on the JNI, be sure to review the complete specification (and also the tutorial), which is listed in the Resources section of this article.

Why do I need to use the JNI?

  • The standard Java class libraries do not support the platform-dependent features needed by the application.

    You might need to set some device-specific properties under program control, or access a device-specific API.

  • You already have a library written in another language, and wish to make it accessible to Java code through the JNI.

    You may want to rewrite the necessary application in Java, but this is not possible in all cases. Many application libraries on the 'Net have taken an enormous amount of time to perfect (we're talking hundreds of thousands of hours); rewriting them in Java is not likely in the short term.

    Unfortunately, using native methods may not be the best solution. When you use native methods in computation-intensive tasks like tight loops, which occur when you repeatedly call a function, you may find that the process of setting up parameters, calling the method, and getting the return values for each iteration of the loop takes longer than the time it takes to execute the function.

  • You want to implement a small portion of time-critical code in a lower-level language such as Assembly.

    You have written a Monte Carlo application that simulates your portfolio, but it runs too slowly. You've tried the JIT (just-in-time) compiler, but it just doesn't do it for you.

What does JNI allow me to do?

  • Create, inspect, and update Java objects (including arrays and strings).

    If you are using the Invocation API for embedding you can take a two-dimensional array of strings and pass it to the Java program. If your C program is called from Java, you can create and return an array of floats or an array of strings that will be understood by the Java calling method.

  • Call Java methods.

    The ability to call a Java method from C/C++ or Assembler is essential to the Invocation API. These JNI functions allow us to call static and non-static Java methods with arguments.

  • Catch and throw exceptions.

    Good programmers raise and catch exceptions. Using C/C++ or Assembler native code to raise exceptions makes for more understandable programs.

  • Load classes and obtain class information.

    A foreign-language program can request information about the class being loaded.

  • Perform runtime type checking.

    The parameters are checked at runtime, not statically as they were in the previous implementation.

Putting the JNI to use once you know when and why to use it is no small task. Proper use requires that you understand numerous technical details. My advice is to pour over as many examples as possible and read the specification in its entirety. (In fact, it may be helpful to view the pdf version of the spec in another window while you work through this article.) When you feel comfortable with the JNI fundamentals, the rest of this article should fall into place.

Calling Java code from C programs

Note: Depending on which language you are using to call Java, the terminology you use may differ from what I use in this section. Please substitute the appropriate differences (for example, function vs. method vs. subroutine) for the language you're using. Because I am unwilling to rewrite useful pieces of Java code in C or C++ for my C programs, I often call Java directly from my C programs. Building a C/C++ or Assembler application that calls a Java method is much less involved than writing a C program to be called from a Java application. As you'll see later in the article, the latter technique requires us to make any modifications to the Java application. In this case, however, we drive the Java application from C, so to speak.

For the C program to communicate with the Java program, we need to use several JNI method calls (more detail on these in a moment). Let's take a look at the steps required to interface to an existing Java application:

  1. Create the Java VM.

    To create the JVM you call JNI_CreateJavaVM(), which returns a pointer to the JNI interface pointer. The calling thread is considered the parent thread. Once you have created a VM, you can call Java methods using the standard calling procedure of getting an object, finding its class, getting the method id, and then calling the method id with the appropriate method.

  2. Start the Java application.

  3. Pass parameters to the Java application and retrieve parameters from the Java application.

  4. Attach to the VM.

    This step is optional and used only if you wish to attach to the VM from another thread. In this case, you will have multiple threads running in your C application, each of which needs to use the Java application (for example, a Java application that provides some service used by many competing processes).

  5. Unload the VM.

    The calling thread must unload the VM.

The JNI specification details a concept called embedding. The idea with embedding is to create a Java VM that exists in your address space bound to a single thread of the target OS. (On some systems, specifically those that support thousands of threads, embedding may not be the optimum solution.) "But what about multithreading?," you ask. Embedding does not do away with multithreading; what it does do, however, is allow the Java threads from the VM to be visible to only the calling thread -- unless additional methods are invoked. (This is an advanced concept and will not be discussed further in this article. For additional information, see The Invocation API in Chapter 5 of the JNI specification 1.1.)

We're going to be working through a fully debugged example, which I encourage you to modify for your own purposes. This example doesn't do very much other than demonstrate how to:

  1. Start the VM.

  2. Reference methods in the application associated with the VM.

  3. Pass objects back and forth between the two environments. In some cases, the objects may not really be objects -- in C, for instance, you might pass in a structure.

The example consists of two parts: InstantiatedFromC.java, a Java class that implements some operation we wish to use from C; and tcl2JavaVM.c, the C program in which we wish to use the Java class. The reason for the name tcl2JavaVM.c is for a future plan to allow TCL/TK to use existing Java classes.

We'll start by specifying an include file that defines the JNI typedefs and defines for the language being used. (You can find this include file in the $JAVA_HOME/lib/include directory, which is included in the JDK developer distribution.) This include file has enough intelligence to determine if you are using C or C++ and generates the appropriate code.

 
#include <jni.h>

The following variables provide us with a handle to the VM, the default starting arguments, and a debugging aid. I'll leave it to you to determine how you would specify options -- other than the default options -- to the VM using the vm_args variable.

JavaVM           *jvm;             /* Pointer to a Java VM */
JNIEnv           *env;             /* Pointer to native method interface */
JDK1_1InitArgs   vm_args;          /* JDK 1.1 VM initialization requirements */
int              verbose = 1;      /* Debugging flag */ 

Not much to explain here; the variables do pretty much what the comments indicate:

  • *jvm points to the Java VM that we'll create.

  • *env points to the native method interface of the VM.

  • vm_args contains args and other options, such as the CLASSPATH.

  • verbose = 1 is a debugging flag that allows debugging information to be turned on and off.

Now let's take a look at the main entry point of the C program. When the computer begins executing tcl2JavaVM, a Java VM will be started with default arguments.

main(int argc, char **argv ) {

jclass cls; jmethodID mid; jthrowable jthr;

/* Set up the environment */ JNI_GetDefaultJavaVMInitArgs ( &vm_args ); // Get the default arguments // Look at pages 75-76 of the JNI spec vm_args.classpath = "H:/opt/jdk/lib/classes.zip;."; // Set the classpath // equivalent to typing -classpath // on command line or setting the // environment variable JNI_CreateJavaVM(&jvm, &env, &vm_args ); // Start the VM

We also allocate some local variables to hold references to Java objects: jclass is a C reference to a Java class, jmethodID is a C reference to a Java method, and jthrowable is a C reference to Java.lang.Throwable. Page 19 of the JNI specification contains an excellent diagram that shows how to reference Java objects from C at the type level.

Next, we need to find the class we want to load.

/* Find the class we want to load */

cls = (*env)->FindClass( env, "InstantiatedFromC" );

The FindClass method causes the Java VM to search for the class specified in the second argument (in this case, InstantiatedFromC). For clarity, I have left out the check for the error condition; be sure you perform the check for cls being NULL -- in C, of course, as shown below:

if ( cls != (jclass)0 ) {

Assuming we have found the class, our next step is to get a pointer to the method we are interested in calling.

/* Find the method we want to use */
mid = (*env)->GetMethodID( env, cls, "test", "(I)I" );

It seems reasonable that you have to specify the class, the method name, and its signature. This technique simply replaces the code you would normally use for calling a method with a mechanism for doing it from C. In Java, you simply create a reference to the class, unless the method in question is static, in which case you can call it without creating an instance.

Now we are ready to call the Java method from C. The relevant portion of code in the following snippet is indicated with bold type.

/* Call the method we want to use */
printf("First call to Java returns:%d\n", (*env)->CallStaticIntMethod(env, cls, mid, 1) );
/* Check for an exception */ 
if ( hasExceptionOccurred ( env ) != (char *)0 ) {
  printf("Exception has occurred.\n");
}

The Java method we are trying to call is shown below. This method returns the input argument +1, as long as the result is not 2. If it is 2, an exception is raised.

public class InstantiatedFromC {

public int test(int number) throws Exception { System.out.println("Number from C: " + number); if ( number == 2 ) throw new Exception("Exception raised in Java seen in C"); return ( number + 1 ); } }

The code segment shown next is identical to the previous one, except that the argument we provide to the Java method is 2 instead of 1. I wanted to demonstrate how to get exception messages from Java in a C program. In this case, when the Java method returns, we call the C method that attempts to determine if there is an exception. We find that there is indeed an exception. A number of error conditions can occur while we are trying to get the exception, and it is important not to confuse these with the error condition we are trying to examine.

/* Call the method we want to use and raise an exception */
printf("Second call to Java returns:%d\n", (*env)->CallStaticIntMethod(env, cls, mid, 2) );
/* Check for an exception */ 
if ( hasExceptionOccurred ( env ) != (char *)0 ) {
  printf("Exception has occurred.\n");
}

The C function hasExceptionOccurred( env ) returns 0 if no exception was raised. If there is an error while attempting to process the exception response, the function hasExceptionOccurred will stop execution and will terminate itself with an exit call.

1 2 3 Page 1