Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

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.

The Runtime and Process Classes

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.

Executing External Programs Using Runtime

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.

Monitoring Executed Applications Using Process

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.

A Practical Example: DAOCmd

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.

Extending Java Using Native Methods

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.

Basic Mechanics of Creating Native Methods

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:

  1. Compile the java class that contains the native method declaration.
  2. This is done by running javac MyClass.java from the command prompt.
  3. Create a header file that declares the structure representing the java class.
  4. 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.
  5. Create a stub file that contains function wrappers that call the native functions you implement.
  6. 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.
  7. 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.

Calling Java from Native Methods

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.

Writing Well-Behaved Native Code

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.

An Interface to Microsoft Data Access Objects (DAO)

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.

Applets and Security Restrictions

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

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.

Summary

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




  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте