by Anil Hemrajani
Java is a feature-rich language that comes bundled with packages (class libraries) for everything from local file I/O to socket communications, to GUI programming, to classes for the World Wide Web. However, there might be times when you need to accomplish specific tasks not supported by Java.
Java provides a native method interface that can be used to invoke functions written in other languagescurrently only an interface to C is supported (of course, C++ source code could be accessed via C functions). This is a nice feature mainly because it provides the flexibility of extending the Java language; there are reasons you might not want to go this route, though.
The following are some pros associated with using the Java native method interface:
The following are some cons associated with using the Java native method interface:
Based on these pros and cons, one somewhat safe bet might be to only use native methods for Java applications and not Java applets. This is mainly due to the fact that Java bytecode files must be distributed to users (or placed on a accessible network directory) who want to run Java applications, so distributing DLLs along with the bytecode files is not entirely a bad implementation of a Java application.
There are approximately six steps required for connecting Java programs to C. The two main steps include writing the Java program and C functions, the remaining steps are required for gluing the two together. In the following lines, I use a simple example to describe each of these six steps. The example shown here consists of a Java program that calls a C function to print the message Hello Java (a twist from the typical Hello World message). To keep the first example simple, no parameters are passed to the C function, and no values are returned from it. Figure 41.1 provides a visual representation of the various components created from the following six steps required to make the Java and C connection:
Components created in the HelloJava example.
These six steps are described in more detail in the following sections.
Listing 41.1 shows a simple Java program that defines a native method, SayHi, the implementation for which is provided in step 5:
Listing 41.1 HelloJava.java.
public class HelloJava
{
public native void SayHi();
public static void main(String args[])
{
System.loadLibrary("hello");
new HelloJava().SayHi();
}
}
Native methods are declared much the same way as regular Java methods except for two differences: the declaration must contain the native keyword, and there is no body for the method in Java because it is provided in the C function.
There are three things to note in the listing 41.1:
Listing 41.1 shows one way of instantiating a native method. Alternatively, the native method could be declared in another Java class in which case, you would instantiate that Java class using the new keyword; then simply call the native method off of the class.
Compile the Java class using the javac compiler provided with the Java Development Kit (JDK). Running the following command generates a Java bytecode file, HelloJava.class:
javac HelloJava.java
Generate a header file for the C function using the javah utility. The generated header file (HelloJava.h) basically provides a prototype for the C function covered in step 5.
javah HelloJava
Generate a stub function using the javah utility, to connect the Java class to the C function. The following command generates a stub file named HelloJava.c:
javah -stubs HelloJava
Provide an implementation for the C function, SayHi, as shown in listing 41.2.
Listing 41.2 HelloJavaImp.cImplementation for SayHi().
/* HelloJavaImp.c: Implementation of Java native method, SayHi(). */
#include "HelloJava.h"
#include <stdio.h>
void HelloJava_SayHi(struct HHelloJava *javaObj)
{
printf("Hello Java!\n");
}
There are three things in listing 41.2 which deserve mention:
Listing 41.3 shows a simple makefile that contains targets for building a dynamic link library on Windows 95 (using the Microsoft C/C++ compiler) and a shared library on SunOS/Solaris (Note, the environment variable JAVAHOME points to the root directory of the JDK).
Listing 41.3 makefileSimple makefile for HelloJava.
all:
@echo Please specify a target: Win95 or Sun.
Win95:
cl HelloJavaImp.c HelloJava.c \
-I$(JAVAHOME)\include -I$(JAVAHOME)\include\win32 \
-Fehello.dll -MD -LD -nologo $(JAVAHOME)\lib\javai.lib
Sun:
cc -G -I$(JAVAHOME)/include -I$(JAVAHOME)/include/solaris HelloJavaImp.c \
HelloJava.c -o libhello.so
Once you have completed these six steps successfully, you are ready to run the sample Java and C application as shown in figure 41.2.
Compile and run the HelloJava program.
Now that you have seen the basic steps required to connect Java programs to C functions, it is time to get a little fancier by passing parameters to a C function and returning values from it. Because steps 2, 3, 4 and 6 are similar for all cases, you concentrate on steps 1 and 5 which deal with a Java program and C function(s), respectively. But before plunging into the specifics of these examples, it might help to get a quick overview of Java data types and how they correspond to C data types. The following table provides a list of Java native data types and their machine sizes:
Type | Size/Format |
byte | 8-bit signed twos complement |
char | 16-bit Unicode characters |
short | 16-bit signed twos complement |
int | 32-bit signed twos complement |
long | 64-bit signed twos complement |
float | 32-bit 32-bit IEEE 754 |
double | 64-bit 32-bit IEEE 754 |
As you might notice, some Java data types are larger than their equivalent C data types. For example, a char is 16-bit in Java and 8-bit in C. The numeric data types are similar to those you find on UNIX systems; however, they are larger than ones on MS-DOS based systems such as an int which is two bytes on MS-DOS but four bytes in Java. The sizes for Java data types are the same across all supported platforms which makes the language more portable.
Listings 41.4 and 41.5 show a sample Java program and a sample C function, respectively. The purpose of this sample application is simple: the Java class calls a C function with certain parameters, the C function prints the values in these parameters to stdout using printf, and returns the number of characters printed to the Java class which in turn prints a message indicating how many characters were printed.
Listing 41.4 ThePrinter.javaJava source file for the PrintInC example.
// ThePrinter: Call native methods to print stuff
public class ThePrinter
{
public static void main(String args[])
{
int count=4;
int i[] = new int[count];
i[0] = 10;
i[1] = 75;
i[2] = 95;
i[3] = 115;
int printed = new PrintInC().doPrint(
25,
i, count,
100.33,
"Hello C");
System.out.println("Java: " +
printed +
" chars printed");
}
}
class PrintInC
{
public native int doPrint(long l,
int i[],
int count,
double d,
String s);
static
{ System.loadLibrary("print"); }
}
Listing 41.5 PrintInCImp.cImplementation for PrintInC native methods.
/* PrintInCImp.c: C functions for Java class */
#include <stdio.h>
#include <StubPreamble.h>
#include "PrintInC.h"
/*** Java to C demo: Print data values ***/
long PrintInC_doPrint(struct HPrintInC *this,
int64_t l,
HArrayOfInt *ai, long iCount,
double d,
struct Hjava_lang_String *s)
{
int charsPrinted=0, idx=0;
long *i;
/* Print value of "long" (Java int) */
charsPrinted += printf("C: l = %ld\n", l);
/* Print array of longs */
i = unhand(ai)->body;
for (idx=0; idx < iCount; idx++)
charsPrinted += printf("C: i[%d] = %d\n", idx, i[idx]);
/* Print value of "double */
charsPrinted += printf("C: d = %f\n", d);
/* Print Java string */
charsPrinted += printf("C: s = %s\n", makeCString(s));
/* Return total characters printed */
return charsPrinted;
}
In the Java program, notice how the native method is placed in a separate class (PrintInC) from the main class (ThePrinter). This was done to show you an alternative way of invoking native methods. Also notice, the static {} block in the PrintInC class; this is a good place to automatically perform tasks when the class is loaded, which in this case happened to be loading a dynamic library:
static { System.loadLibrary("print"); }
Now, lets go over some of the significant items in the C implementation. First of all, notice the inclusion of a StubPreamble.h header file; this is a JDK include file which contains necessary structures (for example, HArrayOfInt), functions prototypes (such as makeCString) and macros (such as unhand). Secondly, notice the mapping of data types used in Java versus what was used in C:
public native int doPrint(long l,
int i[],
int count,
double d,
String s);
long PrintInC_doPrint(struct HPrintInC *this,
int64_t l,
HArrayOfInt *ai, long iCount,
double d,
struct Hjava_lang_String *s)
Notice how the ints have been mapped to longs. This is because an int is only two bytes on MS-DOS and four bytes in Java. Similarly, a Java long argument has been mapped to a int64_t in C.
The other two notable parameters in these examples is the use of a Java array (int[]) and a Java object (String). Note how the Java int[] array gets mapped to a C structure, HArrayOfInt. The HArrayOfInt is one example of a standard naming convention used in the StubPreamble.h header file; other examples include HArrayOfLong, HArrayOfByte, and so on.
Java objects also use standard naming conventions such as H for handle, the name of the language (for example, java), the package containing the class (lang), and the name of the object (String). So a structure for the Java String object, which is part of the lang package, is named Hjava_lang_String.
Lastly, take a look at a C macro (unhand) and function (makeCString) used in this example. The unhand macro provides access to the variables in Javas C structures, such as the HArrayOfInt, as shown here (another example of unhand is shown in the next section):
i = unhand(ai)->body;
The makeCString function takes a Java String as an argument and returns a corresponding char* pointer. There are two other functions related to conversions between Java strings and C character arrays, makeJavaString and makeCString. The makeJavaString is covered in the next section along with an example. The makeCString is similar to makeCString, except it works more like strncpy; that is, it copies a Java String object into an existing char array as shown in the following example:
char OutFile[127+1];
javaString2CString(pOutFile, OutFile, sizeof(OutFile));
So far you have seen how to call C functions from Java. Next you learn how to go the other wayaccessing Java objects from C.
Java provides the ability for C functions to access data members in Java objects and also create instances of Java classes and invoke dynamic and static methods in the Java class. Listings 41.6 and 41.7 demonstrate how to accomplish most of these tasks.
Listing 41.6 contains mostly the same type of Java source code as the previous examples. One aspect done slightly differently and worth mentioning is the JavaAccessor class which illustrates how a Java class containing multiple native methods can be instantiated and then individual methods in that class can be invoked, as shown here:
JavaAccessor ja = new JavaAccessor();
System.out.println("PATH=" + ja.getEnv("PATH"));
ja.deleteFile("dummy.txt");
Listing 41.6 JavaAccessor.javaDemo java object access.
// JavaAccessor: Demo Java Object Access
public class JavaAccessor
{
public synchronized native String getEnv(String var)
throws java.lang.NullPointerException;
public native int deleteFile(String fileName);
public int delRC=0;
public static void main(String args[])
{
System.loadLibrary("ja");
JavaAccessor ja = new JavaAccessor();
try
{
System.out.println("PATH=" +
ja.getEnv("PATH"));
{
catch(java.lang.NullPointerException e)
{
System.out.println("getEnv returned NULL!");
}
ja.deleteFile("dummy.txt");
System.out.println("Return code from"+
" deleteFile() = "+
ja.delRC);
}
public void printRC(int RC)
{
System.out.println("Java: RC=" + RC);
}
}
Listing 41.7 JavaAccessorImp.cDemo Java object access in C.
#include <stdio.h>
#include <string.h>
#include "JavaAccessor.h"
/*** Get environment variable ***/
struct Hjava_lang_String
*JavaAccessor_getEnv(struct HJavaAccessor
*javaObj,
struct Hjava_lang_String
*varName)
{
char *var=makeCString(varName), *value;
value=getenv(var);
if (!value)
{
SignalError(0,
"java/lang/NullPointerException",
"No such environment variable");
return NULL;
}
return makeJavaString(value,
strlen(value));
}
/*** Delete a local file ***/
long JavaAccessor_deleteFile(struct HJavaAccessor
*javaObj,
struct Hjava_lang_String
*fileName)
{
HJavaAccessor *hJavaAccessor;
int rc=unlink(makeCString(fileName));
unhand(javaObj)->delRC=rc;
hJavaAccessor=(HJavaAccessor *)
execute_java_constructor(0,
"JavaAccessor",
0,
"()");
if (hJavaAccessor)
execute_java_dynamic_method(0,
(HObject *)hJavaAccessor,
"printRC",
"(I)V",
rc);
else
printf("Unable to create hJavaAccessor\n");
return rc;
}
First, the getEnv native method returns a Java object (String), unlike the other examples you have seen so far which return native data types. Notice how you have to use the makeJavaString, opposite of makeCString, to return a Java String object:
return makeJavaString(value, strlen(value));
Next, look at how you can access data members from the automatic parameter that is passed to every native method, in this case it is struct HJavaAccessor *. Notice how you can simply reference a data member in a Java class by using the unhand macro to de-reference the Java object:
long JavaAccessor_deleteFile(struct HJavaAccessor *javaObj,
struct Hjava_lang_String *fileName)
{
...
unhand(javaObj)->delRC=rc;
Finally, look at how you can instantiate Java classes and invoke methods off of it. There are three C functions to accomplish this:
The execute_java_constructor expects at least four parameters but it could be more depending on the number of parameters a constructor requires:
The remaining arguments are any parameters to pass to the constructor. The signature specified in the fourth parameter accepts the form (Arguments)ReturnValue which is made up of a set of parentheses containing symbols for a methods parameters and return values following the closing parenthesis. For example, because JavaAccessors constructor expects no parameters, you can simply use a signature of () (the next paragraph contains an example with parameters).
hJavaAccessor=(HJavaAccessor *)
execute_java_constructor(0, "JavaAccessor", 0,
"()");
The execute_java_dynamic_method expects at least four parameters but it could be more depending on the number of parameters the Java method requires:
The remaining arguments are any parameters to pass to the method. As shown in this example, the printRC Java method expects a single argument of type int (I) and does not return any values, so we use a V (for void) to indicate this at the right side of the closing parenthesis (the next section provides a list of letters for the various data types):
if (hJavaAccessor)
execute_java_dynamic_method(0, (HObject *)hJavaAccessor,
"printRC", "(I)V", rc);
The following is a list of letters to use for the various data types as defined in the signature.h include file provided with the JDK:
Letter | Data Type |
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
V | void |
Z | boolean |
[ + array type | array |
L + class name + ; | class |
C functions called by Java classes can throw exceptions which can be caught by Java methods. This is accomplished by the SignalError C function provided in the Java library which contains the following signature:
void SignalError(struct execenv *executionEnvironment,
char *exceptionClass,
char *exceptionMessage)
The following example, taken from Listing 41.7, illustrates how to throw a NullPointerException:
SignalError(0, "java/lang/NullPointerException",
"No such environment variable");
Java is multi-threaded language, hence C functions used in Java could potentially be accessed concurrently by various threads in a given Java program. Java provides a data-locking mechanism which permits thread-safe operations in Java and native methods. For C functions written for Java, thread-safe operations can be accomplished by using the synchronized keyword in the Java method declaration and three C functions provided in the JDK:
These functions are equivalent to the wait(), notify(), and notifyAll() functions provided in the java.lang.Object class:
public synchronized native String getEnv(String var);
C++ classes are not directly supported in Java, but they can be invoked via C functions which serve as connection functions. In other words, functions which accept parameters from Java hand them off to C++ objects, and pass any data or return values from the C++ objects back to Java. In fact, this concept is similar to how Java objects are instantiated from C using execute_java_constructor; their instance methods are invoked using execute_java_dynamic_method and static methods using execute_java_static_method. Similar generic functions could be implemented for C++ objects.
On the other hand, custom C functions can also be implemented. For example, if you wanted to instantiate the C++ fstream class and invoke its methods, you could have C functions such as instantiate_fstream(), fstream_open(), fstream_close(), and so on.
One thing to ensure when using a C++ compiler to compile your C connection functions is that these functions be placed inside an extern C block so their names do not get mangled. The javah utility in JDK 1.0.2 automatically does this for you, but for prior versions you have to add the extern statements yourself.
Portions of this chapter first appeared in C/C++ Users Journal, Volume 14, issue 9, and are reprinted here by permission of Miller Freeman, Inc.
| Previous Chapter | Next Chapter |
| Search | Table of Contents | Book Home Page | Buy This Book |
| Que Home Page | Digital Bookshelf | Disclaimer |
To order books from QUE, call us at 800-716-0044 or 317-361-5400.
For comments or technical support for our books and software, select Talk to Us.
© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company