COMP2100/2500/6442

Lecture 23: The Java/C Interface

Summary

How to incorporate C code into Java projects.

Outline


Introduction

Java is not just a language, it is a platform. That is, it implements both the programming environment (the language per se), and the execution environment (JVM and APIs). Any implementation of the Java platform is guaranteed to support the Java programming language, virtual machine, and API. The Java platform is realised in the host environment: the host operating system, native libraries and the CPU instruction set.

When the Java platform is deployed on top of host environments, it may become desirable or necessary to allow Java applications to work closely with native code written in other languages. Programmers have begun to adopt the Java platform to build applications that were traditionally written in C and C++. Because of the existing investment in legacy code, however, Java applications will coexist with C and C++ code for many years to come.<

(from The JNI. Programmer's guide and Specification by Sheng Liang)

In the real world, we can't restrict ourselves to using nothing but Java (or whatever your favourite language is), no matter how much we'd like to.

One option is just to forget Java and write the system in another language, and this is often the right answer. Language choice should not just be a matter of personal preference, but should be carefully thought out for each project. By the time you finish your degree you should be familiar with several languages and capable of learning a new one in a few weeks.

But what if no language has all the libraries, capabilities and features that we need? We need to be able to make software components written in different languages work together. In practice, large systems often incorporate components written in several different languages.

Today we'll just look at one example of multi-language systems, making Java and C work together. In fact we'll only look at one direction: calling C code from Java.


JNI: The Java Native Interface

JNI is designed to handle situations where you need to combine Java applications with native code. It is a two-way interface: it can support two types of native code: native libraries and native applications.

Although Java offers some alternatives to use of native methods, it's often necessary for a Java application to communicate with native code that resides in the same process. Here are several examples mentioned by Sheng Liang:

The Java Language Specification states that a method may be labelled as native (as you might label other methods abstract), which means that it is "implemented in platform-dependent code, typically written in another programming language such as C, C++, Fortran, or assembly language". But the JLS doesn't specify how to write your e.g. C code and how to connect the two pieces together. The native methods, like usual methods, can be overloaded, overridden, declared final, static, synchronized, and with any access modifier; but the native methods cannot be declared abstract (or, strictfp, if you know what this means).

The Java Native Interface (JNI) is Sun's way of connecting the implementations of native methods to its JVM. It specifies what you have to do in your C code to conform to the requirements of the JVM, and what you have to do in your Java code to tell the JVM to access the external code.

Understand that once we start going down the path to using native code we lose platform-independence (portability and safety). Our examples work with Sun's JDK on Linux. With any other combination of platform and JVM changes may (will?) be needed to both the C code and the Java code.


A native Hello, World!

The process of writing a Java code (here, a single class) which makes use of native methods, consists of several steps:

  1. Create a class (HelloWorld.java) that declares the native method.

  2. Use javac to compile it.

  3. Use javah -jni generate a C header file (HelloWorld.h) containing the function prototype for the native method implementation.

  4. Write the C implementation HelloWorld.c of the native method.

  5. Compile the C implementation into a native library, creating HelloWorld.dll (for Windows) or HelloWorld.so (for Unix/Linuxes)

  6. Run HelloWorld in JVM. Both HelloWorld.class and HelloWorld.so are loaded at runtime.

Step 1.

class HelloWorld {

    private native void print();

    public static void main(String [] args){
        new HelloWorld().print();
    }

    static { System.loadLibrary("HelloWorld"); }
}

The static call System.loadLibrary(String libname) is needed for the native code to be loaded into the VM at runtime; otherwise the native methods cannot be executed. The library specified by libname corresponds to a file in the local file system that is located somewhere where the JVM knows to look for library files. The actual mapping library name —> file name is system dependent. Classes with native methods typically load the corresponding library as a part of initialization of the class (hence use of the static initialization block). The use of load method is the source of the safety loss.

Step 2.

javac HelloWorld.java

(HelloWorld.class is generated.)

Step 3.

javah -jni HelloWorld

This command generates a C header file, which contains the function prototype for Java_HelloWorld_print:

  ..........

JNIEXPORT void JNICALL
Java_HelloWorld_print (JNIEnv *, jobject);

     ..........

Step 4.

Write a C source file HelloWorld.c for implementing the function declared in the header file:

#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"

JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env,jobject obj)
{
   printf("Hello World!\n");
   return;
}

When compiled, it is this native library function which will be dynamically loaded into the JVM alongside with the java class.

Step 5.

gcc -Wall -shared -I/usr/local/java/include \
 -I/usr/local/java/include/linux HelloWorld.c -o libHelloWorld.so

Step 6.

abx@eudyptula:~/comp2100/labs/lab6$ java HelloWorld
HelloWorld!

It is important to set your native library path correctly for your program to run. The native library path is a list of directories that the Java virtual machine searches when loading native libraries. If you do not have a native library path set up correctly, then you see an error similar to ClassNotFoundException runtime error. The environment variable for the native library path is LD_LIBRARY_PATH.


Another simple example

Suppose you wanted to access the system clock so as to measure how long your program takes to perform different operations. This is easy to do in C, using a library routine clock() and a macro CLOCKS_PER_SEC, both declared in the standard header file time.h.

Here's the Java code:

public class Timer {

  private native int ticks();
  private native int ticksPerSecond();

  static {
    System.loadLibrary("timer");
  }
}

And here's the C code it calls.

#include <jni.h>
#include <time.h>
#include "Timer.h"

/* The external C part of a Java system stopwatch package */

JNIEXPORT jint JNICALL Java_Timer_ticks (JNIEnv *env,
                                         jobject obj) {
  return (jint) clock();
}

JNIEXPORT jint JNICALL Java_Timer_ticksPerSecond (JNIEnv *env,
                                              jobject obj) {
  return (jint) CLOCKS_PER_SEC;
}

Java routines such as ticks and ticksPerSecond are known as wrappers for the corresponding C routines.

Notice the phenomenon of name mangling — names in the Java class (e.g. ticks()) have to be matched up with names in the C code (Java_Timer_ticks()). The JNI specification defines how this relationship works.

Because of the mangling, there's no shortcut to accessing C library or system calls. At least, we can stick private on the declaration.

Now about those JNIEXPORT and JNICALL. They are the so called C macros, they are defined (as all jni types) in jni.h header file. Using these declarations, the C compiler will generate code with the correct calling convention needed for export into JVM.

What are the two parameters to the C functions? The first is a means of accessing the JVM if you need it. The second is a reference to this (the object in which the method was invoked; jobject is used for non-static native methods). In this example we don't use either of the parameters.

Argument types in the native method declaration have corresponding types in native programming languages. The JNI defines a set of C and C++ types that correspond to types in the Java programming language, such that primitive types and reference types (instances of classes) are treated differently. The mapping of primitive types is straightforward: int —> jint, char —>jchar, float —>jfloat etc. (these types are defined in the header file jni.h)

Objects to native methods are passed as opaque references, they are C pointer types that refer to internal data structures in the JVM. The exact layout of the internal data structures is hidden from the programmer. The native code manipulates the underlying objects via the appropriate JNI functions, which are available through the JNIEnv *env interface pointer. For example, the corresponding JNI type for java.lang.String is jstring. All JNI references have type jobject. Then the set of reference types ("subtypes") is defined: jstring, jclass, jobjectArray, jintArray, jcharArray etc.

In the Java class Timer (which you will use in this week's lab) there are also attributes for storing the number of 'ticks' when the clock was started and stopped, queries for returning the number of ticks and seconds between starting and stopping it, and commands to start and stop the clock. The excerpt above is just the bit that made the external calls — those routines aren't even visible to the client.


Issues for external calls


Another example

Also from the lab, a routine to access an array element. First, the fragment of Java:

private native int c_item(int array,int index);

then the implementation in C:

/* Return the element at index i of the array a. */
JNIEXPORT jint JNICALL Java_IntegerArray_c_item(JNIEnv *env,
                                 jobject obj, jint a, jint i){
  return (jint)(((int *)a)[i]);
}

Note the casts: first, a is cast to the type int *. Then, a is used as an array, and the i'th element is accessed. Finally, the result is cast back to a jint for use by the JVM.


Further thoughts


Implementation with Sun's JDK on Linux

Remember the previous warning that some of the details will be different for other operating systems and JVMs.


How to compile systems with external calls

To compile the system above you need to say

javac Timer.java
javah -jni Timer
gcc -Wall -shared -I/usr/local/java/include \
 -I/usr/local/java/include/linux timer.c -o libtimer.so

You'll be using this library in the lab. For the JVM to pick up libtimer.so you have to tell it where to look. This can be done including the current directory in the environment variable LD_LIBRARY_PATH. You can set variables on a per-command basis using the env command, e.g.:

env LD_LIBRARY_PATH=. java ArrayTester
which is how it's done in the supplied Makefile.

Calling Java from C

So far we have limited ourselves to calling C routines from Java. What if we want the C part of a system to be able to call Java routines?

Why would we want to do this? One reason is if we were trying to write an Java interface to something like GTK+ (a graphics library), where the main loop is running on the C side, and it needs to call Java routines when various events take place.

How do we do it? Not at all in this course. The details are given in Sun's JNI documentation. The book Java Native Interface: Programmer's Guide and Specification by Sheng Liang is used to be freely available from Sun's web site .

Copyright © 2006,2007 The Australian National University
$Revision: 1.3 $ $Date: 2007/05/21 00:02:59 $
Feedback & Queries to comp2100@cs.anu.edu.au