Welcome!

.NET Authors: Bruce Armstrong, Marek Miesiac, Jason Dolinger, Yeshim Deniz, Liz McMillan

Related Topics: .NET

.NET: Article

Integrating Java into the .NET Environment

Nowadays, a 100% homogeneous development environment is a luxury

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:

  • You have invested heavily in enterprise Java (J2EE), but you also have internal groups or customers who write their applications in C# or VB. How are their .NET applications going to access your JMS or EJB infrastructure on the Java side?
  • You have spent years developing Java applications that heavily leverage the Java runtime libraries and maybe even third-party Java libraries. Now you have customers who are requesting access to your APIs from .NET. How do you solve this problem without porting or reimplementing all the supporting code?
  • You have Java code that you would like to use from .NET applications (for example Word or Excel). How can you do this?
Very possibly none of these scenarios strike a chord with you, but you will probably admit that there isn't a really good reason why Java should not be part of the .NET environment; after all it already supports C#, VB, C++, Eiffel, etc., so why not Java? You might also admit that there are many cases in which porting from Java to .NET is not desirable because the cost is too high and you would either abandon your Java users or end up maintaining two code bases.

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:

  • Totally natural usage of Java types in .NET
  • Near-native performance
  • Ability to work with any version of the platform (.NET and Java)
  • Xcopy deployment of the integrated solution without involving complicated server processes
In short, we wanted to be able to write code like this:

public static void Main( String[] args )
{
InitialContext ictx = new InitialContext();
TopicConnectionFactory tqf =
(TopicConnectionFactory)ictx.lookup( "TCF" );
...
}

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:

  1. Creating .NET types that resemble the underlying Java types so closely that the developer isn't surprised by differences in usage. This is achieved with the help of a code generator.
  2. Launching a correctly configured JVM within the CLR process.
  3. Delegating each .NET proxy– type usage to the corresponding Java type usage.
First, the developer needs to have some .NET types to code against. These types are provided by a code generator that has both a graphical and a command-line user interface. At development time, the programmer points the code generator at the compiled Java classes he or she is interested in and the code generator generates C# source code that mirrors the Java classes as much as possible. The generated C# classes can be used directly in a C# project, or they can be compiled into an assembly for use by other .NET languages. You will probably assume that we used Java reflection for the purpose of analyzing the Java types, but it turns out that reflection is unsuitable for this purpose: You cannot reflect on different versions of system types. If the code generator is running in a JRE 1.4.2, 1.4.2 is going to be the only version that the built-in types can reflect on. Our third design goal was JVM independence, which meant we had to directly analyze the bytecode of the provided Java classes.

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();

loader.JVMPath = @"..\jre\bin\client\jvm.dll";
loader.ClassPath = @"..\lib\myapp.jar";
loader.MaximumHeapSize = 256;

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:

  • Java interfaces map to .NET interfaces. This works very well, with one exception, which I will discuss a little later.
  • Java classes map to .NET classes.
  • Java constructors map to .NET constructors.
  • Java methods map to .NET methods.
  • Java fields map to .NET properties. Every operation on the property is translated to a corresponding JNI operation on a Java field.
If this sounds a little too simplistic, it's because it is a little too simplistic. Let's take a closer look at interfaces, for example: in Java, an interface may and often does contain static field declarations (the Java interface is used for name scoping). In .NET, an interface may not contain any static members if it is to be considered CLS compliant. So what do we do with the static Java fields? We answered that question by generating an additional .NET class, which implements the generated .NET interface and contains all static member declarations (plus some additional services). This class must not have the same name as the interface, so the user has to refer to the static fields through a differently named type. This solution isn't perfect, but it is probably an acceptable workaround for most people. The following code snippets illustrate this design.

The Java interface:

public interface Context
{
public static final String PROVIDER_URL = "ProviderURL";

public Object lookup( String name );
};

The generated .NET interface:

public interface Context
{
public object lookup( string name );
};

The generated .NET class:

public class ContextImpl : Context
{
public static string PROVIDER_URL
{
get { return ...; }
}

public object lookup( string name )
{
return ...;
}
};

The following snippet illustrates the usage of the generated code:

Hashtable env = new Hashtable();
env.put( ContextImpl.PROVIDER_URL, "test" );

Context ctx = new InitialContext( env );
object temp = ctx.lookup( "myLookupName" );

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
{
object o = ctx.lookup( null );
}
catch( NullPointerException npe )
{
Console.WriteLine( npe.Message );
}
catch( Throwable t )
{
Console.WriteLine( t.Message );
}

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

  • If you don't need distributed computing, don't use XML RPC, Web services, or CORBA for language integration projects. All are complete overkill and will introduce as many problems as solutions.
  • Consider all aspects of the integration problem at hand. While sockets-based integration can be simple, it is also usually extremely limiting and you often end up implementing dual type systems and a protocol on top of the sockets solution just to deal with failure modes.
  • Java and .NET are platforms, not just languages. Integrating the two is much harder than simply translating bytecode or writing a wrapper type for one class.
  • I really like the idea of the CLR and I hope that you will find this technology useful if you are one of the unlucky who have to mix Java and .NET. Hopefully, you can even have some fun mixing and matching.

About Alexander R. Krapf

Krapf has over 15 years experience in software engineering, product development, and project management in the United States and Europe. In addition to founding and managing CodeMesh, Krapf has worked for IBM, Thomson Financial Services, Hitachi, Veeder-Root, and Document Directions, Inc. He has been extensively involved in a variety of complex product development efforts using his in-depth understanding of .NET, C++ and Java.

Krapf has been published in technology journals and been a speaker at a variety of industry conferences. He received a Bachelor of Science degree in Electrical Engineering from the University of Stuttgart, Germany, and can be reached at alex@codemesh.com.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.