HomeDigital EditionSearch Dotnet Cd
ASP.NET C# Certification Exams The CLI Data Access Editorials Extending .NET Fundamentals Interoperability Interviews Migrate Mobile .NET Mono .NET Interface Object-Oriented Programming Open Source Optimization Product/Book Reviews Security Source Code UML Visual Studio .NET

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.

Figure 1

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. 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.

Figure 2

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 the Author
Alexander Krapf is president and cofounder of Codemesh, Inc. (www.codemesh.com), a small, four-year-old Massachusetts firm specializing in tools for the integration of programming languages. Codemesh's major products are JunC++ion, a product that makes Java types available to C++ developers; and JuggerNET, a product that makes Java types available to .NET developers. Alex has been developing software since the Apple II days and likes writing his code in object-oriented languages like Java, C#, and C++, often in a mix of all three. alex@codemesh.com



Listing 1: Managed part of a method invocation

object	Context.lookup( string name )
{
  //prepare the arguments
  int       hName = PinvokeHelper.toJavaString( name );

  //call the method
  string    cls = "javax/naming/Context";
  string    mname = "lookup";
  string    sig = "(Ljava/lang/String;)Ljava/lang/Object;";

  int       hResult = PinvokeHelper.callObjectMethod( this.Jobject,          
                        cls, mname, sig, hName );

  //prepare the result
  if( hResult == 0 )
    return null;
  else
    return FrameworkHelper.findBestProxyType( hResult );
}


Listing 2: PInvoke interface defined in PinvokeHelper

[DllImport("unmanagedlib")]
int  toJavaString( [MarshalAs(UnmanagedType.LPWStr)] string name );

[DllImport("unmanagedlib")]
int  callObjectMethod( int theObject,
                       [MarshalAs(UnmanagedType.LPWStr)] string cls,
                       [MarshalAs(UnmanagedType.LPWStr)] string mname, 
                       [MarshalAs(UnmanagedType.LPWStr)] string sig,
                       int arg );


Listing 3: Unmanaged part of a method invocation

extern "C" __declspec(dllexport) int __stdcall callObjectMethod(
                                                 int theObject,
                                                 LPWSTR cls,
                                                 LPWSTR mname,
                                                 LPWSTR sig,
                                                 int arg )
{
  char *	utfClassName = toUtf8( cls ); //utility function
  char *    utfMethodname = toUtf8( mname );
  char *    utfSig = toUtf8( sig );
  JNIEnv *  env = launchOrAttach(); //utility function
  jclass    clazz = env->FindClass( utfClassName );
  int	      methodID = env->GetMethodID(clazz, utfMethodname, utfSig );
  jobject   result = 0;

  result = env->CallObjectMethod( (jobject)theObject, methodID, arg );

  return result;
}

Listing 4: Configuration File XML snippet

<Codemesh>
	<JuggerNET>
		<Loader name="DefaultConfig" />
		<DefaultConfig.JvmSettings>
			<add key="Type" value="Codemesh.JuggerNET.SunJava2JvmLoader" />
			<add key="JVMPath" value="..\jre\bin\client\jvm.dll" />
			<add key="ClassPath" value="..\lib\myapp.jar" />
			<add key="MaximumHeapSize" value="256" />
		</DefaultConfig.JvmSettings>
	</JuggerNET>
</Codemesh>


Listing 5: MessageListener implementation

public class MyMessageListener : javax.jms.MessageListener
{
  public void	onMessage( Message m )
  {
    if( m is TextMessage )
	Console.WriteLine( "The message is {0}", m.getText() );
    Else
      Console.WriteLine( "Sorry, we only handle TextMessages" );
  }
}

Listing 6: MessageListener usage

public static void Main( string[] )
{
  …

  try
  {
    QueueConnectionFactory qcf = ctx.lookup( "QCF" );
    QueueConnection        qc = qcf.createQueueConnection();
    QueueSession           qs = qc.createQueueSession( false,  
                                       SessionImpl.AUTO_ACKNOWLEDGE );
    Queue                  q = ctx.lookup( "testqueue" );
    QueueReceiver          qrcv = qs.createReceiver( q );

    qrcv.setMessageListener( new MyMessageListener() );

    qc.start();

    …
  }
  catch( NamingException ne )
  {
  }
  catch( JMSException jmse )
  {
  }
  catch( Exception e )
  {
  }
}

Listing 7: Swing GUI from .NET

using System;
using Codemesh.JuggerNET;
using javax.swing;

public class SwingGUI
{
  public static void Main( string[] args )
  {
    string color = JOptionPane.showInputDialog( "Your color pick?" );

    Console.WriteLine( "Your favorite color is {0}", color );
  }
}

Listing 8: EJB access from .NET

using System;
using Codemesh.JuggerNET;
using com.codemesh.ejbtest;
using java.lang;
using java.util;
using javax.naming;

public class EJB
{
  public static void Main( string[] args )
  {
    try
    {
      InitialContext      ictx = new InitialContext();
      MyEJBHome           home = (MyEJBHome)ictx.lookup( "myejb" );

      if( home != null )
      {
        Date              date = home.getDeliveryDate();

        Console.WriteLine( "The delivery date is {0}", date );
      }
      else
        Console.WriteLine( "Couldn't find 'myejb'" );
    }
    catch( Exception e )
    {
      Console.WriteLine( "StackTrace: {0}", e.StackTrace );
      Console.WriteLine( "Message: {0}", e.Message );
    }
  }
}

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.

  E-mail: info@sys-con.com