Using the Java Native Interface with C++


Motivation
There are basically two reasons why I chose to explore the Java Native Interface (JNI): Code Reusability and Performance.
The Approach
Although JNI documentation is available from Microsoft and Sun, I wanted to create a document/example that contains all of the important aspects of native code interfacing in one place. I don't just want to reprint what Microsoft and Sun say in their documentation. That would be of no help. In fact, it would probably be of less help. I am a true believer that a picture is worth a thousand words. Show me an example of a callback on a static void method, (instead of specification explaining every single variation of a callback), and I can understand it right away. That is the purpose of this exercise. I want to say later, "Hmmm, now how did I pass an object array to C++ and have it access the components? Let me see, oh yeah, here it is, example #5"

With the exception of maybe one or two examples, the sample code is my own. I dreamed up examples that I thought would be instructive in understanding how certain things are done in the Java/native interaction. They are simple enough to be understood merely by looking at the code, yet each are instructive and focused on illustrating a point. There is basically one Java file that implements the interface to the native C++ code and one C++ file that implements all of the examples. There is a Java source file, Main.java that is used as the driver program. In essence, there are 3 files needed for these tests:
Here is a quick and dirty batch file to build up the project. It is NOT a make-like file, so if you want to build the project, your machine must be properly configured to begin with. (On a PC, this can be more work than writing the code, and I have no good answers.) I got the syntax for compiling a DLL from the command line from Sun's documentation here. I use the Integrated Development Environment for developing Microsoft-based applications and DLLs.

There are several issues that I wanted to "solve" in order to satisfy the requirement of reusing existing C++. (Unless otherwise specified, the term native code can be used interchangeably with the term C++ code.) The essential functionality should include:
With the exception of the last point, examples are included that show how each requirement is handled. The problem with GC is that it was difficult to test. I was able to, what appeared to be, successfully enable/disable GC while in native code, but it was hard to prove this. Microsoft's implementation disables GC while native code is executing. Both Sun and Microsoft give the programmer control over the lifetime of native pointers to Java objects. This breaks a cardinal rule of Java, safety, but since you are executing C++ code, you have pretty much done that already.

As for the performance of native code, there are currently three examples that measure how native code performs compared to Java code. I would have liked to do more real-world native/Java performance testing, but that would have required a much larger base of Java code. I have tons of C++ code, but to be fair, I would like to compare a Java implementation with a C++ implementation. In other words, comparing Java code that performs the same function as C++ code but uses widely different data structures and algorithms isn't really comparing the same things. It would have required a lot of work (not related to the task at hand) to "port" enough C++ code to "equivalent" Java code for testing. Thus, only these performance tests were implemented: (results are shown at the bottom of the page) Development Environments
I implemented and tested the JNI using both Microsoft's JDK version 1.5.1 and Sun's JDK version 1.1.5. Microsoft refers to its implementation as the Raw Native Interface (RNI). This seems to be because Microsoft's implementation exposes the implementation details of the Java code, whereas Sun's implementation hides these details, using more abstract facilities for access. Basically, the JDKs provide header files (.h files) and helper functions, via Dynamic Link Libraries (.dll files) and static Libraries (.lib files). Sun's interface is provided through jni.h and Microsoft provides an interface via native.h. Needless to say, these two implementations, Microsoft's RNI and Sun's JNI, are incompatible. The methodology is related, but the syntax is not. Note that this incompatibility is only at the native code level, that is, at the C++ level. The Java code is compatible. Therefore, there is one implementation of Native.java, one implementation of Main.java, but two implementations of Native.cpp, one for Microsoft's JDK, and one for Sun's JDK.

The "Microsoft" implementation uses:
The "Sun" implementation uses:
The runtime environment used to time the tests:
Originally, I implemented all tests and timings with Microsoft's kit, and it appeared that it was going to be very similar to how Sun's documentation explained things. After about 20 hours of code development with the Microsoft stuff, I began development with Sun's kit. After about 8 hours with Sun's kit, I started to really understand what was going on. I made more elaborate use of objects and callbacks to access Java classes and objects. By the time I finished the second implementation (using Sun's JDK), I had a very different set of functionality than what I originally had with Microsoft. I decided to use the Sun implementation as the basis for re-implementing the Microsoft code. This went pretty smoothly. By looking at the header files created by Microsoft's msjavah and Sun's javah, you can see that Sun's approach is more of an interface rather than Microsoft's code. Here is Microsoft's "interface" to the native code Microsoft's header file, and here is Sun's interface Sun's header file. It is interesting to note that Sun can generate the same header file as Microsoft, but given a switch -jni, Sun's tool generates the one shown here. Exposing the implementation is not necessarily a bad thing. When the implementation is exposed, as in Microsoft's header file, the native code can directly access the members (either public or private). With Sun's header file, you must use JNI methods to gain access. Direct access looks something like this:
  // self is basically the 'this' pointer
long __cdecl Native_getX (HNative *self)
{
  return self->x;
}
This can't be done using Sun's header files, because the members are not exposed. Only method interfaces are given. Access to the members requires JNI function calls which are shown in the sample code. The public and private access specifiers have no effect on native code accessing members. I believe this is because the JDK generates "C" code and not C++ code. If you use a C++ compiler, you can manually modify the structures (a class with public access by default in C++) to include the access. The structure below includes a private modifier so the code above would generate a compiler warning when trying to directly access x via the self pointer.
typedef struct ClassNative {
  struct Hjava_lang_String * string_;
  long boolean_;
  long byte_;
  long char_;
  double double_;
  float float_;
  long int_;
  int64_t long_;
  long short_;

  // now w, x, and y are private and cannot be accessed directly
private:

  struct Hjava_lang_String * w;
  long x;
  long y;
} ClassNative;

Browsing the Results
This document is fairly brief, providing just the overview of the process of exploring the JNI. The real information is in the 4 implementation files described above. Particularly, the Native.cpp has a wealth of information. There are 12 functions defined within the file, and each function shows at least one aspect of JNI programming. Many functions use several JNI mechanisms to accomplish their specific task. Native.java is basically redundant. Once you know how to declare one native function, you can declare them all. The power is in Sun's javah (Microsoft's msjavah) that constructs C equivalents to Java's types. Main.java shows how you would invoke the native methods, although as you'll see, it's impossible to distinguish between calling a Java method or a native C++ method. Those details are hidden in the Java Native class.

In the proverbial nutshell, here is the performance comparison: (here's a sample output)
                                     Windows NT      Solaris
                                   Sun   Microsoft     Sun

1. Native Prime Sieve              405      315        390
   Java Prime Sieve               1510      320       2660

2. Native Function Calls           925      320        980
   Java Function Calls             150       40        275                                 

3. Native Array Initialize         465      <10        715
   Java Array Initialize          3670      270       5080

4. Callbacks to Java void         1510     1660       1250
   Java calls to native void       930      210       1000


5. Callbacks to Java cons.   200    50       50         50  
   Java calls to constructor        50       50         40
                             300    80       70         90
                                    70       70         55
                             400   110      100        120 
                                    90      110         75 
                             500   150      130        150
                                   130      120        110

                             1K    370       F         280 *
                                   270       A         220       
                             2K    910       I         760 *
                                   570       L         440
                             3K   1680       E        1530 *
                                   820       D         680
                             4K   2640                2480 *
                                  1130                 900

All times are in milliseconds
  1. Prime numbers were sieved up to 1,000,000
  2. 1,000,000 function calls
  3. An array of 10,000 elements was initialized 1,000 times.
  4. 1,000,000 calls were made from native to Java, and from Java to native.
  5. 8 tests were done. (200, 300, 400, 500, 1000, 2000, 3000, 4000 calls). For reasons that I can't yet explain, Microsoft's implementation failed when trying to construct about 750 objects.

* The interesting note is that the time taken in calling Java constructors from native does not increase at a linear rate. This was true on both NT and Solaris.


A useful table to have while studying the JNI is the mappings between Java types and C++ types. Rather than copy it myself, why don't I just point to it. This is the same document that I used while developing this code. You can also navigate to other topics related to JNI programming.

Mapping between Java types and Native types
Step by step tutorial

My other JNI work: Updates
12-18-98 Added an example to demonstrate passing a 2-D array to a native method. Only Native.cpp (Sun-compatible code) was updated. Microsoft-compatible code, ms.native.cpp, was not altered to reflect this new example.