|
Integration Integrating Java into the .NET Environment
Nowadays, a 100% homogeneous development environment is a luxury
Feb. 13, 2004 12:00 AM
If you have never had to integrate Java with code written in other languages, you are a very lucky person. Java is a wonderful language when you are looking for platform portability, but unless you are a big CORBA fan, code written in Java is hard to use from all other programming languages. Codemesh set out four years ago to integrate Java with C++ in a platform-portable way, but when Microsoft introduced .NET, we decided that quite obviously a Java/.NET integration product would also be a very good idea. Our JuggerNET product was the result. You might ask: "Why would I ever want to integrate Java and .NET? Porting is one thing, but mixing them? Java believers will continue to use Java, and .NET believers will continue to use .NET." If only it were that easy. Few enterprises have the luxury of creating 100% homogeneous development and deployment environments anymore. Even if you started out with one development environment, customer pressure or mergers and acquisitions ensure that eventually you will be working in a mixed environment. But even if you are not forced to consider Java/.NET interoperability, there are many good reasons to integrate the two platforms. Consider the following scenarios:
There are a million ways to integrate code written in different languages. I've already mentioned CORBA, but there are also the other serialization-based integration approaches, such as XML-RPC, SOAP, Web services, messaging, sockets, etc. These solutions all have something in common: they either have to publish a remote reference or serialize the object's state and transmit it via a wire protocol in order to make the object available to the other language. The first alternative makes every interaction with the object a remote operation; the second requires potentially large amounts of data serialization and deserialization. Both alternatives imply at least two processes, potentially huge performance penalties, and they usually require external infrastructure (name servers, Web servers, proxy servers, etc.) One true alternative to the over-the-wire solutions is the multitude of cross-compiler projects that attempt to compile one environment's source code/bytecode into the other environment's bytecode. All cross-compiler solutions suffer from the fact that the two sides are not just languages but rather entire platforms. Even if you came up with a perfect Java source–to-IL compiler, you would still require a JRE at runtime because of the native runtime libraries that are being referenced. Reproducing all native libraries would be prohibitively expensive and keep you permanently out of sync with the current version of Java. We decided that neither the serialization nor the cross-compilation solutions had all the characteristics we wanted from a good integration solution. In particular, we had these top design goals:
public static void Main( String[] args ) What's so unusual about this code? Well, unless you noticed that Main is capitalized, you might not have realized that this is a C# rather than a Java code snippet. Making this snippet work as expected takes a lot of work, namely:
Once we have generated the proxy types, we need to launch a JVM from within the CLR. This can be done through the invocation interface, which is part of JNI. "Not so fast!" you say, "JNI is a C-API, so how do we call the C-API from C#?" There are two possible answers: managed C++ or PInvoke. At first we tried managed C++ because we already had a lot of experience with C++ bindings for Java. After getting a prototype to work on one of our development machines, we tested on a different machine and immediately ran into the "deadlock during initialization bug" between the managed and unmanaged runtime libraries. None of the published workarounds were acceptable, so we chose to go the PInvoke route instead. This worked beautifully. The resulting design is illustrated in Figure 1. User-written code references a generated proxy class (a C# type that is a stand-in for a Java type). The proxy class delegates all its work to a runtime assembly written in C#. This managed runtime assembly delegates to an unmanaged runtime library written in C, which in turn delegates to the JVM via the Java Native Interface.
![]() To show you what the process involves, I have taken a method invocation as an example and combined the entire invocation process in Listings 1–3. (All of the code listings referenced in this article can be downloaded from www.sys-con.com/dotnet/sourcec.cfm.) Listing 1 holds all the managed code, Listing 2 holds the PInvoke interface, and Listing 3 holds all the unmanaged code. (All exception- and performance-related details have been omitted for brevity; the entire example is extremely simplified.) This process sounds complicated, but a carefully designed API makes almost all of these function calls perform like regular, in-process method invocations, thereby achieving the (second) design goal of near-native performance. One of the problems we needed to solve was the configuration of the Java runtime environment for the .NET application. Virtually all useful Java applications need to have their classpath configured, and possibly some additional JVM options set before they can execute. The same is of course true for any .NET application that internally executes Java code. We decided that there should be two ways to provide the configuration information: the .NET way via a .config file (see Listing 4) and the programmatic way, as shown in the following code snippet, in which the developer specifies the information in code. Adding the provided XML snippet to your application's configuration file provides all necessary information about the desired Java runtime environment (strictly speaking, the MaximumHeapSize setting is not necessary and is included simply to illustrate that you can control every aspect of the JVM). SunJava2JvmLoader loader = new SunJava2JvmLoader(); Using this code has exactly the same effect as a .config file but it hides the configuration information from the application's user. Notice that both examples use relative paths for the JVMPath and the ClassPath settings. By installing a private JRE as part of your application and using only relative paths in your JVM configuration, you can achieve Xcopy deployability, our fourth design goal. But what about the most important of our design goals: totally natural usage of Java types in .NET? This turned out to be in some ways easier than expected, and in others, harder. .NET has many similarities to Java, plus some additional features that were very helpful in making the two sides work together:
The Java interface:
The generated .NET interface:
The generated .NET class:
The following snippet illustrates the usage of the generated code: Hashtable env = new Hashtable(); The String type introduces another problem. Both Java and .NET have powerful, built-in String types, and we certainly need to allow the programmer to use .NET string literals as function arguments or right-hand-side values in assignments. This requirement generally means that we have to allow the use of any object in places where a string literal is a legal value, because both strings and proxy types need to be allowed. It also means that when we map java.lang.String types into .NET, we don't map them to a String proxy type but rather directly to the System.String type. One of the nastiest problems is related to exception handling. We want the programmer to be able to write the following code: try This means that the proxy exception types have to be derived from the .NETSystem. Exception type; otherwise they cannot be thrown or caught. Because this takes up the sole class inheritance slot, they cannot be derived from the proxy type for java.lang.Object either, thereby neatly breaking the inheritance hierarchy. Figure 2 illustrates this problem. The orange types represent the .NET object hierarchy; the gray types the mapped Java proxy types. The red lines indicate inheritance relationships that should be present but cannot be reproduced because of .NET constraints. This could impose a usage restriction when exceptions are passed as arguments, but it will not usually be a problem.
![]() The trickiest part of the entire integration involves callbacks. We wanted a .NET developer to be able to implement a Java interface in .NET. Why is this necessary? Because many useful APIs in Java are defined by interfaces that are expected to be implemented by the developer. Take JMS as an example: the javax.jms.Message Listener interface needs to be implemented by the developer and then an instance of the implementation type is registered with a Topic or a Queue to receive asynchronous message notifications. We built a mechanism by which developers can simply implement a generated interface in their .NET language of choice and use an instance of that type as a callback object. Listings 5 and 6 illustrate this functionality. Altogether, I believe that we came up with a mapping that allows a developer to use Java from within .NET without having to be too aware of the origin of any given type. Listings 7 and 8 contain real applications that illustrate the usability of the proxy types. Conclusion
|
|||