Java Native Interface

jni1

Overview

JNI allows you to use native code when an application cannot be written entirely in the Java language. The following are typical situations where you might decide to use native code:

  • You want to implement time-critical code in a lower-level, faster programming language.
  • You have legacy code or code libraries that you want to access from Java programs.
  • You need platform-dependent features not supported in the standard Java class library.

Six steps to call C/C++ from Java code

The process of calling C or C ++ from Java programs consists of six steps. We’ll go over each step in depth in the sections that follow, but let’s start with a quick look at each one.

  1. Write the Java code. We’ll start by writing Java classes to perform three tasks: declare the native method we’ll be calling; load the shared library containing the native code; and call the native method.
  2. Compile the Java code. We must successfully compile the Java class or classes to bytecode before we can use them.
  3. Create the C/C++ header file. The C/C++ header file will declare the native function signature that we want to call. This header will then be used with the C/C++ function implementation (see Step 4) to create the shared library (see Step 5).
  4. Write the C/C++ code. This step consists of implementing the function in a C or C++ source code file. The C/C++ source file must include the header file we created in Step 3.
  5. Create the shared library file. We’ll create a shared library file from the C source code file we created in Step 4.
  6. Run the Java program. We’ll run the code and see if it works. We’ll also go over some tips for dealing with the more commonly occurring errors.

Step 1: Write the Java code

We’ll start by writing the Java source code file, which will declare the native method (or methods), load the shared library containing the native code, and actually call the native method.

Here’s our example Java source code file, called Sample1.java:

public class Sample1
{
   public native int intMethod(int n);
   public native boolean booleanMethod(boolean bool);
   public native String stringMethod(String text);
   public native int intArrayMethod(int[] intArray);

   public static void main(String[] args)
   {
      System.loadLibrary("Sample1");
      Sample1 sample = new Sample1();
      int square = sample.intMethod(5);
      boolean bool = sample.booleanMethod(true);
      String text = sample.stringMethod("JAVA");
      int sum = sample.intArrayMethod(
                  new int[]{1,1,2,3,5,8,13});
      System.out.println("intMethod: "+square);
      System.out.println("booleanMethod: "+bool);
      System.out.println("stringMethod: "+text);
      System.out.println("intArrayMethod: "+sum);
   }
}

What’s happening in this code?

First of all, note the use of the native keyword, which can be used only with methods. The native keyword tells the Java compiler that a method is implemented in native code outside of the Java class in which it is being declared. Native methods can only be declared in Java classes, not implemented, so a native method cannot have a body.

Now, let’s look at the code line by line:

  • In lines 3 through 6 we declare four native methods.
  • On line 10 we load the shared library file containing the implementation for these native methods. (We’ll create the shared library file when we come to Step 5.)
  • Finally, in lines 12 through 15 we call the native methods. Note that this operation is no different from the operation of calling non-native Java methods.

Note: Shared library files on UNIX-based platforms are usually prefixed with “lib”. In this case, line 10 would be System.loadLibrary("libSample1"); . Be sure to take notice of the shared library file name that you generate in Step 5: Create the shared library file.

Step 2: Compile the Java code

Next, we need to compile the Java code down to bytecode. One way to do this is to use the Java compiler, javac, which comes with the SDK. The command we use to compile our Java code to bytecode is:


$ javac Sample1.java

Step 3: Create the C/C++ header file

The third step is to create a C/C++ header file that defines native function signatures. One way to do this is to use the native method C stub generator tool, javah.exe, which comes with the SDK. This tool is designed to create a header file that defines C-style functions for each native method it finds in a Java source code file. The command to use here is:


$ javah Sample1

Results of running javah.exe on Sample1.java

Sample1.h, below, is the C/C++ header file generated by running the javah tool on our Java code:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Sample1 */

#ifndef _Included_Sample1
#define _Included_Sample1
#ifdef __cplusplus
extern "C"{
#endif

JNIEXPORT jint JNICALL Java_Sample1_intMethod
   (JNIEnv *, jobject, jint);

JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   (JNIEnv *, jobject, jboolean);


JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   (JNIEnv *, jobject, jstring);


JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   (JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif

 

About the C/C++ header file

As you’ve probably noticed, the C/C++ function signatures in Sample1.h are quite different from the Javanative method declarations in Sample1.java. JNIEXPORT and JNICALL are compiler-dependent specifiers for export functions. The return types are C/C++ types that map to Java types.

The parameter lists of all these functions have a pointer to a JNIEnv and a jobject, in addition to normal parameters in the Java declaration. The pointer to JNIEnv is in fact a pointer to a table of function pointers. As we’ll see in Step 4, these functions provide the various faculties to manipulate Java data in C and C++.

The jobject parameter refers to the current object. Thus, if the C or C++ code needs to refer back to the Java side, this jobject acts as a reference, or pointer, back to the calling Java object. The function name itself is made by the “Java_” prefix, followed by the fully qualified class name, followed by an underscore and the method name.

Step 4: Write the C/C++ code

When it comes to writing the C/C++ function implementation, the important thing to keep in mind is that our signatures must be exactly like the function declarations from Sample1.h. We’ll look at the complete code for both a C implementation and a C++ implementation, then discuss the differences between the two.

The C function implementation

Here is Sample1.c, an implementation written in C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 #include "Sample1.h"
 #include <string.h>
 
 JNIEXPORT jint JNICALL Java_Sample1_intMethod
   (JNIEnv *env, jobject obj, jint num) {
    return num * num;
 }
 
 JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   (JNIEnv *env, jobject obj, jboolean boolean) {
   return !boolean;
 }
 
 JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   (JNIEnv *env, jobject obj, jstring string) {
     const char *str = (*env)->GetStringUTFChars(env, string, 0);
     char cap[128];
     strcpy(cap, str);
     (*env)->ReleaseStringUTFChars(env, string, str);
     return (*env)->NewStringUTF(env, strupr(cap));
 }
 
 JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   (JNIEnv *env, jobject obj, jintArray array) {
     int i, sum = 0;
     jsize len = (*env)->GetArrayLength(env, array);
     jint *body = (*env)->GetIntArrayElements(env, array, 0);
     for (i=0; i<len; i++)
     {   sum += body[i];
     }
     (*env)->ReleaseIntArrayElements(env, array, body, 0);
     return sum;
 }
 
 void main(){}

The C++ function implementation

And here’s Sample1.cpp, the C++ implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 #include "Sample1.h"
 #include <string.h>
 
 JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *env, jobject obj, jint num) {
   return num * num;
 }
 
 JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   (JNIEnv *env, jobject obj, jboolean boolean) {
   return !boolean;
 }
 
 JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   (JNIEnv *env, jobject obj, jstring string) {
     const char *str = env->GetStringUTFChars(string, 0);
     char cap[128];
     strcpy(cap, str);
     env->ReleaseStringUTFChars(string, str);
     return env->NewStringUTF(strupr(cap));
 }
 
 JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   (JNIEnv *env, jobject obj, jintArray array) {
     int i, sum = 0;
     jsize len = env->GetArrayLength(array);
     jint *body = env->GetIntArrayElements(array, 0);
     for (i=0; i<len; i++)
     {   sum += body[i];
     }
     env->ReleaseIntArrayElements(array, body, 0);
     return sum;
 }
 
 void main(){}

C and C++ function implementations compared

The C and C++ code is nearly identical; the only difference is the method used to access JNI functions. In C, JNI function calls are prefixed with “(*env)->in order to de-reference the function pointer. In C++, theJNIEnv class has inline member functions that handle the function pointer lookup. This slight difference is illustrated below, where the two lines of code access the same function but the syntax is specialized for each language.

C syntax:       jsize len = (*env)->GetArrayLength(env,array);

C++ syntax:  jsize len = env->GetArrayLength(array);

Step 5: Create the shared library file

Next, we create a shared library file that contains the native code. Most C and C++ compilers can create shared library files in addition to machine code executables. The command you use to create the shared library file depends on the compiler you’re using. Below are the commands that will work on Windows and Solaris systems.

Windows:  cl -Ic:\jdk\include -Ic:\jdk\include\win32 -LD Sample1.c -FeSample1.dll

Solaris:       cc -G -I/usr/local/jdk/include -I/user/local/jdk/include/solaris Sample1.c -o Sample1.so

Step 6: Run the Java program

The last step is to run the Java program and make sure that the code works correctly. Because all Java code must be executed in a Java virtual machine, we need to use a Java runtime environment. One way to do this is to use the Java interpreter, java, which comes with the SDK. The command to use is:

1
java Sample1

When we run the Sample1.class program, we should get the following result:

$java Sample1
intMethod: 25
booleanMethod: false
stringMethod: JAVA
intArrayMethod: 33

Troubleshooting

You can run into many problems when using JNI to access native code from Java programs. The three most common errors you’ll encounter are:

  • A dynamic link cannot be found. This results in the error message:java.lang.UnsatisfiedLinkError. This usually means that either the shared library cannot be found, or a specific native method inside the shared library cannot be found.
  • The shared library file cannot be found. When you load the library file using the file name with theSystem.loadLibrary(String libname) method, make sure that the file name is spelled correctly and that you do not specify the extension. Also, make sure that the library file is accessible to the JVM by ensuring that the library file’s location is in the classpath.
  • A method with the specified signature cannot be found. Make sure that your C/C++ function implementation has a signature that is identical to the function signature in the header file.

Conclusion

Calling C or C++ native code from Java, while not trivial, is a well-integrated function in the Java platform. Although JNI supports both C and C++, the C++ interface is somewhat cleaner and is generally preferred over the C interface.As you have seen, calling C or C++ native code requires that you give your functions special names and create a shared library file. When taking advantage of existing code libraries, it is generally not advisable to change the code. To avoid this, it is common to create proxy code, or a proxy class in the case of C++, that has the specially named functions required by JNI. These functions, then, can call the underlying library functions, whose signatures and implementations remain unchanged.

Leave A Comment

Your email address will not be published. Required fields are marked *