This appendix was
contributed by and used with the permission of Andrea Provaglio
(www.AndreaProvaglio.com).
The Java language and its standard
API are rich enough to write full-fledged applications. But in some cases you
must call non-Java code; for
example, if you want to access operating-system-specific features,
interface with special hardware
devices, reuse a pre-existing, non-Java code base, or implement
time-critical sections of code. Interfacing with
non-Java code requires dedicated support in the compiler and in the Virtual
Machine, and additional tools to map the Java code to the non-Java code.
(There’s also a simple approach: in Chapter 15, the section titled
“a Web application” contains an example of connecting to non-Java
code using standard input and output.) Currently, different vendors offer
different solutions: Java 1.1 has the
Java Native Interface (JNI),
Netscape has proposed its Java Runtime Interface, and
Microsoft offers J/Direct,
Raw Native Interface (RNI), and
Java/COM
integration.
This fragmentation among different
vendors implies serious drawbacks for the programmer. If a Java application must
call native methods, the programmer might need to implement different versions
of the native methods depending on the platform the application will run on. The
programmer might actually need different versions of the Java code as well as
different Java virtual machines.
Another solution is
CORBA (Common Object Request Broker Architecture), an
integration technology developed by the OMG (Object Management Group, a
non-profit consortium of companies). CORBA is not part of any language, but is a
specification for implementing a common communication bus and services that
allow interoperability among objects implemented in different languages. This
communication bus, called an ORB
(Object Request Broker), is a product implemented by third-party vendors, but it
is not part of the Java language specification.
This appendix gives an overview of
JNI, J/Direct, RNI, Java/COM integration, and CORBA. This is not an in-depth
treatment, and in some cases you’re assumed to have partial knowledge of
the related concepts and techniques. But in the end, you should be able to
compare the different approaches and choose the one that is most appropriate to
the problem you want to solve.
JNI is a fairly rich programming
interface that allows you to call native methods from a Java application. It was
added in Java 1.1, maintaining a certain degree of compatibility with its Java
1.0 equivalent, the native
method interface (NMI). NMI has design characteristics that make it unsuitable
for adoption in all virtual machines. For this reason, future versions of the
language might no longer support NMI, and it will not be covered
here.
Currently, JNI is designed to
interface with native methods written only in C or C++.
Using JNI, your native methods can:
Thus, virtually
everything you can do with classes and objects in ordinary Java you can also do
in native methods.
We’ll start with a simple
example: a Java program that calls a native method, which in turn calls the
Win32 MessageBox( ) API function to display a graphical text box.
This example will also be used later with J/Direct. If your platform is not
Win32, just replace the C header include:
#include <windows.h>
with
#include <stdio.h>
and replace the call to
MessageBox( ) with a call to printf( ).
The first step is to write the Java
code declaring a native method and its arguments:
class ShowMsgBox { public static void main(String [] args) { ShowMsgBox app = new ShowMsgBox(); app.ShowMessage("Generated with JNI"); } private native void ShowMessage(String msg); static { System.loadLibrary("MsgImpl"); } }
The native method declaration is
followed by a static block that calls System.loadLibrary( )
(which you could call at any time, but this style is more appropriate).
System.loadLibrary( ) loads a DLL in memory and links to it. The DLL
must be in your system path or in the directory containing the Java class file.
The file name extension is automatically added by the JVM depending on the
platform.
Now compile your Java source file
and run javah on the resulting .class
file. Javah was present in version 1.0, but since you are using Java 1.1
JNI you must specify the –jni switch:
javah –jni ShowMsgBox
Javah reads the Java class
file and for each native method declaration it generates a function prototype in
a C or C++ header file. Here’s the output: the ShowMsgBox.h source
file (edited slightly to fit into the book):
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ShowMsgBox */ #ifndef _Included_ShowMsgBox #define _Included_ShowMsgBox #ifdef __cplusplus extern "C" { #endif /* * Class: ShowMsgBox * Method: ShowMessage * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_ShowMsgBox_ShowMessage (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
As you can see by the #ifdef
__cplusplus preprocessor directive, this file can be compiled either by a C
or a C++ compiler. The first #include directive includes jni.h, a
header file that, among other things, defines the types that you can see used in
the rest of the file. JNIEXPORT and
JNICALL are macros that expand to match
platform-specific directives; JNIEnv, jobject and jstring
are JNI data type definitions.
JNI imposes a naming convention
(called name mangling) on native methods; this is important, since
it’s part of the mechanism by which the virtual machine links Java calls
to native methods. Basically, all native methods start with the word
“Java,” followed by the name of the class in which the Java native
declaration appears, followed by the name of the Java method; the underscore
character is used as a separator. If the Java native method is overloaded, then
the function signature is appended to the name as well; you can see the native
signature in the comments preceding the prototype. For more information about
name mangling and native method signatures, please refer to the JNI
documentation.
At this point, all you have to do
is write a C or C++ source file that includes the javah-generated header file
and implements the native method, then compile it and generate a dynamic link
library. This part is platform-dependent, and I’ll assume that you know
how to create a DLL. The code below implements the native method by calling a
Win32 API. It is then compiled and linked into a file called MsgImpl.dll
(for “Message Implementation”).
#include <windows.h> #include "ShowMsgBox.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void** lpReserved) { return TRUE; } JNIEXPORT void JNICALL Java_ShowMsgBox_ShowMessage(JNIEnv * jEnv, jobject this, jstring jMsg) { const char * msg; msg = (*jEnv)->GetStringUTFChars(jEnv, jMsg,0); MessageBox(HWND_DESKTOP, msg, "Thinking in Java: JNI", MB_OK | MB_ICONEXCLAMATION); (*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg); }
If you have no interest in Win32,
just skip the MessageBox( ) call; the interesting part is the
surrounding code. The arguments that are passed into the native method are the
gateway back into Java. The first, of type JNIEnv, contains all
the hooks that allow you to call back into the JVM. (We’ll look at this in
the next section.) The second argument has a different meaning depending on the
type of method. For non-static methods like the example above (also
called instance methods), the second argument is the equivalent of the
“this” pointer in C++ and similar to this in Java: it’s
a reference to the object that called the native method. For static
methods, it’s a reference to the Class object where the method is
implemented.
The remaining arguments represent
the Java objects passed into the native method call. Primitives are also passed
in this way, but they come in by value.
In the following sections
we’ll explain this code by looking at how to access and control the JVM
from inside a native method.
JNI functions are those that the
programmer uses to interact with the JVM from inside a native method. As you can
see in the example above, every JNI native method receives a special argument as
its first parameter: the JNIEnv argument, which is a pointer to a special
JNI data structure of type JNIEnv_. One element of the JNI data structure
is a pointer to an array generated by the JVM; each element of this array is a
pointer to a JNI function. The JNI functions can be called from the native
method by dereferencing these pointers (it’s simpler than it sounds).
Every JVM provides its own implementation of the JNI functions, but their
addresses will always be at predefined offsets.
Through the JNIEnv argument,
the programmer has access to a large set of functions. These functions can be
grouped into the following categories:
The number of
JNI functions is quite large and won’t be covered here. Instead,
I’ll show the rationale behind the use of these functions. For more
detailed information, consult your compiler’s JNI
documentation.
If you take a look at the
jni.h header file, you’ll see that inside the #ifdef
__cplusplus preprocessor conditional, the JNIEnv_ structure is
defined as a class when compiled by a C++ compiler. This class contains a number
of inline functions that let you access the JNI functions with an easy and
familiar syntax. For example, the line in the preceding example
(*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg);
can be rewritten as follows in
C++:
jEnv->ReleaseStringUTFChars(jMsg,msg);
You’ll notice that you no
longer need the double dereferencing of the jEnv pointer, and that the
same pointer is no longer passed as the first parameter to the JNI function
call. In the rest of these examples, I’ll use the C++
style.
As an example of accessing a JNI
function, consider the code shown above. Here, the JNIEnv argument
jEnv is used to access a Java String. Java Strings are in
Unicode format, so if you receive one and want to pass it to a non-Unicode
function (printf( ), for example), you must first convert it into
ASCII characters with the JNI function GetStringUTFChars( ). This
function takes a Java String and converts it to UTF-8 characters. (These
are 8 bits wide to hold ASCII values or 16 bits wide to hold Unicode. If the
content of the original string was composed only of ASCII, the resulting string
will be ASCII as well.)
GetStringUTFChars is the
name of one of the fields in the structure that JNIEnv is indirectly
pointing to, and this field in turn is a pointer to a function. To access the
JNI function, we use the traditional C syntax for calling a function though a
pointer. You use the form above to access all of the JNI
functions.
In the previous example we passed a
String to the native method. You can also pass Java objects of your own
creation to a native method. Inside your native method, you can access the
fields and methods of the object that was received.
To pass objects, use the ordinary
Java syntax when declaring the native method. In the example below,
MyJavaClass has one public field and one public method. The
class UseObjects declares a native method that takes an object of class
MyJavaClass. To see if the native method manipulates its argument, the
public field of the argument is set, the native method is called, and
then the value of the public field is printed.
class MyJavaClass { public void divByTwo() { aValue /= 2; } public int aValue; } public class UseObjects { public static void main(String [] args) { UseObjects app = new UseObjects(); MyJavaClass anObj = new MyJavaClass(); anObj.aValue = 2; app.changeObject(anObj); System.out.println("Java: " + anObj.aValue); } private native void changeObject(MyJavaClass obj); static { System.loadLibrary("UseObjImpl"); } }
After compiling the code and
handing the .class file to javah, you can implement the native
method. In the example below, once the field and method ID are obtained, they
are accessed through JNI functions.
JNIEXPORT void JNICALL Java_UseObjects_changeObject( JNIEnv * env, jobject jThis, jobject obj) { jclass cls; jfieldID fid; jmethodID mid; int value; cls = env->GetObjectClass(obj); fid = env->GetFieldID(cls, "aValue", "I"); mid = env->GetMethodID(cls, "divByTwo", "()V"); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); env->SetIntField(obj, fid, 6); env->CallVoidMethod(obj, mid); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); }
The first argument aside, the C++
function receives a jobject, which is the native side of the Java object
reference we pass from the Java code. We simply read aValue, print it
out, change the value, call the object’s divByTwo( ) method,
and print the value out again.
To access a field or method, you
must first obtain its identifier. Appropriate JNI functions take the class
object, the element name, and the signature. These functions return an
identifier that you use to access the element. This approach might seem
convoluted, but your native method has no knowledge of the internal layout of
the Java object. Instead, it must access fields and methods through indexes
returned by the JVM. This allows different JVMs to implement different internal
object layouts with no impact on your native methods.
If you run the Java program,
you’ll see that the object that’s passed from the Java side is
manipulated by your native method. But what exactly is passed? A pointer or a
Java reference? And what is the garbage collector doing during native method
calls?
The garbage
collector continues to operate during native method execution, but it’s
guaranteed that your objects will not be garbage collected during a native
method call. To ensure this, local references are created before, and
destroyed right after, the native method call. Since their lifetime wraps the
call, you know that the objects will be valid throughout the native method
call.
Since these references are created
and subsequently destroyed every time the function is called, you cannot make
local copies in your native methods, in static variables. If you want a
reference that lasts across function invocations, you need a global reference.
Global references are not created by the JVM, but the programmer can make a
global reference out of a local one by calling specific JNI functions. When you
create a global reference, you become responsible for the lifetime of the
referenced object. The global reference (and the object it refers to) will be in
memory until the programmer explicitly frees the reference with the appropriate
JNI function. It’s similar to malloc( ) and
free( ) in C.
With JNI,
Java exceptions can be thrown, caught, printed, and rethrown just as they are
inside a Java program. But it’s up to the programmer to call dedicated JNI
functions to deal with exceptions. Here are the JNI functions for exception
handling:
Among these, you
can’t ignore ExceptionOccurred( ) and
ExceptionClear( ). Most JNI functions can generate exceptions, and
there is no language feature that you can use in place of a Java try block, so
you must call ExceptionOccurred( ) after each JNI function call to
see if an exception was thrown. If you detect an exception, you may choose to
handle it (and possibly rethrow it). You must make certain, however, that the
exception is eventually cleared. This can be done in your function using
ExceptionClear( ) or in some other function if the exception is
rethrown, but it must be done.
You must ensure that the exception
is cleared, because otherwise the results will be unpredictable if you call a
JNI function while an exception is pending. There are few JNI functions that are
safe to call during an exception; among these, of course, are all the exception
handling functions.
Since Java is a multithreaded
language, several threads can call a native method concurrently. (The native
method might be suspended in the middle of its operation when a second thread
calls it.) It’s entirely up to the programmer to guarantee that the native
call is thread-safe, i.e. it does not modify shared data in an unmonitored way.
Basically, you have two options: declare the native method as
synchronized or implement some other strategy within the native method to
ensure correct, concurrent data manipulation.
Also, you should never pass the
JNIEnv pointer across threads, since the internal structure it points to
is allocated on a per-thread basis and contains information that makes sense
only in that particular thread.
The easiest way to implement JNI
native methods is to start writing native method prototypes in a Java class,
compile that class, and run the .class file through javah. But
what if you have a large, pre-existing code base that you want to call from
Java? Renaming all the functions in your DLLs to match the JNI name mangling
convention is not a viable solution. The best approach is to write a wrapper DLL
“outside” your original code base. The Java code calls functions in
this new DLL, which in turn calls your original DLL functions. This solution is
not just a work-around; in most cases you must do this anyway because you must
call JNI functions on the object references before you can use
them.
At the time of this writing,
Microsoft does not support JNI, but provides proprietary support to call
non-Java code. This support is built into the compiler, the Microsoft JVM, and
external tools. The features described in this section will work only if your
program was compiled using the Microsoft Java compiler and run on the Microsoft
Java Virtual Machine. If you plan to distribute your application on the
Internet, or if your Intranet is built on different platforms, this can be a
serious issue.
The Microsoft interface to Win32
code provides three ways to connect to Win32:
I’ll cover
all three techniques in the following sections.
At the time of writing, these
features were tested on the Microsoft SDK for Java 2.0 beta 2, which was
downloaded (with a painful process they call “Active Setup”) from
the Microsoft Web site. The Java SDK is a set of command-line tools, but the
compilation engine can be easily plugged into the Developer Studio environment,
allowing you to use Visual J++ 1.1 to compile Java 1.1
code.
J/Direct is the simplest way to
call functions in a Win32 DLL. It was designed primarily to interface with the
Win32 API, but you can use it to call any other APIs. The ease of use of this
feature is counterbalanced by some limitations and reduced performance (compared
to RNI). But J/Direct has distinct advantages. First, there is no need to write
additional non-Java code, except the code in the DLL you want to call. In other
words, you do not need a wrapper or proxy/stub DLL. Second, function arguments
are automatically converted to and from standard data types. (If you must pass
user-defined data types, J/Direct might not be the way to go.) Third, it’s
simple and straightforward, as the example below shows. In just a few lines,
this example calls the Win32 API function MessageBox( ), which pops
up a little modal window with a title, a message, an optional icon, and a few
buttons.
public class ShowMsgBox { public static void main(String args[]) throws UnsatisfiedLinkError { MessageBox(0, "Created by the MessageBox() Win32 func", "Thinking in Java", 0); } /** @dll.import("USER32") */ private static native int MessageBox(int hwndOwner, String text, String title, int fuStyle); }
Amazingly, this code is all you
need to call a function in a Win32 DLL using J/Direct. The key is the
@dll.import directive before the MessageBox( ) declaration,
at the bottom of the example code. It looks like a comment, but it’s not:
it tells the compiler that the function below the directive is implemented in
the USER32 DLL, and should be called accordingly. All you must do is supply a
prototype that matches the function implementation in the DLL and call the
function. But instead of typing in the Java version of each Win32 API function
that you need, a Microsoft Java package does this for you (I’ll describe
this shortly). For this example to work, the function must be exported by
name by the DLL, but the @dll.import directive can be used to link
by ordinal as well, i.e., you can specify the entry position of the
function in the DLL. I’ll cover the features of the @dll.import
directive later.
An important issue in the process
of linking with non-Java code is the automatic marshaling of the function
parameters. As you can see, the Java declaration of MessageBox( )
takes two String arguments, but the original C implementation takes two
char pointers. The compiler automatically converts the standard data
types for you, following the rules described in a later
section.
Finally, you might have noticed the
UnsatisfiedLinkError exception in the declaration of main( ).
This exception occurs when the linker is unable to resolve the symbol for the
non-Java function at run-time. This happens for a number of reasons: the
.dll file was not found, it is not a valid DLL, or J/Direct is not
supported by your virtual machine. For the DLL to be found, it must be in the
Windows or Windows\System directory, in one of the directories listed in your
PATH environment variable, or in the directory where the .class file is
located. J/Direct is supported in the Microsoft Java compiler version 1.02.4213
or above, and in the Microsoft JVM version 4.79.2164 or above. To get the
compiler version number, run JVC from the command line with no parameters. To
get the JVM version number, locate the icon for msjava.dll, and using the
context menu look at its
properties.
The @dll.import directive,
your one and only way to J/Direct, is quite flexible. It has a number of
modifiers that you can use to customize the way you link to the non-Java code.
It can also be applied to some methods within a class or to a whole class,
meaning that all of the methods you declare in that class are implemented in the
same DLL. Let’s look at these features.
For the @dll.import
directive to work as shown above, the function in the DLL must be exported by
name. However, you might want to use a different name than the original one in
the DLL (aliasing), or the function might be exported by number (i.e. by
ordinal) instead of by name. The example below declares
FinestraDiMessaggio( ) (the Italian equivalent of
“MessageBox”) as an alias to MessageBox( ). As you can
see, the syntax is pretty simple.
public class Aliasing { public static void main(String args[]) throws UnsatisfiedLinkError { FinestraDiMessaggio(0, "Created by the MessageBox() Win32 func", "Thinking in Java", 0); } /** @dll.import("USER32", entrypoint="MessageBox") */ private static native int FinestraDiMessaggio(int hwndOwner, String text, String title, int fuStyle); }
The next example shows how to link
to a function in a DLL that is not exported by name, but by its position inside
of the DLL. The example assumes that there is a DLL named MYMATH somewhere along
your path, and that this DLL contains at position 3 a function that takes two
integers and gives you back the sum.
public class ByOrdinal { public static void main(String args[]) throws UnsatisfiedLinkError { int j=3, k=9; System.out.println("Result of DLL function:" + Add(j,k)); } /** @dll.import("MYMATH", entrypoint = "#3") */ private static native int Add(int op1,int op2); }
You can see the only difference is
the form of the entrypoint argument.
The
@dll.import directive can be applied to an entire
class, meaning that all of the methods in that class are implemented in the same
DLL and with the same linkage attributes. The directive is not inherited by
subclasses; for this reason, and since functions in a DLL are by nature
static functions, a better design approach is to encapsulate the API
functions in a separate class, as shown here:
/** @dll.import("USER32") */ class MyUser32Access { public static native int MessageBox(int hwndOwner, String text, String title, int fuStyle); public native static boolean MessageBeep(int uType); } public class WholeClass { public static void main(String args[]) throws UnsatisfiedLinkError { MyUser32Access.MessageBeep(4); MyUser32Access.MessageBox(0, "Created by the MessageBox() Win32 func", "Thinking in Java", 0); } }
Since the
MessageBeep( ) and MessageBox( ) functions are now
declared as static in a different class, you must call them specifying their
scope. You might think that you must use the approach above to map all of
the Win32 API (functions, constants, and data types) to
Java classes. Fortunately, you don’t have
to.
The Win32 API is fairly big –
on the order of a thousand functions, constants, and data types. Of course, you
do not want to write the Java equivalent of every single Win32 API function.
Microsoft took care of this, distributing a Java package that maps the Win32 API
to Java classes using J/Direct. This package, named
com.ms.win32, is installed in your classpath
during the installation of the Java SDK 2.0 if you select it in the setup
options. The package is made up of large number of Java classes that reproduce
the constants, data structures, and functions of the Win32 API. The three
richest classes are User32.class, Kernel32.class, and
Gdi32.class. These contain the core of the Win32 API. To use them, just
import them in your Java code. The ShowMsgBox example above can be
rewritten using com.ms.win32 as follows (I also took care of the
UnsatisfiedLinkError in a more civilized way):
import com.ms.win32.*; public class UseWin32Package { public static void main(String args[]) { try { User32.MessageBeep( winm.MB_ICONEXCLAMATION); User32.MessageBox(0, "Created by the MessageBox() Win32 func", "Thinking in Java", winm.MB_OKCANCEL | winm.MB_ICONEXCLAMATION); } catch(UnsatisfiedLinkError e) { System.out.println("Can’t link Win32 API"); System.out.println(e); } } }
The package is imported in the
first line. The MessageBeep( ) and MessageBox( )
functions can now be called with no other declarations. In
MessageBeep( ) you can see that importing the package has also
declared the Win32 constants. These constants are defined in a number of Java
interfaces, all named winx (x is the first letter of the constant you want to
use).
At the time of this writing, the
classes in the com.ms.win32 package are still under development, but
usable nonetheless.
Marshaling means converting a
function argument from its native binary representation into some
language-independent format, and then converting this generic representation
into a binary format that is appropriate to the called function. In the example
above, we called the MessageBox( ) function and passed it a couple
of Strings. MessageBox( ) is a C function, and the binary
layout of Java Strings is not the same as C strings, but the arguments
are nonetheless correctly passed. That’s because J/Direct takes care of
converting a Java String into a C string before calling the C code. This
happens with all standard Java types. Below is a table of the implicit
conversions for simple data types:
Java |
C |
byte |
BYTE or CHAR |
short |
SHORT or WORD |
int |
INT, UINT, LONG, ULONG, or
DWORD |
char |
TCHAR |
long |
__int64 |
float |
Float |
double |
Double |
boolean |
BOOL |
String |
LPCTSTR (Allowed as return value
only in ole mode) |
byte[] |
BYTE * |
short[] |
WORD * |
char[] |
TCHAR * |
int[] |
DWORD * |
The list continues, but this gives
you the idea. In most cases, you do not need to worry about converting to and
from simple data types, but things are different when you must pass arguments of
user-defined data types. For example, you might need to pass the address of a
structured, user-defined data type, or you might need to pass a pointer to a raw
memory area. For these situations, there are special compiler directives to mark
a Java class so that it can be passed as a pointer to a structure (the
@dll.struct directive). For details on the use of
these keywords, please refer to the product
documentation.
Some Win32 API functions require a
function pointer as one of the parameters. The Windows API function may then
call the argument function, possibly at a later time when some event occurs.
This technique is called a callback function.
Examples include window procedures and the callbacks you set up during a print
operation (you give the print spooler the address of your callback function so
it can update the status and possibly interrupt printing).
Another example is the
EnumWindows( ) API function that enumerates all top-level windows
currently present in the system. EnumWindows( ) takes a function
pointer, then traverses a list maintained internally by Windows. For every
window in the list, it calls the callback function, passing the window handle as
an argument to the callback.
To do the same thing in Java, you
must use the Callback class in the com.ms.dll package. You inherit
from Callback and override callback( ). This method will
accept only int parameters and will return int or void. The
method signature and implementation depends on the Windows API function
that’s using this callback.
Now all we need to do is create an
instance of this Callback-derived class and pass it as the function
pointer argument to the API function. J/Direct will take care of the
rest.
The example below calls the
EnumWindows( ) Win32 API; the callback( ) method in the
EnumWindowsProc class gets the window handle for each top-level window, obtains
the caption text, and prints it to the console window.
import com.ms.dll.*; import com.ms.win32.*; class EnumWindowsProc extends Callback { public boolean callback(int hwnd, int lparam) { StringBuffer text = new StringBuffer(50); User32.GetWindowText( hwnd, text, text.capacity()+1); if(text.length() != 0) System.out.println(text); return true; // to continue enumeration. } } public class ShowCallback { public static void main(String args[]) throws InterruptedException { boolean ok = User32.EnumWindows( new EnumWindowsProc(), 0); if(!ok) System.err.println("EnumWindows failed."); Thread.currentThread().sleep(3000); } }
There are two more J/Direct
features you can get using modifiers in the @dll.import directive. The
first is simplified access to OLE functions, and the second is the selection of
the ANSI versus Unicode version of API functions. Here is a short description of
the two.
By convention, all OLE functions
return a value of type HRESULT, which is a structured integer value defined by
COM. If you program at the COM level and you want something different returned
from an OLE function, you must pass it a pointer to a memory area that the
function will fill with data. But in Java we don’t have pointers; also,
this style is not exactly elegant. With J/Direct, you can easily call OLE
functions using the ole modifier in the @dll.import directive. A
native method marked as an ole function is automatically translated from
a Java-style method signature, which is where you decide the return type, into a
COM-style function.
The second feature selects between
ANSI and Unicode string handling. Most Win32 API functions that handle strings
come in two versions. For example, if you look at the symbols exported by the
USER32 DLL, you will not find a MessageBox( ) function, but instead
MessageBoxA( ) and MessageBoxW( ) functions, which are
the ANSI and Unicode version, respectively. If you do not specify which version
you want to call in the @dll.import directive, the JVM will try to figure
it out. But this operation takes some time during program execution time that
you can save with the ansi, unicode, or auto
modifiers.
Compared to J/Direct, RNI is a
fairly complex interface to non-Java code, but it’s much more powerful.
RNI is closer to the JVM than J/Direct, and this lets you write much more
efficient code, manipulate Java objects in your native methods, and in general
gives you a much higher degree of integration with the JVM internal
operations.
RNI is conceptually similar to
Sun’s JNI. Because of this, and because the product is not yet completed,
I’ll just point out the major differences. For further information, please
refer to Microsoft’s documentation.
There are several notable
differences between JNI and RNI. Below is the C header file generated by
msjavah, the Microsoft equivalent of Sun’s javah, applied to
the ShowMsgBox Java class file used previously for the JNI
example.
/* DO NOT EDIT - automatically generated by msjavah */ #include <native.h> #pragma warning(disable:4510) #pragma warning(disable:4512) #pragma warning(disable:4610) struct Classjava_lang_String; #define Hjava_lang_String Classjava_lang_String /* Header for class ShowMsgBox */ #ifndef _Included_ShowMsgBox #define _Included_ShowMsgBox #define HShowMsgBox ClassShowMsgBox typedef struct ClassShowMsgBox { #include <pshpack4.h> long MSReserved; #include <poppack.h> } ClassShowMsgBox; #ifdef __cplusplus extern "C" { #endif __declspec(dllexport) void __cdecl ShowMsgBox_ShowMessage (struct HShowMsgBox *, struct Hjava_lang_String *); #ifdef __cplusplus } #endif #endif /* _Included_ShowMsgBox */ #pragma warning(default:4510) #pragma warning(default:4512) #pragma warning(default:4610)
Apart from being less readable,
there are more technical issues disguised in the code, which we’ll
examine.
In RNI, the native method
programmer knows the binary layout of the objects. This allows you to directly
access the information you want; you don’t need to get a field or method
identifier as in JNI. But since not all virtual machines necessarily use the
same binary layout for their objects, the native method above is guaranteed to
run only under the Microsoft JVM.
In JNI, the JNIEnv argument
gives access to a large number of functions to interact with the JVM. In RNI,
the functions for controlling JVM operations are directly available. Some of
them, like the one for handling exceptions, are similar to their JNI
counterparts, but most of the RNI functions have different names and purposes
from those in JNI.
One of the most remarkable
differences between JNI and RNI is the garbage collection model. In JNI, the GC
basically follows the same rules during native method execution that it follows
for the Java code execution. In RNI, the programmer is responsible for starting
and stopping the Garbage Collector during native method activity. By default,
the GC is disabled upon entering the native method; doing so, the programmer can
assume that the objects being used will not be garbage collected during that
time. But if the native method, let’s say, is going to take a long time,
the programmer is free to enable the GC, calling the GCEnable( ) RNI
function.
There is also something similar to
the global handles features – something the programmer can use to be sure
that specific objects will not be garbage collected when the CG is enabled. The
concept is similar but the name is different: in RNI these guys are called
GCFrames.
The fact that RNI is tightly
integrated with the Microsoft JVM is both its strength and its weakness. RNI is
more complex than JNI, but it also gives you a high degree of control of the
internal activities of the JVM, including garbage collection. Also, it is
clearly designed for speed, adopting compromises and techniques that C
programmers are familiar with. But it’s not suitable for JVMs other than
Microsoft’s.
COM (formerly known as OLE) is the
Microsoft Component Object Model, the foundation of all ActiveX technologies.
These include ActiveX Controls, Automation, and ActiveX Documents. But COM is
much more; it’s a specification (and a partial implementation) for
developing component objects that can interoperate using dedicated features of
the operating system. In practice, all of the new software developed for Win32
systems has some relationship with COM – the operating system exposes some
of its features via COM objects. Third-party components can be COM, and you can
create and register your own COM components. In one way or another, if you want
to write Win32 code, you’ll have to deal with COM. Here, I’ll just
recap the fundamentals of COM programming, and I’ll assume that you are
familiar with the concept of a COM server (any COM object that can expose
services to COM clients) and a COM client (a COM object that uses the services
provided by a COM server). This section kept things simple; the tools are
actually much more powerful, and you can use them in a more sophisticated way.
But this requires a deep knowledge of COM, which is beyond the scope of this
appendix. If you’re interested in this powerful but platform-dependent
feature, you should investigate COM and the Microsoft documentation on Java/COM
integration. For more information, Dale Rogerson’s “Inside
COM” (Microsoft Press, 1997) is an excellent book.
Since COM is the architectural
heart of all the new Win32 applications, being able to use, or to expose, COM
services from Java code can be important. The Java/COM integration is no doubt
one of the most interesting features of the Microsoft Java compiler and virtual
machine. Java and COM are so similar in their models that the integration is
conceptually straightforward and technically seamless – there’s
almost no special code to write in order to access COM. Most the details are
handled by the compiler and/or by the virtual machine. The effect is that the
COM objects are seen as ordinary Java objects by the Java programmer, and COM
clients can use COM servers implemented in Java just like any other COM server.
Again, I use the generic term COM, but by extension this means that you can
implement an ActiveX Automation server in Java, or you can use an ActiveX
Control in your Java programs.
The most notable similarities
between Java and COM revolve around the relationship between COM interfaces and
the Java interface keyword. This is a near-perfect match
because:
This tight
mapping between Java and COM not only allows the Java programmer to easily
access COM features, but it also makes Java an interesting language for writing
COM code. COM is language-independent, but the de facto languages for COM
development are C++ and Visual Basic. Compared to Java, C++ is much more
powerful for COM development and generates much more efficient code, but
it’s hard to use. Visual Basic is much easier than Java, but it’s
too far from the underlying operating system, and its object model does not map
very well to COM. Java is an excellent compromise between the
two.
Let’s take a look at some of
the keys points of COM development that you need to know to write Java/COM
clients and servers.
COM is a binary specification for
implementing interoperable objects. For example, COM describes the binary layout
an object should have to be able to call services in another COM object. Since
it’s a description of a binary layout, COM objects can be implemented in
any language that’s able to produce such a layout. Usually the programmer
is freed from these low level details, since the compiler takes care of
generating the correct layout. For example, if you program in C++, most
compilers generate a virtual function table that is COM-compliant. With
languages that do not produce executable code, such as VB and Java, the runtime
takes care of hooking into COM.
The COM Library also supplies a few
basic functions, such as the ones for creating an object or locating a COM class
registered in your system.
The main goals of a component
object model are:
The first
point is exactly what object-oriented programming is about: you have a client
object that makes requests to a server object. In this case, the terms
“client” and “server” are used in a generic way, and not
to refer to some particular hardware configuration. With any object-oriented
language, the first goal is easy to achieve if your application is a monolithic
piece of code that implements both the server object code and the client object
code. If you make changes to the way client and the server objects interface
with each other, you simply compile and link again. When you restart your
application, it uses a new version of the components.
The situation is completely
different when your application is made up of component objects that are not
under your control – you don’t control their source code and they
can evolve separately from your application. This is exactly the case, for
example, when you use a third-party ActiveX Control in your application. The
control is installed in your system, and your application is able, at runtime,
to locate the server code, activate the object, link to it, and use it. Later,
you can install a newer version of the control, and your application should
still be able to run; in the worst case, it should gracefully report an error
condition, such as “Control not found,” without hanging
up.
In these scenarios, your components
are implemented in separate executable code files: DLLs or EXEs. If the server
object is implemented in a separate executable code file, you need a standard,
operating system supplied method to activate these objects. Of course, in your
code you do not want to use the physical name and location of the DLL or EXE,
because these might change; you want some identifier maintained by the operating
system. Also, your application needs a description of the services exposed by
the server. This is exactly what I’ll cover in the next two
sections.
COM uses structured integer
numbers, 128 bits long, to unequivocally identify COM entities registered in the
system. These numbers, called GUIDs (Globally Unique
IDentifiers) can be generated by specific utilities, and are guaranteed to be
unique “in space and in time,” to quote Kraig Brockschmidt. In
space, because the number is generator reads the id of your network card, and in
time because the system date and time are used as well. A GUID can be used to
identify a COM class (in which case it’s called a CLSID) or a COM
interface (IID). The names are different but the concept and the binary
structure are the same. GUIDs are also used in other situations that I will not
cover here.
GUIDs, along with their associated
information, are stored in the
Windows Registry, or
Registration Database. It’s a hierarchical database, built into the
operating system, which holds a great amount of information about the hardware
and software configuration of your system. For COM, the Registry keeps track of
the components installed in your system, such as their CLSIDs, the name and
location of the executable file that implement them, and a lot of other details.
One of these details is the ProgID of the component; a ProgID is conceptually
similar to a GUID in the sense that it identifies a COM component. The
difference is that a GUID is a binary, algorithmically-generated value, whereas
a ProgID is a programmer-defined string value. A ProgID
is associated with a CLSID.
A COM component is said to be
registered in the system when at least its CLSID and its executable file
location are present in the Registry (the ProgID is usually present as well).
Registering and using COM components is exactly what we’ll do in the
following examples.
One of the effects of the Registry
is as a decoupling layer between the client and server objects. The client
activates the server using some information that is stored in the Registry; one
piece of information is the physical location of the server executables. If the
location changes, the information in the Registry is updated accordingly, but
this is transparent to the client, which just uses ProgIDs or CLSIDs. In other
words, the Registry allows for location transparency of the server code. With
the introduction of DCOM (Distributed COM), a server that was running on a local
machine can even be moved to a remote machine on the network, without the client
even noticing it (well, almost...).
Because of COM’s dynamic
linking and the independent evolution of client and server code, the client
always needs to dynamically detect the services that are exposed by the server.
These services are described in a binary, language-independent way (as
interfaces and method signatures) in the type
library. This can be a separate file (usually with the .TLB extension), or a
Win32 resource linked into the executable. At runtime, the client uses the
information in the type library to call functions in the
server.
You can generate a type library by
writing a Microsoft Interface Definition Language (MIDL) source file and
compiling it with the MIDL compiler to generate a .TLB file. MIDL is a language
that describes COM classes, interfaces, and methods. It resembles the OMG/CORBA
IDL in name, syntax, and purpose. The Java programmer has no need to use MIDL,
though. A different Microsoft tool, described later, reads a Java class file and
generates a type library.
COM functions exposed by a server
return a value of the predefined type HRESULT. An
HRESULT is an integer containing three fields. This
allows for multiple failure and success codes, along with additional
information. Because a COM function returns an HRESULT, you cannot use the
return value to hand back ordinary data from the function call. If you must
return data, you pass a pointer to a memory area that the function will fill.
This is known as an out parameter. You don’t need to worry about
this as a Java/COM programmer since the virtual machine takes care of it for
you. This is described in the following
sections.
The Microsoft Java compiler,
Virtual Machine, and tools make life a lot easier for the Java/COM programmer
than it is for the C++/COM programmer. The compiler has special directives and
packages for treating Java classes as COM classes, but in most cases,
you’ll just rely on the Microsoft JVM support for COM, and on a couple of
external tools.
The Microsoft Java Virtual Machine
acts as a bridge between COM and Java objects. If you create a Java object as a
COM server, your object will still be running inside the JVM. The Microsoft JVM
is implemented as a DLL, which exposes COM interfaces to the operating system.
Internally, the JVM maps function calls to these COM interfaces to method calls
in your Java objects. Of course, the JVM must know which Java class file
corresponds to the server executable; it can discover this information because
you previously registered the class file in the Windows Registry using
Javareg, a utility in the Microsoft Java SDK. Javareg reads a Java
class file, generates a corresponding type library and a GUID, and registers the
class in the system. Javareg can be used to
register remote servers as well, for example, servers that run on a different
physical machine.
If you want to write a Java/COM
client, you must go through a different process. A Java/COM client is Java code
that wants to activate and use one of the COM servers registered on your system.
Again, the virtual machine interfaces with the COM server and exposes its
services as methods in a Java class. Another Microsoft tool,
jactivex, reads a type library and generates Java
source files that contain special compiler directives. The generated source
files are part of a package named after the type library you specified. The next
step is to import that package in your COM client Java source
files.
This section shows the process you
will apply to the development of ActiveX Controls, Automation Servers, or any
other COM-compliant server. The following example implements a simple Automation
server that adds integer numbers. You set the value of the addend with
the setAddend( ) method, and each time you call the
sum( ) method the addend is added to the current
result. You retrieve the result with getResult( ) and
reset the values with clear( ). The Java class that implements this
behavior is straightforward:
public class Adder { private int addend; private int result; public void setAddend(int a) { addend = a; } public int getAddend() { return addend; } public int getResult() { return result; } public void sum() { result += addend; } public void clear() { result = 0; addend = 0; } }
To use this Java class as a COM
object, the Javareg tool is applied to the compiled Adder.class
file. This tool has a number of options; in this case we specify the Java class
file name (“Adder”), the ProgID we want to put in the Registry for
this server (“JavaAdder.Adder.1”), and the name we want for the type
library that will be generated (”JavaAdder.tlb”). Since no CLSID is
given, Javareg will generate one; if we call Javareg again on the
same server, the existing CLSID will be used.
javareg /register /class:Adder /progid:JavaAdder.Adder.1 /typelib:JavaAdder.tlb
Javareg also registers the
new server in the Windows Registry. At this point, you must remember to copy
your Adder.class file into the Windows\Java\trustlib directory. For
security reasons, related mostly to the use of COM services by applets, your COM
server will be activated only if it is installed in the trustlib
directory.
You now have a new Automation
server installed on your system. To test it, you need an Automation controller,
and “the” Automation Controller is Visual
Basic (VB). Below, you can see a few lines of VB code. On the VB form, I put a
text box to input the value of the addend, a label to show the result, and two
push buttons to invoke the sum( ) and clear( ) methods.
At the beginning, an object variable named Adder is declared. In the
Form_Load subroutine, executed when the form is first displayed, a new
instance of the Adder automation server is instantiated and the text
fields on the form are initialized. When the user presses the “Sum”
or “Clear” buttons, appropriate methods in the server are
invoked.
Dim Adder As Object Private Sub Form_Load() Set Adder = CreateObject("JavaAdder.Adder.1") Addend.Text = Adder.getAddend Result.Caption = Adder.getResult End Sub Private Sub SumBtn_Click() Adder.setAddend (Addend.Text) Adder.Sum Result.Caption = Adder.getResult End Sub Private Sub ClearBtn_Click() Adder.Clear Addend.Text = Adder.getAddend Result.Caption = Adder.getResult End Sub
Note that this code has no
knowledge that the server was implemented in Java.
When you run this program and the
CreateObject( ) function is called, the Windows Registry is searched
for the specified ProgID. Among the information related to the ProgID is the
name of the Java class file, so in response the Java Virtual Machine is started,
and the Java object instantiated inside the JVM. From then on, the JVM takes
care of the interaction between the client and server
code.
Now let’s jump to the other
side and develop a COM client in Java. This program will call services in a COM
server that’s installed on your system. The example is a client for the
server we implemented in the previous example. While the code will look familiar
to a Java programmer, what happens behind the scenes is quite unusual. This
example uses a server that happens to be written in Java but applies to any
ActiveX Control, ActiveX Automation server, or ActiveX component installed in
your system for which you have a type library.
First, the Jactivex tool is
applied to the server’s type library. Jactivex has a number of
options and switches, but in its basic form it reads a type library and
generates Java source files, which it stores in your
windows/Java/trustlib directory. In the example line below, it is applied
to the type library that was generated for out COM Automation
server.
jactivex /javatlb JavaAdder.tlb
If, after Jactivex has
finished, you take a look at your windows/Java/trustlib directory,
you’ll find a new subdirectory called javaadder that contains the
source files for a new package. This is the Java equivalent of the type library.
These files use compiler directives specific to the Microsoft compiler: the
@com directives. The reason jactivex generated more than one file
is that COM uses more than one entity to describe a COM server (and also because
I did not fine-tune the use of MIDL files and the Java/COM
tools).
The file named Adder.java is
the equivalent of a coclass directive in a MIDL file: it’s the
declaration of a COM class. The other files are the Java equivalent of the COM
interfaces exposed by the server. These interfaces, such as
Adder_DispatchDefault.java, are dispatch interfaces, part of the
mechanism of interaction between an Automation controller and an Automation
server. The Java/COM integration feature also supports the implementation and
use of dual interfaces. IDispatch and dual interfaces are beyond the scope of
this appendix.
Below, you can see the client code.
The first line just imports the package generated by jactivex. Then an
instance of the COM Automation server is created and used, as if it was an
ordinary Java class. Notice the typecast on the line where the COM object is
instantiated. This is consistent with the COM object model. In COM, the
programmer never has a reference to the whole object; instead, the programmer
can only have references to one or more of the interfaces implemented in the
class.
Instantiating a Java object of the
Adder class tells COM to activate the server and to create an instance of this
COM object. But then we must specify which interface we want to use, choosing
among the ones implemented by the server. This is exactly what the typecast
does. The interface used here is the default dispatch interface, the
standard interface that an Automation controller uses to communicate with an
Automation server (for details, see Inside COM, ibid.). Notice how simple
it is to activate the server and select a COM interface:
import javaadder.*; public class JavaClient { public static void main(String [] args) { Adder_DispatchDefault iAdder = (Adder_DispatchDefault) new Adder(); iAdder.setAddend(3); iAdder.sum(); iAdder.sum(); iAdder.sum(); System.out.println(iAdder.getResult()); } }
Now you can compile and run the
code.
The
com.ms.com package defines a number of classes
for COM development. It supports the use of GUIDs – the Variant and
SafeArray Automation types – interfacing with ActiveX Controls at a
deeper level and handling COM exceptions.
I cannot cover all of these topics
here, but I want to point out something about COM exceptions. By convention,
virtually all COM functions return an HRESULT value that tells you if the
function invocation succeeded or not and why. But if you look at the Java method
signature in our server and client code, there no HRESULT. Instead, we use the
function return value to get data back from some functions. The virtual machine
is translating Java-style function calls into COM-style function calls, even for
the return parameter. But what happens inside the virtual machine if one of the
functions you call in the server fails at the COM level? In this case, the JVM
sees that the HRESULT value indicates a failure and generates a native Java
exception of class com.ms.com.ComFailException. In this way, you can
handle COM errors using Java exception handling instead of checking function
return values.
An interesting result of Java/COM
integration is the ActiveX/Beans integration, by which a
Java Bean can be hosted by an ActiveX container such as VB or any Microsoft
Office product, and an ActiveX Control can be hosted by a Beans container such
as Sun’s BeanBox. The Microsoft JVM takes care of the details. An ActiveX
Control is just a COM server exposing predefined, required interfaces. A Bean is
just a Java class that is compliant with a specific programming style. At the
time this was written, however, the integration was not perfect. For example,
the virtual machine is not able to map the JavaBeans event model to the COM
event model. If you want to handle events from a Bean
inside an ActiveX container, the Bean must intercept system events such as mouse
actions via low-level techniques, not the standard JavaBeans delegation event
model.
Apart from this, the ActiveX/Beans
integration is extremely interesting. The concept and tools are exactly the same
as discussed above, so please consult Microsoft’s documentation for more
details.
Native methods face the security
issue. When your Java code calls a native method, you pass control outside of
the virtual machine “sandbox.” The native method has complete access
to the operating system. Of course, this is exactly what you want if you write
native methods, but it is not acceptable for applets, at least not implicitly.
You don’t want an applet, downloaded from a remote Internet server, to be
free to play with the file system and other critical areas of your machine
unless you allow it to do so. To prevent this situation with J/Direct, RNI, and
COM integration, only trusted Java code has permission to make native method
calls. Different conditions must be met depending on the feature the applet is
trying to use. For example, an applet that uses J/Direct must be digitally
signed to indicate full trust. At the time of this writing, not all of these
security mechanisms are implemented (in the Microsoft SDK for Java, beta 2), so
keep an eye on the documentation as new versions become
available.
In large, distributed applications,
your needs might not be satisfied by the preceding approaches. For example, you
might want to interface with legacy datastores, or you might need services from
a server object regardless of its physical location. These situations require
some form of Remote Procedure Call (RPC), and possibly language independence.
This is where CORBA can help.
CORBA is
not a language feature; it’s an integration technology. It’s a
specification that vendors can follow to implement CORBA-compliant integration
products. CORBA is part of the OMG’s effort to define a standard framework
for distributed, language-independent object interoperability.
CORBA supplies the ability to make
remote procedure calls into Java objects and non-Java objects, and to interface
with legacy systems in a location-transparent way. Java adds networking support
and a nice object-oriented language for building graphical and non-graphical
applications. The Java and OMG object model map nicely
to each other; for example, both Java and CORBA implement the interface concept
and a reference object model.
The object interoperability
specification developed by the OMG is commonly referred to as the Object
Management Architecture (OMA). The OMA defines two components: the Core Object
Model and the OMA Reference Architecture. The Core Object Model states the basic
concepts of object, interface, operation, and so on. (CORBA is a refinement of
the Core Object Model.) The OMA Reference Architecture defines an underlying
infrastructure of services and mechanisms that allow objects to interoperate.
The OMA Reference Architecture includes the Object Request Broker (ORB), Object
Services (also known as CORBAservices), and common facilities.
The ORB is the communication bus by
which objects can request services from other objects, regardless of their
physical location. This means that what looks like a method call in the client
code is actually a complex operation. First, a connection with the server object
must exist, and to create a connection the ORB must know where the server
implementation code resides. Once the connection is established, the method
arguments must be marshaled, i.e. converted in a binary stream to be sent across
a network. Other information that must be sent are the server machine name, the
server process, and the identity of the server object inside that process.
Finally, this information is sent through a low-level wire protocol, the
information is decoded on the server side, and the call is executed. The ORB
hides all of this complexity from the programmer and makes the operation almost
as simple as calling a method on local object.
There is no specification for how
an ORB Core should be implemented, but to provide a basic compatibility among
different vendors’ ORBs, the OMG defines a set of services that are
accessible through standard interfaces.
CORBA is designed for language
transparency: a client object can call methods on a server object of different
class, regardless of the language they are implemented with. Of course, the
client object must know the names and signatures of methods that the server
object exposes. This is where IDL comes in. The CORBA IDL is a language-neutral
way to specify data types, attributes, operations, interfaces, and more. The IDL
syntax is similar to the C++ or Java syntax. The following table shows the
correspondence between some of the concepts common to three languages that can
be specified through CORBA IDL:
CORBA IDL |
Java |
C++ |
Module |
Package |
Namespace |
Interface |
Interface |
Pure abstract
class |
Method |
Method |
Member function |
The inheritance concept is
supported as well, using the colon operator as in C++. The programmer writes an
IDL description of the attributes, methods, and interfaces that will be
implemented and used by the server and clients. The IDL is then compiled by a
vendor-provided IDL/Java compiler, which reads the IDL source and generates Java
code.
The IDL compiler is an extremely
useful tool: it doesn’t just generate a Java source equivalent of the IDL,
it also generates the code that will be used to marshal method arguments and to
make remote calls. This code, called the stub and skeleton code, is organized in
multiple Java source files and is usually part of the same Java package.
The naming service is one of the
fundamental CORBA services. A CORBA object is accessed through a reference, a
piece of information that’s not meaningful for the human reader. But
references can be assigned programmer-defined, string names. This operation is
known as stringifying the reference, and one of the OMA components, the
Naming Service, is devoted to performing string-to-object and object-to-string
conversion and mapping. Since the Naming Service acts as a telephone directory
that both servers and clients can consult and manipulate, it runs as a separate
process. Creating an object-to-string mapping is called binding an
object, and removing the mapping is called unbinding. Getting an
object reference passing a string is called resolving the
name.
For example, on startup, a server
application could create a server object, bind the object into the name service,
and then wait for clients to make requests. A client first obtains a server
object reference, resolving the string name, and then can make calls into the
server using the reference.
Again, the Naming Service
specification is part of CORBA, but the application that implements it is
provided by the ORB vendor. The way you get access to the Naming Service
functionality can vary from vendor to vendor.
The code shown here will not be
elaborate because different ORBs have different ways to access CORBA services,
so examples are vendor specific. (The example below uses JavaIDL, a free product
from Sun that comes with a light-weight ORB, a naming service, and a IDL-to-Java
compiler.) In addition, since Java is young and still evolving, not all CORBA
features are present in the various Java/CORBA products.
We want to implement a server,
running on some machine, that can be queried for the exact time. We also want to
implement a client that asks for the exact time. In this case we’ll be
implementing both programs in Java, but we could also use two different
languages (which often happens in real situations).
The first step is to write an IDL
description of the services provided. This is usually done by the server
programmer, who is then free to implement the server in any language in which a
CORBA IDL compiler exists. The IDL file is distributed to the client side
programmer and becomes the bridge between languages.
The example below shows the IDL
description of our exact time server:
module RemoteTime { interface ExactTime { string getTime(); }; };
This is a declaration of the
ExactTime interface inside the RemoteTime namespace. The interface
is made up of one single method the gives back the current time in string
format.
The second step is to compile the
IDL to create the Java stub and skeleton code that we’ll use for
implementing the client and the server. The tool that comes with the JavaIDL
product is idltojava:
idltojava –fserver –fclient RemoteTime.idl
The two flags tell idltojava
to generate code for both the stub and the skeleton. Idltojava generates
a Java package named after the IDL module, RemoteTime, and the generated
Java files are put in the RemoteTime subdirectory.
_ExactTimeImplBase.java is the skeleton that we’ll use to implement
the server object, and _ExactTimeStub.java will be used for the client.
There are Java representations of the IDL interface in ExactTime.java and
a couple of other support files used, for example, to facilitate access to the
naming service operations.
Below you can see the code for the
server side. The server object implementation is in the ExactTimeServer
class. The RemoteTimeServer is the application that creates a server
object, registers it with the ORB, gives a name to the object reference, and
then sits quietly waiting for client requests.
import RemoteTime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*; // Server object implementation class ExactTimeServer extends _ExactTimeImplBase{ public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } } // Remote application implementation public class RemoteTimeServer { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Create the server object and register it: ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Assign a string name to the // object reference (binding): NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ncRef.rebind(path, timeServerObjRef); // Wait for client requests: java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } }
As you can see, implementing the
server object is simple; it’s a regular Java class that inherits from the
skeleton code generated by the IDL compiler. Things get a bit more complicated
when it comes to interacting with the ORB and other CORBA
services.
This is a short description of what
the JavaIDL-related code is doing (primarily ignoring the part of the CORBA code
that is vendor dependent). The first line in main( ) starts up the
ORB, and of course, this is because our server object will need to interact with
it. Right after the ORB initialization, a server object is created. Actually,
the right term would be a transient servant object: an object that
receives requests from clients, and whose lifetime is the same as the process
that creates it. Once the transient servant object is created, it is registered
with the ORB, which means that the ORB knows of its existence and can now
forward requests to it.
Up to this point, all we have is
timeServerObjRef, an object reference that is known only inside the
current server process. The next step will be to assign a stringified name to
this servant object; clients will use that name to locate the servant object. We
accomplish this operation using the Naming Service. First, we need an object
reference to the Naming Service; the call to
resolve_initial_references( ) takes the stringified object reference
of the Naming Service that is “NameService,” in JavaIDL, and returns
an object reference. This is cast to a specific NamingContext reference
using the narrow( ) method. We can use now the naming
services.
To bind the servant object with a
stringified object reference, we first create a NameComponent object,
initialized with “ExactTime,” the name string we want to bind to the
servant object. Then, using the rebind( ) method, the stringified
reference is bound to the object reference. We use rebind( ) to
assign a reference, even if it already exists, whereas bind( )
raises an exception if the reference already exists. A name is made up in CORBA
by a sequence of NameContexts – that’s why we use an array to bind
the name to the object reference.
The servant object is finally ready
for use by clients. At this point, the server process enters a wait state.
Again, this is because it is a transient servant, so its lifetime is confined to
the server process. JavaIDL does not currently support persistent objects
– objects that survive the execution of the process that creates
them.
Now that we have an idea of what
the server code is doing, let’s look at the client code:
import RemoteTime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*; public class RemoteTimeClient { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Get (resolve) the stringified object // reference for the time server: NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Make requests to the server object: String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } }
The first few lines do the same as
they do in the server process: the ORB is initialized and a reference to the
naming service server is resolved. Next, we need an object reference for the
servant object, so we pass the stringified object reference to the
resolve( ) method, and we cast the result into an ExactTime
interface reference using the narrow( ) method. Finally, we call
getTime( ).
Finally we have a server and a
client application ready to interoperate. You’ve seen that both need the
naming service to bind and resolve stringified object references. You must start
the naming service process before running either the server or the client. In
JavaIDL, the naming service is a Java application that comes with the product
package, but it can be different with other products. The JavaIDL naming service
runs inside an instance of the JVM and listens by default to network port
900.
Now you are ready to start your
server and client application (in this order, since our server is transient). If
everything is set up correctly, what you’ll get is a single output line on
the client console window, giving you the current time. Of course, this might be
not very exciting by itself, but you should take one thing into account: even if
they are on the same physical machine, the client and the server application are
running inside different virtual machines and they can communicate via an
underlying integration layer, the ORB and the Naming Service.
This is a simple example, designed
to work without a network, but an ORB is usually configured for location
transparency. When the server and the client are on different machines, the ORB
can resolve remote stringified references using a component known as the
Implementation Repository. Although the Implementation Repository is part
of CORBA, there is almost no specification, so it differs from vendor to
vendor.
As you can see, there is much more
to CORBA than what has been covered here, but you should get the basic idea. If
you want more information about CORBA, the place to start is the OMG Web site,
at http://www.omg.org. There you’ll find
documentation, white papers, proceedings, and references to other CORBA sources
and products.
Java applets can act as CORBA
clients. This way, an applet can access remote information and services exposed
as CORBA objects. But an applet can connect only with the server from which it
was downloaded, so all the CORBA objects the applet interacts with must be on
that server. This is the opposite of what CORBA tries to do: give you complete
location transparency.
This is an issue of network
security. If you’re on an Intranet, one solution is to loosen the security
restrictions on the browser. Or, set up a firewall policy for connecting with
external servers.
Some Java ORB products offer
proprietary solutions to this problem. For example, some implement what is
called HTTP Tunneling, while others have their special firewall
features.
This is too complex a topic to be
covered in an appendix, but it is definitely something you should be aware
of.
You saw that one of the main CORBA
features is RPC support, which allows your local objects to call methods in
remote objects. Of course, there already is a native Java feature that does
exactly the same thing: RMI (see Chapter 15). While RMI makes RPC possible
between Java objects, CORBA makes RPC possible between objects implemented in
any language. It’s a big difference.
However,
RMI can be used to call services on remote, non-Java
code. All you need is some kind of wrapper Java object around the non-Java code
on the server side. The wrapper object connects externally to Java clients via
RMI, and internally connects to the non-Java code using one of the techniques
shown above, such as JNI or J/Direct.
This approach requires you to write
a kind of integration layer, which is exactly what CORBA does for you, but then
you don’t need a third-party
ORB.
What you’ve seen in this
appendix are the most common techniques to call non-Java code from a Java
application. Each technique has its pros and cons, but currently the major
problem is that not all of these features are available on all JVMs, so a Java
program that calls native methods on a specific platform might not work on a
different platform with a different JVM.
Sun’s JNI is flexible,
reasonably simple (although it requires a lot of control over the JVM
internals), powerful, and it’s available on most JVMs, but not all.
Microsoft, at the time of this writing, does not support JNI, but offers
J/Direct, a simple way to call Win32 DLL functions, and RNI, which is designed
for high-performance code but requires a good understanding of the JVM
internals. Microsoft also offers its proprietary Java/COM integration feature,
which is powerful and makes Java an interesting language for writing COM servers
and clients. J/Direct, RNI, and Java/COM integration are supported only by the
Microsoft compiler and JVM.
Finally, we took a look at CORBA,
which allows your Java objects to talk to other objects regardless of their
physical location and implementation language. CORBA is different from the
techniques above because it is not integrated with the Java language, but
instead uses third-party integration technology and requires that you buy a
third-party ORB. CORBA is an interesting and general solution, but it might not
be the best approach if you just want to make calls into the operating
system.