| By Andrew Chalk | Article Rating: |
|
| June 1, 2002 12:00 AM EDT | Reads: |
15,072 |
Although we try to make our applications pure Java, outside forces sometimes make this impossible. We had such a case recently in our shop when we had to interface to an external device with an API that supported C language calls.
This is a typical case for the Java Native Interface (JNI). The JNI provides Java programs with a gateway to other languages and enables applications written in other languages to invoke the Java Virtual Machine.
This article focuses on the first of these two uses. Specifically, it discusses supporting a C/C++ API in Java to allow a Java application to use it. This is probably the situation in which the JNI is most frequently employed in production environments. Indeed, the seminal work on the JNI, The Java Native Interface: Programmer's Guide and Specification by Sheng Liang, devotes a chapter to this question. There's no theory involved, but if you use the following techniques you should save yourself many hours of work.
Phase one of the JNI programmer's acclimatization process: dump the sacred principle of Java development - platform independence. It must, as a purely logical implication, go out the window. All the C/C++ code in this article compiles under GNU C/C++ on Solaris and Microsoft Visual C++ on the Win32 platform. Other platforms would have to be tested individually.
I'll focus on the practical problem of implementing a large and complex API using the JNI within a production environment. Space constraints required Liang to discuss passing parameters that consisted of Java primitives from Java to C/C++ (hereafter C++, unless the distinction matters) and sending the C++ return code to the Java application. However, most APIs are more complex. This is something not frequently addressed in the literature, despite its prevalence in the real world. APIs are not restricted to primitives as parameters but pass structures and classes to the device. They not only use return statements but also assign values to the passed parameters for use by the calling program. They present questions for interpreting Java classes in C++, handling structure alignment and data format issues, and implementing code that is source compatible with both 32-bit and 64-bit processors.
The JNI, Power - at a Price
Every developer who has used the JNI is aware that it's powerful and comprehensive. However, the price of these features is an inordinate amount of work to accomplish some very mundane tasks. In particular, parameter handling is a time- and code-intensive business that's subject to errors and latent bugs. Consider the code in Listing 1, which might be described as the simplest function to access a JNI. The native function is declared at (1) in the listing. (In Listings 1, 4, and 6 I've numbered specific lines for reference purposes.)
The method passes one integer to a C++ native function. The native function prints the passed parameter to stdout and also returns it. The native code is shown in Listing 2. Note the obscure function name, which is generated by javah.
The native function just prints the integer parameter to stdout. How this appears depends on your Java development environment. In Borland JBuilder the output from the native code and the System.out.println()call both appear in the IDE message window, as shown below.
Value is 5
Value sent and returned is 5
The native code prints the first line and Java prints the second. The important points about this example are:
Most data types are more complicated than primitives. In Listings 3 and 4 the Java and native code for passing and returning a string type are shown. The following is the output from these listings.
Value is Hello World
Value returned is Goodbye World
While there's virtually no change in the Java code (essentially just a change of the operative variable type from int to string), the native code is substantially more involved. Because a string is neither a primitive type nor architecturally the same as any string in the native language, it must be handled differently. First, the JNI function GetStringUTFChars() [at (1) in Listing 4] both converts the string to the C++ single-byte character set and retrieves a pointer to the resultant object (other JNI functions can handle conversion to C++ wide, e.g., Unicode, strings). Second, the JNI function ReleaseStringUTF() frees the memory allocated for the converted string. Third, the JNI function NewStringUTF() allocates a Java string from a C++ single-byte character string to return a string to Java. The important point about Listing 3 is that passing strings increase the amount of native code necessary to process the parameter.
String parameters don't present even the normal level of variable complexity. Real-world data types are likely to be represented as classes in Java and structures or classes in C++. Listings 5 and 6 provide the Java and C++ code for passing a simple class.
The Java code in Listing 5 is very simple. It just instantiates instances of ClassAccess and Device2 where Device2 aggregates an instance of Device1, which it instantiates in the constructor chain. The code is straightforward, indeed mechanical. However, the native method (see Listing 6) is completely different. While not conceptually sophisticated, it's a complicated tangle of repetitive operations on different data types. First, at (1), the code fetches the class of the object passed to the native function. This is the Device2 object passed in the Java line classAccess.accessClass(dev2). It then proceeds to retrieve each parameter in turn. The block at (2) retrieves the int field intValue. The code at (3) retrieves the long field longValue. The block at (4) is more involved as it retrieves the string field stringValue and resembles Listing 3. Finally, the block that begins at (5) shows the more involved case of obtaining a field from an object contained within another object. In this example the Device2 object contains a Device1 object. It's the int member of Device1, intValue, that we wish to eventually obtain. [This is obtained at (6).]
Simplifying the Implementation
Code like this will be repeated unless we find a way to move its regularities into separate functions. The string and contained object cases generate the most overhead, so they're the ones chosen in Listing 7. Now the native code in Listing 5 can be abbreviated to that shown in Listing 8.
Furthermore, the two access functions repeat the simplification that they provide here in each native function they're called in. In each instance that we use this code we reduce the amount of code required to be implemented in the project by roughly two-thirds.
Assigning Data to Java Structures
Now we're at the point at which productive use of the JNI became a major issue in our shop. The vendor API that we supported required some structure passing and what we discussed earlier proved useful for this. However, the API's main purpose was to return data from the vendor's device, which it did by providing over 100 functions that required a pointer to an API structure to be passed to an API function. The vendor library allocated memory for the structure and instantiated the fields with the requested data. This worked well for the C++ programmer who could examine the data by dereferencing the pointer after the API function call returned.
The Java programmer acquired a new set of problems. We decided to define a Java class for each C++ structure (a practice recommended by Bloch). Each field of each API structure had to be assigned to a member of the corresponding Java class, passed as a parameter. The one-to-one mapping of Java classes to C++ structures gave us over 60 Java classes. The number of structure members meant that over 500 member assignments had to be made. Code resembling that in Listings 1-5 would have to be written and, more important, maintained - a daunting prospect.
We turned to a set of functions like those in Listing 7. However, now these functions would read a member variable from a C++ structure and assign the value to the Java class (see Listing 9 for an example). For continuity, we continue to use the Java classes Device1 and Device2 defined earlier. Their C++ equivalents are:
struct Device1
{
int intValue;
};
struct Device3
{
int intValue;
long longValue;
char * stringValue;
Device1 dev1Value;
};
Assume that at runtime they contain the values shown below:
struct Device1 dev1 = {10};
struct Device3 dev3 = {
5,
8,
"Hello World",
{10}
};
Listing 9 is the Java program that will print out these values.
The C++ code (see Listing 10) shows the implementation of the native method assignClass. One interesting feature is that it shows how to make assignments to nested objects (Device1 is aggregated by Device3 in Listing 9). Just as in the case of passing parameters from Java to C++ we provide a set of routines to expedite the process and make the code more reliable (see Listing 10). Listing 11 can then be replaced with Listing 12. (Listings 11 and 12 can be downloaded from the JDJ Web site, www.sys-con.com/java/sourcec.cfm.)
Benefits
The family of Parse... functions has substantially reduced the amount of code required to make the necessary assignments to the Java object passed as jOutput and its contained objects. Furthermore, we've moved the intricate code in which errors can occur into a set of reusable functions. This expedites the implementation of a large vendor API in Java and makes maintenance more straightforward. These functions are extensible to other variable types as necessary. We've implemented only the ones that we need.
Summary
The JNI is a powerful, comprehensive solution to the problem of accessing an API written to support other languages, especially C or C++. That power comes at the price of intricate parameter manipulations. In this article we addressed the problem of using the JNI in a production situation and presented a set of techniques that simplify most of the repetitive tasks of passing information to the native code and getting it back into our Java program. These techniques reduced by around two-thirds the amount of C++ code that had to be written. These techniques can be extended to cover other constructs, and it's a pragmatic choice as to which should be implemented in a particular project.
Postscript
After completing the project in which these techniques were developed, another JNI project came up. Implementation of the native parts of the project took about a quarter of the time that we had forecast, based on raw coding of JNI function calls. The code also passed all unit tests on Solaris and Windows the first time, an indication of the reliability gains of a unified method of handling parameter passing. While an unscientific estimate (different APIs, etc.), the difference in terms of schedule improvements was very noticeable.
References
Published June 1, 2002 Reads 15,072
Copyright © 2002 SYS-CON Media, Inc. — All Rights Reserved.
Syndicated stories and blog feeds, all rights reserved by the author.
More Stories By Andrew Chalk
The author is President of Magna Carta Software, Inc. in Plano, TX USA. He can be contacted at achalk@magnacartasoftware.com
![]() |
Dmitry Leskov 06/08/02 08:10:00 AM EDT | |||
There are products collectively called JNI Proxies that let you use C APIs in Java without writing a line of JNI code - see "Proxy Products" under the link. Of those listed, xFunction is also available for Linux. |
||||
- Cloud CEOs, CTOs & SVPs to Speak at 4th International Cloud Computing Expo
- Kindle 2 vs Nook
- Why IBM’s Server Chief Got Busted
- The Difference Between Web Hosting and Cloud Computing
- Cloud Computing Journal Opens "Readers' Choice Awards" Nominations
- Cloud Computing Expo: Exclusive Q&A with Yahoo! SVP Cloud Computing
- Industry Experts Discuss the State of Cloud Computing
- Ajax in RichFaces 3.3, JSF 2 and RichFaces 4
- It's the Java vs. C++ Shootout Revisited!
- The End of IT 1.0 As We Know It Has Begun
- An Introduction to Abbot
- Java Kicks Ruby on Rails in the Butt
- Interviewing Java Developers With Tears in My Eyes
- Cloud CEOs, CTOs & SVPs to Speak at 4th International Cloud Computing Expo
- 1st Annual Government IT Expo: Call for Papers Deadline July 15
- How to Diagnose Java Resource Starvation
- REA Is Where RIA Becomes the Norm
- Kindle 2 vs Nook
- Anatomy of a Java Finalizer
- Why IBM’s Server Chief Got Busted
- A Cup of AJAX? Nay, Just Regular Java Please
- Java Developer's Journal Exclusive: 2006 "JDJ Editors' Choice" Awards
- The i-Technology Right Stuff
- JavaServer Faces (JSF) vs Struts
- Rich Internet Applications with Adobe Flex 2 and Java
- Java vs C++ "Shootout" Revisited
- Bean-Managed Persistence Using a Proxy List
- Reporting Made Easy with JasperReports and Hibernate
- Creating a Pet Store Application with JavaServer Faces, Spring, and Hibernate
- What's New in Eclipse?




























