devxlogo

Hosting .NET Controls in Java

Hosting .NET Controls in Java

any organizations have applications already written in Java; but applications often aren’t static?they need constant attention and modifications to remain productive. When it’s time to upgrade, rather than rewriting the base functionality underlying these applications, it’s often more feasible to extend them by taking advantage of the rich new user interfaces and technologies present in Microsoft .NET. You can use the techniques discussed in this article to host .NET Controls in Java at the user interface level.

Microsoft .NET and Java are two powerful frameworks that provide a rich set of classes. While either of these frameworks can solve many of the same problems, each has their advantages and disadvantages. When you have existing, tested components, you may find it beneficial to use both .NET and Java in your application, perhaps to support or extend legacy applications that have already been written in Java but adding value using the .NET Framework.

By using the Component Object Model (COM) interoperability services provided by the .NET Framework and the Java Native Interfaces (JNI) present in Java, you can host .NET controls in your Java application.

In this article, you’ll build and host a control built with Microsoft C# in a Java application. Figure 1 shows the completed control hosted in a Java app.

Designing the Java Container Control
You need a Java control to host the .NET control. Both AWT and Swing controls provide a window handle, or HWND, to which you can attach a Windows control. That control can be a simple java.awt.Canvas to which the Java Virtual Machine (JVM) provides an HWND.

Because you’ll be using JNI to attach the .NET control to the Java control’s HWND, you need to add the following line into the class’s static constructor:

   public class COMControl   {      static      {         System.loadLibrary("JNIBridge");      }      // See sample code for implementation.   }

That line tells the JVM to load the JNIBridge.dll (or JNIBridge.so library in BSD or Linux) when it loads the class. The system uses the JNIBridge library whenever your code calls any native methods in the class. The library must exist in a directory specified in the PATH environment variable as with most libraries. The JVM will not, however, look for libraries in the current working directory. You’ll see a workaround for that later in this article.

Declaring the native methods is trivial. They are simply declarations that use the native Java keyword. Later, you’ll create a C header file from these definitions. Two native methods are required to initialize and destroy the .NET control. The JVM calls the addNotify and removeNotify methods to allow native code to attach controls to a window handle and to destroy those controls, so the native methods will be called from the implementation of these two methods. The native methods are declared below, and the two methods are overridden that call these two native methods:

   public class COMControl   {      // See sample code for implementation.      public void addNotify()      {         super.addNotify();         initialize();      }      public void removeNotify()      {         destroy();         super.removeNotify();      }      private native void initialize();      private native void destroy();   }

Other native methods are similarly called by overridden methods.

Compile the class using javac.exe from the Java Development Kit (JDK), for example:

   javac.exe *.java

Creating the C Header for Native Methods
After the Java class is compiled, you must create a C header file that declares the native functions to be implemented. The javah.exe application makes this procedure easy by reading the class information and automatically writing a C header file using the specially named functions for each declared native method:

   javah.exe -jni -o JNIBridge.h COMControl

The preceding code line creates functions in the form Java__. You can find out more in the JNI tutorial. For example, here’s the generated COMControl.initialize method:

   JNIEXPORT void JNICALL Java_COMControl_initialize(JNIEnv *,       jobject);

The JNIEnv parameter references the Java environment and you can use it to access classes, methods, and properties exposed by the JVM. The second parameter references the object itself?an instance of COMControl class in this case.

Implementing the Native Functions
Implementing the native functions in your C/C++ source code typically depends on your .NET control and how you exposed it as a COM component. Two of the native functions?initialize and destroy?are required to initialize the component and attach it to the HWND of the Java control.

Create a new C/C++ project. You’ll need to #include the jni.h header file, so make sure you include both the include and include/win32 directories from the JDK installation directory. Import the jni.h header and the JNIBridge.h header from this sample in a C source file.

To begin implementing the native functions, copy the declarations from the header file you generated into the source file. Add parameter names and curly braces to each, setting up each function for a definition. You’ll define the initialize function first (see Listing 1 or the sample source for the complete implementation).

This initialize function gets the HWND from the Canvas. This window handle is provided by the JVM. After getting the HWND, execution is passed to a thread so that the method can return and allow the Java control to continuing initializing and handling events. The new thread creates the .NET control you exposed as a COM component and attaches it to the HWND. It then initializes properties of the control when necessary and starts the message loop to process messages and to keep the control running (see Listing 2 or the sample source for implementation).

The destroy function destroys the COM component window resources, decrements the reference count, and deletes the pointer. This makes sure that the .NET control is destroyed and memory is reclaimed (see Listing 3 or sample source for implementation).

Implementing the getUsername native function requires that you get a string from the .NET control and return it to the Java control:

   JNIEXPORT jstring JNICALL Java_COMControl_getUsername     (JNIEnv *env, jobject obj)   {      HRESULT hr = S_OK;      CComBSTR username;      jstring jstr;         if (g_spControl != NULL)      {         username.Empty();         hr = g_spControl->get_Username(&username);            if (SUCCEEDED(hr))         {            jstr = env->NewStringUTF(CW2A(username.m_str));            return jstr;         }      }         return NULL;   }

The Common Language Runtime (CLR)?the runtime for the .NET Framework?creates a COM-Callable Wrapper (CCW) that marshals System.String values as BSTR values. The Microsoft Active Template Library (ATL) provides a handy wrapper class, CComBSTR, to make this easier and to automatically free space allocated to the BSTR. You should first make sure that you have a valid reference to the .NET control. Then call get_Username?the get accessor of the Username property?passing a reference to the CComBSTR for the username. If the method returns a successful HRESULT code, use the NewStringUTF method of the JNIEnv object to convert the Unicode string to a jstring and return it.

You’ll encounter an interesting problem when implementing the getBirthday native function: there is no intrinsic type for the java.util.Date class provided by JNI. You must create an object of type Date in the native implementation as shown in the following code fragment:

   // Get the java.util.Date class and its ctor.   cls = env->FindClass("java/util/Date");   mid = env->GetMethodID(cls, "", "(J)V");      if (mid != 0)   {      // Allocate a new Date object.      jdate = env->AllocObject(cls);         // Adjust for differences in whole days.      ldate = ((LONG64)date - 25568) * 86400000;         // Instantiate the Date object.      env->CallVoidMethod(jdate, mid, ldate);         // Return the Date object.      return jdate;   }

Use the FindClass and GetMethodID methods from the JNIEnv object to get the jclass reference and find the method ID for the appropriate constructor. The JNI specifications state that you should use the string to find the constructor. The third parameter, “(J)V”, dictates that the constructor should take a long value?a 64-bit integer in Java?and should return void, as all constructors do.

Another problem arises when dealing with dates. The DateTime structure in .NET stores the number of ticks?or 100 nanoseconds?from January 1, 0001 A.D. using the local time zone, as described in the NET Framework SDK documentation.

The CLR marshals the DateTime structure as a DATE, which is a typedef for a double. The DATE data type documentation describes that the CLR stores time as the number of milliseconds since December 30, 1899, also using the local time zone.

Unfortunately, Java stores the number of milliseconds since January 1, 1970 in Universal Coordinate Time (UTC). Not only do you have to worry about adjusting the base time, you must also take the time zone difference into account. The java.util.Calendar class can assist with the time zone problem.

The size and location methods in the Java class are straight-forward. Any method calls to change the position or size of the Java COMControl class will call the native functions which marshal the values to the .NET control. All three environments use 32-bit integers to represent the top and left coordinates, and the width and height of the control.

Assigning the background and foreground can be difficult, but there is a workaround to simplify your implementation. Both environments use a structure to hold color values similarly, but each uses a slightly different structure. Marshaling these two types would add complexity to your code. To make marshaling easier, the Java control instead passes the RGB value of a java.awt.Color object as a long, which is then marshaled as an OLE_COLOR to the .NET control?which converts the value to a System.Drawing.Color structure.

Finish implementing your native functions. Just remember that passing complex types can lead to marshaling problems so try to use basic types as much as possible. Understanding the marshaling services provided by the .NET Framework in the System.Runtime.InteropServices namespace and understanding the JNI specifications can help solve these problems if you must use complex types.

When you finish implementing the native functions, configure the project to link against the jawt.lib static library. I’ve done this for you in the sample project. You should make sure to add the lib directory in the JDK to your LIB environment variable. Compile the project.

Deploying and Running the Solution
Now that you’ve completed the code, you must deploy and test it in a production environment. The Java Runtime Environment (JRE) and the .NET Framework are required, of course, as well as the C/C++ runtime against which your JNI library was linked.

You must register the .NET assembly so that the path to the assembly can be resolved, or installed the assembly into the GAC. This procedure was discussed earlier in the section “Designing the .NET Control.”.

Install the JNI library into a directory listed in the target machine’s PATH environment variable. If you prefer to keep all the application files together in a single directory, you should either add the current working directory (represented by “.”) or your application installation directory to the PATH environment variable, or change the lookup paths in the command line shown below used to start the Java application:

   java.exe -Djava.library.path=".;%PATH%" -classpath       .;%CLASSPATH% App

The command line ensures that the current working directory is included in the search path for any native libraries required by the JVM. Adding the current working directory to the CLASSPATH also ensures that the the JVM can find the Java application class files. See the Sun Java Web site for more information about deploying and executing Java classes.

Setting up your C/C++ project correctly allows you to compile and debug your native C/C++ code and your .NET code in whatever language you use. The sample project is configured to run the Java application using the command line above. You could instead use the jdb.exe application to debug the Java application at the same time.

When you have completed your testing plan, you should recompile your entire source in a release build and deploy the solution in the same way described above.

Although the process to host .NET controls in Java isn’t trivial, being able to use the rich user interfaces and technologies in .NET within your Java application is advantageous. You can reuse your .NET controls in both .NET and Java applications and can potentially reduce the development time of producing and maintaining separate class libraries in different languages.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist

©2024 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.