HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Native methods are functions written in C or C++, compiled into a library, and dynamically loaded by the Java runtime. This article describes how Java programs can call native methods, and how native C code can create Java objects and invoke Java methods. The examples are written for Windows 95, Windows NT and Solaris, and should be portable to other platforms.

Benefits and Limitations of Native Methods
We begin with a short discussion of the pros and cons of using native methods. Using native methods adds complexity and effort to a development project, so you would usually only use them if you had to.

Why Use Native Methods?
Some of the most common reasons for using native methods are:

  • You need to do something Java can't do, such as accessing a system library that has a C API.
  • You want to re-use an existing body of code implemented in another language.
  • You need the performance of native compilation.
Limitations
Although native methods are capable of doing some things Java can't, they have some drawbacks. Native implementations are generally less portable, less secure, and more difficult to develop, maintain and distribute.

Portability- A native library must be ported, compiled and tested for each combination of processor and operating system on which the program will run.

Security- Because native methods can bypass Java security mechanisms, they don't have any of the built-in runtime protections. You need to convince each user that they should download and use an unrestrained library.

Development- If your Java development environment does not also support native code development (many don't), you need to switch back and forth between Java and C development environments. Extra effort is required to ensure dependencies between the two are synchronized. Your object-oriented design may be weakened by the fact that related functionality is split in locations dictated by the demands of library implementations and calling conventions, rather than logic.

Maintenance- The number of files and complexity of the source code is higher than for Java-only development projects. A class definition is spread across a .java file, one or more C files, and a header file, all of which must be kept in sync. Changes must be compiled and tested on each supported platform.

Native method interfaces aren't part of an existing specification. In a Java implementation from another vendor, or even in a future implementation from Sun, some interfaces could change. For this reason, native code has a higher risk of becoming incompatible with future Java versions than code written entirely in Java.

Distribution- Users can't just view your web page to run your applet. They need to get a copy of the library, install it in the correct directory, and correctly set environment variables. Each software update requires a new distribution.

Performance- The advances in Java compilation techniques, particularly just-in-time compilers, have narrowed the performance gap between compiled native code and Java to within a few percent for some applications. Java is young and improvements are still coming.

A possible alternative: A Java program can communicate with a C program in ways other than through native methods. Both programs could run as separate processes and communicate through shared files or sockets. Socket I/O is used to make HTTP requests to a web server, and to request mail from a POP3 server. Future libraries will provide additional mechanisms. For example, the JDBC library from Sun will allow Java programs to communicate with others through shared databases.

In summary, native methods re-introduce many of the problems Java was designed to solve. Sometimes, though, your need is great enough to outweigh the disadvantages. The rest of this article demonstrates how you can create and use native methods.

Native Data Types and Storage
The first step to learning to use native methods is to have a good understanding of the memory representation of Java data. This section defines the basic object data structures, and shows how simple types, such as integers and objects, and more complex types, such as arrays and strings, are stored and accessed by native C code.

Primitive Types and Objects
Most Java primitive types are stored as 32-bit values, usually a C type long. An object is a 32-bit pointer to a structure. Multiple references to a Java object are all pointers to the same structure. The class SimpleClass is used to demonstrate these basic types, as shown in Listing 1.

The access specifiers for the instance data don't matter; native methods can access any instance data, even if it is private. Compile this file and then generate the header file with javah.

javac SimpleClass.java
javah SimpleClass

Javah generates a header file SimpleClass.h that includes the declarations shown in Listing 2.

Notice that both the integer and boolean take 32 bits of storage. All Java primitive types, except long, float, and double, take 32 bits. Although somewhat inefficient for storage, this format avoids processor alignment and structure padding issues. This simplifies communication and persistence algorithms, and makes the Java runtime more portable to other platforms. The macro HandleTo (SimpleClass) equivalent is shown in Listing 3.

A reference to a SimpleClass object is declared in C as
HSimpleClass *ref;

To access the instance data, use the macro unhand() (defined in interpreter.h) as follows:
unhand(ref)->anInteger

which is equivalent to:
ref->obj->anInteger

Accessing primitive types and objects from C is fairly straightforward. Java provides additional C functions and macros for accessing two special kinds of objects: arrays and strings.

Arrays
Arrays are a little more complex than basic objects, because in addition to storing their data, they also store the size of the array and the type of the data elements. There are two types of arrays: arrays of primitive types and arrays of objects. Both types can be created with ArrayAlloc(). An array of len integers is allocated as follows:

HArrayOfInt *intList =
(HarrayOfInt *) ArrayAlloc(T_INTEGER, len);

The array types are defined in typecodes.h. For an array of any class of objects, use the value T_CLASS. For such arrays, Java identifies the class of object in the array by the value stored in the last slot of the array. An example of code that creates an array of Objects is shown in Listing 4.

Some other useful array functions are shown in Table 1.

Strings
A string is represented as a Java object containing an array of characters. The main differences between Java strings and ANSI C strings are:

  • Java strings contain 16 bit characters.
  • Java strings are not (usually) null terminated.
  • The character array may be allocated larger than the number of characters stored in it.
To convert a C string to a Java string:
// char *cstr;
Hjava_lang_String *jstr = makeJavaString(cstr,
strlen(cstr));

To convert from a Java string to a C string:
// Hjava_lang_String *jstr;
char *cstr = makeCString(jstr);

The storage allocated by makeCString is temporary, and garbage collected later. If you need to use the string after the native function returns, use malloc to allocate heap memory and copy cstr into the new memory space. Don't call free on memory returned by makeCString.

Some other useful string functions are shown in Table 2.

Calling C from Java
Now that we know how to access Java data from C, we can look at how the two worlds of Java and C communicate. This section describes how Java calls C functions. The next section describes how native methods invoke functions of the Java virtual machine. The steps required to create and execute native C code are:

  • Declare the native function in Java and compile the Java class
  • Create the C header file and stub file from the Java class
  • Implement the native C code
  • Compile and link the C code and stub file into a library
  • Load the library at runtime
  • Call the function from Java
Native methods are declared in Java with the keyword native, and the declarations have no function body. The class SimpleClass2 contains a native add function, as shown in Listing 5. Implement the native method in C as shown in Listing 6.

When using a C++ compiler, you may need to surround the include statements with extern "C" { } to tell the compiler that the files follow C syntax conventions.

Native method names are of the form class_method. The class includes the package name, with underscores separating the class path components. For example, if String.length() were native, the C function name would be java_lang_String_length. The first parameter of all methods is the object whose method was invoked I use the variable name self for the instance pointer because this is reserved in C++.

To compile, add /java/include;/java/ include/platform (where platform is win32, solaris, etc.) to your include path. Use javah to generate SimpleClass2.h and use javah -stubs to generate a C stub file. The stub file contains a wrapper function to set up the C parameters from values in the Java stack.. Compile the C program and stub file and link them with /java/lib/javai.lib into mylib.dll. Copy the DLL to some directory in your executable path, so that it can be found by System.loadLibrary(). On Solaris, the library should be called libmylib.so and its directory should be included in LD_LIBRARY_PATH. When you run SimpleClass2, the main function loads mylib.dll, calls the add function, and prints the result.

Note for Microsoft Foundation Class/Win32 users. Many MFC classes require the current thread to be initialized by CreateThread(), whereas Java uses _beginthreadex() to create all its threads in Win32 platforms. To use MFC classes, your native method must create another thread with the CreateThread() function, and then create and invoke MFC objects from within that new thread. Your DLL must also include a DLLMain function to call the Afx initialization and cleanup routines appropriately. If you aren't using MFC in your native method DLL, don't worry about any of this.

Note for Netscape users: To use a DLL with Netscape, it needs to be linked with a Netscape library instead of javai.dll, and placed inside Netscape's execution directory program/ java/bin. As of this writing, that library is not part of the standard Netscape distribution, but they do have some developer kits. See their web page for more information.

Calling Java from C
This section describes the built-in functions that allow native methods to access the Java runtime system. The most commonly used functions allow C code to create Java objects and invoke Java methods.

The following examples assume that the functions are called from a C native method that was called from Java. In other words, the flow of control goes from Java to C back to Java again. A C program can call into Java, but there are several complexities that make it beyond the scope of an introductory article. In a future article in this series, I will show how to call Java asynchronously from C.

The Basic Functions
To call a Java instance method, use:
long execute_java_dynamic_method(ExecEnv *ee,
HObject *obj,
char *method_name, char *signature, ...);

The return value is the return value of the method, which may have to be cast to the appropriate type.

To create an instance, use:
HObject *execute_java_constructor(ExecEnv *ee,
char *classname,
ClassClass *cb,
char *signature, ...);

The return value is a reference to the new object. Save it in a Java variable or the new object will be garbage collected!

To call a Java class method, use:
long execute_java_static_method(ExecEnv *ee,
ClassClass *cb,
char *method_name, char *signature, ...);

The return value is the return value of the method, which may have to be cast to the appropriate type.

Description of Parameters

  • ee- the execution environment. The environment is a function of the calling stack and the current task. The current environment can be obtained by calling the function EE(). In the execute_... functions, you can pass a value of 0 for ee and the called function calculates the correct environment by calling EE().
  • method_name- the name of the method, as an ANSI null-terminated string. For example "getLength"
  • classname- the name of the class, as an ANSI null-terminated string.
Internal class names include the entire package path, with each term separated by a "/". For example, the Java class java.lang.String would be referred to in C as "java/lang/String". For the call to execute_java_constructor, either the class name or cb should be supplied, but not both. The other parameter should be 0.
  • signature- a string of characters defining the parameters of the function and the return value in the form ( parameter_types ) return_value
Parameter types are a series of the following:
array `[' + array type
byte `B'
char `C'
class `L' + class name + ;'
float `F'
double `D'
int `I'
long `J'
short `S'
void `V'
boolean `Z'
Some examples are shown in Table 3.

  • cb- The class pointer (also called a class block)
    This pointer is the base of all the information about a class, including its method table and descriptions of each field. To get the class pointer from a class object, use:
ClassClass *cb = unhand(cls)

To get the class pointer from a class name, use:

ClassClass *cb = FindClass(0, classname, TRUE);

If the class has not yet been loaded, this call causes the class to be loaded from the CLASSPATH.

To get the class pointer from one of its instances, use
ClassClass *cb = obj_classblock(obj);

For the call to execute_java_constructor, either the class name or cb should be supplied, but not both. The other parameter should be 0.

  • _the parameters described in the signature. If the method has no parameters, no additional arguments to the execute_ functions need to be supplied.
Exceptions
To throw an exception, use:
SignalError(ExecEnv *ee, char *exceptionClassName,
char *detailString)

For example,
SignalError(0, "java/io/IOException", "The disk
is full.");

There can be only one pending exception per execution environment. If you use execute_java_dynamic_method and the called method throws an exception, that exception information is overwritten if you throw another one with SignalError. You can check whether an exception has been thrown with
exceptionOccurred(EE())
which is true if there is a pending exception in the current execution environment. You cannot execute further Java code with a pending exception. To pass the exception back to Java for normal processing, just return from the current native method. To replace the pending exception with another, call
exceptionClear(EE())

and then use SignalError to throw your own.

Although the Java compiler has no way to know which exceptions your native code might throw, you should still want to add "throws" clauses to the native methods declarations in Java as a form of documentation.

Conclusion
This article introduces the basic features of native methods. You can get more information by looking at the JDK header files, particularly oobj.h and interpreter.h. If you have any questions, comments, or suggestions for future articles in this series, I would like to hear from you. Send correspondence to [email protected]

About the Author
Mr. Schoettler is President of B Mobile. B Mobile develops applications that allow mobile workers to access back-office systems.

	

Listing 1

public class SimpleClass {
	int       anInteger;
	boolean   aBoolean;
	Object    anObject;
}

Listing 2

typedef struct ClassSimpleClass {
	long                      anInteger;
	/*boolean*/ long          aBoolean;
	struct Hjava_lang_Object *anObject;
} ClassSimpleClass;
HandleTo(SimpleClass);

Listing 3

typedef struct HSimpleClass {
	ClassSimpleClass    *obj;
	struct methodtable  *methods;
} HSimpleClass;

Listing 4

HObject *objList =
(HArrayOfObject *) ArrayAlloc(T_CLASS, len);
memset(unhand(objList)->body,0,len * sizeof(Hobject *));
// replace "java/lang/Object" below with the class
// you are storing in the array
struct ClassClass *clsObject = 
FindClass(0, "java/lang/Object", TRUE);
unhand(objList)->body[len] = (HObject *)clsObject;

Listing 5

public class SimpleClass2 {
	public native int add(int a, int b);
	public static void main(String[] args) {
		System.loadLibrary("mylib");
		SimpleClass2 simple = new SimpleClass2();
		int sum = simple.add(2,3);
		System.out.println("The sum is "+sum);
	}
}

Listing 6

#include <native.h>
#include "SimpleClass2.h"
long SimpleClass2_add(struct HSimpleClass2 *self, long a, long b) {
	return a + b;
}

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.