Chapter 14 -- Extending Java
Chapter 14
Extending Java
CONTENTS
By any measure, Java provides a deep set of standard tools that
can be used to create terrific applets. Various Java packages
described in the preceding chapters provide excellent access to
network, file input/output, window manipulation, and numerous
desirable functions.
Naturally, Java doesn't provide a completely comprehensive set
of tools; it's currently impossible to do so in a cross-platform
manner, given the diversity of platforms and operating systems
as well as the rapid pace of technological innovation and change.
Sometimes it's necessary to compensate for a deficiency in Java
by extending it.
Note: |
Sun appears to have some interest in the development of a Java-based operating system. In a Java operating system, every aspect of the OS could be exposed as an integral java class.
|
Two basic means of extending the Java environment are provided
with the Java development kit: the Runtime
and Process classes, and
native methods. The Runtime
and Process classes can be
used to call external non-Java programs. Native methods can be
used to directly integrate methods implemented in C or C++ with
standard Java methods. Each mechanism has certain advantages.
For very simple tasks, it is easier to implement a separate executable
than a class with native methods. However, it's more difficult
to pass parameters to separate executables, and parameters are
less integrated with the Java runtime execution environment than
native methods. Subsequent sections of this chapter examine the
use of separate executables and classes with native methods.
One of the simplest means of integrating Java with non-Java systems
involves the creation of one or more command-line driven executables
that use the non-Java components directly. The Runtime
and Process classes in the
java.lang package provide
methods that allow a Java application to execute and monitor non-Java
processes.
The Java application communicates with the executable by creating
a command array and calling Runtime.exec().
The executable can communicate a simple integer back using its
return code. If more complex communication is required, the Java
application can read from the process standard output and write
to the process standard input.
Typical use of the Runtime
and Process objects involves
setting up the command or command/argument array to execute, executing
the command, retrieving the input and output streams of the spawned
process, and monitoring the process status.
The disadvantage to using command-line driven executables is that
the executables are not very tightly integrated with the Java
application. Parameter passing is complex, and the interface certainly
doesn't resemble the clean clarity of a set of defined Java classes.
However, for simple cases, it is probably simpler to define separate
executables than classes with C-based native methods.
The Runtime class exposes
several versions of an exec()
method that may be used to execute non-Java processes. The simplest
version takes one argument-a string containing the system command
to execute. Other versions include arrays of arguments to pass
to the system command. The exec()
method returns a Java Process
object that can be used to monitor the spawned process.
Note: |
Because the Runtime class can be used to execute non-Java applications, the Runtime.exec() method isn't available from Java applets. Applets can't be allowed to execute non-Java processes because the processes aren't forced to obey Java
security restrictions.
|
The Runtime class can't be
used to instantiate a Runtime
object. Instead, the Runtime
class includes a static method, getRuntime(),
that returns a pre-existing Runtime
object. You can call the exec()
method from that object.
The Runtime.exec() method
returns a Process object.
Process objects can be used
to control and monitor the execution of spawned processes. The
getErrorStream(), getInputStream(),
and getOutputStream() methods
of the Process object can
be used to get the stderr,
stdout, and stdin
streams of the spawned process. The user might wait for the natural
termination of the process using the waitFor()
method, or can force termination using the destroy()
method. The exit code of the process after termination can be
retrieved using the exitValue()
method.
The following code demonstrates the use of the Runtime
object to spawn a Process
whose output and exit value are monitored by a Java application:
Process proc = Runtime.getRuntime().exec(ANonJava.exe@);
InputStream in = proc.getInputStream();
byte buff[] = new byte[1024];
int cbRead;
try {
while ((cbRead = in.read(buff)) != -1)
{
// Use the output
of the process...
}
} catch (IOException e) {
// Insert code to handle exceptions that
occur
// when reading the process output
}
// No more output was available from the process, so...
// Ensure that the process completes
try {
proc.waitFor();
} catch (InterruptedException) {
// Handle exception that could occur when
waiting
// for a spawned process to terminate
}
// Then examine the process exit code
if (proc.exitValue() == 1) {
// Use the exit value...
}
Tip: |
When you retrieve a handle to the output stream of a spawned process with the Process.getInputStream() method, it isn't possible to use the available() method on the returned InputStream object. A reasonable workaround is to use
the read(byte[]) method of the InputStream, which returns -1 when the end of the stream is reached.
|
If you're developing executables that pass information back to
Java classes using standard output, you should probably adopt
an easily parsable output format. This enables your java
class to split up and coerce the data from your executable as
necessary.
The DAOCmd project, available on the source code CD-ROM, includes
a java class that calls a
non-Java executable that returns the results of a database query.
The parameters are passed from the java
class to the non-Java executable by means of the command line,
and the results of the database query are written to standard
output by the executable. The java
class reads the results and, in the example, simply echoes the
results to the standard output of the Java environment.
The TestDAO class does all
of the work on the Java side; the main()
method creates the command line for the executable, runs the executable,
and reads and echoes the results. The implementation of the main()
method follows:
public static void main(String args[])
{
Runtime rt = Runtime.getRuntime();
Process proc;
// Create the command array to pass to
the executable
String cmd[] = new String[args.length+1];
cmd[0] = "DAOCmd";
// Prepare the command array for the executable
for (int iArg = 0; iArg < args.length;
iArg++) {
// All arguments
are quoted to ensure that
// arguments are
passed correctly to the
// spawned process
cmd[iArg+1] =
""" + args[iArg] + """;
}
// Attempt to loop and retrieve all of
the output
// from the spawned process
try {
proc = rt.exec(cmd);
DataInputStream
in = new DataInputStream(proc.getInputStream());
String strLine;
boolean tContinue
= true;
byte buf[] = new
byte[256];
int cbRead;
while ((cbRead
= in.read(buf)) != -1) {
//
Simply echo the output from the spawned process
//
to the Java application's stdout
System.out.print(new
String(buf, 0, 0, cbRead));
}
// Wait for the
spawned process to terminate
proc.waitFor();
} catch (Exception e) {
System.out.println(e);
}
}
The first step is to create the command array for the executable.
The first element of the command array is the constant DAOCmd,
which is the name of the executable. The remaining arguments are
from the command line that was used to run the java
class. Each argument is quoted to ensure that it is interpreted
by the executable as a string (if an argument to the java
class was one or more words enclosed in quotes-the Java interpreter
strips off the quotes but maintains the string with embedded white
space as a single string within the argument array passed to main()).
Next, the executable is executed using the Runtime.exec()
method. The standard output stream of the executable is retrieved
from the Process object returned
by exec(). The Java method
loops to retrieve the output of the Process
using the read() method.
When no more data is available to read, the loop terminates, at
which point the java class
waits for the process to terminate using the waitFor()
method. After the process has terminated, the java
class can retrieve the process exit code using the Process.exitValue()
method, and act accordingly.
One of the best aspects of Java is its platform independence.
Any applet that you write basically performs in exactly the same
manner, regardless of the platform or operating system of the
host computer. Thanks to Java's broad support for everything from
GUI windowing in the java.awt
classes to networking support in the java.net
class, most tasks can be accomplished directly within Java.
Caution: |
Even though Java is platform independent, there are some platform-specific bugs. For example, in the Windows 95 AWT implementation, windows shown modally do not actually behave modally. The status of bugs in Sun's Java interpreter is available at
http://www.javasoft.com.
|
Because Java is platform independent, however, it doesn't support
all of the features of its host computer or operating system.
The Win32 API supported by Microsoft Windows NT and Windows 95
includes, among many other useful features, a set of functions
for establishing network connections with the modem, using Remote
Access Services (RAS). Programmatically establishing a remote
connection to a network can be very useful to support, for example,
automatic registration of a commercial Java-based application.
Another reason to use native methods is the multitude of libraries
that provide C interfaces to various systems. Many APIs are currently
supplied as a set of statically linked library files with associated
C header files and possibly some dynamically linked libraries.
Unfortunately, very few APIs are currently supplied with Java
wrapper classes.
There are basically two means of accessing non-Java libraries;
the first, the use of separate processes, has been discussed earlier
in this chapter. The disadvantage of using a separate process
is its loose integration with the Java environment. Parameter
passing is very limited, and communication between Java and the
separate process and runtime may be impossible, or simply too
tedious. The second means of accessing non-Java libraries is through
the use of native methods.
Native methods are methods defined within Java classes using the
native keyword. Within the
java class, they have no
implementation specified-only the name of the method, access specification,
parameters, and return value.
When creating a native method, you must first define it in a java
class using the native keyword.
For example, to define a public native method returning an integer
called fastStringScan(String str, String
strToFind) in a class named StringUtils,
you would use the following code:
class StringUtils {
public native int fastStringScan(String
str, String strToFind);
...
}
Note: |
Literally any Java method, with the exception of object constructors, can be implemented as a native method. If you need to call some function implemented as a native method during the creation of your object, you can create a private native method that
performs the initialization and call the method when the constructor executes.
|
Obviously, the implementation of the native method must reside
somewhere. It is typically part of a dynamically-linked library;
on the Microsoft Windows 95 or Windows NT platform it is in a
DLL.
Java provides tools to generate wrappers for native code implementations
in C. The wrappers generated by Java provide a fairly easy-to-use
interface between native method implementations and the Java runtime
environment. The use of wrappers is not optional; the Java interpreter
expects to find functions with specific names determined from
the native method definition. Implement the following steps to
create native code wrappers:
- Compile the java class
that contains the native method declaration.
- This is done by running javac MyClass.java
from the command prompt.
- Create a header file that declares the structure representing
the java class.
- The Java JDK provides a utility, javah,
that does this for you. Typing javah
MyClass from the command line generates a MyClass.h
file containing the structure of the class (as used by native
methods) and native method function prototypes.
- Create a stub file that contains function wrappers that call
the native functions you implement.
- Using javah -stubs MyClass
generates a file called MyClass.c
that contains function stubs. A section later in this chapter,
"The Stubs File," provides more information about the
stubs file.
- Develop the implementation of the native methods.
The header file created in step 2 contains the structure definitions
and function prototypes that you need to implement your native
methods. You must implement each function listed in the header
file.
Caution: |
Be very careful if you add another member to your class! Make sure to recompile the java class with the native method declarations, and make sure that you rerun the javah utility and recompile your DLL. Failing to recompile or rerun can
lead to some very frustrating-but not straightforward-problems! You may set a class member, but continue to see it as null because the offsets calculated by the compiler no longer match up with the java class declaration.
It's definitely worthwhile to modify your makefile to include a dependency step that updates the native method header file.
|
The Stubs File
The following are the contents of the StringUtils.c
stub file generated from the previously mentioned StringUtils
class.
/* DO NOT EDIT THIS FILE - it is machine
generated */
#include <StubPreamble.h>
/* Stubs for class StringUtils */
/* SYMBOL: "StringUtils/fastStringScan(Ljava/lang/String;
ÂLjava/lang/String;)I", Java_StringUtils_fastStringScan_stub
*/
__declspec(dllexport) stack_item *Java_StringUtils
ÂfastStringScan_stub(stack_item *_P_,struct execenv *_EE_)
{
extern long StringUtils_fastStringScan(void
*,void *,void *);
P_[0].i = StringUtils_fastStringScan(_P_[0].p,((_P_[1].p)),((_P_[2].p)));
return _P_ + 1;
}
The Stubs file contains function stubs that Java expects when
it attempts to call a function with a dynamic link library that
implements a native method. The included StubPreamble.h
file contains all of the type and structure definitions required
by Java stub functions. Notice that all of the stub functions
are exported using the _declspec(dllexport)
directive. They are the entry points to the DLL that contains
the native methods; the functions that you write do not need to
be exported.
The stub functions basically repackage the arguments from a Java
interpreter function call into single parameters with specific
types. The internal prototype, in this case extern
long StringUtils_fastStringScan(void *,void *,void *),
uses void pointers for all of the arguments. However, the function
that you implement has specific parameter types. The function
declarations for the functions that you implement are contained
within the StringUtils.h
header file. The fastStringScan()
function is defined within that file as extern
long StringUtils_fastStringScan(struct HStringUtils *,struct Hjava_lang_String
*,struct Hjava_lang_String *).
The Header File
The following are the contents of the StringUtils.h
header file generated by javah
from the previously mentioned StringUtils
class.
/* DO NOT EDIT THIS FILE - it is machine
generated */
#include <native.h>
/* Header for class StringUtils */
#ifndef _Included_StringUtils
#define _Included_StringUtils
typedef struct ClassStringUtils {
char PAD; /* ANSI
C requires structures to have a least one member */
} ClassStringUtils;
HandleTo(StringUtils);
#ifdef __cplusplus
extern "C" {
#endif
struct Hjava_lang_String;
extern long StringUtils_fastStringScan(struct HStringUtils *,
Âstruct Hjava_lang_String *,struct Hjava_lang_String *);
#ifdef __cplusplus
}
#endif
#endif
The header file contains all of the function prototypes that you
need to implement, as well as all of the includes that you need
to call useful Java functions. Peeking through the Java API files
included automatically in the header file is a worthwhile exercise.
Several include files are nested within the native.h
header. Without additional documentation, it's very difficult
to determine the purpose of many of the structures and functions
in some of the included files; but the native.h
header file can occasionally shed light on problems that you may
encounter during compilation.
If you are implementing the native methods in C++, you must be
sure to wrap the inclusion of the StringUtils.h
header file in an extern "C"
block, as the following example demonstrates:
/**
* StringUtilsImpl.cpp
*
* Contains implementation of native methods.
*/
extern "C" {
#include <StringUtils.h>
}
The StringUtils class is
represented as a C structure in the header file. Accessing object
instance variables involves using members of the ClassStringUtils
structure; further descriptions are listed in the following sections.
It is frequently necessary to call Java methods from within native
methods. In order to do so, it is essential to understand method
signatures, and to know how to dispatch Java method calls. The
following sections describe method signatures and method call
dispatching in detail.
It is also frequently important to be able to create Java objects
from within native methods. This is particularly important, for
example, when returning a Java object, such as a String,
from a native method.
Identifying Methods: Method Signatures
Within a class, each method is distinguished from other methods
by the method's name and signature. The name is simply the name
of the method. The signature is a string that describes the method
parameters and the method return value. This allows Java to perform
function overloading by enabling the interpreter to dynamically
look up and dispatch functions that have the same name but have
different arguments or return types.
Method signatures consist of a set of method parameter type descriptions
enclosed within parentheses and followed by a return type description.
Primitive types are designated differently from object types;
object types are prefixed with an "L", include the fully
distinguished name of the object
class delimited by "/" rather than ".", and
are terminated with a ";".
Given a Java method repeatSubString(),
defined as String repeatSubString(int
begOffset, int endOffset,
int repeatCount, String
string), the method signature would be "(IIILjava/lang/String;)Ljava/lang/String;".
If an array is passed, the type of the array element should be
prefixed by a "["
in the method signature. The method signature for int
findString(String stringToFind, String[] strings)
would be "(Ljava/lang/String;[Ljava/lang/String;)I".
The signature prefixes or characters are defined within the signature.h
header file. The most frequently used signature characters follow:
[ - Array
B - Byte
C - Char
L - Beginning of Class name
; - End of Class name
F - Float
D - Double
I - Integer
J - Long
S - Short
V - Void
Z - Boolean
Calling Java Object Methods from Native Methods
You will frequently want to make a Java object perform some action
from a native method. To do so, you call a method on the object.
To call a method on an object from a native method, you use the
execute_java_dynamic_method()
function.
The full definition of the function from interpreter.h
follows:
long execute_java_dynamic_method(ExecEnv
*,
HObject
*obj,
char
*method_name,
char
*signature,
...);
The first argument is the execution environment, or ExecEnv.
You should generally use the EE()
function, which returns the current execution environment. The
execution environment has little or no documentation provided
with the JDK 1.0 release; you can glean some of its uses by examining
the execenv structure in
interpreter.h and associated
macros. The second argument is an object instance that provides
the method you want to call. The third argument is the method
name, sans signature. It is the raw name of the method without
access specifiers, parameters, return types, etc. For a method
defined as public int foo(String str),
the method name would simply be foo.
The fourth argument is the signature of the method, as described
in the previous section.
The remaining arguments are the object or primitive datatype parameters
required by the method. The arguments must correspond to the types
defined in the method signature.
Notice that the execute_java_dynamic_method()
returns a long. Your code should cast the long appropriately,
given the return type defined in the method signature.
The execute_java_dynamic_method()
function enables you to call methods on an object instance, but
it doesn't enable you to call static methods defined for a class.
Calling static methods requires the use of the execute_java_static_method()
function, defined as follows in interpreter.h:
long execute_java_static_method(ExecEnv
*,
ClassClass
*cb,
char
*method_name,
char
*signature,
...);
The arguments of execute_java_static_method()
are almost identical to the arguments for execute_java_dynamic_method().
The only difference is the second argument, which is a pointer
to a Class object rather
than a Java object instance. The Java interpreter creates a Class
object for every loaded class. The ClassClass
structure is defined in the oobj.h
header file.
You can use the FindClass()
function to get a Class object
with a class name; it's defined in interpreter.h
as:
ClassClass *FindClass(struct execenv
*ee, char *name, bool_t resolve);
To find the java.lang.System
class object, you use the following function call:
ClassClass *System = FindClass(EE(),
"java/lang/System", FALSE)
Accessing Java Object Instance Variables from Native Methods
One of the most common reasons to access object instance variables
from native methods is to set instance variables for the object
that contains the native method that you implement. Using the
following definition for a NonJavaFile
class:
public class NonJavaFile {
public native void getFileAttributes(String
strFile);
public String strAttr1;
public int iAttr2;
public boolean bAttr3;
}
The native method implementation for getFileAttributes()
would no doubt require the ability to set the various Attribute
variables of the NonJavaFile
instance. The native method shell, generated using javah
as described previously, includes a parameter that is not visible
in the Java getFileAttributes()
method declaration. The additional parameter is the pointer to
the handle of the object instance that was used to call the native
method. Use the unhand()
function to acquire the C structure that contains the Java object
instance variables.
Using the NonJavaFile example,
the implementation of getFileAttributes()
might look like:
void NonJavaFile_getFileAttributes(struct
HNonJavaFile *me,
struct
Hjava_lang_String *strFile)
{
ClassNonJavaFile *NonJavaFile = (ClassNonJavaFile*)unhand(me);
// Use the strFile argument to perform
some action(s)
...
// Modify some of the Java object instance
variables (or, in other
// words, members of the ClassNonJavaFile
structure)
char achAttr[] = "Some File Attribute";
NonJavaFile->strAttr1 = makeJavaString(achAttr,
sizeof(achAttr));
NonJavaFile->iAttr2 = 100;
NonJavaFile->bAttr3 = TRUE;
}
Creating Java Objects in Native Methods
The Java API function execute_java_constructor()
is the key to creating Java objects from native methods. Its arguments
include the name of the class to create, the desired constructor
signature, and the arguments (which must, of course, correspond
to the constructor signature) to the constructor. If the constructor
executes successfully, it returns a pointer to a new Java object.
If failure occurs, the function returns NULL, and an exception
is raised.
The full prototype for the execute_java_constructor()
function, as defined in interpreter.h,
follows:
execute_java_constructor(ExecEnv *,
char
*classname,
ClassClass
*cb,
char
*signature, ...);
If you already have a Class
object for the class instance that you want to create, you can
pass it in as the cb argument
and pass NULL for the classname.
This is somewhat faster than the alternative, which is to pass
the classname and omit the
Class object (pass NULL for
cb), because the Java interpreter
must find the Class object
based on the class name.
The Java String class is
a special case; the process of creating Strings
is simplified by the makeJavaString()
function prototyped in the javaString.h
header file. The full function prototype follows:
Hjava_lang_String *makeJavaString(char
*, int);
The function returns a new String
object given a C character pointer and the length of the string.
All of the classes provided with the Java Development Kit fully
use Java's standard security, error handling, and synchronization
mechanisms. Native methods aren't forced to conform to any of
the aforementioned standards. Deliberate effort is required on
the behalf of native method implementer to use them.
The following sections describe the standard mechanisms, as well
as means of conforming to their requirements.
Error Handling in Native Methods
Java's error handling mechanism is centered around the use of
exceptions. Literally any method called on any Java object may
throw one or more types of exceptions. Exceptions are used to
indicate that an anomalous situation occurred during the execution
of method code. The members of the Exception
object describe the type of exception that occurred.
Java enforces the explicit capturing or throwing of exceptions.
If your Java code calls a method that indicates in its definition
that it throws one or more exceptions, your calling code must
either catch the exceptions, or explicitly indicate that it throws
the exceptions. Use of exceptions within Java code is further
described in Chapter 10, "The Order
Entry System: Exception Handling and Browser Interaction."
Native methods should, when appropriate, throw Java exceptions.
They should also declare the Java exceptions that can be thrown
by Java class methods executed by the Java code, giving the Java
compiler information that it needs to enforce Java's rules for
exception capturing.
Handling Exceptions Thrown by Java Code
Within Java code, the handling of exceptions is automatic. Frames
with exception handlers can be established to catch exceptions.
Native methods must use the following Java API function to detect
exceptions thrown by Java methods:
exceptionOccurred(ee)
The exceptionOccurred() function
returns true if a Java exception has been raised. To handle the
exception, you can retrieve additional information about it using
the exc member of the exception
union within the execenv
structure. The exc member
contains a pointer to the Java exception object.
Typical native code that calls a Java method, then checks for
and handles Java exceptions, might look like the following:
// Call some Java method
long lResult = execute_java_dynamic_method(EE(), theObj, "someMethod",
"()V");
if (exceptionOccurred(EE())
{
// Check if the exception that occurred
is a dao.DaoException
// in this example (NOTE: the dao.DaoException
is mentioned in
// the DAOLayer example)
JHandle *exception = EE()->exc;
ClassClass *DaoExceptionClass = FindClass(EE(),
"dao/DaoException", TRUE);
if (is_instance_of(exception, DaoExceptionClass,
EE()))
{
// The exception
is a dao.DaoException, so I can include
// code to handle
the exception
EXCEPTION HANDLING
CODE GOES HERE...
// After I have
handled the exception, I clear it so that
// it isn't propagated
back to the Java interpreter
exceptionClear(EE());
}
}
This is the rough functional equivalent of the following Java
code:
try {
theObj.someMethod();
} catch (dao.DaoException e) {
EXCEPTION HANDLING CODE GOES HERE...
}
The native code is much more involved than the Java code. That's
one of the disadvantages of using native methods. Java code doesn't
have to worry about explicitly testing the type of the exception
that is generated-the Java interpreter matches the exception with
the corresponding catch clause,
if an appropriate catch clause
exists. After the code in the catch
clause executes, the Java interpreter handles clearing the exception
and resetting the Java execution environment.
The native code clears the Java exception explicitly using exceptionClear().
The exceptionClear() function
is implemented as a macro in interpreter.h;
its only parameter is a pointer to an execenv
structure. The exceptionClear()
macro is used to clear the current exception. It should be used
if you catch and handle an exception in your native method code.
Use it with caution; you will want to propagate (throw) some exceptions
back to the code that called your native method.
If an exception occurs, the type of the exception is tested using
the is_instance_of() function,
which returns true if an
object is an instance of a specific class. The definition of is_instance_of,
from interpreter.h, follows:
bool_t is_instance_of(JHandle * h, ClassClass
*dcb, ExecEnv *ee);
The first argument is the Java object you want to test, the second
is the class object that you want to check the object against,
and the third is a pointer to an execenv
structure. The is_instance_of
function returns true if
the object is an instance of the class or a subclass of the class.
Throwing Exceptions
Throwing exceptions from native methods is very straightforward.
The only Java API function that is required is SignalError().
The prototype for SignalError()
follows:
void SignalError(struct execenv *, char
*, char *);
The first argument is a pointer to an execenv
structure. You can use the EE()
function to pass the active execenv
structure to SignalError().
The second argument is a null-terminated C string indicating the
fully distinguished name of the exception that you're throwing.
As usual, the periods separating the packages in which the class
is defined should be replaced with the forward slash ( / ).
The third argument is a null-terminated string that describes
details of the exception; you can pass null if you don't want
to specify additional information.
You can learn more about Java exceptions in Chapter 10.
The DAOLayer native methods example, described later in this chapter,
also demonstrates throwing exceptions from native methods.
Security in Native Methods
As previously indicated, Java native methods aren't subject to
the same security restrictions as pure Java methods. That's not
to say that they are completely exempt; for example, if you attempted
to open a file using the java.io
classes from a native method, the open call would fail with a
SecurityException if the
Java application doesn't have sufficient security privileges.
However, a native method could circumvent that security by using
non-Java input-output mechanisms. If the native method uses standard
C library functions for file input and output, it would be allowed
to do so irrespective of the security restrictions of the current
Java execution environment.
Consequently, to write truly well-behaved native methods, it is
necessary to explicitly check the active security restrictions.
As a matter of fact, this is precisely the behavior of implementations
in the Java standard library. The following excerpt, from File.java
in the java.io package, illustrates
a security check:
/**
* Deletes the specified file. Returns true
* if the file could be deleted.
*/
public boolean delete() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkDelete(path);
}
return delete0();
}
Obviously, the security check is performed here within a Java
wrapper method that calls a native method, delete0(),
that actually performs the file deletion. If you need to perform
a simple, single-step security check, using the wrapper method
may suffice.
If you have a native method that performs several different types
of actions that might each be subject to different security restrictions,
it may be more convenient for you to include the security checks
within the body of the native method. The following code illustrates
one means of performing those types of security checks:
ClassClass *System = FindClass(EE(),
"java/lang/System", TRUE);
Hobject *phSecMgr = (Hobject*)execute_java_static_method(EE(),
System,
"getSecurityManager", "()Ljava/lang/SecurityManager;");
if (NULL != phSecMgr)
{
// Perform security check - for this example,
check the ability
// to delete a file in the local file
system
execute_java_dynamic_method(EE(), phSecMgr,
"checkDelete",
"(Ljava/lang/String;)V", phFile);
if (exceptionOccurred())
{
// Perform
some action
}
}
Note: |
The default SecurityManager that comes with the Java development kit throws a SecurityException on every check that is performed.
|
Using Java's Synchronization/Wait-Notification Mechanisms
The architecture of Java was defined with the goal of directly
supporting multithreading. Consequently, thread creation, notification,
and synchronization mechanisms are provided within Java. Because
every class is a sub-class of java.lang.Object,
every class supports the wait()
and notify() synchronization
functions. Entire methods can be protected automatically by Java
if they are defined with the synchronized
keyword. The java.lang.Thread
classes contain a synchronized static nextThreadNum()
method; the synchronized
keyword assures that the function may be called by only one Java
object at a time.
Native Method Wait/Notify
To wait on a Java object you must call the object's wait()
method. Doing so is straightforward, as the following code snippet
illustrates:
execute_java_dynamic_method(EE(), theObject,
"wait", "()V");
To notify a Java object you must call the object's notify()
or notifyAll() method. The
following code illustrates a call to the notify()
method:
execute_java_dynamic_method(EE(), theObject,
"notify", "()V");
Using wait() and notify()
with Java objects is very useful when, for example, your native
method consumes objects from a queue filled by a separate Java
thread.
You should note that waits
may be interrupted, in which case an InterruptedException
is thrown by the wait method.
Consequently you may want to check for a Java exception after
executing the wait, as shown
in the following snippet:
// Wait for the object to be notify()ed
execute_java_dynamic_method(EE(), theObject, "wait",
"()V");
// Check for the occurrence of an InterruptedException
if (exceptionOccurred(EE()))
{
// Handle the exception - check for Interrupted,
or other...
}
If you don't check for the InterruptedException,
you may erroneously execute code that assumes that a waited object
was notified. This isn't an issue within pure Java code because
exceptions are enforced by the Java interpreter.
One of the most useful and easy-to-use commercial APIs is the
Microsoft DAO object model for Windows 95 and Windows NT. The
DAO objects provide a simple, powerful abstraction that wraps
ODBC-compliant database systems. DAO objects are directly exposed
as a set of COM and OLE automation classes; the Microsoft Visual
C++ development environment includes the DAO C++ API, which provides
a simple mechanism to use the DAO OLE classes. The DAO classes
are used to access databases and tables in the previously mentioned
DAOCmd project.
Note: |
Microsoft recently released an open beta of their Visual J++ Java development environment, available at http://www.microsoft.com/VisualJ. The Visual J++ environment, which runs on Windows 95 and
Windows NT, incorporates support for the COM object model and allows the developer to instantiate and use OLE objects directly within Java, as well as create COM/OLE objects using Java. Because of the built-in COM support, Visual J++ applications can call
DAO objects directly.
|
Microsoft Visual C++ also provides some DAO wrapper classes within
the Microsoft Foundation Classes. The DAO wrappers handle some
of the details of the creation and destruction of DAO objects
for you. Because of their ease of use, they are a compelling choice
as a set of classes to integrate with Java. The DAOLayer project,
included with source code on the CD-ROM provided with this book,
demonstrates fairly simple integration of Java classes with Microsoft
MFC DAO C++ classes.
DAOLayer is designed to illustrate the integration of Java with
Microsoft MFC DAO C++ classes using native methods. The DAOLayer
project consists of several C/C++ header files and source files,
as well as two Java classes that provide class definitions. You
can get information about the DAO objects from the on-line help
provided with the Microsoft Visual C++ compiler package.
Design Issues: Mapping C++ Objects to Java Objects
The Microsoft MFC DAO classes are already divided into discrete
functional units. Consequently, defining a mapping from C++ to
equivalent Java classes is straightforward. Wrapping a C API is
slightly more complex due to the fact that there aren't necessarily
any inherent objects within a C API. Your options when mapping
a C API are basically to either create objects with methods that
provide an interface to several logically related C functions,
or to simply create a class that exposes static methods that wrap
the C functions.
The DAOLayer project wraps the MFC CDaoDatabase
class and the CDaoRecordset
class. The Java classes dao.Database
and dao.Recordset wrap the
CDaoDatabase class and the
CDaoRecordset class respectively.
Only a few of the C++ member functions are exposed within the
Java wrapper classes; dao.Database
provides open and close methods, and dao.Recordset
provides open, close, navigation, and field value retrieval functions.
An implementation that was more full would provide additional
functionality, such as write access, to field values in a Recordset.
Defining the Database
Java Wrapper Object
The dao.Database class is
a wrapper for the MFC CDaoDatabase
class. It provides the ability to open databases that may be used
to retrieve recordsets using the Recordset
object.
The dao.Database object is,
in some respects, the most important class in the DAOLayer dao
package. It contains Java code to load the library containing
the native methods that implement the dao.Database
functionality. The excerpt that loads the DAOLayer dynamic link
library follows:
static {
System.loadLibrary("DAOLayer");
}
The dao.Database object also
contains two very important static native methods, initDAO()
and termDAO(). The MFC DAO
library must be initialized using explicit calls to AfxDaoInit()
and AfxDaoTerm() functions
when it is used within a dynamic link library; the native methods
initDAO() and termDAO()
wrap those functions respectively. It's the responsibility of
the Java code that uses the dao.*
package to call dao.Database.initDAO()
before using any classes in the package, and call dao.Database.termDAO()
when finished.
The following section describing the Recordset
object includes more details about wrapping C++ classes with Java
objects.
Defining the Recordset
Java Wrapper Object
The DAO Recordset object
provides the ability to access a set of records from a DAO database.
The MFC DAO CDaoRecordset
class has numerous member functions that can be used to retrieve
a set of records using an SQL statement, navigate through the
records, and retrieve and update field values in records. For
the sake of simplicity, the implementation of the Recordset
provides only a few of the functions from the C++ class. It should
be fairly simple for you to extend the class to add more functionality.
The definition of the dao.Recordset
object follows:
package dao;
public class Recordset {
public native void open(String strSQL)
throws DaoException;
public native void close() throws DaoException;
public native boolean isEOF() throws DaoException;
public native void moveFirst() throws
DaoException;
public native void moveNext() throws DaoException;
public native String getFieldValue(String
strField) throws DaoException;
protected dao.Database db;
protected int pRec; // Pointer to CDaoRecordset
instance
protected native void allocRecObject();
protected native void deleteRecObject();
public Recordset(dao.Database db) {
this.db = db;
allocRecObject();
}
protected void finalize() throws Throwable
{
super.finalize();
deleteRecObject();
}
}
Each of the public methods defined in the dao.Recordset
class corresponds to a member function in the C++ class. Literally
the only difference is that the names of the methods are prefixed
with a lowercase letter, in accordance to standard Java method
capitalization conventions, as opposed to uppercase, as per the
C++ class member definitions. The effects of the functions correspond
to the equivalently named C++ member functions. Most of the native
methods are simple dispatching functions that call C++ functions.
At this point you may be wondering how the native methods call
C++ functions. The private instance variable pRec
is used to store a pointer to a CDaoRecordset
object. The native method casts the pRec
from an integer (which is a C long) back to a CDaoRecordset
pointer, then calls the desired CDaoRecordset
member function. The implementation of the moveNext()
native method follows:
void dao_Recordset_moveNext(struct Hdao_Recordset*
me)
{
try {
getRecPtr(me)->MoveNext();
} catch (CDaoException* pe) {
throwTranslatedDaoException(pe);
pe->Delete();
}
}
Note: |
Notice that the name of the native method, dao_Recordset_moveNext(), includes the name of the package as a prefix. If the Recordset class were defined in a web.db.dao package, the Recordset_moveNext() method would have
the prefix web_db_dao_.
|
The getRecPtr() function
is a useful utility function that returns a pointer to a CDaoRecordset
object, given a handle to a Java Recordset
object. It is implemented as follows:
CDaoRecordset* getRecPtr(struct Hdao_Recordset*
daoRec)
{
return ((CDaoRecordset*)
((struct Classdao_Recordset*)unhand(daoRec)->pRec));
}
The complementary set function,
setRecPtr(), includes the
assertion that a Java int
member variable, which is defined in the Java C structure as a
long, is the same length
in bytes as a CDaoRecordset
pointer. The function is implemented as
void setRecPtr(struct Hdao_Recordset*
daoRec, CDaoRecordset* pRec)
{
ASSERT(sizeof(long) == sizeof(CDaoRecordset*));
((struct Classdao_Recordset*)unhand(daoRec))->pRec
= (long)pRec;
}
The get and set
functions are used as simple convenience functions that obviate
the need to maintain complicated sequences of casts and calls
to the unhand() function.
As previously indicated, the java
class includes a member that is used to store a pointer to a CDaoRecordset
instance. One of the issues in mapping Java classes to C++ classes
is the lifetime of the objects. You'll notice a call to a protected
allocRecObject() function
in the constructor of dao.Recordset.
That function is used to create a CDaoRecordset
instance and connect it to pRec.
The class finalizer includes a call to the matching delete
function, deleteRecObject(),
that deletes the C++ object when the lifetime of the Java object
ends.
Passing C++ Exceptions to Java
In the implementation of the Recordset.moveNext()
method, you may notice that the call to the C++ CDaoRecordset::MoveNext()
member function is contained within a C++ try/catch
block. The MoveNext() function
may raise a C++ exception of type CDaoException.
To maintain the semantics of the class, from the Java perspective,
with regards to the exception behavior, the C++ exception is converted
to a Java exception by the throwTranslatedDaoException()
function. The throwTranslatedDaoException()
function is implemented as follows:
void throwTranslatedDaoException(CDaoException*
pe)
{
CString strErr;
CDaoErrorInfo *pErr = pe->m_pErrorInfo;
strErr.GetBuffer(512);
strErr.Format("%s (%ld) - %s",
pErr->m_strSource, pErr->m_lErrorCode,
pErr->m_strDescription);
SignalError(EE(), "dao/DaoException",
(char*)(LpcSTR)strErr);
}
The function takes a pointer to a CDaoException
object. When an exception occurs, a pointer to the exception is
acquired in the catch block
that brackets the call to CDaoRecordset::MoveNext().
Using the convenient MFC CString
class, the function creates a readable string that represents
the CDaoException that occurred.
After the string is created, an exception is raised using the
SignalError() Java API function.
For the throwTranslatedDaoException()
function, a dao.DaoException
is thrown. The Java dao.DaoException
class, defined in DaoException.java,
is a straightforward subclass of the java.lang.Exception
class.
After the Java exception is thrown, the C++ exception is deleted.
For the CDaoException class,
the CDaoException::Delete()
member function must be called. Other C++ exceptions may be deleted
by the standard C++ delete
operator. The Java interpreter doesn't detect the occurrence of
the Java exception until the native method returns, at which point
the Java exception object is available to the Java code that called
the native method.
Most Web browsers severely restrict the security for executing
applets. Consequently, most applets can't be extended using either
the separate process method with Runtime.exec(),
or native methods implemented in C or C++.
This is obviously a fairly severe restriction. It's possible to
use some type of client-server applet to access functionality
unavailable through Java, or to perform remote procedure calls
using RMI (described in the following section). To implement a
client-server applet without using RMI, you need to create an
applet that uses, for example, sockets to communicate with a Java
server application that uses the external process or calls native
methods. This avoids the applet security restrictions because
the server does the work, not the applet. However, those mechanisms
are only useful when integrating with systems that don't need
to run on the host machine.
For example, if you want to develop an applet that uses GUI functionality
unavailable through the java.awt
package, you may want to use native methods. In that situation,
a call to a Java object on a server won't suffice.
To remedy this situation, there has been some talk of signed applets
or applets with associated certificates. Applets would only be
allowed to run if they had an associated certificate trusted by
the browser loading the applet. If certificates are implemented,
then a future browser may allow applets with native methods to
run, assuming that the user of the browser trusts the presenter
of the certificate for the applet.
Java Remote Method Invocation (RMI) provides a simple mechanism
for converting standard Java classes into client/server classes.
Java RMI is essentially an integrated remote procedure call mechanism
that allows method calls to a local object to be forwarded transparently
to a server object. To use RMI, you define an implementation class
and an interface class; the interface class uses some RMI magic
to package parameters and transfer them to a remote object, an
instance of the implementation class.
RMI could be used to provide a simple workaround to applet native
method security restrictions. Instead of implementing some arbitrary
and complex client/server applet, you could create two versions
of the class that uses native methods. The interface class would
simply provide function stubs that, due to the fact that they're
implemented in Java, would be callable by a Java applet. The implementation
class, running on the remote object server, would use native method
implementations.
Java RMI is currently in Beta. If you want to find out more about
RMI, and download a Beta version, check out Sun's Java Web site
at http://www.javasoft.com.
Java has a great set of standard packages that provide much of
the functionality required by Java applets or applications. However,
there are occasionally tasks that require the use of non-Java
objects. For example, an interface to a non-Java database may
require some intermediary between a Java application and the non-Java
database engine or API.
Within Java, there are two basic options. The simplest option
may be to create a command-line driven executable that returns
information to Java using its standard output. The previously
mentioned DAOCmd project illustrates this means of integration.
The problem with that type of integration is that it's weakly
integrated with the Java runtime environment; dynamically passing
parameters or exception information between Java and the running
non-Java process is difficult or impossible.
The second option is to create a class with native methods. Native
methods provide a very powerful means of extending the Java environment
with non-Java code. This chapter's examples and the DAOLayer project
illustrate that it is possible to tie well-behaved native methods
seamlessly into the Java environment.
Next
Previous
Contents
|