Day 20

Using Native Methods and Libraries

by Laura Lemay and Charles L. Perkins


CONTENTS

Up to this point in the book you've been learning specifically about programming in the Java language and with the Java class libraries. That's why this book is called Teach Yourself Java, after all. Today I'm going to digress a little bit and talk about native methods and libraries.

Native methods and libraries are bits of executable code that are written in the traditional way: They are written in a language such as C or C++ and compiled into a platform-specific library such as a DLL or a shared library. Inside your Java applications you can gain access to the functions inside those libraries, allowing you to create a sort of hybrid Java and native code application. Although using native methods can give you some extra benefits Java does not provide (such as faster execution or access to a large body of existing code), there are significant disadvantages in using native methods as well.

New Term
Native methods and native libraries are bits of platform-specific executable code (written in languages such as C or C++) contained in libraries or DLLs. You can create a hybrid Java application that has access to those native libraries.

Today's lesson covers various topics relating to native methods, including the following:

Note
In today's lesson you'll learn the basic techniques for writing native methods in the current version of Java. For the Java 1.1 release, Sun will publish further guidelines for writing native methods to help make sure that native implementations will work between different versions of the Java runtime. These guidelines will be in addition to the technique you will learn in today's lesson, and will build on the skills you learn here.

Why Use Native Methods?

Before I get into the nitty-gritty details of creating native methods, you should first be aware of what native methods give you-and what they take away. Although native methods provide some advantages, those advantages may not appear too exciting when viewed in light of native methods' disadvantages. This section describes both.

Advantages of Using Native Methods

There are several reasons that you might want to consider using native methods in your own Java programs. By far the best of these reasons are

The first, and by far the best, reason to implement native methods 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, but 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 use native methods is speed. The argument has been made that because interpreted bytecode is terribly slow in comparison to how quickly native code runs (and it is far slower, as much as 25 times slower), Java code is unsuitable for most applications. In many cases this simply isn't true, or you may be able to extract a fair amount of speed out of your Java program without resorting to native methods (as we'll explore in greater detail later in today's lesson). If, however, your Java application uses very processor-intensive calculations (for example, number crunching or 3D rendering), using native methods for the speed-critical functions and Java for the more general interfaces creates a system with more benefits than a system written in either pure native code or pure Java. In fact, the Java class library uses this approach for many critical system classes to raise the overall level of efficiency in the system. As a user of the Java environment, you don't even know (or see) any side effects of this (except, perhaps, a few classes or methods that are final that might not be otherwise).

The third reason to use native classes is if your project has a large body of existing code (what's called legacy code, which may be hundreds of lines of code written and maintained by other people over the years). As a good Java programmer and advocate you would, of course, want to port this large body of code to Java. However, real-life considerations of time and resources often don't allow this option. Native methods allow you to write a single interface to that code through Java and link into the existing code as it's needed.

Disadvantages of Native Methods

After reading the advantages of using native methods, you may be all set to jump to the section on how to use them and skip this section. Don't. For every good thing native methods provide in your Java code, they take away a benefit that Java provides in the first place: the ability for your code to run anywhere and be easily ported from one system to another.

Using pure Java, an application or applet can be run on any Java environment in the world by downloading it via the Web or by simply loading the class file on that system. Any new architectures created-or new operating systems written-are irrelevant to your code. All you need is that the (tiny) Java Virtual Machine (or a browser that has one inside it) be available, and it can run anywhere, anytime-now and in the future.

With a hybrid Java and native method program, however, you've given up that cross-platform capability. First of all, Java programs that use native methods cannot be applets. Period. For security reasons, applets cannot load native code. So if you use native methods, you've just removed the enormous number of users on the World Wide Web from your market.

Even if you're just creating a Java application, however, and don't intend your code to be run on the Web, using native methods also negates the capability of your program to run on any platform. Native code is, by definition, platform specific. The native code must exist on the platform your Java program is running on for that program to work. For your program to work on different platforms, you'll have to port your native code to that specific platform-which may not be a trivial task. And as new systems or new versions of operating systems appear, you may have to update or re-release new versions of that native code for every system. The write-it-once-run-it-everywhere advantage of Java ceases to exist when you use native methods.

The Illusion of Required Efficiency

Let's digress for a moment and talk about the concept of speed and efficiency of Java programs-or the supposed lack thereof, which may drive you to using native code in your Java programs.

Java bytecode has acquired the reputation of being extraordinarily slow to run in comparison with native executable code. And, examining the benchmarks, Java bytecode is indeed very much slower-as much as 25 times slower. However, that doesn't necessarily make a Java program unbearable to use. Simple applets or applications that rely on user interface elements will appear to run just as fast as their native equivalents. Button clicks are just as fast in Java as they are in native code, and your users are very slow compared to modern computers. It's only in the case of very processor-intensive operations that Java starts to come up short in comparison to native code.

At any rate, worrying over the speed of your Java programs before you write them is often a rathole that can distract you from the larger issues. In this section I'll look at both those larger issues and at the solutions that can make your Java programs run faster.

Design First, Efficiency Later

When you design your program, all your 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.

So 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 time-and 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 you're trying to solve, and thus will be amenable to "speeding up" later.

Note
There are always cases where you must be fanatical about efficiency in many parts of a set of classes. The Java class library itself is such a case, as is anything that must run in real-time for some critical real-world application (such as flying a plane). Such applications are rare, however.
When speaking of a new kind of programming that must soon emerge, Bill Joy likes to invoke the four S's of Java: small, simple, safe, and secure. The "feel" of the Java language itself encourages the pursuit of clarity and the reduction of complexity. The intense pursuit of efficiency, which increases complexity and reduces clarity, is antithetical to these goals.

Once you build a solid foundation, debug your classes, and your program (or applet) works as you'd like it to, then it's time to begin optimizing it.

Just-in-Time Compilers

The first thing to keep in mind about the execution speed of Java is that lots of people are working on fixing it. And the most promising of these technical advancements is the just-in-time (JIT) compiler.

Just-in-time compilers translate Java bytecode into native machine code on-the-fly as the bytecode is running. Depending on how good the JIT compiler is, you can often get very close to native execution speeds out of a standard Java program-without needing to use native code and without needing to make any modifications to your Java program-it just works.

The disadvantage, however, is that to get the speed increase your Java program must be run on a platform that has a JIT compiler installed. At the time of this writing, JIT compilers are still new. Many companies are working on JIT compilers, however, and most of them have versions working or bundled in with development tools so you can experiment with their power. Microsoft's Internet Explorer Web browser, for example, has a JIT compiler built into it. (You'll learn more about the available JIT compilers are expected on Day 22, "Java Programming Tools.") JIT compilers are expected to become much more popular and widespread over the next year.

Simple Optimization Tricks

In addition to relying on JIT technology to speed up your Java programs, there are usually simple optimization tricks you can do to make your programs run more efficiently. Your development environment may even provide a profiler, which tells you where the slowest or more frequently run portions of your program are occurring. Even if you don't have a profiler, you can often use debugging tools to find the bottlenecks in your programs and begin to make targeted changes to your classes.

Whole books have been written for optimizing various bits of code in any language, and they can describe it much better than we can. But there are a few simple tricks you can try for the first pass.

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 program's time). If those methods contain loops, examine the inner loops to see whether they

If you notice that a long chain of, for example, four or more method calls is needed to reach a destination method's 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.

Writing Native Methods

If, after all these tricks, your Java code is still just too slow, it's time to consider using native methods. In this section you'll learn the steps you must take to write your Java code so that it uses native methods, how to write the native code to implement those native methods, and how to compile and link it all together so it works. This involves four basic steps:

Note
This discussion-and, in fact, the JDK itself-assumes that you'll be writing your native code in C and C++. Other Java development environments may support other languages.

Write Your Java Code

The first step to implementing native methods is to decide which methods in which classes of your Java program will be native. The mapping between Java and native libraries is through methods (functions), so designing your Java code and keeping track of which methods are native is the most important first step.

To declare that a method will be native inside your Java code, you add the native modifier to that method signature, like this:

public native void goNative(int x, int y);

Note
The native modifier can be used with many of the modifiers you learned about on Day 15, "Modifiers, Access Control, and Class Design," including public, private, protected, final, and so on. It cannot be used with abstract because abstract methods do not have definitions, native or otherwise.

Note also that the native method in your Java code has no method body. Because this is a native method, its implementation will be provided by the native code, not by Java. Just add a semicolon to the end of the line.

The other change you'll have to make to your Java code is to explicitly load the native library that will contain the native code for these methods. To do this, you add the following boilerplate code to your Java class:

static {
    System.loadLibrary("libmynativelibrary.so");
}

This bit of code, called a static initializer, is used to run code only once when the class is first loaded into the system. In this case, the static initializer executes the System.loadLibrary() method to load in your native library as the class itself is being loaded. If the native library fails to load for some reason, the loading of the Java class fails as well, guaranteeing that no half-set-up version of the class can ever be created.

You can pick any name you want for your native library-here we've used the UNIX convention that libraries start with the word lib and end with the extension .so. For Windows systems, libraries typically end with the extension .DLL.

You can also use the System.load() method to load your native libraries. The difference is that the single argument to load() is the complete pathname to your native library, whereas the argument to loadLibrary() is just the library name, and Java uses the standard way of finding libraries for your system to locate that library (usually environment variables such as LD_LIBRARY_PATH). The latter is more flexible and general-purpose, so it's recommended you use it instead.

And that's all you need to do in your Java code to create native methods and libraries. 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 you'd expect).

Listing 20.1 shows an example of a Java program called SimpleFile that was written to use native methods. This program might be used in a version of the Java environment that does not provide file input or output (I/O). Because file I/O is typically system-dependent, native methods must be used to implement those operations.

Note
This example combines simplified versions of two actual Java library classes, java.io.File and java.io.RandomAccessFile.


Listing 20.1. SimpleFile, a Java program that uses native methods.
 1: public class  SimpleFile {
 2:     public static final  char    separatorChar = '>';
 3:     protected    String  path;
 4:     protected    int     fd;
 5: 
 6:     public  SimpleFile(String s) {
 7:         path = s;
 8:     }
 9: 
10:     public String  getFileName() {
11:         int  index = path.lastIndexOf(separatorChar);
12: 
13:         return (index < 0) ? path : path.substring(index + 1);
14:     }
15: 
16:     public String  getPath() {
17:         return path;
18:     }
19: 
20:     public native boolean  open();
21:     public native void     close();
22:     public native int      read(byte[]  buffer, int  length);
23:     public native int      write(byte[]  buffer, int  length);
24: 
25:     static {
26:         System.loadLibrary("simple");  // runs when class first loaded
27:     }
28: }

The first thing you notice about SimpleFile's 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 (getFileName() and getPath()). Then, in lines 20 through 23, there are four native method declarations, which are just normal method declarations 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, note the call to System.loadLibrary() in line 26, which loads a native library called simple. (We've intentionally violated library-naming standards here to make this example simpler.)

Note
The unusual separatorChar ('>') is used simply to demonstrate what an implementation might look like on some strange computer whose file system didn't use any of the more common path-separator conventions. Early Xerox computers used '>' as a separator, and several existing computer systems still use strange separators today, so this is not all that farfetched.

After you write the native part of your Java program, SimpleFile objects 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();

Generate Header and Stub Files

The second step to implementing native code is to generate a special set of header and stub files for use by your C or C++ files that implement those native methods. To generate these header and stub files, you use the javah program, which is part of the JDK (it's called JavaH in the Mac JDK).

First, you'll need to compile your Java program as you would any other Java program, using the Java compiler.

Header Files

To generate the headers you need for your native methods, use the javah program. For the SimpleFile class listed in the previous section, use one of the following:

To generate header files for a class, use the javah program with the name of the class file, minus the .class extension. For example, to generate the header file for the SimpleFile class, use this command line:

javah SimpleFile

To generate the header file for the SimpleFile class, drag-and-drop the class file onto the JavaH icon.

The file SimpleFile.h will be created in the same directory as the SimpleFile.class file.

Note that if the class you've given to javah is inside a package, javah prepends the package's full name to the header filename (and to the structure names it generates inside that file) with all the dots (.) replaced by underscores (_). 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.

Listing 20.2 shows the header file that is generated by javah.


Listing 20.2. SimpleFile.h (a header file).
 1: #include <native.h>
 2: /* Header for class SimpleFile */
 3: 
 4: #ifndef _Included_SimpleFile
 5: #define _Included_SimpleFile
 6: struct Hjava_lang_String;
 7: 
 8: typedef struct ClassSimpleFile {
 9: #define SimpleFile_separatorChar 62L
10:     struct Hjava_lang_String *path;
11:     long fd;
12: } ClassSimpleFile;
13: HandleTo(SimpleFile);
14: 
15: #ifdef __cplusplus
16: extern "C" {
17: #endif
18: extern /*boolean*/ long SimpleFile_open(struct HSimpleFile *);
19: extern void SimpleFile_close(struct HSimpleFile *);
20: extern long SimpleFile_read(struct HSimpleFile *,HArrayOfByte *,long);
21: extern long SimpleFile_write(struct HSimpleFile *,HArrayOfByte *,long);
22: #ifdef __cplusplus
23: }
24: #endif
25: #endif

There are a few things to note about this header file. First, note the struct ClassSimpleFile, which contains variables that parallel the instance variables inside your class. Second, note the method signatures at the end of the file; these are the function definitions you'll use in your C or C++ file to implement the actual native methods in the Java code.

Stub Files

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, which translate arguments and return values between Java and C.

Stubs are pieces of "glue" code that tie together Java and C. Stubs translate arguments and values and convert the various constructs in each language to something that can be understood in the other.

Stubs can be automatically generated by javah, just like headers. There isn't much you need to know about the stub file, just that it has to be compiled and linked with the C code you write to allow it to interface properly with Java.

To create stub files, you also use the javah program:

Use the javah program with the -stubs option to create the stub file:

javah -stubs SimpleFile

The file SimpleFile.c will be generated in the same directory as the class file.

The stub file was generated at the same time you created the header file.

Listing 20.3 shows the result of the stub file for the SimpleFile class.


Listing 20.3. SimpleFile.c (a stub file).
 1:/* DO NOT EDIT THIS FILE - it is machine generated */
 2:#include <StubPreamble.h>
 3: 
 4:/* Stubs for class SimpleFile */
 5:/* SYMBOL: "SimpleFile/open()Z", Java_SimpleFile_open_stub */
 6:__declspec(dllexport) stack_item *Java_SimpleFile_open_stub(stack_item *_P_,
 7:    struct execenv *_EE_) {
 8:        extern long SimpleFile_open(void *);
 9:        _P_[0].i = (SimpleFile_open(_P_[0].p) ? TRUE : FALSE);
10:        return _P_ + 1;
11:}
12:/* SYMBOL: "SimpleFile/close()V", Java_SimpleFile_close_stub */
13:__declspec(dllexport) stack_item *Java_SimpleFile_close_stub(stack_item *_P_,
14:    struct execenv *_EE_) {
15:        extern void SimpleFile_close(void *);
16:        (void) SimpleFile_close(_P_[0].p);
17:        return _P_;
18:}
19:/* SYMBOL: "SimpleFile/read([BI)I", Java_SimpleFile_read_stub */
20:__declspec(dllexport) stack_item *Java_SimpleFile_read_stub(stack_item *_P_,
21:    struct execenv *_EE_) {
22:        extern long SimpleFile_read(void *,void *,long);
23:        _P_[0].i = SimpleFile_read(_P_[0].p,((_P_[1].p)),((_P_[2].i)));
24:        return _P_ + 1;
25:}
26:/* SYMBOL: "SimpleFile/write([BI)I", Java_SimpleFile_write_stub */
27:__declspec(dllexport) stack_item *Java_SimpleFile_write_stub(stack_item *_P_,
28:    struct execenv *_EE_) {
29:        extern long SimpleFile_write(void *,void *,long);
30:        _P_[0].i = SimpleFile_write(_P_[0].p,((_P_[1].p)),((_P_[2].i)));
31:        return _P_ + 1;
32:}

Implementing the Native Library

The last step, and the most difficult, is to write the C code for your native methods.

The header file generated by javah gives you the prototypes of the functions you need to implement to make your native code complete. You then write some C code that implements those functions and provides the native facilities that your Java class needs (in the case of SimpleFile, some low-level file I/O routines).

You'll want to include your header file as part of the initial includes for your native implementation:

#include <SimpleFile.h>

Note
This description glosses over a lot of what you might want to do to actually implement those methods. In particular, Java provides several utility functions that help your native methods interact with Java methods and classes and help map C and C++ constructs to their Java equivalents. We'll describe several of these functions later on in today's lesson in the section "Tools and Techniques for Writing Native Implementations."

Listing 20.4 shows the native implementation of the methods from the SimpleFile class.


Listing 20.4. SimpleFileNative.c, a C implementation of a native method from SimpleFile.
 1: #include "SimpleFile.h"     /* for unhand(), among other things */
 2: 
 3: #include <sys/param.h>      /* for MAXPATHLEN */ 
 4: #include <fcntl.h>          /* for O_RDWR and O_CREAT */
 5: 
 6: #define LOCAL_PATH_SEPARATOR  '/'    /* UNIX */
 7: 
 8: static void  fixSeparators(char *p) { 
 9:     for (;  *p != '\0';  ++p)
10:         if (*p == SimpleFile_separatorChar) 
11:             *p = LOCAL_PATH_SEPARATOR;
12: }
13: 
14: long  SimpleFile_open(struct HSimpleFile  *this) { 
15:     int   fd;
16:     char  buffer[MAXPATHLEN];
17: 
18:     javaString2CString(unhand(this)->path, buffer, sizeof(buffer)); 
19:     fixSeparators(buffer);
20:     if ((fd = open(buffer, O_RDWR | O_CREAT, 0664)) < 0)    /* UNIX open */
21:         return(FALSE);   /* or, SignalError() could "throw" an exception */
22:     unhand(this)->fd = fd;         /* save fd in the Java world */ 
23:     return(TRUE);
24: }
25: 
26: void  SimpleFile_close(struct HSimpleFile  *this) { 
27:     close(unhand(this)->fd);
28:     unhand(this)->fd = -1;
29: }
30: 
31: long  SimpleFile_read(struct HSimpleFile  *this, 
32:     HArrayOfByte  *buffer, _ long  count) {
33:     char  *data     = unhand(buffer)->body;  /* get array data   */ 
34:     int    len      = obj_length(buffer);    /* get array length */ 
35:     int    numBytes = (len < count ? len : count);
36: 
37:     if ((numBytes = read(unhand(this)->fd, data, numBytes)) == 0) 
38:         return(-1);
39:     return(numBytes);       /* the number of bytes actually read */ 
40: }
41: 
42: long  SimpleFile_write(struct HSimpleFile  *this, 
43:     HArrayOfByte  *buffer,_ long  count) {
44:     char  *data = unhand(buffer)->body; 
45:     int    len  = obj_length(buffer);
46: 
47:     return(write(unhand(this)->fd, data, (len < count ? len : count))); 
48: }

Compile Everything into a Shared Library

The final step is to compile all the .c files, including the stub file and your native method files. Use your favorite C compiler to compile and link those two files into a shared library (a DLL on Windows). On some systems, you may need to specify special compilation flags that mean "make it relocatable and dynamically linkable." (Those flags, if they are required, may vary from system to system; check with your compiler documentation for details.)

Note
If you have several classes with native methods, you can include all their stubs in the same .c file, if you like. Of course you might want to name it something else, such as Stubs.c, in that case.

The resulting library should be the same name as you gave in your original Java class file as the argument to System.loadLibrary(). In the SimpleFile class, that library was called libmynativelibrary.so. You'll want to name the library that same name and install it wherever your particular system needs libraries to be installed.

Using Your Library

With all the code written and compiled and installed in the right place, all you have to do is run your Java program using the Java bytecode interpreter. When the Java class is loaded, it will also try to load the native library automatically; if it succeeds you should be able to use the classes in your Java class, and they will transparently run the native libraries as they are needed.

If you get an error that the library was not found, the most likely problem is that you do not have your environment set up correctly or that you have not installed your library in the right place.

DLL files are located according to the standard Windows algorithm: the directory the application was located in, the current directory, the System directory in Windows 95 (System32 in NT), the System directory in NT, the Windows directory, and then directories listed in the PATH environment variable.

UNIX systems use the environment variable LD_LIBRARY_PATH to search for libraries. This environment variable should include the standard places shared libraries are stored, as well as the current directory (.). After LD_LIBRARY_PATH has been set, Java will be able to find your library.

Shared libraries for Java must be stored in the folder System Folder: Extensions:JavaSoft Folder. Rather than copying your native library there, you can also just create an alias to your native library and put it in that folder.

Tools and Techniques for Writing Native Implementations

When writing the code for native implementations, a whole set of useful macros and functions is available for mapping between C and C++ and Java, and for accessing Java runtime structures. (Several of them were used in SimpleFileNative.c.) In addition, there are several rules and techniques for dealing with the conversion between Java and C. In this section you'll learn about those functions and techniques to make writing your native code easier.

Names

Java names for classes, methods, and variables can be used inside native methods with the following changes (if needed):

Accessing Java Objects

Java objects are passed to native methods using handles to structures. The handle name is the name of the object (including any package names), prepended with the letter H. So, for example, the class SimpleFile would have a handle called HSimpleFile. The class java.lang.String would convert to Hjava_lang.String (remember, class names have package names included, with underscores to separate them).

Handles are references to structures that represent Java objects. Each handle has the same name as the class it references, with the letter H prepended.

Each native function automatically gets passed at least one handle in its parameter list. This is called the automatic parameter, and it's a handle to the class that contained the original native method. Even if the original name method has no arguments, the C equivalent for that method is passed a handle to the class so it can reference other parts of that object or pass data back to it. In fact, because the handle to the original class behaves as if it were the this object, it's often called this in the native code's method signature as well.

The automatic parameter is a handle to the original Java class that called the native method. Because it is roughly equivalent to this in Java, the automatic parameter is also often called this.

Note the native method signature for the open() method in SimpleFileNative.c, which shows the automatic parameter:

long  SimpleFile_open(struct HSimpleFile  *this)

To get to the methods or variables inside a class, you must dereference that class's handle. To do this, you can use the macro unhand() (as in "Unhand that object!"). The unhand() macro returns a pointer to a struct. So, for example, to get at the variables inside the this handle, you'd reference it like this:

unhand(this);

After the handle is dereferenced, you can access its variables as if they were normal struct elements:

unhand(this)->path;

References to arrays are slightly different than references to objects, although both are passed as handles, and you can reference their elements by "unhanding" them as well. In the case of arrays, however, the name of the handle includes the words ArrayOf prepended to the type of the array, and the letter H prepended to that. So, for example, an array of integers, declared like this in Java:

int[] lotsOfInts;

would look like this on the native side:

HArrayOfInt *lotsOfInts;

Calling Methods

In the previous section you learned how to deal with references to Java objects as handles. Using unhand(), you can dereference those handles and get to the object's variables. But what about methods? From your native code, you can call methods inside Java objects using several utility functions for just that purpose.

In addition, as you pass data back and forth between the Java side and the native side, you'll need to know how data types convert and how to deal with those types in either side.

Functions for Executing Methods

To call methods inside Java objects from within native code, you use special utility functions. To call a regular Java method, use the function execute_java_dynamic_method(). To call a class method, use the function execute_java_static_method(). Here's the signature for these functions (from the Java include file interpreter.h, which defines things like this):

long execute_java_dynamic_method(ExecEnv *env, HObject *obj,
     char *method_name, char *signature, ...);
long execute_java_static_method(ExecEnv *env, ClassClass *cb,
     char *method_name, char *signature, ...);

Both functions take at least four arguments:

Any remaining arguments to the execute_java_static_method() and execute_java_dynamic_method() functions are arguments to the method itself.

Method signatures can be complex, because in this case they are not simply the list of arguments and the return types. Method signatures, for this function, are strings with a set of parentheses containing an argument list, and a return type just after the closing parentheses. Both the argument list and the return type are letters or strings that represent a type.

For the primitive types, use single-letter codes for the argument list and the return type (B is byte, I is int, V is void, and Z is boolean). For arrays, use an open square bracket before the type (for example, [B denotes a byte array). More letter codes for different types are contained in the Java include file signature.h. So, for example, a method that has no arguments and returns void would have a signature of ()V. One that take three integer arguments and returns an integer would have a signature of (III)V.

For object arguments, the code is the letter L, then the class name (including the package, with all elements separated by slashes), followed by a semicolon. So, for example, a reference to a String object would be Ljava/lang/String;.

Got all that? Here are a few examples:

execute_java_dynamic_method(0, this, "close", "()Z"
execute_java_static_method(0, MyClass, "reverseString", 
   "(Ljava/lang/String;)Ljava/lang/String;", "This is my string");
execute_java_dynamic_method(0, this, "open_speaker()", 
   "(Lcom/lne/audio/Device;)Z", theDevice);

The FindClass() and FindClassFromClass() functions can be used to get a reference to a class structure (a pointer of type ClassClass) for use with the execute_java_static_method() function. Here are their signatures:

ClassClass *FindClass(ExecEnv *env, 
    char *className, bool_t resolve);
ClassClass *FindClassFromClass(ExecEnv *env, 
    char *className, bool_t resolve, ClassClass *from);

As with the functions for calling methods, the first argument should be 0 to indicate that this function is to be run in the current environment. The second argument is the class name to find. The resolve argument is a boolean which, if TRUE or 1, indicates that the resolve Class() method should be called on that class (class resolution is a function of the class loader; it's probably safe to use TRUE for this argument in most cases). In the case of FindClassFromClass, the fourth argument is an already existing class; the class loader that loaded that class will also be used to find and load the new class.

Passing Parameters Back and Forth

To pass parameters to Java methods from native code or vice versa, you have to understand how data types convert between the two sides of the process.

The primitive data types in Java convert to their nearest equivalents in C. All the Java integer types (char, byte, short, int) and boolean convert to C long types; long converts to int64_t, and float and double remain floats and doubles. Keep in mind that because of these conversions, your original native method definitions may need return types that reflect the values sent back from the C side of the native method (for example, all methods that return integer types must actually return long).

Object types are passed as handles to structures, as you learned earlier, and must be dereferenced using unhand() in order to be used.

Creating New Java Objects

Because you can access Java objects and call methods from inside your native code, the one thing left is the capability to create new objects. You can do this too, using the execute_class_constructor() function. This function is very similar to the functions for calling methods; in fact, it has the same set of arguments that execute_java_static_method() does:

HObject *execute_java_constructor(ExecEnv *, char *classname,
   ClassClass *cb, char *signature, ...);

The execute_java_static_method() function has four arguments, but can have more. The four required arguments are

Here are some examples:

execute_java_constructor(0, "MyClass", NULL, "()");
execute_java_constructor(0, "MyOtherClass", NULL, "(II)", 10, 12);

The first example creates an instance of the MyClass class, using the constructor with no arguments. The second creates an instance of MyOtherClass, in which the constructor has two integer arguments. Those arguments, 10 and 12, are included at the end of the parameter list.

Handling Exceptions

To handle errors, Java has exceptions. In your native C code, you can set up a Java exception using SignalError, like this:

SignalError(0, JAVAPKG "ExceptionClassName", "message");

Here, the exception class name is the name of a Java exception class, including its package name, with the separation of package names delineated with a slash rather than a period as in Java. So, for example, the class java.io.IOException would be "java/io/IOException" when used inside SignalError.

The exception will be thrown in Java when your native method returns (which it should immediately after the SignalError). Note that just like regular methods, native methods that throw exceptions must be declared to throw those exceptions using the throw keyword.

Dealing with Strings

Several functions and macros are available in the include file javaString.h to help manage strings. To gain access to these functions, include that header as part of your native code:

#include <javaString.h>

The makeJavaString() function creates a Java String object out of a C string. To convert a Java String object into a C string, you can use makeCString() or allocCString() (where the former allocates the string from temporary storage and the latter from the heap). Here are their signatures:

Hjava_lang_String  *makeJavaString(char  *string, int  length)

char  *makeCString(Hjava_lang_String *s)
char  *allocCString(Hjava_lang_String *s)

To copy Java Strings into preexisting Unicode or ASCII C buffers, you can use javaString2unicode() and javaString2CString():

unicode  *javaString2unicode(Hjava_lang_String *s, unicode  *buf, int  len)
char     *javaString2CString(Hjava_lang_String *s, char     *buf, int  len)

Finally, the javaStringPrint() function prints a Java String object (just like System.out.print()), and the javaStringLength() function gets its length:

void  javaStringPrint(Hjava_lang_String *s)
int   javaStringLength(Hjava_lang_String *s)

Summary

Today you have learned about the advantages and 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.

Finally, you learned the procedure for creating native methods, from both the Java and the C sides, in detail-by generating header files and stubs, and by compiling and linking a full example.

After working your way through today's difficult material, you've mastered one of the most complex parts of the Java language. As a reward, tomorrow we'll look "under the hood" to see some of the hidden power of Java, and you can just sit back and enjoy the ride.

Q&A

Q:
Your descriptions here are somewhat sparse. What can I use to supplement what I've learned here?
A:
Look at Sun's Java tutorial (online or on the CD-ROM included with this book) for a more detailed version of how to work with native methods.
Q:
Does the Java class library need to call System.loadLibrary() to load the built-in classes?
A:
No, you won't see any loadLibrary() calls in the implementation of any classes in the Java class library. That's because the Java team had the luxury of being able to statically link most of their code into the Java environment, something that really makes sense only when you're in the unique position of providing an entire system, as they are. Your classes must dynamically link their libraries into an already-running copy of the Java system. This is, by the way, more flexible than static linking; it allows you to unlink old and relink new versions of your classes at any time, making updating them trivial.
Q:
Can I statically link my own classes into Java like the Java team did?
A:
Yes. You can, if you like, ask Sun Microsystems for the sources to the Java runtime environment itself, and, as long as you obey the (relatively straightforward) legal restrictions on using that code, you can relink the entire Java system plus your classes. Your classes are then statically linked into the system, but you have to give everyone who wants to use your program this special version of the Java environment. Sometimes, if you have strong enough requirements, this is the only way to go, but most of the time, dynamic linking is not only good enough, but preferable.