All functions in Java are object methods. Even the declaration part of your native methods must be contained within a class. Therefore, the first step to creating native methods is to define one or more class interfaces. If you are dealing with a large number of functions, it will be useful for you to partition them into logical, coherent groups. These should form the nucleus of each class. There are basically two approaches for defining a class interface that will wrap the native methods. Suppose you are dealing with native methods to provide file access. Assume that your target platform's standard runtime library supports multithreading. If it does not, you will have to serialize access to all RTL functions.
One approach we could take is to create a Fileclass that contains a mixture of native and Java methods that implement an abstraction of a disk file. (This is the approach used most often by the Java development team.) As an alternative design, we could create two classes: a Fileclass that presents an abstract view of files, and a FileImpclass that contains all of the native methods relevant to files. The File class would then contain a reference to a single global instance of FileImp.
Ultimately, both techniques result in a Fileclass that presents a clean abstraction of a file. The first approach (the single-class approach) has a significant drawback in terms of maintainability, however. Whenever the Fileclass changes, the dynamically loadable library must be rebuilt. In fact, you would need to regenerate the C header and C stub files and recompile the library for every interface change in File.
With the second approach (the split interface), FileImpis insulated from changes to File. Likewise, changes to FileImpdo not affect File. The methods of FileImp do not need to be a direct mapping from the target API. You can give them abstract names and easily define an interface, which will rarely (if ever) need to change. For example, the POSIX API defines two sets of file-manipulation functions. Roughly, the sets correspond to those which use handles returned by open, and those which use a structure pointer returned by fopen. The Win32 API defines a group of file-manipulation functions that use a Handle returned by CreateFile. Your FileImpclass would define a single method called Openas illustrated by Listing 19.1. Now any client of Filecan call Open without regard to the underlying system-level API.
After you have designed your class interfaces, you will need to create the Java classes. These will be identical to any other Java class, except for the use of the keyword native. Native signals to javac that this method body will be provided by a library loaded at runtime. Therefore, you do not provide a method body for a native method. The method declaration looks the same as if you were declaring an abstract method. This is no coincidence. In both cases, you are telling javac that you want to defer the definition of this method until later. With an abstract method, you are saying that "later" means a subclass. With a native method, "later" means runtime loadable C code.
After creating and compiling your Java classes, you will use javah to create two C files: a header file and a stub file. The header file contains a structure definition and function declarations (prototypes) for the native methods. The stub file contains some "glue" code, which you should never need to change. Because both files created by javah are generated automatically, you should never change either of them. javah maps the instance variables and methods of the class directly into the C files. So, if you ever change the Java class, you must regenerate the C files. If you do not, strange and unpredictable behavior will result. For example, javah created the header in Listing 19.3 from the class in Listing 19.2.
Now suppose that you add a new integer member, NumberOfShares, to the Java class, right after the TickerSymbol. If you do not recreate the header file, your C code will be using the old structure layout. Therefore, your C code would access every member from LastQuote. Your C code would use the offset for NumberOfShares(an integer) as if it were LastQuote(a float)! This mismatch can cause some of the most irreproducible and hard-to-find bugs. A similar problem can arise with the methods. In that case, you are even more likely to have problems, because any change in a method's signature means that you have to regenerate the stub and header files.
One good way to avoid this problem is to make sure that you rarely need to change your classes after you write them. Good design will help there, but changes are inevitable. Creating a makefile is the best way to ensure that you always use the most up-to-date header and stub files. Some version of "make" is available for every platform supported by the JDK. Later in this chapter, you will find sample makefiles for Java classes with native methods.
Javah creates two files for you, but it leaves the most important file up to you. Along with the header and stub files, you will need to write an implementation file. The implementation file must contain the definitions of the functions that the header file declares. All of the really interesting things happen in the implementation file. Figure 19.2 depicts the relationships among the four files-the Java source, the C header, the C stubs, and the C implementation files.
Figure 19.2: The relationship between the Java class file and the three C files..
At this point, a simple example will illustrate the interaction between the Java class and the three C files.
One approach we could take is to create a Fileclass that contains a mixture of native and Java methods that implement an abstraction of a disk file. (This is the approach used most often by the Java development team.) As an alternative design, we could create two classes: a Fileclass that presents an abstract view of files, and a FileImpclass that contains all of the native methods relevant to files. The File class would then contain a reference to a single global instance of FileImp.
Note |
For concrete details on how the Java development team designed this solution, look in the JDK source code. The source code is available from JavaSoft at http://java.sun.com/products/JDK/1.0.2. Check out the file src/java/io/File.java. The File class is an excellent example of integrating native methods with Java methods. |
With the second approach (the split interface), FileImpis insulated from changes to File. Likewise, changes to FileImpdo not affect File. The methods of FileImp do not need to be a direct mapping from the target API. You can give them abstract names and easily define an interface, which will rarely (if ever) need to change. For example, the POSIX API defines two sets of file-manipulation functions. Roughly, the sets correspond to those which use handles returned by open, and those which use a structure pointer returned by fopen. The Win32 API defines a group of file-manipulation functions that use a Handle returned by CreateFile. Your FileImpclass would define a single method called Openas illustrated by Listing 19.1. Now any client of Filecan call Open without regard to the underlying system-level API.
Listing 19.1. Definition of the native Openmethod.
class FileImp {
.
public native boolean Open(String Pathname);
.
}
After you have designed your class interfaces, you will need to create the Java classes. These will be identical to any other Java class, except for the use of the keyword native. Native signals to javac that this method body will be provided by a library loaded at runtime. Therefore, you do not provide a method body for a native method. The method declaration looks the same as if you were declaring an abstract method. This is no coincidence. In both cases, you are telling javac that you want to defer the definition of this method until later. With an abstract method, you are saying that "later" means a subclass. With a native method, "later" means runtime loadable C code.
When and Where Can I Use "nativ"? |
You can put "native" on almost any method declaration. How does javac interpret an abstract, native method? Not very well. You can provide a method body for an abstract native method in C, but it will never be called. A static native method behaves just as you would expect. Its semantics are the same as they would be in a static method defined in Java. A static native method is always passed a NULL for its this argument. Native methods that are overridden in a subclass work normally. In fact, you can mix native and Java methods arbitrarily when subclassing. |
Listing 19.2. This is the original PortfolioEntryclass.
class PortfolioEntry {
String TickerSymbol;
float LastQuote;
float BoughtAtPrice;
float LastDividends;
float LastEPS;
}
Listing 19.3. The structure definition javah created from PortfolioEntry.
typedef struct ClassPortfolioEntry {
struct Hjava_lang_String *TickerSymbol;
float LastQuote;
float BoughtAtPrice;
float LastDividends;
float LastEPS;
} ClassPortfolioEntry;
Now suppose that you add a new integer member, NumberOfShares, to the Java class, right after the TickerSymbol. If you do not recreate the header file, your C code will be using the old structure layout. Therefore, your C code would access every member from LastQuote. Your C code would use the offset for NumberOfShares(an integer) as if it were LastQuote(a float)! This mismatch can cause some of the most irreproducible and hard-to-find bugs. A similar problem can arise with the methods. In that case, you are even more likely to have problems, because any change in a method's signature means that you have to regenerate the stub and header files.
One good way to avoid this problem is to make sure that you rarely need to change your classes after you write them. Good design will help there, but changes are inevitable. Creating a makefile is the best way to ensure that you always use the most up-to-date header and stub files. Some version of "make" is available for every platform supported by the JDK. Later in this chapter, you will find sample makefiles for Java classes with native methods.
Javah creates two files for you, but it leaves the most important file up to you. Along with the header and stub files, you will need to write an implementation file. The implementation file must contain the definitions of the functions that the header file declares. All of the really interesting things happen in the implementation file. Figure 19.2 depicts the relationships among the four files-the Java source, the C header, the C stubs, and the C implementation files.
Figure 19.2: The relationship between the Java class file and the three C files..
No comments:
Post a Comment