In this chapter well discuss all the reasons you might (or might not) want to write native methods in Java, all of Javas built-in optimizations, and the tricks you can use to make your programs faster. Youll also learn the procedure for creating, making headers and stubs for, and linking native methods into a dynamically loadable library.
Lets begin, however, with the reasons that you might want to implement native methods in the first place.
There are only two reasons that you might need to declare some of your methods nativethat is, implemented by a language other than Java.
The first, and by far the best reason to do so, is because you need to utilize a special capability of your computer or operating system that the Java class library does not already provide for you. Such capabilities include interfacing to new peripheral devices or plug-in cards, accessing a different type of networking, or using a unique and valuable feature of your particular operating system. Two more concrete examples are acquiring real-time audio input from a microphone or using 3D accelerator hardware in a 3D library. Neither of these is provided to you by the current Java environment, so you must implement them outside Java, in some other language (currently C or any language that can link with C).
The second, and often illusory, reason to implement native methods is speedillusory, because you rarely need the raw speeds gained by this approach. Its even more rare to not be able to gain that speed-up in other ways (as youll see later in the chapter). Using native methods in this case takes advantage of the fact that, at present, the Java release does not perform as well as, for example, an optimized C program on many tasks. For those tasks, you can write the needs to be fast part (critical, inner loops, for example) in C, and still use a larger Java shell of classes to hide this trick from your users. In fact, the Java class library uses this approach for certain critical system classes to raise the overall level of efficiency in the system. As a user of the Java environment, you dont even know (or see) any side effects of this (except, perhaps, a few classes or methods that are final that might not be otherwise).
Once you decide youd like to, or must, use native methods in your program, this choice costs you dearly. Although you gain the advantages mentioned earlier, you lose the portability of your Java code.
Before, you had a program (or applet) that could travel to any Java environment in the world, now and forever. Any new architectures createdor new operating systems writtenwere irrelevant to your code. All it required was that the (tiny) Java Virtual Machine (or a browser that had one inside it) be available, and it could run anywhere, anytimenow and in the future.
Now, however, youve created a library of native code that must be linked with your program to make it work properly. The first thing you lose is the ability to travel as an applet; you simply cant be one! No Java-capable browser currently in existence allows native code to be loaded with an applet, for security reasons (and these are good reasons). The Java team has struggled to place as much as possible into the java packages because they are the only environment you can count on as an applet. (The sun packages, shipped primarily for use with stand-alone Java programs, are not always available to applets.)
Losing the ability to travel anywhere across the Net, into any browser written now or in the future, is bad enough. Whats worse, now that you cant be an applet, you have further limited yourself to only those machines that have had the Java Virtual Machine ported to their operating system. (Applets automatically benefit from the wide number of machines and operating systems that any Java-capable browser is ported to, but now you do not.)
Even worse, you have assumed something about that machine and operating system by the implementation of your native methods. This often means that you have to write different source code for some (or all) of the machines and operating systems on which you want to be able to run. Youre already forced, by using native methods, to produce a separate binary library for every machine and operating system pair in the world (or at least, wherever you plan to run), and you must continue to do so forever. If changing the source is also necessary, you can see that this is not a pleasant situation for you and your Java program.
If, even after the previous discussion, you must use native methods anyway, theres help for you later in this chapterbut what if youre still thinking you need to use them for efficiency reasons?
You are in a grand tradition of programmers throughout the (relatively few) ages of computing. It is exciting, and intellectually challenging, to program with constraints. If you believe efficiency is always required, it makes your job a little more interestingyou get to consider all sorts of baroque ways to accomplish tasks, because it is the efficient way to do it. I myself was caught up in this euphoria of creativity when I first began programming, but it is creativity misapplied.
When you design your program, all that energy and creativity should be directed at the design of a tight, concise, minimal set of classes and methods that are maximally general, abstract, and reusable. (If you think that is easy, look around for a few years and see how bad most software is.) If you spend most of your programming time on thinking and rethinking these fundamental goals and how to achieve them, you are preparing for the future. A future where software is assembled as needed from small components swimming in a sea of network facilities, and anyone can write a component seen by millions (and reused in their programs) in minutes. If, instead, you spend your energy worrying about the speed your software will run right now on some computer, your work will be irrelevant after the 18 to 36 months it will take hardware to be fast enough to hide that minor inefficiency in your program.
Am I saying that you should ignore efficiency altogether? Of course not! Some of the great algorithms of computer science deal with solving hard or impossible problems in reasonable amounts of timeand writing your programs carelessly can lead to remarkably slow results. Carelessness, however, can as easily lead to incorrect, fragile, or nonreusable results. If you correct all these latter problems first, the resulting software will be clean, will naturally reflect the structure of the problem youre trying to solve, and thus will be amenable to speeding up later.
Once you build a solid foundation and debug your classes, and your program (or applet) works as youd like it to, then its time to begin optimizing it. If its just a user interface applet, you may need to do nothing at all. The user is very slow compared to modern computers (and getting relatively slower every 18 months). The odds are that your applet is already fast enoughbut suppose it isnt.
Your next job is to see whether your release supports turning on the just-in-time compiler, or using the java2c tool.
The first of these is an experimental technology that, while a methods bytecodes are running in the Java Virtual Machine, translates each bytecode into the native binary code equivalent for the local computer, and then keeps this native code around as a cache for the next time that method is run. This trick is completely transparent to the Java code you write. You need know nothing about whether or not its being doneyour code can still travel anywhere, anytime. On any system with just-in-time technology in place, however, it runs a lot faster. Experience with experimental versions of this technology shows that, after paying a small cost the first time a method is run, this technique can reach the speed of compiled C code.
NOTE |
---|
More details on this technique, and the java2c tool, are presented in the next chapter. As of the 1.0 release, neither of these tools are in the Java environment, but both are expected in a later release (perhaps 1.1). |
The java2c translator takes a whole .class file full of the bytecodes for a class and translates them (all at once) into a portable C source code version. This version can then be compiled by a traditional C compiler on your computer to produce a native-method-like cached library of fast code. This large cache of native code will be used whenever the classs methods are called, but only on the local computer. Your original Java code can still travel as bytecodes and run on any other computer system. If the virtual machine automatically takes these steps whenever it makes sense for a given class, this can be as transparent as the just-in-time technology. Experience with an experimental version of this tool shows that fully optimized C performance is achievable. (This is the best anyone can hope to do!)
So you see, even without taking any further steps to optimize your program, you may discover that for your release of Java (or for releases elsewhere or coming in the near future), your code is already fast enough. If it is not, remember that the world craves speed. Java will only get faster, and the tools will only get better. Your code is the only permanent thing in this new worldit should be the best you can make it, with no compromises.
Suppose that these technologies arent available or dont optimize your program far enough for your taste. You can profile your applet or program as it runs, to see in which methods it spends the most time. Once you know this crucial information, you can begin to make targeted changes to your classes.
First, identify the crucial few methods that take most of the time (there are almost always just a few, and often just one, that take up the majority of your programs time). If they contain loops, examine the inner loops to see whether they: call methods that can be made final, call a group of methods that can be collapsed into a single method, or create objects that can be reused rather than created anew each loop.
If you notice that a long chain of, for example, four or more method calls is needed to reach a destination methods code, and this execution path is in one of the critical sections of the program, you can short-circuit directly to that destination method in the topmost method. This may require adding a new instance variable to reference the object for that method call directly. This quite often violates layering or encapsulation constraints. This violation, and any added complexity, is the price you pay for efficiency.
If, after all these tricks (and the numerous others you should try that have been collected over the years into various programming books), your Java code is still just too slow, you will have to use native methods after all.
For whatever reasons, youve decided to add native methods to your program. Youve already decided which methods need to be native, and in which classes, and youre rarin to go.
First, on the Java side, all you need to do is delete the method bodies (all the code between the brackets{ and }and the brackets themselves) of each method you picked and replace them with a single semicolon (;). Then add the modifier native to the methods existing modifiers. Finally, add a static (class) initializer to each class that now contains native methods to load the native code library youre about to build. (You can pick any name you like for this librarydetails follow.) Youre done!
Thats all you need to do in Java to specify a native method. Subclasses of any class containing your new native methods can still override them, and these new Java methods are called for instances of the new subclasses (just as youd expect).
Unfortunately, what needs to be done in your native language environment is not so simple.
NOTE |
---|
Imagine a version of the Java environment that does not provide file I/O. Any Java program needing to use the file system would first have to write native methods to get access to the operating system primitives needed to do file I/O.
This example combines simplified versions of two actual Java library classes, java.io.File and java.io.RandomAccessFile, into a single new class, SimpleFile:
public class SimpleFile { public static final char separatorChar = >; private protected String path; private protected int fd; public SimpleFile(String s) { path = s; } public String getFileName() { int index = path.lastIndexOf(separatorChar); return (index < 0) ? path : path.substring(index + 1); } public String getPath() { return path; } public native boolean open(); public native void close(); public native int read(byte[] buffer, int length); public native int write(byte[] buffer, int length); static { System.loadLibrary(simple); // runs when class first loaded } }
SimpleFiles can be created and used in the usual way:
SimpleFile f = new SimpleFile(>some>path>and>fileName); f.open(); f.read(...); f.write(...); f.close();
The first thing you notice about SimpleFiles implementation is how unremarkable the first two-thirds of its Java code is! It looks just like any other class, with a class and an instance variable, a constructor, and two normal method implementations. Then there are four native method declarations. Youll recognize these, from previous discussions, as being just a normal method declaration with the code block replaced by a semicolon and the modifier native added. These are the methods you have to implement in C code later.
Finally, there is a somewhat mysterious code fragment at the very end of the class. You might recognize the general construct here as a static initializer. Any code between the brackets{ and }is executed exactly once, when the class is first loaded into the system. You take advantage of that fact to run something you want to run only oncethe loading of the native code library youll create later in this chapter. This ties together the loading of the class itself with the loading of its native code. If either fails for some reason, the other fails as well, guaranteeing that no half-set-up version of the class can ever be created.
In order to get your hands on Java objects and data types, and to be able to manipulate them in your C code, you need to include some special .h files. Most of these will be located in your release directory under the subdirectory called include. (In particular, look at native.h in that directory, and all the headers it points to, if youre a glutton for detail punishment.)
Some of the special forms you need must be tailored to fit your classs methods precisely. Thats where the javah tool comes in.
To generate the headers you need for your native methods, first compile SimpleFile with javac, just as you normally would. This produces a file named SimpleFile.class. This file must be fed to the javah tool, which then generates the header file you need (SimpleFile.h).
If the class handed to javah is inside a package, it prepends the package name to the header filename (and to the structure names it generates inside that file), after replacing all the dots (.) with underscores (_) in the packages full name. If SimpleFile had been contained in a hypothetical package called acme.widgets.files, javah would have generated a header file named acme_widgets_files_SimpleFile.h, and the various names within it would have been renamed in a similar manner. When running javah, you should pass it only the class name itself, and not the full filename, which has .class on the end. |
Heres the output of javah SimpleFile:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <native.h> /* Header for class SimpleFile */ #ifndef _Included_SimpleFile #define _Included_SimpleFile struct Hjava_lang_String; typedef struct ClassSimpleFile { #define SimpleFile_separatorChar 62L struct Hjava_lang_String *path; long fd; } ClassSimpleFile; HandleTo(SimpleFile); extern /*boolean*/ long SimpleFile_open(struct HSimpleFile *); extern void SimpleFile_close(struct HSimpleFile *); extern long SimpleFile_read(struct HSimpleFile *,HArrayOfByte *,long); extern long SimpleFile_write(struct HSimpleFile *,HArrayOfByte *,long); #endif
NOTE |
---|
HandleTo() is a magic macro that uses the structures created at runtime by the stubs youll generate later in this chapter. |
The members of the struct generated above are in a one-to-one correspondence with the variables of your class.
In order to massage an instance of your class gently into the land of C, use the macro unhand() (as in unhand that Object!). For example, the this pseudo-variable in Java appears as a struct HSimpleFile * in the land of C, and to use any variables inside this instance (you), you must unhand() yourself first. Youll see some examples of this in a later section in this chapter.
To run interference between the Java world of Objects, arrays, and other high-level constructs and the lower-level world of C, you need stubs. (Stubs are pieces of glue code that automatically translate arguments and return values back and forth between the worlds of Java and C.)
Stubs can be automatically generated by javah, just like the headers. There isnt much you need to know about the stubs file, just that it has to be compiled and linked with the C code you write to allow it to interface with Java properly. A stubs file (SimpleFile.c) is created by running javah on your class by using the option -stubs.
Heres the result of running javah -stubs SimpleFile:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <StubPreamble.h> /* Stubs for class SimpleFile */ /* SYMBOL: 93"SimpleFile/open()Z, Java_SimpleFile_open_stub */ stack_item *Java_SimpleFile_open_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_open(void *); _P_[0].i = SimpleFile_open(_P_[0].p); return _P_ + 1; } /* SYMBOL: SimpleFile/close()V, Java_SimpleFile_close_stub */ stack_item *Java_SimpleFile_close_stub(stack_item *_P_,struct execenv *_EE_) { extern void SimpleFile_close(void *); (void) SimpleFile_close(_P_[0].p); return _P_; } /* SYMBOL: SimpleFile/read([BI)I, Java_SimpleFile_read_stub */ stack_item *Java_SimpleFile_read_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_read(void *,void *,long); _P_[0].i = SimpleFile_read(_P_[0].p,((_P_[1].p)),((_P_[2].i))); return _P_ + 1; } /* SYMBOL: SimpleFile/write([BI)I, Java_SimpleFile_write_stub */ stack_item *Java_SimpleFile_write_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_write(void *,void *,long); _P_[0].i = SimpleFile_write(_P_[0].p,((_P_[1].p)),((_P_[2].i))); return _P_ + 1; }
Each comment line contains the method signature for one of the four native methods youre implementing. You can use one of these signatures to call into Java and run, for example, a subclasss overriding implementation of one of your native methods. More often, youd learn and use a different signature to call some useful Java method from within C to get something done in the Java world.
You do this by calling a special C function that is part of the Java run-time called execute_java_dynamic_method(). Its arguments include the target object of the method call and the methods signature. The general form of a fully qualified method signature is any/package/name/ClassName/methodName(...)X. (You can see several in the stubs outputs comments, where SimpleFile is the class name and there is no package name.) The X is a letter (or string) that represents the return type, and the ... contains a string that represents each of the arguments types in turn. (Here are the letters and stringsused, and the types they represent, in the example: [T is array of type T, B is byte, I is int, V is void, and Z is boolean.)
The method close(), which takes no arguments and returns void, is represented by the string SimpleFile/close()V and its inverse, open(), that returns a boolean instead, is represented by SimpleFile/open()Z. Finally, read(), which takes an array of bytes and an int as its two arguments and returns an int, is SimpleFile/read([BI)I. (See the Method Signatures section in the next chapter for the full details.)
Now you can, at last, write the C code for your Java native methods.
The header file generated by javah, SimpleFile.h, gives you the prototypes of the four C functions you need to implement to make your native code complete. You then write some C code that provides the native facilities that your Java class needs (in this case, some low-level file I/O routines). Finally, you assemble all the C code into a new file, include a bunch of required (or useful) .h files, and name it SimpleFileNative.c. Heres the result:
#include SimpleFile.h /* for unhand(), among other things */ #include <sys/param.h> /* for MAXPATHLEN */ #include <fcntl.h> /* for O_RDWR and O_CREAT */ #define LOCAL_PATH_SEPARATOR / /* UNIX */ static void fixSeparators(char *p) { for (; *p != \0; ++p) if (*p == SimpleFile_separatorChar) *p = LOCAL_PATH_SEPARATOR; } long SimpleFile_open(struct HSimpleFile *this) { int fd; char buffer[MAXPATHLEN]; javaString2CString(unhand(this)->path, buffer, sizeof(buffer)); fixSeparators(buffer); if ((fd = open(buffer, O_RDWR | O_CREAT, 0664)) < 0) /* UNIX open */ return(FALSE); /* or, SignalError() could throw an exception */ unhand(this)->fd = fd; /* save fd in the Java world */ return(TRUE); } void SimpleFile_close(struct HSimpleFile *this) { close(unhand(this)->fd); unhand(this)->fd = -1; } long SimpleFile_read(struct HSimpleFile *this, HArrayOfByte *buffer, Â long count) { char *data = unhand(buffer)->body; /* get array data */ int len = obj_length(buffer); /* get array length */ int numBytes = (len < count ? len : count); if ((numBytes = read(unhand(this)->fd, data, numBytes)) == 0) return(-1); return(numBytes); /* the number of bytes actually read */ } long SimpleFile_write(struct HSimpleFile *this, HArrayOfByte *buffer, Â long count) { char *data = unhand(buffer)->body; int len = obj_length(buffer); return(write(unhand(this)->fd, data, (len < count ? len : count))); }
Once you finish writing your .c file, compile it by using your local C compiler (usually called cc or gcc). On some systems, you may need to specify special compilation flags that mean make it relocatable and dynamically linkable.
When writing the C code for native implementations, a whole set of useful (internal) macros and functions is available for accessing Java run-time structures. (Several of them were used in SimpleFileNative.c.)
Lets take a brief digression to understand some of them a little better.
WARNING |
---|
Dont rely on the exact form given for any of the following macros and functions. Because theyre all internal to the Java run-time, theyre subject to change at any moment. Check to see what the latest versions of them look like in your Java release before using them. |
NOTE |
---|
The following brief descriptions are taken from an alpha release of Java, because descriptions of them for the 1.0 release were not available as of this writing. How Java data types map into C types, and vice versa, will be detailed in future documentation. Refer to it for more details on that or on any of the sparsely documented items below. (Many are listed just to give you a taste of the capabilities of the available functions.) |
The following
Object *unhand(Handle *) int obj_length(HArray *)
returns a pointer to the data portion of an object and returns the length of an array. The actual pointer type returned is not always Object *, but varies, depending on the type of Handle (or HArray).
While the following:
ClassClass *FindClass(struct execenv *e, char *name, bool_t resolve) HArrayOfChar *MakeString(char *string, long length) Handle *ArrayAlloc(int type, int length)
finds a class (given its name), makes an array of characters of length length, and allocates an array of the length and type.
Use the function:
long execute_java_dynamic_method(ExecEnv *e, HObject *obj, char *method_name, Âchar *signature, ...);
to call a Java method from C. e is NULL to use the current environment. The target of the method call is obj. The method method_name has the given method signature. It can have any number of arguments and returns a 32-bit value (int, Handle *, or any 32-bit C type).
Use the following:
HObject *execute_java_constructor(ExecEnv *e, char *classname, ClassClass *c, Âchar *signature, ...); long execute_java_static_method(ExecEnv *e, ClassClass *c, char *method_name, Âchar *signature, ...);
to call a Java constructor from C and call a class method from C. c is the target class; the rest are as in execute_java_dynamic_method().
Calling this:
SignalError(0, JAVAPKG ExceptionClassName, message);
posts a Java exception that will be thrown when your native method returns. It is somewhat like the Java code:
throw new ExceptionClassName(message);
Finally, here are some useful string functions:
void javaStringPrint(Hjava_lang_String *s) int javaStringLength(Hjava_lang_String *s) Hjava_lang_String *makeJavaString(char *string, int length) char *makeCString(Hjava_lang_String *s) char *allocCString(Hjava_lang_String *s) unicode *javaString2unicode(Hjava_lang_String *s, unicode *buf, int len) char *javaString2CString(Hjava_lang_String *s, char *buf, int len)
The first two methods print a Java String (like System.out.print()), and get its length, respectively. The third makes a Java String out of a C string. The fourth and fifth do the reverse, turning a Java String into a C string (allocated from temporary or heap storage, respectively). The final two methods copy Java Strings into preexisting Unicode or ASCII C buffers.
The final step you need to take in the C world is to compile the stubs file SimpleFile.c by using the same compilation flags you used for SimpleFileNative.c.
NOTE |
---|
Youre now finished with all the C code that must be written (and compiled) to make your loadable native library.
Now youll finally be able to tie everything together and create the native library, simple, that was assumed to exist at the beginning of this chapter.
Its time to link everything youve done into a single library file. This looks a little different on each system that Java runs on, but heres the basic idea, in UNIX syntax:
cc -G SimpleFile.o SimpleFileNative.o -o simple
The -G flag tells the linker that youre creating a dynamically linkable library; the details differ from system to system.
Now, when the Java class SimpleFile is first loaded into your program, the System class attempts to load the library named simple, which (luckily) you just created. Look back at the Java code for SimpleFile to remind yourself.
How does it locate it? It calls the dynamic linker, which consults an environment variable named LD_LIBRARY_PATH that tells it which sequence of directories to search when loading new libraries of native code. Because the current directory is in Javas load path by default, you can leave simple in the current directory, and it will work just fine.
This chapter discussed the numerous disadvantages of using native methods, about the many ways that Java (and you) can make your programs run faster, and also about the often illusory need for efficiency.
It detailed the procedure for creating native methods, from both the Java and the C sides, in detail97Äby generating header files and stubs, and by compiling and linking a full example.
After working your way through this difficult material, youve mastered one of the most complex parts of the Java language. You now know how the Java run-time environment itself was created, and how to extend that powerful environment yourself, at its lowest levels.