In this chapter, you will work through developing three native Java applications a step at a time. If you skipped the discussion in Chapter 30, "When and Why to Use Native Methods," before implementing any Java native class, you should first consider the trade-offs between implementing in Java versus C. If you are developing from scratch, you should consider whether your application needs to use native code at all. Native code should be used only in the following situations:
There are two downsides to native C code:
With that said, there are several ideal applications of Java native code. For compute-intensive tasks such as number crunching or sorting, there really is no comparison. Because the Java interpreter runs off a virtual instruction set known as the Java Virtual Machine, it is inherently less efficient than a native language such as C that has been compiled down to native assembly language. Instructions that perform simple operations such as A*B can be completed in one or two instructions in native assembly code, but they take multiple instructions when written in the Java Virtual Machine. This isn't bad for a single calculation, but when this calculation occurs within a loop, it becomes a serious bottleneck. Although Just-In-Time (JIT) compilers promise to take Java Virtual Machine instructions at the client end and compile them into native assembly language instructions, nobody has committed to writing a multiplatform version of a JIT compiler as of this writing. For that reason, your best performance solution until at least early 1997 will be to use native methods.
You can save yourself a lot of design optimization time by reading Chapter 33 and absorbing a few rules of thumb about native methods before laying out your design. It'll be worth it.
With that said, let's get back to the implementation of native methods. The major difficulty with native methods is passing and converting Java data into a form usable by C and back again. You will look at three examples of doing this in this chapter.
In the first example, the Triangle class, you look at the process of developing a native method and the files that the Java Development Kit (JDK) generates for linking Java and C together. Next, you discover how you can access member variables from within a native method for that class. Finally, you see how to pass back a simple type using the return stack.
In the second example, the SortedList class, you explore passing strings to and from native methods. You also see firsthand how much more complicated a native method can be when you do memory allocation in C rather than as a member variable of Java.
A common application of native methods is to improve the speed with which Java can process arrays of data. In the third and final section, you explore the mechanisms necessary for accessing arrays from within a native method.
Listing 31.1 is a simple look at a Java class containing native methods. This class encapsulates the base and height of a triangle and contains a native method named ComputeArea that calculates the area of the triangle. Although writing a native method to do a very simple calculation is one of the things you learned not to do in Chapter 30, keeping the method simple makes it easier to understand the mechanics of how a native method is written. You consider more complicated examples in the next two examples.
Listing 31.1. Triangle.java.
public class Triangle {
public void SetBase(float fInBase) {
fBase = fInBase;
}
public void SetHeight(float fInHeight) {
fHeight = fInHeight;
}
public native float ComputeArea();
// Load the native routines.
static {
System.loadLibrary("Triangle");
}
float fBase;
float fHeight;
}
As you can see, the definition for ComputeArea is slightly different from the definition of a normal method. The keyword native is added just after the scope of the method and just before the return type. This tells the javac compiler and the Java interpreter that it should look for the function body in a dynamically linked library (a DLL, in a Microsoft Windows environment) that is loaded using loadLibrary, a static method in the Java System package.
The loadLibrary definition directly following the ComputeArea definition specifies where this dynamically loaded library may be found. In the Windows operating system (and with all its variants), DLLs are searched for by tracing through the PATH environmental variable set in your autoexec.bat. If the path you set in your autoexec.bat file (or in a manner specific to UNIX and Macintosh machines; all the examples in this chapter will be for Windows 95) doesn't include the directory containing the native method DLL, your DLL won't be found, and the Java interpreter will throw an UnsatisfiedLinkError exception. You should trap this exception if there is any chance that your DLL won't be found.
This and all the files you use in the remainder of the chapter can be found on the bundled CD. To build your class, copy the entire ..\ch34\Triangle directory into \java\classes\Triangle. From \java\classes\Triangle, compile the Triangle class using javac just as you normally would:
C:\java\classes\Triangle> javac Triangle.java
C:\java\classes\Triangle>
In a normal Java program, this would be it-your class would be ready. In a native application, however, you have to generate or supply three more source files to tie everything together.
The first file you need to generate is a header file for the Java Triangle class (see Listing 31.2). This header file gives the native C code routine the following:
To generate this from the Triangle.class file, execute javah in the C:\java\classes\native directory:
C:\java\classes\Triangle> javah Triangle.java
C:\java\classes\Triangle>
Listing 31.2. Triangle.h (generated by javah).
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <native.h>
/* Header for class Triangle */
#ifndef _Included_Triangle
#define _Included_Triangle
typedef struct ClassTriangle {
float fBase;
float fHeight;
} ClassTriangle;
HandleTo(Triangle);
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) float Triangle_ComputeArea(struct HTriangle *);
#ifdef __cplusplus
}
#endif
#endif
The header file defines a new type called ClassTriangle.
This structure enables access to the internal variables of the
Triangle class. Each intrinsic
type (a type not defined by the Java class library or the developer)
in Java has a corresponding type in Java. Table 31.1 shows this
correlation (for Windows 95 and Microsoft development platforms;
other combinations may have slight differences).
Java Type | C Type |
float | float |
double | double |
int | long |
short | int |
long | long |
boolean | long |
byte | char |
For developer-defined objects or Java library objects (for example, the String), there can be cases where there are no one-to-one type correspondences with C. This can be a major headache in Java-you tackle this problem in the second native class example.
In the second part of the javah-generated header file are the prototypes for the native functions defined in the Java class. For the Triangle class, there is only one prototype, for the ComputeArea method. The return type is float, as expected from the type translation table, but the function contains a unexpected input parameter of type struct Htriangle *. This parameter is a handle to the instance of the Triangle class that called the native function. This handle enables you to access the Triangle class variables through the class' ClassTriangle structure. You see how to access these variables later in this chapter when you implement the native function in C.
Your next task in implementing a native method is to build a stub file from Java's class file representation of the Triangle class. This stub file is responsible for finding the parameters and return values on Java's call stack and translating them into parameters for the native C method. The java interpreter calls this stub, which in turn calls the native method from within the DLL you loaded with the System.loadLibrary call a few sections earlier.
To create the stub file, type the following at your command line:
C:\java\classes\Triangle> javah -stubs Triangle
C:\java\classes\Triangle>
This creates the file Triangle.c, which is shown in Listing 31.3.
Listing 31.3. Triangle.c (generated by javah-stubs).
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <StubPreamble.h>
/* Stubs for class Triangle */
/* SYMBOL: "Triangle/ComputeArea()F", Java_Triangle_ComputeArea_stub */
__declspec(dllexport) stack_item *Java_Triangle_ComputeArea_stub(stack_item Â*_P_,struct execenv *_EE_) {
extern float Triangle_ComputeArea(void *);
_P_[0].f = Triangle_ComputeArea(_P_[0].p);
return _P_ + 1;
}
If it seems like this is a lot of work for one puny native method, it is; you use a makefile to automate your work in future sessions. The worst is over-you're ready now to build an implementation of the native function.
In order for the Java interpreter to be able to find the native
function, it has to match the prototype in the Triangle.h file
type for type.
Caution |
The most common mistake in developing native method java classes is a mistake in the return type or one of the passed parameters. This isn't a difficult problem to fix, but it is a tremendous time waster because it isn't noticed until link time. You can save development time by double-checking your prototypes before you compile your program. |
Here is the prototype from the generated h file:
__declspec(dllexport) float Triangle_ComputeArea(struct HTriangle *);
In the implementation file, you need to match everything from the float return type exactly right. __declspec(dllexport) doesn't need to be included because it is simply a directive to the compiler to inform it that the native functions will eventually end up in a DLL.
As shown in Listing 31.3, the implementation file also needs to
include two header files, StubPreamble.h and Triangle.h. Triangle.h
is the file you generated with javah
in the previous sections. StubPreamble.h is a Java library that
includes definitions needed to enable you to access your Java
data parameters and use certain Java C interpreter calls that
you will see in later examples.
Tip |
Always name your implementation file with a suffix that uniquely identifies it so that you don't confuse the implementation file with the stubs file generated by javah-stubs. Note that all the native method implementation files are named with the suffix Imp to distinguish them from the JDK-generated files. |
The final building block you need in order to implement the native
ComputeArea function is a
way of accessing the class variables from within the native function.
This is accomplished by using the unhand
macro provided by the StubPreamble.h header file. The unhand
function takes a pointer to a handle to a class, such as the struct
Htriangle*, and returns a pointer to a Class
structure, such as the ClassTriangle
structure described previously. As you may remember, this structure
contains the representation of the variables in the Java class.
Caution |
Because static variables do not belong to any one instantiation of a Java class, you cannot view nor modify them from a native function in C. If you need a static variable that you can modify from your native method, define it in C instead and create accessor methods in the Java class to access it. |
To access the value of the fBase class variable, use the following syntax:
unhand(hthis)->fBase
Doing this for fHeight as well, you can compute the area of the triangle and return it to the Java interpreter and your Java program, as shown in Listing 31.4.
Listing 31.4. TriangleImp.c.
extern "C" {
#include <StubPreamble.h>
#include "Triangle.h"
float Triangle_ComputeArea(struct HTriangle *hthis) {
return(0.5f * unhand(hthis)->fBase * unhand(hthis)->fHeight);
}
}
With the four implementation files for the the native method-containing Java class completed, you now need only to compile two C source files and link them with the java library to form the Triangle DLL file.
This discussion used the following command line in Visual C++ 4.0 for Windows 95 to develop the native functions. Because Windows 95 is the most prevalent Java platform in use today, you'll learn how to link the library by using Visual C++. The instructions for compiling in UNIX are very similar. Consult the manual supplied with your JDK for instructions.
Before you start your build, you need to add a few environmental variables so that the Visual C++ compiler can find your tools and Java/Netscape can find your native DLL files.
To do this, add the following lines to your autoexec.bat file (you use the paths for the standard directories):
SET LIB=\msdev\lib;\java\lib
SET IncLUDE=\java\include;\java\include\win32;\msdev\include
SET CLASSPATH=\java\classes;.;C:\netscape20\Program\java\classes
With that change made, reexecute your autoexec.bat file either by rebooting your computer or by directly executing it. Then, move back to your Java source directory and compile the implementation of the native Triangle class with the following (cl is the Microsoft Visual C++ command line compiler. For other development platforms, substitute the equivalent command here):
C:\java\classes\Triangle> cl /c W3dTriangleImp.c
Likewise, the stubs file should be compiled with this command line:
C:\java\classes\Triangle> cl /c W3dTriangleImp.c
Finally, these two obj files should be linked with the Java interpreter library (javai.lib) to form the finished DLL file:
cl Triangle.obj TriangleImp.obj -FeTriangle.dll -MD -LD javai.lib
Listing 31.5 shows a skeleton makefile used to build Java native applications. It includes all the commands and dependencies necessary to build a Triangle DLL. By replicating the dependency section and changing the names, you can reuse this makefile to build your own native functions.
Listing 31.5. Triangle.mak.
OBJS = Triangle.obj TriangleImp.obj
LIBS = javai.lib
Triangle.dll: $(OBJS)
cl $(OBJS) -FeTriangle.dll -MD -LD $(LIBS)
# Build Triangle class
Triangle.class: Triangle.java
javac Triangle.java
Triangle.h: Triangle.class
javah Triangle
Triangle.obj: TriangleImp.cpp Triangle.h
cl /c /MLd /Gm /Od /Zi W3dTriangleImp.c
Triangle.c: Triangle.class
javah -o Triangle.c -stubs Triangle
Triangle.obj: Triangle.c
cl /c /MLd /Gm /Od /Zi Triangle.c
Let's build a test class to demonstrate the new Triangle class in action. Shown in Listing 31.6 is a test application that uses the triangle class. After building it with javac, use the Java interpreter to run the application.
Listing 31.6. TestTriangle.java.
public class TestTriangle {
public static void main(String argv[]) {
Triangle theTri;
theTri = new Triangle();
theTri.SetBase(2.0f);
theTri.SetHeight(6.0f);
System.out.println("The Area of the Triangle is" +theTri.ComputeArea());
}
}
The following is the output of the TestTriangle execution:
C:\java\classes\Triangle> java TestTriangle
The area of the triangle is 6.0
C:\java\classes\Triangle>
Hooray! Your first native method works correctly.
Now, let's look at a class that has native methods that accept a Java class as one of its parameters.
The class SortedList, shown in Listing 31.7, maintains a sorted list of strings. Because sorting is a compute-intensive task, let's write the sorting algorithm as a native method.
The first thing you notice about the native SortedList class is that there are two Java constructors that immediately call native C constructor implementations. This is done for two reasons. First, Java doesn't allow constructors to be native, so for classes that require native constructors, you must wrap this call to the native function inside the Java constructor. Secondly, native functions cannot be overloaded within Java, because all C function names must be unique and cannot depend on type, and the process for converting a Java method to a C function name in Java always follows Package_Class_Method. For Java classes with overloaded constructors, therefore, you must overload the constructors in Java and then call the corresponding native methods. Not pretty, but the end user of your class won't notice the difference.
The next thing you should notice about the SortedList class is that the class takes and returns a String from its AddString and GetString methods, respectively. This is different from the first example because you are dealing with a Java class, String, that has no direct counterpart type in C/C++. (Although (char *) may seem like a direct counterpart, it isn't, because it doesn't encapsulate a string's length or allocation.)
Listing 31.7. SortedList.java.
public class SortedList {
public native void constructorA();
public SortedList() {
constructorA();
}
public native void constructorB(int nInitialAllocation);
public SortedList(int nInitialAllocation) {
constructorB(nInitialAllocation);
}
public native void AddString(String szString);
public native String GetString(int nIndex);
public native int HowMany();
static {
System.loadLibrary("SortedList");
}
}
The C/C++ implementation of the SortedList
implementation is shown in Listing 31.8. The compiler directive
extern "C" { in
the first line tells the compiler that the definitions of functions
and variables contained within its braces should be defined internally
using a C-style definition rather than a C++ style. The difference
between these two is that the C++ style of definition includes
information about the types passed to and returned from the function,
and the C definition style contains only the function name. If
you leave out the extern "C"
{ directive, all the functions will be be defined
in the C++ style by default. The linker then, looking for a definition
to match the C-style definitions of the stub file, will be unable
to link the functions together correctly because they will be
defined using the C++ style. Notice also, that the extern
"C" { directive also surrounds the Java
include files. These include files also have prototypes for Java
interpreter functions that must be defined using the C-style definition.
Tip |
The linker errors from omitting the extern "C" { definition are shown in Listing 31.8. These errors are confusing the first time you see them, but basically they signal the developer that the extern "C" { directive was omitted. Add the extern "C" { directive as appropriate, and your linking problems should be solved. |
Listing 31.8. Output of linker when extern "C" { is omitted.
Microsoft (R) 32-Bit Incremental Linker Version 3.00.5270
Copyright (C) Microsoft Corp 1992-1995. All rights reserved.
/out:SortedList.dll
/dll
/implib:SortedList.lib
SortedListImp.obj
SortedList.obj
\java\lib\javai.lib
Creating library SortedList.lib and object SortedList.exp
SortedListImp.obj : error LNK2001: unresolved external symbol "char * __cdecl makeCString(structHjava_lang_String*)"(?makeCString@@YAPADPAUHjava_lang_String@@@Z)
SortedListImp.obj : error LNK2001: unresolved external symbol "struct ÂHjava_lang_String * __cdecl makeJavaString(char *,int)"(?makeJavaString@@YAPAUHjava_lang_String@@PADH@Z)
SortedList.dll : fatal error LNK1120: 2 unresolved externals
NMAKE : fatal error U1077: 'cl' : return code '0x2'
Stop.
From the length of the implementation file, you should note the
complexity of implementing data management for your Java native
class in C. Because C isn't object-oriented, special wrapper
code needs to be developed for any native Java class that manages
its data in C. This is an important design tradeoff-Chapter 32,
"Interfacing to Existing C and C++ Libraries," describes
how to manage it.
Tip |
Store your data in Java and use unhand() to access it whenever it yields acceptable performance. This will cut down your development time and simplify your code considerably, because it will be unnecessary to write wrapper code to manage the instantiations of your Java class. |
You can't follow the previous tip for the SortedList implementation, because the overhead from converting strings back and forth from Java and C will choke the sorting routine and rob you of much of the C performance advantage. By moving the data management to C, you only have to convert strings in the AddString and GetString methods and not in the performance critical BubbleSort function.
Next, let's focus on the interface for data from and to Java, starting with the AddString native method.
The AddString method takes a String and adds it to the list maintained by the given object. The prototype for the native C function looks like this:
void SortedList_AddString(struct HSortedList *hthis,
struct Hjava_lang_String *AddString);
This is pretty much as you expect: the first parameter is the handle to the Java SortedList object and the second is the handle to the Java String being passed. Notice that the structure name again follows the package_package_..._class nomenclature.
Java strings are not passed by value, as were all the types that you previously considered. Instead, a handle to the Java String is passed. If you haven't run into them before, handles can be likened to a license plate on a car-they uniquely identify an object. Handles are used for two reasons:
With a pointer, you can do almost anything to the object. With a handle, you are constrained to the API given to you, providing as much security as desired by the API designer.
A Java string is stored in UNICODE to make it more portable. UNICODE is a standard similar to ASCII; it enables easier internationalization, whereas ASCII is very Eurocentric in nature. For native methods that use C and only ASCII, this means that you first must convert the Java string from UNICODE into ASCII before you can use strings passed to it. The makeCString interpreter API call does just this. makeCString converts the String referenced by a handle and allocates enough memory for the converted string, for which it returns a (char *) pointer. Here is a use of makeCString, copied from the AddString function:
//Add String to the end of the List specified by hList.
lLists[hList][nStringsUsed[hList]++] = makeCString(AddString);
Interpreter calls are unfortunately a common occurrence in developing native Java routines. As of this writing, many of the interpreter functions are poorly documented by Sun, but some additional documentation can be found in the interpreter.h and javaString.h files found in the \java\include directory of the JDK.
You should include javaString.h in your #include section whenever you use the makeCString function or the makeJavaString function described in the next section. This include file defines the prototypes for these two interpreter functions.
The GetString native function has a similiar conversion need, as you might expect. In order to return the String referenced by the passed index, it needs to convert the C string stored in the C string table back into a handle to a Java String. The analogous Java interpreter function call needed to convert C (char *) variables into strings is called makeJavaString(char* theString, int strlength). makeJavaString takes a (char *) and a string length as an (int), instantiates a String variable in Java with that value, and returns a handle to that string (struct Hjava_lang_String *). The GetString native function uses makeJavaString as follows:
return (makeJavaString(lLists[hList][nIndex],strlen(lLists[hList][nIndex])));
to return the [nIndex]th String in the [hList]th List. Note that the GetString function returns a handle to a Java string (struct Hjava_lang_String*) just as the AddString native function accepted one.
The complete SortedList implementation is shown below in Listing 31.9 for your reference. This shows how all the parts described in the previous sections fit together.
Listing 31.9. SortedListImp.cpp.
extern "C" {
#include <StubPreamble.h>
#include <javaString.h>
#include "SortedList.h"
#include <string.h>
/* nLists is a long that contains the number of lists that lString
has been allocated to contain. */
long nListsAllocated = 0;
long nListsUsed = 0;
/* nAllocated is an array of longs that contain the number of
strings that the indexed List element has been allocated for.
e.g. if nAllocated[3] = 16, then the SortedList object whose
handle is 3 is allocated for 16 strings in its list. */
long *nStringsAllocated;
/* lLists is a pointer to an array of lists of strings:
lLists --> List [0]
| |
| V
| String[0] (char *)
| String[1]
| ...
|
--> List [1]
|
V
String[0] (char *)
... & nbsp; */
char ***lLists;
/* nStringsUsed is an array of longs that contain the number of
strings that the indexed List element actually contains. */
long *nStringsUsed;
long SortedList_ResizeNumLists(long nNewAllocation) {
char*** NewlLists;
long* NewnStringsAllocated;
long* NewnStringsUsed;
NewlLists = new char** [nNewAllocation];
NewnStringsAllocated = new long [nNewAllocation];
NewnStringsUsed = new long [nNewAllocation];
long i;
for (i=0; (i < nListsAllocated) && (i < nNewAllocation); i++) {
NewlLists[i] = lLists[i];
NewnStringsAllocated[i] = nStringsAllocated[i];
NewnStringsUsed[i] = nStringsUsed[i];
}
for (; (i < nNewAllocation); i++) {
NewlLists[i] = NULL;
NewnStringsAllocated[i] = 0;
NewnStringsUsed[i] = 0;
}
delete lLists;
delete nStringsAllocated;
delete nStringsUsed;
lLists = NewlLists;
nStringsAllocated = NewnStringsAllocated;
nStringsUsed = NewnStringsUsed;
return (nNewAllocation);
}
long SortedList_ResizeNumStrings(long hList, long nNewAllocation) {
char** NewlStrings;
NewlStrings = new char* [nNewAllocation];
long i;
for (i=0; (i < nListsAllocated) && (i < nNewAllocation); i++) {
NewlStrings[i] = lLists[hList][i];
}
for (; (i < nNewAllocation); i++) {
NewlStrings[i] = NULL;
}
delete lLists[hList];
lLists[hList] = NewlStrings;
return (nNewAllocation);
}
void SortedList_constructorA(struct HSortedList *hthis) {
if (nListsAllocated == 0)
nListsAllocated = SortedList_ResizeNumLists(1);
long i;
int done = FALSE;
if (nListsUsed == nListsAllocated)
nListsAllocated = SortedList_ResizeNumLists(nListsAllocated*2);
nStringsAllocated[nListsUsed] = 0;
nStringsUsed[nListsUsed] = 0;
unhand(hthis)->hList = nListsUsed++;
}
void SortedList_constructorB(struct HSortedList *hthis,
long InitialAllocation) {
if (nListsAllocated == 0)
nListsAllocated = SortedList_ResizeNumLists(1);
long i;
int done = FALSE;
if (nListsUsed == nListsAllocated)
nListsAllocated = SortedList_ResizeNumLists(nListsAllocated*2);
nStringsUsed[nListsUsed] = 0;
nStringsAllocated[nListsUsed] = SortedList_ResizeNumStrings(nListsUsed,
& nbsp; InitialAllocation);
unhand(hthis)->hList = nListsUsed++;
}
void BubbleSort(char* lSortStrings[], int nElements) {
long i,j;
int changed=TRUE;
for (j=0; (j < nElements-1) && changed; j++) {
/* Maintain changed flag to remove unneeded interations. */
changed = FALSE;
for (i=0; i < nElements-1; i++) {
if (strcmp(lSortStrings[i], lSortStrings[i+1]) > 0) {
/* [i] belongs below [i+1] --> swap! */
char* temp = lSortStrings[i];
lSortStrings[i] = lSortStrings[i+1];
lSortStrings[i+1] = temp;
changed = TRUE;
}
}
}
}
void SortedList_AddString(struct HSortedList *hthis,
struct Hjava_lang_String *AddString) {
int hList = unhand(hthis)->hList;
if (nStringsUsed[hList] == nStringsAllocated[hList])
nStringsAllocated[hList] =
SortedList_ResizeNumStrings(hList, nStringsAllocated[hList]*2);
/* makeJavaString: Defined in javaString.h in \java\include. */
/* Takes a Java string, allocates memory for a */
/* C or C++ (char *) with the same contents, and */
/* returns the (char *) to the string. */
lLists[hList][nStringsUsed[hList]++] = makeCString(AddString);
BubbleSort(lLists[hList], nStringsUsed[hList]);
}
struct Hjava_lang_String* SortedList_GetString(
struct HSortedList *hthis,
long nIndex) {
int hList = unhand(hthis)->hList;
// Check for out of bounds.
if (nIndex > nStringsUsed[hList]) {
return(NULL);
}
/* makeJavaString: Defined in javaString.h in \java\include. */
/* Takes a C or C++ (char *), instantiates */
/* a Java String with the same value, and */
/* returns the Java handle to the String. */
return (makeJavaString(lLists[hList][nIndex],strlen(lLists[hList][nIndex])));
}
long SortedList_HowMany(struct HSortedList* hthis) {
return(nStringsUsed[unhand(hthis)->hList]);
}
}
A simple applet that uses the native SortedList class is shown in Listing 31.10. SortPresidents was written as an applet just to show that applets also can call native functions. (There is one important caveat: the native code DLL must be already installed on the client machine within a directory contained in your PATH before the applet is presented to Netscape or appletviewer.) SortPresidents takes a list of Presidents and sorts them based on last name and prints out the results. Notice how SortedList still "feels" like a Java class to the user. You should strive for this feeling in your native Java class design.
Listing 31.10. SortPresidents.java.
import java.applet.*;
public class SortPresidents extends Applet {
public void init() {
thePresidents = new SortedList();
thePresidents.AddString("Washington, George");
thePresidents.AddString("Lincoln, Abraham");
thePresidents.AddString("Kennedy, John F");
thePresidents.AddString("Nixon, Richard");
thePresidents.AddString("Carter, Jimmy");
thePresidents.AddString("Reagan, Ronald");
thePresidents.AddString("Bush, George");
thePresidents.AddString("Clinton, Bill");
int i;
int nNames = thePresidents.HowMany();
System.out.println("There are "+nNames+" entries in our string list.");
for (i=0; i < nNames; i++)
System.out.println(thePresidents.GetString(i));
}
SortedList thePresidents;
}
To build your SortedList class, copy the entire ...\ch34\SortedList directory into \java\classes\SortedList. From there you use nmake and the SortedList.mak makefile to build the SortedList class. This makefile looks identical to the one used to build the Triangle class in the last section.
Your SortedList should look something like:
C:\java\classes\SortedList> nmake SortedList.mak
Microsoft (R) Program Maintenance Utility Version 1.60.5270
Copyright Microsoft Corp 1988-1995. All rights reserved.
javac SortedList.java
javah SortedList
...
...
C:\java\classes\SortedList>
We also need to compile our sample applet SortPresidents that uses the SortedList class:
C:\java\classes\SortedList> javac SortPresidents.java
You are now ready to run the applet, Listing 31.11.
Listing 31.11. SortedList Build.
C:\java\classes\SortedList> appletviewer RunIt.html
There are 8 entries in our string list.
Bush, George
Carter, Jimmy
Clinton, Bill
Kennedy, John F
Lincoln, Abraham
Nixon, Richard
Reagan, Ronald
Washington, George
C:\java\classes\SortedList>
Due to the drastic performance advantage that C has in performing array-based operations, it is very common to pass arrays into a native routine. For this reason, in this section you examine how to pass and receive arrays from a native method. To demonstrate passing arrays to and from Java, the GradeBook class and its C implementation are shown in Listings 31.12 and 31.13. The GradeBook class implements a simple grade tracking and average computing system.
The GradeBook class supports only one instantiation, unlike the previous SortedList class. GradeBook was designed this way to simplify the implementation of GradeBook to remove the details that were present in the SortedList class of the last section. A real application that wants to use more than one GradeBook requires a rewrite of this class to support multiple instantiations.
Looking closer at the Java GradeBook class, you should see two new data types that have not previously been passed into a native method. First, the NameStudents method accepts an array of String containing the list of students enrolled for the class. Second, the AddTest method accepts an array of float for the list of scores achieved by the respective students on a test. In the implementation file, let's use this example to see how you can decode Java arrays into C arrays.
This native class exploits C's array operation performance advantage over Java to improve the speed of searching for a student's records and computing test and class averages.
Listing 31.12. GradeBook.java.
public class GradeBook {
public native void constructor(int nStudents, int nTests);
public GradeBook(int nStudents, int nTests) {
constructor(nStudents, nTests);
}
public native void NameStudents(String lStudents[]);
public native int AddTest(float lScores[]);
public native float GetTestAvg(int nTestNumber);
public native float GetStudentAvg(String szStudentName);
public native int HowManyTests();
static { System.loadLibrary("GradeBook"); }
}
The implementation file for the GradeBook class is shown in Listing 31.13. The file's general skeleton is very similar to the last two classes you have considered.
The NameStudents method is responsible for associating the names of all the students in the class with the test scores. This enables the user of this class to pull a student's record using the student's name as a query. Here is the NameStudents prototype:
void GradeBook_NameStudents(struct HGradeBook* hthis,
struct HArrayOfString* JavalStudents) {
Notice that Java has a special handle type for an array of String, struct HArrayOfString*. This handle contains only one element, body, that you need to consider. This body element is common to all struct HArrayOfObject* handles in native method programming and is a pointer to a list of handles or, in the case of intrinsic Java types that have C equivalents, the actual array of values. For the struct HArrayOfString, body points to a list of String handles that contains the names of all the students in the class.
Reading in the names of the students in the class is then as easy as writing a loop that grabs each student's string handle and converts it to a C (char *) using makeCString, as described in the previous section. To grab the individual string handle and convert it, you use the following construct in NameStudents:
struct Hjava_lang_String* hStudentName;
.
.
.
hStudentName = (struct Hjava_lang_String *)
(unhand(JavalStudents)->body)[i];
lStudents[i] = makeCString(hStudentName);
This accesses the body variable by first unhanding the JavalStudents (struct HArrayOfString*) variable using unhand. As explained previously, body is a pointer to a list of String handles, so to obtain the ith name handle in the list, you simply suffix an array index [i] to the body pointer. By iterating with a loop over the entire class list, you can fill lStudents, the C (char *) version of the class list with the names of the students in the class. lStudents is then used by the GetStudentAvg native method to search for the student's grade records by name.
Accessing an array of a basic Java type is easier. In the GradeBook class, the AddTest method accepts a list of float test scores for each student. Its native method implementation prototype looks like the following:
long GradeBook_AddTest(struct HGradeBook *hthis,
struct HArrayOfFloat *lTestScoresIN);
Again, you have a handle to an array of objects, but this time the handle is to ArrayOfFloat. Accessing ArrayOfFloat is very similar to accessing the String array, but because float is an intrinsic type, you can simply cast the body pointer into a (float *). This pointer can then be used as a normal (float *) to copy the elements of the array from the Java array object into your C array object:
float* lJavaTestScores = (float *)(unhand(lTestScoresIN)->body);
float* lCTestScores = new float[nStudents];
int i;
for (i=0; i < nStudents; i++) {
lCTestScores[i] = lJavaTestScores[i];
}
lTests[nTests] = lCTestScores;
nTests++;
Caution |
It is tempting to assign lJavaTestScores directly to lTests rather than to allocate a new C array and copy element by element. If the Java array object you are indirectly referencing with this pointer passes out of scope, however, and is garbage collected by the Java interpreter (because it doesn't have any knowledge of your C copy of the pointer), the pointer you assigned to lTests will also suddenly be invalid. Copying the array and assigning the pointer to this copy to lTests protects you from this bug. Remember this when you are designing your own classes. |
Listing 31.13. GradeBookImp.cpp.
extern "C" {
#include <StubPreamble.h>
#include "GradeBook.h"
#include <string.h>
#include <javaString.h>
long nStudents;
char** lStudents;
long nTests;
float** lTests;
void GradeBook_constructor(struct HGradeBook *hthis,
long nStudentsIN,
long nTotalTests) {
nStudents = nStudentsIN;
lStudents = new char* [nStudents];
nTests = 0;
lTests = new float* [nTotalTests];
}
long GradeBook_AddTest(struct HGradeBook *hthis,
struct HArrayOfFloat *lTestScoresIN) {
float* lJavaTestScores = (float *)(unhand(lTestScoresIN)->body);
float* lCTestScores = new float[nStudents];
int i;
for (i=0; i < nStudents; i++) {
lCTestScores[i] = lJavaTestScores[i];
}
lTests[nTests] = lCTestScores;
nTests++;
return (nTests);
}
void GradeBook_NameStudents(struct HGradeBook* hthis,
struct HArrayOfString* JavalStudents) {
struct Hjava_lang_String* hStudentName;
int i;
for (i=0; i < nStudents; i++) {
hStudentName = (struct Hjava_lang_String *)
(unhand(JavalStudents)->body)[i];
lStudents[i] = makeCString(hStudentName);
}
}
float GradeBook_GetTestAvg(struct HGradeBook* hthis,
long nTestNumber) {
int i;
float* lTestScores = lTests[nTestNumber-1];
float fScoreAccum = 0;
for (i=0; i < nStudents; i++) {
fScoreAccum += lTestScores[i];
}
return (fScoreAccum/((float)nStudents));
}
float GradeBook_GetStudentAvg(struct HGradeBook* hthis,
struct Hjava_lang_String* hStudentName) {
char* szSearchStudent = makeCString(hStudentName);
long cStudentIndex, bDone;
for (cStudentIndex=0, bDone=FALSE; (cStudentIndex < nStudents) && !bDone;) {
if (strcmp(szSearchStudent, lStudents[cStudentIndex]) == 0) {
bDone = TRUE;
} else {
cStudentIndex++;
}
}
if (!bDone) // Student not found!
return (-1.0f);
long cTestNum;
float fTestScoreAccum;
for (cTestNum=0, fTestScoreAccum=0.0f; cTestNum < nTests; cTestNum++) {
fTestScoreAccum += lTests[cTestNum][cStudentIndex];
}
float fTestAvg = fTestScoreAccum/((float)nTests);
return(fTestAvg);
}
long GradeBook_HowManyTests(struct HGradeBook* hthis) {
return(nTests);
}
}
A sample Java application, TeachersPet, that uses the GradeBook
class is shown in Listing 31.14. This application creates a new
GradeBook with 5
students and 3 tests. With
this database created, it then finds the overall average for each
student and the average for the class as a whole.
Listing 31.14. TeachersPet.java.
public class TeachersPet {
public static void main(String argv[]) {
int nStudents = 5;
int nTests = 3;
GradeBook myClass = new GradeBook(nStudents, nTests);
// The student list
String lszStudents[] = new String[nStudents];
lszStudents[0] = new String("Susan Harris");
lszStudents[1] = new String("Thomas Thompson");
lszStudents[2] = new String("Blake Cronin");
lszStudents[3] = new String("Rotten Johnson");
lszStudents[4] = new String("Harrison Jackson");
myClass.NameStudents(lszStudents);
// The student scores for test 1.
float lTest1Grades[] = new float[nStudents];
lTest1Grades[0] = 93;
lTest1Grades[1] = 86;
lTest1Grades[2] = 89;
lTest1Grades[3] = 65;
lTest1Grades[4] = 78;
myClass.AddTest(lTest1Grades);
// The student scores for test 2.
float lTest2Grades[] = new float[nStudents];
lTest2Grades[0] = 100;
lTest2Grades[1] = 83;
lTest2Grades[2] = 91;
lTest2Grades[3] = 55;
lTest2Grades[4] = 83;
myClass.AddTest(lTest2Grades);
// The student scores for test 3.
float lTest3Grades[] = new float[nStudents];
lTest3Grades[0] = 89;
lTest3Grades[1] = 94;
lTest3Grades[2] = 82;
lTest3Grades[3] = 59;
lTest3Grades[4] = 85;
myClass.AddTest(lTest3Grades);
// Compute each student's average.
float fStudentAvg=0.0f;
int cStudent;
for (cStudent = 0; cStudent < nStudents; cStudent++) {
fStudentAvg = myClass.GetStudentAvg(lszStudents[cStudent]);
System.out.println(lszStudents[cStudent]+
"'s average on the 3 tests is "+fStudentAvg);
}
System.out.println("");
// Compute the class average.
float fClassAvg=0.0f;
float fTestAvg;
int cTest;
for (cTest = 1; cTest < nTests+1; cTest++) {
fTestAvg = myClass.GetTestAvg(cTest);
System.out.println("The class average on Test #"+cTest+" is "+fTestAvg);
fClassAvg += fTestAvg;
}
fClassAvg /= ((float)nTests);
System.out.println("\nThe class average on the 3 tests is "+fClassAvg);
}
}
Compile the GradeBook class library and the sample application TeachersPet just as in the last two examples:
C:\java\classes\SortedList> nmake GradeBook.mak
Microsoft (R) Program Maintenance Utility Version 1.60.5270
Copyright Microsoft Corp 1988-1995. All rights reserved.
.
.
.
C:\java\classes\SortedList> javac TeachersPet.java
C:\java\classes\SortedList>
Finally, give the application a test run:
C:\java\classes\SortedList> java TeachersPet
Susan Harris's average on the 3 tests is 94
Thomas Thompson's average on the 3 tests is 87.6667
Blake Cronin's average on the 3 tests is 87.3333
Rotten Johnson's average on the 3 tests is 59.6667
Harrison Jackson's average on the 3 tests is 82
The class average on Test #1 is 82.2
The class average on Test #2 is 82.4
The class average on Test #3 is 81.8
The class average on the 3 tests is 82.1333
C:\java\classes\SortedList>
Native methods compose a very powerful expansion interface for Java. Unfortunately, for its importance to many developers, the intricacies of native methods are very poorly documented. This chapter uncovered some of the difficulties that native methods present to you when you first develop them.
If you take nothing else away from this chapter, remember this rule of thumb: It is much more complex to handle memory management in C than in Java. (Compare the general complexity of a Triangle class method implementation with the SortedList class implementation.) Not only is the design more complex, but memory management bugs and leaks are also more likely to appear.
In this chapter's Triangle class, you saw how to access objects encapsulated by the class containing the native method and also how to return intrinsic types that have a C counterpart.
In the SortedList class, you learned how to pass more complex Java objects into and from a native method, using the Java String object as an example. You also saw firsthand the complexities of doing memory management in C as opposed to accessing the variables in the Java class.
In the final example, the GradeBook class, you examined passing arrays of objects into a native method. In this class, you passed both an array of floats and an array of Strings into native methods.
You've only scratched the surface of the native method interface, even though these examples cover the most common needs for the native method developer. In Chapter 32, you examine interfacing Java with C and C++ libraries, and in the process, learn more about the intricacies of the native interface.