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.
- Code Reusability. It may be argued that (superficially) Java is a better C++ (maybe in the way that C++ is a better C). However, the benefits of using Java
at this level do not outweigh the overwhelming task of re-implementing currently tested and debugged C++ code. Given large amounts of proven C++ code, it may be more cost-effective
to incorporate its functionality into a Java program, rather than to port (re-implement) the C++ functionality in Java.
- Performance. Given that Java is interpreted (more or less), one would assume that compiled native code would perform much better. It may be useful to "hand-code" native
methods where speed is critical.
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:
- Native.java This is the public interface that Java uses to interact with the native C++ functions.
- Main.java This is the driver program that exercises the examples. It instantiates Java Native objects.
- Native.cpp This is the native code that implements the functions that Java will call. It is for Sun's implementation of the JDK. Here is my
source code for Microsoft's JDK. By comparing the two, one can clearly see the different ways that Sun and Microsoft approached
their implementations.
- Nativec.h This is the machine-generated header file that describes the Java Native class. Generated by Sun's javah.
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:
- Calling native code from Java.
- Passing parameters to native code from Java. This includes Java Strings, arrays, and other Java objects, as well as basic types (int, long, etc.)
- Returning values from native code to Java. This includes Java Strings, arrays, and other Java objects, as well as basic types (int, long, etc.)
- Calling Java methods from native code (callbacks). This should include both static and non-static methods.
- Accessing fields of Java classes and objects from native code. This should include both static and non-static fields.
- Detecting and handling Java exceptions from within native code.
- Garbage collection in the JVM should not interfere with native code, and vice-versa. At a minimum, GC is disabled when native code runs.
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)
- Function-call overhead when calling a native method without parameters and no return value. Basically, void func(void).
- Initializing a large array multiple times.
- Sieving prime numbers using a large array.
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:
- Microsoft's JDK 1.5.1 for run-time support.
- Microsoft's Visual C++ 5.0 for developing the native code.
- Microsoft's Visual J++ 1.1 for developing the Java code.
The "Sun" implementation uses:
- Sun's JDK 1.1.5 for developing the Java code and to provide the run-time support.
- Microsoft's Visual C++ 5.0 for developing the native code.
The runtime environment used to time the tests:
- Intel Pentium II 233 MHz (running at 266 MHz) with 128 MB of RAM
- Windows NT 4.0 (with Service Pack 3)
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
- Prime numbers were sieved up to 1,000,000
- 1,000,000 function calls
- An array of 10,000 elements was initialized 1,000 times.
- 1,000,000 calls were made from native to Java, and from Java to native.
- 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:
-
Programming with the Java Native Interface This is a tutorial that shows how to use C++ with
the Java Native Interface on Linux, Solaris, and Windows NT.
-
Using the Java Native Interface with Delphi. These pages describe how to use
the Java Native Interface with Delphi. Part two of
Using the Java Native Interface with Delphi is essentially a port of the C/C++
code found on this page to Delphi. Much of the Java code is the same. There is also an in-depth tutorial.
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.