by Tim Park
This chapter explains how to use the native method interface to speed up your Java programs and to interface with legacy C and C++ libraries. Native methods are the mechanism in Java that allows you to interface with programs written in C. Legacy C and C++ libraries are any libraries that were developed first for the C/C++ platform, but that you now want to interface with in Java. We'll first look at three examples of how to use the native method interface, working through the basics of Java native method programming. After learning the basics, we will then use a simple 3D graphics library to show how you can use the native method interface as a bridge to legacy C++ code.
The first example uses the Triangle class to explain the process of developing a native method and the files the Java Developers Kit (JDK) generates to link together Java and C. Then you discover how to 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.
The second example uses the SortedList class to explore passing strings to and from native methods. You also see first-hand 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. The third example explores the mechanisms necessary for accessing arrays from within a native method.
In the second part of this chapter, you will look at My3D, a C++ graphics library. The second part of the chapter also helps you work through the steps necessary to build a Java interface library to My3D.
Listing 33.1 gives you a first 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.
Listing 33.1. Triangle.java
contains a native method called ComputeArea().
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 only 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 they should look for the
function body in a dynamically linked library (a DLL, in a Microsoft
Windows environment, which is named Triangle.DLL)
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 can
be found.
Note |
The Triangle class shown in Figure 33.1 and all the files you use in the remainder of this chapter can be found on the CD-ROM that accompanies this book. |
To build your class, copy the entire \WIN95NT4\SOURCE\CHAP33\TRIANGLE directory from the CD-ROM that accompanies this book into \java\classes\Triangle on your own computer. Macintosh users will find the code in \SOURCE\CHAP33\TRIANGLE; Windows NT 3.51 users must either install the source code to their hard drives or select the files from the zipped source code on the CD-ROM. From \java\classes\Triangle, compile the Triangle class using javac just as you normally would:
C:\java\classes\Triangle> javac Triangle.java
In a normal Java program, this statement would do 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 for a native application is a header file for the Java Triangle class (see Listing 33.2). This header file gives the native C code routine a layout of how data is arranged within your Java class. It also provides a prototype of how the methods from your object-oriented naming-space class files translate into C's flat naming space.
To generate the stub header file from the Triangle.class file, execute javah in the C:\java\classes\native directory:
C:\java\classes\Triangle> javah Triangle.java
Listing 33.2. The Triangle.h
file (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 (types not defined by the Java class library or the developer)
in Java has a corresponding type in Java. Table 33.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, String objects), there can be cases where there are no one-to-one type correspondences with C. This can be a major headache in Java-but we tackle this problem in the second native method interface example.
The second part of the javah-generated header file contains the prototypes for the native functions defined in the Java class. For the Triangle class, there is only one prototype: the ComputeArea() method. The return type is float (as expected from the type-translation chart in Table 33.1), but the function contains an 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 lets you access the Triangle class variables through the class's 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 the DLL you loaded with the System.loadLibrary() call a few sections earlier.
To create the stub file, type the following at the command line:
C:\java\classes\Triangle> javah -stubs Triangle
This statement creates the file Triangle.c (see Listing 33.3).
Listing 33.3. The Triangle.c
file 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; }
This is a lot of work for one puny native method. Fortunately, you'll use a makefile to automate your work in future sessions. The worst is over-you're ready to build an implementation of the native function.
For the Java interpreter to find the native function, the interpreter has to match the prototype in the Triangle.h file type for type.
Here is the prototype from the generated header file:
__declspec(dllexport) float Triangle_ComputeArea(struct HTriangle *);
In the implementation file, you must match exactly everything from the float return type to the right.
As shown in Listing 33.3, the implementation file must also 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.
The final building block you need to implement the native ComputeArea()
function is a way of accessing the class variables from within
the native function. You accomplish this using the unhand
macro provided by the StubPreamble.h
header file. The unhand macro
takes a pointer to a handle to a class, such as struct
Htriangle*, and returns a pointer to a ClassClass
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 or 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
By 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 33.4.
Listing 33.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 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 uses command lines 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 have 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. Add the following lines to your autoexec.bat file (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 these changes made, reexecute your autoexec.bat file by rebooting your computer. Then move back to your Java source directory and compile the implementation of the native Triangle class with the following command line. (Note that cl is the Microsoft Visual C++ command-line compiler. For other development platforms, substitute the equivalent command.)
C:\java\classes\Triangle> cl /c W3dTriangleImp.c
Likewise, compile the stub file with this command line:
C:\java\classes\Triangle> cl /c W3dTriangleImp.c
Finally, link these two OBJ files 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 33.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 33.5. Triangle.mak.
OBJS = Triangle.obj TriangleImp.obj LIBS = javai.lib COMPFLAGS = /c /MLd /Gm /Od /Zi 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 $(COMPFLAGS) W3dTriangleImp.c Triangle.c: Triangle.class javah -o Triangle.c -stubs Triangle Triangle.obj: Triangle.c cl $(COMPFLAGS) Triangle.c
Let's build a test class to demonstrate the new Triangle class in action. Listing 33.6 shows a test application that uses the Triangle class. After building it with javac, use the Java interpreter to run the application.
Listing 33.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()); } }
Following is the output of the TestTriangle execution:
C:\java\classes\Triangle> java TestTriangle The Area of the Triangle is 6.0
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 33.7, maintains a sorted list of strings. Because sorting is a compute-intensive task, let's write the sorting algorithm as a native method. Native methods are good at compute-intensive tasks because C code runs as compiled instructions for the underlying hardware; Java, on the other hand, runs virtual machine instructions that are very far from the underlying hardware.
Listing 33.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 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 because 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 Java class won't notice the difference.
The next thing you should note about the SortedList class is that the class takes and returns a String from its AddString() and GetString() methods, respectively. This arrangement is different than what we did in the first example because here 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.)
The C/C++ implementation of the SortedList implementation is shown in Listing 33.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 definitions is that the C++-style definition includes information about the types passed to and returned from the function; the C-style definition contains only the function name. If you leave out the extern "C" { directive, all the functions are 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. Also notice that the extern "C" { directive surrounds the Java include files. These include files also have prototypes for Java interpreter functions that must be defined using the C-style definition.
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
must be developed for any native Java class that manages its data
in C.
Tip |
Store your data in Java and use unhand() to access it whenever it yields acceptable performance. Doing so can 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 preceding 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.
Now 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 follows the package_package_..._class nomenclature.
Java strings are not passed by value, as were all the types you considered previously. 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 to avoid the overhead in passing large objects to functions and to provide security that doesn't exist if a pointer is given to reference the object. 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.
Because Java strings and C strings are stored differently, you must use the makeCString() interpreter API call to convert your string first. 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);
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.
As you may expect, the GetString() native function has a similar conversion need. To return the String referenced by the passed index, Java has 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, instan-tiates 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 to return the [nIndex]th String in the [hList]th List as follows:
return (makeJavaString(lLists[hList][nIndex],strlen(lLists[hList][nIndex])));
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 in Listing 33.8. This listing shows how all the parts described in the previous sections fit together.
Listing 33.8. 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 nLists; long nListsAllocated = 0; long nListsUsed = 0; long *nStringsAllocated; char ***lLists; 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 = 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, 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++) { changed = FALSE; for (i=0; i < nElements-1; i++) { if (strcmp(lSortStrings[i], lSortStrings[i+1]) > 0) { 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); 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; if (nIndex > nStringsUsed[hList]) { return(NULL); } 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 33.9. 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 statement before the applet is presented to Netscape or the applet viewer.) SortPresidents takes a list of presidents' names, sorts them based on last name, and prints 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 33.9. 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 \WIN95NT4\SOURCE\CHAP33\SORTEDLIST directory on the CD-ROM that accompanies this book into \java\classes\SortedList on your machine. Macintosh users will find the code in \SOURCE\CHAP33\SORTEDLIST; Windows NT 3.51 users must either install the source code to their hard drives or select the files from the zipped source code on the CD-ROM. 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 build of SortedList should look something like this:
C:\java\classes\SortedList> nmake SortedList.mak Microsoft (R) Program Maintenance Utility Version 1.60.5270 Copyright (c) Microsoft Corp 1988-1995. All rights reserved. javac SortedList.java javah SortedList ...
We also should compile our sample applet SortPresidents, which uses the SortedList class:
C:\java\classes\SortedList> javac SortPresidents.java
You are now ready to run the applet, as shown in Listing 33.10. The HTML file RunIt.html is bundled on the CD-ROM that accompanies this book; it contains a link to this applet.
Listing 33.10. 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
Because of the drastic performance advantage that C has in performing array-based operations, it is very common to pass arrays into a native routine. In this section, you explore 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 33.11 and 33.12 (in the following section). 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 is designed this way to simplify the implementation of GradeBook and to remove the details present in the SortedList class from the last section. A real application that wants to use more than one GradeBook class requires a rewrite of the class to support multiple instantiations.
Listing 33.11. 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"); } }
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 a String array that contains 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.
The implementation file for the GradeBook class is shown in Listing 33.12. The file's general skeleton is very similar to the last two classes you have considered.
Listing 33.12. 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); for (long cTestNum=0, float 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); }
The NameStudents() method is responsible for associating the names of all the students in the class with the test scores. This arrangement 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. The 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 struct HArrayOfString, body points to a list of String handles that contain the names of all the students in the class.
Reading in the names of the students in the class is 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 = (struct Hjava_lang_String *) (unhand(JavalStudents)->body)[i]; lStudents[i] = makeCString(hStudentName);
This method 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; 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 an intrinsic Java type is easier than accessing an array of Java classes. In the GradeBook class, the AddTest() method accepts a list of float test scores for each student. Its native method implementation prototype looks like this:
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++;
A sample Java application, TeachersPet, uses the GradeBook class and is shown in Listing 33.13. This application creates a new GradeBook with five students and three sets of test results. With this database created, the application then finds the overall average for each student and the average for the class as a whole.
Listing 33.13. TeachersPet.java.
public class TeachersPet { public static void main(String argv[]) { int nStudents = 5; int nTests = 3; GradeBook myClass = new GradeBook(nStudents, nTests); 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); float lTest1Grades[] = new float[nStudents]; lTest1Grades[0] = 93; lTest1Grades[1] = 86; lTest1Grades[2] = 89; lTest1Grades[3] = 65; lTest1Grades[4] = 78; myClass.AddTest(lTest1Grades); float lTest2Grades[] = new float[nStudents]; lTest2Grades[0] = 100; lTest2Grades[1] = 83; lTest2Grades[2] = 91; lTest2Grades[3] = 55; lTest2Grades[4] = 83; myClass.AddTest(lTest2Grades); float lTest3Grades[] = new float[nStudents]; lTest3Grades[0] = 89; lTest3Grades[1] = 94; lTest3Grades[2] = 82; lTest3Grades[3] = 59; lTest3Grades[4] = 85; myClass.AddTest(lTest3Grades); for (int cStudent = 0, float fStudentAvg=0.0f; cStudent < nStudents; cStudent++) { fStudentAvg = myClass.GetStudentAvg(lszStudents[cStudent]); System.out.println(lszStudents[cStudent]+"'s average on the 3 tests is "+fStudentAvg); } for (int cTest = 1, float fClassAvg=0.0f, float fTestAvg = 0.0f; 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 you did with the preceding two examples, and 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
Having explored the basics of the native method interface, let's shift to one of the most common uses of native methods-as an interface to existing C and C++ libraries. First, you learn a methodology for building an interface to existing C libraries using a very simple signal-processing library as an example.
Next, you investigate interfacing to C++ libraries by developing a wrapper system for overcoming Java's C-only native method interface using a very simple 3D library as an example. As you go along, you examine all the problems in interfacing to this library.
A tremendous number of C libraries exist in the development world today-primarily because of the popularity of the C language. Because of either the time necessary to develop these legacy libraries, or the sheer performance required of them, it may be very difficult to port a C library entirely to Java. Instead, it may be necessary to develop a Java interface to the library.
Interfacing C with Java is difficult for one main reason: Java is entirely object oriented. Unlike C++, Java does not enable you to use procedural functions within the context of an object-oriented program. Every construct must involve an object of some sort.
There are two general methods of working around Java's object requirement. First, you could sit down with the list of functions in the C library and carve it up into functionally related blocks. For each block of the library, you could then develop a class that contains each function as a static Java method within the class. The class is, of course, then titled with some moniker that indicates the relation of all the functions it contains.
Although this is probably the fastest way to convert your library over to Java, it may not be the smartest. Although your current C library users will have no difficulties getting used to the Java interface, your new Java users may have some difficulty because the interface won't be object oriented. As a solution to this dilemma, consider the feasibility of developing an object-oriented interface to your library. In the next section, we look at how this can be done.
Listing 33.14 shows the DataSample object, which implements storage for a set of signal samples and provides methods to calculate the real FFT, the cosine FT, and the sin FT. Notice how it isn't just a static wrapper for the FFT functions, but rather a class library that encapsulates data-sampling functions with a set of FFT functions added.
Listing 33.14. The DataSample
class.
package mySigProcLib; class DataSample { public void AddSample(float sample) { // ...Some signal management logic here... } public void DeleteSample(int I) { // ...Some more signal management logic here... } public native void realFFT(float FFTresult[]); public native void cosFFT(float FFTresult[]); public native void sinFFT(float FFTresult[]); int nSamples; float fSamples[]; }
The implementation of the DataSample class is shown in Listing 33.15. There's nothing new in this implementation, but it does show how to structure your C implementation to achieve the feel of an object-oriented library in your Java interface object.
Listing 33.15. The implementation of the DataSample
class.
#include <native.h> #include <FFT.h> void mySigProcLib_DataSample_realFFT(struct mySigProcLib_DataSample* hthis, struct HArrayOfFloat* FFTresult) { realFFT(unhand(unhand(hthis)->fSamples)->body, unhand(hthis)->nSamples, unhand(FFTresult)->body); } void mySigProcLib_DataSample_cosFFT(struct mySigProcLib_DataSample* hthis, struct HArrayOfFloat* FFTresult) { cosFFT(unhand(unhand(hthis)->fSamples)->body, unhand(hthis)->nSamples, unhand(FFTresult)->body); } void mySigProcLib_DataSample_sinFFT(struct mySigProcLib_DataSample* hthis, struct HArrayOfFloat* FFTresult) { realFFT(unhand(unhand(hthis)->fSamples)->body, unhand(hthis)->nSamples, unhand(FFTresult)->body); }
With the explosion of interest in object-oriented design has come an explosion in the number of available C++ class libraries. This section extends the discussion of interfacing to cover the development of an interface to existing C++ libraries. To make the description of this design process more concrete, we'll take an existing C++ class library and develop a parallel set of Java classes that transparently classes provide the same look and feel as the C++ classes.
To demonstrate an interface between Java objects and C++ objects, let's use a few components of a very primitive 3D graphics class library, My3D.
Assume that the My3D C++ library is either too performance-sensitive to be converted to Java, or that redevelopment of the Java library would involve so much time that developing a Java interface to the C++ class is a better investment.
Listing 33.16 contains the World object, the first C++ object we'll consider in this simple 3D library. The World object is responsible for handling the details of attaching and detaching objects from a 3D scene. The methods AttachNode() and DetachNode(), as you may have guessed, are responsible for taking a Node object and attaching or detaching the Node from the scene graph.
The World class also contains a list of pointers to Node objects in the private section of the definition, which is used to store the scene graph. However, as you'll see when you implement the World class, this information isn't necessary to interface the class with Java-you really have to know only the public methods and variables for the class.
Listing 33.16. World
C++ class definition.
class World { public: void AttachNode (Node* theNode); Node* DetachNode (Node* RemoveNode); private: Node** NodeList; };
The next class in the My3D C++ class library is the Node class, shown in Listing 33.17. This class contains the mechanisms necessary to give an object in World a location. This superclass is the base class for all objects that appear in the rendered 3D scenes. The class contains only two accessor methods, SetLocation() and GetLocation(), which are used to set and retrieve the position of the node in space. The data structure PointFW_t is used to encapsulate these points (see Listing 33.18).
Listing 33.17. Node
C++ class definition.
class Node { public: void SetLocation (PointFW_t& loc); PointFW_t GetLocation(); private: PointFW_t theLocation; }
Listing 33.18. PointFW_t
C++ class definition.
typedef struct PointFW_t { float x; float y; float z; float w; } PointFW_t;
The Light class is a subclass of Node, which makes it attachable within your World scenes (see Listing 33.19) and gives it position. The Light object models a light in the scene by adding a direction and color to the subclass with the associated accessors, Set/GetDirection() and Set/GetColor().
Listing 33.19. Light
C++ class definition.
class Light : public Node { public: void SetDirection (PointF_t& dir); PointF_t& GetDirection(); void SetColor (ColorF_t& theColor); ColorF_t& GetColor(); private: PointF_t TheDirection; ColorF_t TheColor; };
The Light class also introduces
two new data structures, PointF_t
and ColorF_t (see List-
ings 33.20 and 33.21). The PointF_t
data structure encapsulates a vector that points in the direction
<x,y,z>. The ColorF_t
type represents a color with the red, green, and blue components,
<r,g,b>.
Listing 33.20. PointF_t
C++ struct
definition.
typedef struct PointF_t { float x; float y; float z; } PointF_t;
Listing 33.21. ColorF_t
C++ struct
definition.
typedef struct ColorF_t { float r; float g; float b; } ColorF_t;
The final class in the My3D graphics library (if only real 3D graphics class libraries could be so simple!) is the Geometry class. This class is also a subclass of Node, which enables the class library user to specify a geometric object in the graphics scene. The constructor for the Geometry class takes all the information necessary to specify the object in space: the number of polygons and vertices, the points in space for all the vertices and the vertex normals, and the ordering of which points go with which polygon.
Two methods control rotations and scaling around its central location specified with SetLocation() from the Node class: RotateObject() and ScaleObject(). The Geometry class is shown in Listing 33.22.
Listing 33.22. Geometry
C++ class definition.
class Geometry : public Node { public: Geometry(long INnPolygons, long INnVertices, PointF_t* INpVertices, PointF_t* INpNormals, long* INVerOrder); void RotateObject(double theta, PointF_t* RotationAxis); void ScaleObject(PointF_t* ScaleFactors); private: long nPolygons; long nVertices; PointF_t* pVertices; PointF_t* pNormals; PointF_t* pFacets; PointF_t* pTexture; ColorFA_t* pColors; long* VerOrder; }
Now you can move on to the real task of building Java objects that interface with the C++ classes you have just developed. Before doing this, however, you first need a class that encapsulates all the interface information you need about your C++ class, the InterfaceObject class (see Listing 33.23).
Listing 33.23. Java InterfaceObject
definition.
package My3D; public class InterfaceObject { public int KindOf() { return (ObjKindOf); } // Returns true if the object type passed to IsOf matches this // object's type. public boolean IsOf(int k) { if (k == ObjKindOf) { return(true); } else { return(false); } } public int hCPPObj; public int ObjKindOf; static { System.load("my3d.dll"); } }
The InterfaceObject class is the base for all your object classes. It contains two methods to help with object identification (you'll see where this is useful in a few sections): KindOf() and IsOf(). KindOf() is used to access the object type. This object type is a constant that uniquely identifies the object's type. The IsOf() method tests the object type passed to it, to see whether it is the same as this object's type, by using the internal constant as a check.
The InterfaceObject class also encapsulates two variables: hCPPObj and ObjKindOf. ObjKindOf contains the ordinal type of the object. hCPPObj contains a handle to the parallel C++ instantiation of this object. You'll see how both these variables are set in the next section.
Let's start interfacing your library to Java with the World class; the implementation of the Java class is shown in Listing 33.24.
Listing 33.24. World
Java class definition.
package My3D; public class World extends InterfaceObject { public native void constructor(); public World() { constructor(); } public native void finalize(); public native void AttachNode (Node theNode); public native Node DetachNode (Node afterNode); }
Your first impression of this class should be that it looks very similar to the C++ one. This is good! However, you have to learn a few more things about native implementations before this illusion can become a reality for your Java class library users.
Let's start at the beginning, with the Java World class constructor. As you saw in Listing 33.24, the World Java implementation class calls a native constructor in its constructor. You want your native constructor to accomplish the following tasks:
The native constructor implementation for the World class is shown in Listing 33.25.
Listing 33.25. World
interface constructor.
long My3D_World_AllocLength = 0; #define INITIAL_My3D_World_ALLOC 2 World** My3D_World_ObjPtr = NULL; long* My3D_World_ObjRef = NULL; void My3D_World_constructor(struct HMy3D_World *jthis) { if (My3D_World_AllocLength == 0) { My3D_World_AllocLength = My3D_World_Resize(INITIAL_My3D_World_ALLOC); } // Search for an empty position (empty position == NULL). long pos; for ( pos=0; (pos != My3D_World_AllocLength) && (My3D_World_ObjPtr[pos] != NULL); pos++ ) ; if (pos == My3D_World_AllocLength) { // All allocated positions are full. // So use exponential allocation to create some more. My3D_World_AllocLength = My3D_World_Resize(My3D_World_AllocLength*2); } My3D_World_ObjPtr[pos] = new World(); // Stub for handling out of memory condition. assert (My3D_World_ObjPtr[pos] != NULL); // Increment Reference counter. My3D_World_IncRefCntr(pos); // Store handle (== position in array) for this // object. unhand(jthis)->hCPPObj = pos; }
In every instantiable class considered in this chapter, your interface functions maintain a list of pointers to all the C++ objects you have instantiated indirectly by instantiating its Java interface class. In the preceding constructor, the first statement checks to see whether this list should be initialized:
if (My3D_World_AllocLength == 0) { My3D_World_AllocLength = My3D_World_Resize(INITIAL_My3D_World_ALLOC); }
My3D_World_Resize() is a helper function responsible for increasing and decreasing the amount of allocation for the pointer list to World objects. There is a similar function for each of the interface files considered in this chapter. The code for the My3D_World_Resize() function is shown in Listing 33.26.
Listing 33.26. My3D_World_Resize()
function definition.
long My3D_World_Resize(long newsize) { World** NewObjPtr = new World* [newsize]; long* NewRefPtr = new long[newsize]; long i; for (i=0; i != My3D_World_AllocLength; i++) { NewObjPtr[i] = My3D_World_ObjPtr[i]; NewRefPtr[i] = My3D_World_ObjRef[i]; } for (; i != newsize; i++) { NewObjPtr[i] = NULL; NewRefPtr[i] = 0; } delete My3D_World_ObjPtr; delete My3D_World_ObjRef; My3D_World_ObjPtr = NewObjPtr; My3D_World_ObjRef = NewRefPtr; return (newsize); }
Unused pointers in the C++ object array are set to NULL in the My3D_World_Resize() function. After checking in the interface constructor code to see whether the pointer list has been initialized, the constructor next tries to find an empty slot in the pointer list:
long pos; for ( pos=0; (pos != My3D_World_AllocLength) && (My3D_World_ObjPtr[pos] != NULL); pos++ ) ;
The loop quits before it reaches My3D_World_AllocLength if it does find an empty slot. If it doesn't, the constructor allocates new pointer space:
if (pos == My3D_World_AllocLength) { // All allocated positions are full. // So use exponential allocation to create some more. My3D_World_AllocLength = My3D_World_Resize(My3D_World_AllocLength*2); }
Notice that the constructor allocates pointer space exponentially-every time the constructor is forced to resize the pointer list, it does so to twice the size of the last pointer list size. This helps reduce the number of times that memory allocation is needed for large object databases, without wasting memory for small object databases. Because memory allocation is such an expensive event, you should implement this or an equivalent scheme in your interface code unless the size of the pointer array is known beforehand or the number of objects typically needed is very small (for example, fewer than eight).
With an empty spot on the list found for the instantiated pointer, the constructor allocates a World object and stores it in the list:
My3D_World_ObjPtr[pos] = new World();
The World object's constructor doesn't take any arguments, and for that reason, none are passed into the World constructor. If your constructor does take arguments, this is where you should change your routines to pass them in. If your class contains multiple constructors, you have to create multiple constructor interfaces in your implementation file to handle each variant-ugly, but because C does not have any functionality such as overloading in C++, it is the only way to accomplish this.
Directly following this allocation is an assert() function call that checks to make sure that the memory allocation occurred safely. If it didn't, the library will fail with the assertion. For your library, you should replace this assertion statement so that your program can handle any problems in a graceful manner.
As you'll see in later sections, the underlying C++ library can sometimes return pointers to objects during calls to C++ methods. To mimic this operation in your Java library, you will want to add functionality to translate this pointer to one of the Java objects. Because multiple Java interface objects can refer to a single C++ pointer, you must maintain a reference counter to keep track of how many objects have a copy:
My3D_World_IncRefCntr(pos);
This reference counter is implemented as an array of integers called My3D_World_ObjRef. The reference count for an object is stored in the same position in the array as its pointer is in My3D_World_ObjPtr (or equivalently, at the hCPPObj position stored in the Java interface object). The implementation of the IncRefCntr() and DecRefCntr() functions is shown in Listing 33.27.
Listing 33.27. IncRefCntr
function definition.
void My3D_World_IncRefCntr(long ThehCPPObj) { My3D_World_ObjRef[ThehCPPObj]++; }
The DecRefCntr() function, used when a Java interface object using a pointer moves out of scope, decrements the reference counter. It then checks to see whether any other Java objects are still using this pointer, indicated by the reference count being greater than 0. If not, DecRefCntr() deletes the underlying object. The implementation of DecRefCntr() is shown in Listing 33.28.
Listing 33.28. DecRefCntr()
function definition.
void My3D_World_DecRefCntr(long ThehCPPObj) { My3D_World_ObjRef[ThehCPPObj]-; if (My3D_World_ObjRef[ThehCPPObj] == 0) My3D_World_ObjPtr[ThehCPPObj]->Destroy(); } }
The final statement in the World constructor assigns the position in the pointer list to the Java interface object's hCPPObj variable so that when a C++ object pointer is needed, it is available by lookup in the pointer table. This completes all the tasks you set out to do in your World constructor.
Now let's focus on how you implement the methods within the Java World interface. This task really boils down to translating a call to one of these Java methods to an equivalent call with the same data (but possibly in a different C++ format) to the C++ version of the class.
Listing 33.29 shows the implementation for the AttachNode()method:
Listing 33.29. AttachNode()
implementation.
void My3D_World_AttachNode (struct HMy3D_World *hthis, struct HMy3D_Node *hNode) { My3D_World_ResolvePtr(hthis)->AttachNode(My3D_Node_ResolvePtr(hNode)); }
The AttachNode() function prototype should look very familiar to you from earlier in this chapter. This function is passed the Java World object handle as the first parameter and a handle to the Node to be attached as the second parameter. You haven't explored the interface constructor of the Node object yet, but you really don't need to-it is completely analogous to the one you saw for the World object. Literally, only the names have been changed.
The body of the AttachNode() native method contains only one statement: The call to My3D_World_ResolvePtr() looks up the World C++ pointer referenced by this and calls this World object's AttachNode() method with the Node object referenced by the hNode and resolved by using My3D_Node_ResolvePtr().
The My3D_World_ResolvePtr() and My3D_Node_ResolvePtr() functions referenced earlier are used to find the C++ pointers to the Node and World objects. These functions take a handle to an InterfaceObject and return the C++ pointer to the Java interface object. They do this translation using the hCPPObj handle for the World class. The implementation is shown in Listing 33.30.
Listing 33.30. ResolvePtr()
function definition.
World* My3D_World_ResolvePtr(struct HMy3D_World *jthis) { return( My3D_World_ObjPtr[unhand(jthis)->hCPPObj] ); }
ResolvePtr() is a very simple function. It retrieves the object handle stored in hCPPObj and returns the World object pointer referenced by it. You'll see a more complicated example of this function when you consider the implementation of this function for an abstract class such as the Node class.
The DetachNode() method is the final explicit method needed for the World object. Its implementation is shown in Listing 33.31.
Listing 33.31. DetachNode()
function definition.
struct HMy3D_Node* My3D_World_DetachNode(struct HMy3D_World *hthis, struct HMy3D_Node *hNode) { Node* CPPNode = My3D_World_ResolvePtr(hthis)->DetachNode (Node_ResolvePtr(hNode)); ClassClass *ccNode = NULL; ccNode = FindClass(NULL, "My3D/Node\0", TRUE); assert(ccNode != NULL); struct HMy3D_Node *hNode = (struct HMy3D_Node*)execute_java_constructor(NULL,"My3D/Node\0", ccNode,"()"); assert(hNode != NULL); My3D_Node_PtrEmul(CPPNode, hNode); return(hNode); }
There are a lot of new things in the implementation of the My3D_World_DetachNode() function. First, it returns a class that isn't one of the standard set of objects available in Java. This is the main reason that the function is so complicated. Earlier in this chapter, you learned how to return a java.lang.String class. However, the String class has a Java interpreter call, makeJavaString(), that enables you to instantiate a String for the return. Because Node is a developer-defined class and is not from the Java class library, there is no equivalent call for Node in the Java interpreter API. Instead, you have to learn how to use the interpreter calls to instantiate and return arbitrary Java objects.
The first two lines of the function are pretty straightforward. The function resolves the pointer for the World object and the passed Node object and calls the DetachNode() method. The DetachNode() method returns a pointer to the Node detached, which it stores in CPPNode.
Now you want to find the handle of the Java object that corresponds to the returned C++ pointer of the detached Node and create a Node handle that encapsulates this handle to return.
Before you instantiate a Java Node class, you must fill out a ClassClass structure (as discussed earlier in this section). You did this in My3D_World_DetachNode() using the following statement:
ccNode = FindClass(NULL, "My3D/Node\0", TRUE);
This statement tells the Java interpreter to look up the class information for the My3D.Node Java class. The first parameter, for which you passed NULL, enables you to specify an interpreter to service this request through an ExecEnv* (the type ExecEnv is defined in interpreter.h, but an understanding of its contents is not necessary for this discussion). Passing NULL indicates that you want to use the default interpreter to look up this information. Unless you are writing native programs that use multiple interpreters (you know if you are-it's a painful experience), you can ignore this parameter and pass NULL.
The third parameter tells the interpreter whether to resolve the name passed into FindClass. You should always pass TRUE for this parameter. Sun hasn't documented exactly what this function does if you don't, so as of this writing, there is no definitive reason.
Next, you want to instantiate a Java Node that you can use to return the C++ Node returned from the DetachNode() method. To do this, you use the Java interpreter function execute_java_constructor():
hNode = (struct HMy3D_Node*) execute_java_constructor(NULL,"My3D/Node\0", ccNode, "()");
The execute_java_constructor() function takes an ExecEnv* as the first parameter and the name of the class as the second, just as the FindClass() function call did. For the third parameter, you should pass the ClassClass structure returned by FindClass() in the last step.
The final parameter specifies the signature of the Node constructor you want to call. This signature indicates the types you want to pass and receive from the invoked Java method. The signature passed in the preceding code ("()") is the simplest-a constructor that accepts no parameters and returns nothing. All constructors do not have a return type, but if you have a constructor in the Node class that accepts an int as a parameter for the constructor, its constructor would have been "(I)". The return type is specified after the closing parenthesis for methods that return values. For this hypothetical constructor that accepts an integer, you pass the integer parameter for the constructor as the fifth parameter. This parameter list is unbounded, like the parameter list for printf() in the C language, so to pass in additional parameters, you merely keep adding them to the list. For example, the following statement passes a 4 to a hypothetical Node constructor that accepts an integer:
hNode = (struct HSolidCoffee_PointFW_t*) execute_java_constructor(NULL,"My3D/Node\0", ccNode,"(I)", 4);
Now that you have a Java Node instantiated, you want to do the following:
All this is done by the function PtrEmul(), as used in the My3D_World_DetachNode() function:
My3D_Node_PtrEmul(CPPNode, hNode);
The My3D_Node_PtrEmul() implementation is shown in Listing 33.32. It emulates a pointer reference system for your interface code.
Listing 33.32. Node PtrEmul()
function definition.
void My3D_Node_PtrEmul(Node* pNode, struct HMy3D_Node* hNode) { long hRetNode = My3D_Node_FindHandle(pNode); My3D_Node_IncRefCntr(hRetNode); My3D_Node_DecRefCntr(unhand(hNode)->hCPPObj); unhand(hNode)->hCPPObj = hRetNode; }
The My3D_Node_PtrEmul() function is relatively straightforward (you learn the internals later in this chapter). For now, all you need to know is that My3D_Node_FindHandle() returns the handle to the Node object (or any of its subclasses) to which the C++ pointer refers.
After finding the corresponding handle for the Node C++ object, the function increments its reference count because another Java Node object will now be using this pointer. The function then decrements the reference count for the target's current Node C++ object because it is no longer referenced by this Java Node. Finally, the function copies the handle into the target, completing the copy.
With that done, you now have a handle to a Java Node object that refers to the Node returned by DetachNode(). To complete the interface function, you merely return this handle to the caller, which is done with the last statement of the function.
To complete the discussion of the Java World interface object, let's look at its My3D_World_FindHandle() implementation. The My3D_World_FindHandle() function is responsible for taking a pointer to a World object and translating it back into a handle that can be used to reference the World object's My3D_World_ObjPtr array. The implementation for this is shown in Listing 33.33.
Listing 33.33. World FindHandle()
implementation.
long My3D_World_FindHandle(World *FindWorld) { for (long pos=0; (pos != My3D_World_AllocLength) && (My3D_World_ObjPtr[pos] != FindWorld); pos++); // Stub for appropriate handling of pointer not // found errors. assert (pos != My3D_World_AllocLength); return (pos); }
Because in the 3D graphics library, you expect that there is only a small number of pointer lookups using the FindHandle() function, this lookup function is written using a simple linear search function. If a large number of lookups is anticipated, this function should be rewritten with a hashing search method to reduce its performance impact.
The final interface function for the World class is the interface for the My3d_World_finalize() method. If you are not familiar with this method, it is called when the Java garbage collector has to dispose of an object because it has moved out of scope. This method enables the object to clean up after itself and perform any last-minute maintenance tasks before the allocation for its data is freed. For your Java interface objects, you use this function to decrement the reference counter for the indicated object to keep your reference count coherent. The implementation of My3D_World_finalize() is shown in Listing 33.34.
Listing 33.34. World finalize()
implementation definition.
void My3D_World_finalize(struct HMy3D_World *jthis) { My3D_World_DecRefCntr(unhand(jthis)->hCPPObj); }
The next interface implementation is the interface to the Node object. Listing 33.35 shows the Java object definition. It doesn't look much different from its C++ counterpart (which is helpful for any converts you may have from your C++ product).
Listing 33.35. Node
Java class definition.
public class Node extends InterfaceObject{ public native void SetLocation (PointFW_t loc); public native PointFW_t GetLocation(); public native finalize(); }
You have to define the Java version of the C++ PointFW_t structure so that you can present the implementation of the SetLocation() and GetLocation() methods. The Java version is very similar to the C++ version, and its implementation is shown in Listing 33.36.
Listing 33.36. PointFW_t
Java class definition.
class PointFW_t { float x; float y; float z; float w; };
The native implementation of the SetLocation() method is shown in Listing 33.37.
Listing 33.37. SetLocation()
implementation.
void My3D_Node_SetLocation (struct HMy3D_Node *hthis, struct HMy3D_PointFW_t *hLocation) { PointFW_t CPPLocation; CPPLocation.x = unhand(hLocation)->x; CPPLocation.y = unhand(hLocation)->y; CPPLocation.z = unhand(hLocation)->z; CPPLocation.z = unhand(hLocation)->z; My3D_Node_ResolvePtr(hthis)->SetLocation(CPPLocation); }
This implementation is very similar to the AttachNode()
interface function from earlier in this chapter, but because the
PointFW_t class is a simple
C++ object, you create a C++ PointFW_t
structure with each invocation of SetLocation()
to pass the location into the C++ method instead of maintaining
a list of PointFW_t pointers.
Note |
You explicitly copy the members of the structure instead of de-referencing the pointer because of the chance of a mismatch between Java's representation of the PointFW_t class and C++'s representation. Specifically, the order of the elements of C++'s representation may be the reverse of the Java implementation, yielding SetLocation (<w,z,y,x>) when C++'s implementation is SetLocation (<x,y,z,w>). Although the correlation is one-to-one with Visual C++ 4.0 and JDK 1.0, it isn't set in stone. The safe programming practice is to assume no correlation and copy the elements one by one. In any case, compared to the overhead of calling the native method, the performance impact of these four copies is negligible. |
The native implementation of the GetLocation() method is shown in Listing 33.38.
Listing 33.38. GetLocation()
implementation.
struct HMy3D_PointFW_t* My3D_Node_GetLocation() { PointFW_t CPPLocation = My3D_Node_ResolvePtr(hthis)->GetLocation(); ClassClass *ccNode = FindClass(NULL, "My3D/PointFW_t\0", TRUE); assert(ccNode != NULL); struct HMy3D_PointFW_t *hLocation = (struct HSolidCoffee_PointFW_t*) execute_java_constructor(NULL, "My3D/PointFW_t\0", ccNode, "()"); assert(hLocation != NULL); unhand(hLocation)->x = CPPLocation.x; unhand(hLocation)->y = CPPLocation.y; unhand(hLocation)->z = CPPLocation.z; unhand(hLocation)->w = CPPLocation.w; return(hLocation); }
The implementation of GetLocation() is very similar to the DetachNode() implementation. The only difference is the deletion of the PtrEmul() call and its replacement with a straight element-by-element copy of the returned PointFW_t location.
Before you leave the Node object, notice that the Node Java object doesn't have a constructor. This is important because it means that no C++ Node objects can be instantiated. This was intended in the C++ design, because only the Light and Geometry objects were designed to be instantiated. You'll see how this works into the design when you consider the implementation of the My3D_World_PtrEmul() function later in this chapter. Before you do that, let's lay the groundwork by discussing the two subclasses of Node: Light and Geometry.
Listing 33.39 shows the Java implementation for the interface class to the Light object. Because Light is a subclass of Node in the C++ definition, it is also a subclass in the Java version. Whenever possible, you should strive to keep your object hierarchies the same between your Java and C++ versions to prevent difficulties in interfacing the two libraries and to preserve the same hierarchy with which your C++ users are familiar.
Because it can be instantiated, Light does have a constructor. The implementation of this constructor follows nearly exactly the same format as the World constructor.
The SetDirection() and GetDirection() methods also introduce a new class, PointF_t. The definition of this class is very similar to the PointFW_t class and is shown in Listing 33.40.
Listing 33.39. Light
Java class definition.
public class Light extends Node { private native void constructor(); public Light() { constructor(); } public native void finalize(); public void SetDirection(PointF_t dir); public void PointF_t GetDirection(); } Listing 33.40. PointF_t Java class definition. class PointF_t { float x; float y; float z; };
The native implementation of the SetDirection and GetDirection interfaces for the Light class are shown in Listing 33.41.
Listing 33.41. SetDirection
and GetDirection
implementation.
void My3D_Light_SetDirection (struct HMy3D_Light *hthis, struct HMy3D_PointF_t *hDirection) { PointF_t CPPDirection; CPPDirection.x = unhand(hDirection)->x; CPPDirection.y = unhand(hDirection)->y; CPPDirection.z = unhand(hDirection)->z; CPPDirection.z = unhand(hDirection)->z; My3D_Light_ResolvePtr(hthis)->SetDirection(CPPDirection); } struct HMy3D_PointF_t* My3D_Light_GetDirection() { PointF_t CPPDirection = My3D_Light_ResolvePtr(hthis)->GetDirection(); ClassClass *ccLight = FindClass(NULL, "My3D/PointF_t\0", TRUE); assert(ccLight != NULL); struct HMy3D_PointF_t *hDirection = (struct HSolidCoffee_PointF_t*) execute_java_constructor(NULL, "My3D/PointF_t\0", ccLight,"()"); assert(hDirection != NULL); unhand(hDirection)->x = CPPDirection.x; unhand(hDirection)->y = CPPDirection.y; unhand(hDirection)->z = CPPDirection.z; unhand(hDirection)->w = CPPDirection.w; return(hDirection); }
As you can see, the implementation of the SetDirection and GetDirection classes is very similar to the implementation of the SetLocation() and GetLocation() methods in the Node class.
The final interface class you need to know is the Geometry class. The Java interface class for the Geometry object is shown in Listing 33.42 and consists of the constructor and finalize methods for the Geometry class. The rest of the functionality for the Geometry class is provided by the Node class that it extends. Don't worry if you don't understand how the Geometry object specifies a polygon-it really isn't important to the discussion that follows. Instead, focus on how the interface between the Java and the C++ class works.
Listing 33.42. Geometry
Java class definition.
public class Geometry extends Node { private native void constructor(int nPolygons, int nVertices, PointF_t pVertices[], PointF_t pNormals[], PointF_t pFacets[], PointF_t pTexture[], ColorFA_t pColors[], int VerOrder[]); public Geometry(int nPolygons, int nVertices, PointF_t pVertices[], PointF_t pNormals[], PointF_t pFacets[], PointF_t pTexture[], ColorFA_t pColors[], int VerOrder[]) { constructor(); } public native void finalize(); }
The Geometry class introduces one final class to your expanding library: the ColorFA_t class, which is responsible for specifying the colors of all the vertices in the object. Listing 33.43 shows the implementation of the ColorFA_t class.
Listing 33.43. ColorFA_t
Java struct
definition.
class ColorFA_t { float r; float g; float b; float a; };
The implementation for the Geometry class constructor is shown in Listing 33.44. This constructor demonstrates how to handle arrays of passed objects in your interface code, which is a common source of confusion for native method users.
Listing 33.44. Geometry
constructor implementation.
long My3D_Geometry_AllocLength = 0; #define INITIAL_My3D_Geometry_ALLOC 2 Geometry** My3D_Geometry_ObjPtr = NULL; long* My3D_Geometry_ObjRef = NULL; void My3D_Geometry_constructorb( struct HMy3D_Geometry *jthis, short cNP, short cNV, HArrayOfObject* pAP, HArrayOfObject* pAN, HArrayOfObject* pFN, HArrayOfObject* pAT, HArrayOfObject* pAC, if (My3D_Geometry_AllocLength == 0) { My3D_Geometry_AllocLength = My3D_Geometry_Resize(INITIAL_My3D_Geometry_ALLOC); } long pos; for ( pos=0; (pos != My3D_Geometry_AllocLength) && (My3D_Geometry_ObjPtr[pos] != NULL); pos++ ) ; if (pos == My3D_Geometry_AllocLength) { My3D_Geometry_AllocLength = My3D_Geometry_Resize(My3D_Geometry_AllocLength*2); } int PassNP = cNP; int PassNV = cNV; // Copy the Vertex points from the Java Array into a C++ array int len = obj_length(pAP); PointF_t* PassAP = new PointF_t[len]; struct HMy3D_PointF_t* hAP; for (i=0; i != len; i++) { hAP = (struct HMy3D_PointF_t *) (unhand(pAP)->body)[i]; PointF_t* PtrAP = (PointF_t *) unhand(hAP); PassAP[i] = *PtrAP; } // Copy the Normals from the Java Array into a C++ array len = obj_length(pAN); PointF_t* PassAN = new PointF_t[len]; struct HMy3D_PointF_t* hAN; for (i=0; i != len; i++) { hAN = (struct HMy3D_PointF_t *) (unhand(pAN)->body)[i]; PointF_t* PtrAN = (PointF_t *) unhand(hAN); PassAN[i] = *PtrAN; } // Copy the Facet Normals from the Java Array into a C++ array. len = obj_length(pFN); PointF_t* PassFN = new PointF_t[len]; struct HMy3D_PointF_t* hFN; for (i=0; i != len; i++) { hFN = (struct HMy3D_PointF_t *) (unhand(pFN)->body)[i]; PointF_t* PtrFN = (PointF_t *) unhand(hFN); PassFN[i] = *PtrFN; } // Copy the Texture points from the Java Array into a C++ array. len = obj_length(pAT); PointF_t* PassAT = new PointF_t[len]; struct HMy3D_PointF_t* hAT; for (i=0; i != len; i++) { hAT = (struct HMy3D_PointF_t *) (unhand(pAT)->body)[i]; PointF_t* PtrAT = (PointF_t *) unhand(hAT); PassAT[i] = *PtrAT; } // Copy the Color points from the Java Array into a C++ array. len = obj_length(pAC); ColorFA_t* PassAC = new ColorFA_t[len]; struct HMy3D_ColorFA_t* hAC; for (i=0; i != len; i++) { hAC = (struct HMy3D_ColorFA_t *) (unhand(pAC)->body)[i]; ColorFA_t* PtrAC = (ColorFA_t *) unhand(hAC); PassAC[i] = *PtrAC; } // Make a pointer to the vertex orders. len = obj_length(pAV); Fixed16_t* PassAV = (Fixed16_t *)(unhand(pAV)->body); My3D_Geometry_ObjPtr[pos] = new Geometry(PassNP, PassNV, PassAP, PassAN, PassFN, PassAT, PassAC, PassAV); assert (My3D_Geometry_ObjPtr[pos] != NULL); My3D_Geometry_IncRefCntr(pos); unhand(jthis)->hCPPObj = pos; // Delete all the temporary variables. delete PassAP; delete PassAN; delete PassFN; delete PassAT; delete PassAC; }
Take, for example, the pAP array, which contains handle HArrayOfObject. When unhand() is applied to the handle HArrayOfObject, unhand() returns an array of handles to PointF_t objects for each of the points in its body element. By iterating down this array of handles, you can translate these Java PointF_t objects into C++ PointF_t objects.
Listing 33.45 shows the implementation of the My3D_Node_ResolvePtr() function.
Listing 33.45. Node
C++ ResolvePtr()
implementation.
Node* My3D_Node_ResolvePtr(struct HMy3D_Node *hNode) { switch (unhand(jthis)->ObjKindOf) { case LIGHT: return ( ((Node*) My3D_Light_ResolvePtr(hNode)); break; case GEOMETRY: return ( ((Node*) My3D_Geometry_ResolvePtr(hNode)); break; default: assert(0); return (NULL); break; }; }
Obviously, this is much different from the implementations of My3D_xxxx_ResolvePtr() for other interface classes-none of the other classes you have considered have subclasses.
Because you want your Java subclasses of Node to be able to stand in for methods requiring a Node (just as when Node was used in your C++ library), special mechanisms must be built in your superclass interface implementation to handle this requirement.
For example, when you attach a Light object to World using the method AttachNode(Node), you use the properties of Light as a subclass of Node to enable it to stand in as a Node to be attached. Remember the implementation of the My3D_World_AttachNode() for the World interface; you used the following statement to call AttachNode():
My3D_World_ResolvePtr(hthis)->AttachNode(My3D_Node_ResolvePtr(hNode));
Because there are two subclasses to Node-Light and Geometry-the hNode handle passed to My3D_Node_ResolvePtr() could be either a Light or a Geometry object. This is the reason for the switch statement in the My3D_xxxx_ResolvePtr() function implementation-you have to know to which object this handle refers. With the object determined, you can then call the My3D_xxxx_ResolvePtr() function for the appropriate object to retrieve the pointer.
With this in mind, case statements were added for both the Light and Geometry objects in the ResolvePtr function. The LIGHT and GEOMETRY constants can be anything, but they must be different. This Java interface defines them in a global header file (see Listing 33.46), which also contains all the public interface functions for each of the objects for linking.
Listing 33.46. Global header file.
#define WORLD 1 #define GEOMETRY 2 #define LIGHT 3 Node* My3D_Node_ResolvePtr(struct HMy3D_Node *hNode); Node* My3D_Node_FindHandle (Node* CPPNode); void My3D_Node_DecRefCntr(struct HMy3D_Node *hNode); void My3D_Node_IncRefCntr(struct HMy3D_Node *hNode);
Unfortunately, to do the inverse and find a Java Node handle given a Node*, you must modify the C++ subclass to tell you what class it represents. To do this in your graphics library, add to the C++ class a method int KindOf() that returns the object type as defined in the global header file in Listing 33.46. With this information, the class can chain the request of My3D_xxxx_FindHandle() down to the correct class interface code, as shown in Listing 33.47.
Listing 33.47. Node
C++ FindHandle()
implementation.
struct HMy3D_Node* My3D_Node_FindHandle (Node* CPPNode) { switch (CPPNode->KindOf()) { case LIGHT: return (My3D_Light_FindHandle(CPPNode)); break; case GEOMETRY: return (My3D_Geometry_FindHandle(CPPNode)); break; default: assert(0); return (NULL); break; }; }
In the same manner, the My3D_Node_IncRefCntr() and My3D_Node_DecRefCntr() interface functions also need this mechanism. Their implementation is shown in Listing 33.48.
Listing 33.48. Node
C++ DecRefCntr()
implementation.
void My3D_Node_IncRefCntr (struct HMy3D_Node* hNode) { long hNode; switch (unhand(hNode)->ObjKindOf) { case LIGHT: My3D_Light_IncRefCntr(hNode); break; case GEOMETRY: My3D_Geometry_IncRefCntr(hNode); break; default: assert(0); break; }; } void My3D_Node_DecRefCntr (struct HMy3D_Node* hNode) { long hNode; switch (unhand(hNode)->ObjKindOf) { case LIGHT: My3D_Light_DecRefCntr(hNode); break; case GEOMETRY: My3D_Geometry_DecRefCntr(hNode); break; default: assert(0); break; }; }
The final component you should integrate into your interface is automatic handling of Java objects that move out of scope. When a Java object moves out of scope, the method finalize() is called. The behavior you want in your interface code is to adjust the reference counters appropriately. You accomplish this simply by linking the call to My3D_xxxx_finalize() with a call to My3D_xxxx_DecRefCntr(), as shown in Listing 33.49 for the World class. All Java classes that link to C++ classes that can be instantiated should include interface code for finalize() methods like this (that is, in the classes you have seen in this chapter, the Node class should be the only class that doesn't include code like this).
Listing 33.49. IncRefCntr()
implementation.
void My3D_World_finalize(struct HMy3D_World* hthis) { My3D_World_DecRefCntr(unhand(hthis)->hCPPObj); }
That covers most of what you need to know to build a Java interface to a C++ library! A sample program using the My3D interface library is shown in Listing 33.50. Because this simple 3D library doesn't have enough functionality to render the scene, you can't run the program; but note how the semantics of the Java version of the My3D application are the same as the semantics of a normal Java program, meeting the design goals at the beginning of the chapter.
Listing 33.50. Sample3DProg
implementation.
import My3D; class Sample3DProg { void main(String argv[]) { World theWorld = new World(); PointF_t LightDir = new PointF_t(); LightDir.x = 0.0f; LightDir.y = 0.0f; LightDir.z = 1.0f; PointF_t LightLoc = new PointFW_t(); LightDir.x = 0.0f; LightDir.y = 0.0f; LightDir.z = 10.0f; LightDir.w = 1.0f; Light theLight = new Light(); theLight.SetLocation(LightLoc); theLight.SetDirection(LightDir); } }
This chapter covered a lot of ground! Take some time to let it soak in before you continue. The first section of the chapter explained the basics of the Java native method interface and how Java passes data to your C/C++ program and how C/C++ programs should pass data back to Java.
The second half of the chapter dealt with the issues of building a set of Java interface libraries to legacy C and C++ libraries. Because of limitations on the scope of this chapter, we didn't get a chance to go into how to handle all the limitations of Java's lack of some object-orientation features (such as multiple inheritance issues and templates). If you're interested in how to handle these types of problems, take a look at Chapter 31 in Tricks of the Java Programming Gurus, published by Sams.net.