Java .Net interoperability


Java .Net interoperability

Have you ever felt the urge and need to connect your Java program to a particular .Net assembly? And more specifically without the hassle of COM bridges, web services, .Net remoting or other service type components. I came across a project in which a simple Java program needed to be connected to an existing .Net assembly (btw written in C#). Everything had to be as simple as possible.
On the Internet a couple of articles were helpful: Espresso, C# method calls within Java Program and Calling a .NET Method from Java via JNI. However it didn’t quite work the way we liked so the ulimate solution came (it so often happens) from Jelle Hissink (http://www.codewise.nl)

The solution is based on:

  • Use JNI for the communication between the Java VM and the .Net CLR
  • Intercept the call to the name resolver and resolve the call to the C# class with your own method

Graphically this looks like:

Let’s work our way from the initial Java call to the final C# implementation.

Java

In Java we have the simple program:

class HelloWorld {
   public static void main(String[] args)
  {
      DotNetBridge dotNetBridge = new DotNetBridge();
      String getString = dotNetBridge.GetString();
      System.out.println("GetString() returned:   " + getString);

      String newString = "Java2.Net world";
      dotNetBridge.SetString(newString);
	getString = dotNetBridge.GetString();
      System.out.println("GetString() returned:   " + getString);
   }
}

Nothing special, just straightforward java code.
However in addition to this code we define an extra class with the DotNetBridge code on the java side. This is needed to kick start the bridging.

public class DotNetBridge
{
  private native void Initialize(String localPath);
  public native String GetString();
  public native void SetString(String newString);

  static
  {
    String path = DotNetBridge.class.getProtectionDomain().getCodeSource().getLocation().getPath();
    int idx = path.lastIndexOf("/");
    if (idx >= 0) {
      path = path.substring(0, idx);
    }
    if (path.startsWith("/")) {
      path = path.substring(1);
    }

    System.load(path + "/DotNetBridge.dll");
    DotNetBridge dotNetBridge = new DotNetBridge();
    dotNetBridge.Initialize(path);
  }
}

In a few words what does it do? It is a class with native methods telling the java compiler that it can find the interface of the methods in this class but the implementation is to be found in a native Windows dll file. On top of that when to class is instantiated for the first time the static part will ensure that the Initialize method is called in which the full path of the dll file with the implementation is passed. Be a little more patient, later on we’ll see why we need this.

DotNetBridge

We now need a simple interface from the Java to the .Net world in which strings can be passed in and out. For this purpose we create a managed C++ project in Visual Studio of the name DotNetBridge. The project consists of:

  • A C++ header file with the definition of the bridge interface
  • A C++ file with the implementation of the bridge
  • A reference to the .Net component with the implementation of the actual HelloWorld code

The header file DotNetBridge.h makes use of JNI (ofcourse) to define the interface of the bridge:

#include "jni.h"

JNIEXPORT void JNICALL Java_DotNetBridge_Initialize(JNIEnv *env, jobject thisobject, jstring localPath);
JNIEXPORT jstring JNICALL Java_DotNetBridge_GetString(JNIEnv *env, jobject thisobject);
JNIEXPORT void JNICALL Java_DotNetBridge_SetString(JNIEnv *env, jobject thisobject, jstring newString);

I’ll not dive into the details of JNI too much (see JNI Primer) but this is the most minimal definition. There are tools to generate this type of definitions (javah –jni) but we leave those out of the equation today. For now watch the prefix Java_DotNetBridge_ which are needed in case of JNI. Also the env and thisobject parameters are specific for each JNI call.

The implementation in C++ has become much more easily with the new .Net framework primitives for managed C++. The two main methods on the interface are the get and set string methods, their implementation looks like:

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace HelloWorldDotNet;

#include 

JNIEXPORT jstring JNICALL
Java_DotNetBridge_GetString (JNIEnv *env, jobject thisobject)
{
    IHelloWorld ^support = gcnew HelloWorld();

    // Get a managed string.
    String ^managedString = support->GetString();

    std::string str = managedToString(managedString);

    // Create the java string...
    jstring result = env->NewStringUTF(str.c_str());

    return result;
}

JNIEXPORT void JNICALL
Java_DotNetBridge_SetString (JNIEnv *env, jobject thisobject,
								jstring newString)
{
    IHelloWorld ^support = gcnew HelloWorld();

    std::wstring newWString = jstringToWString(env, newString);

    String ^ managedString = gcnew String(newWString.c_str());

    support->SetString(managedString);
}

To enable all this we do need some extra string management functions which can translate the java formatted strings to .Net strings and vice versa. Also the transition from the managed to unmanaged environment has its effect on the string management as can be seen from:

std::wstring jstringToWString(JNIEnv *env, jstring jstr)
{
  jboolean iscopy;
  const jchar *strPtr = env->GetStringChars(jstr, &iscopy);

  const jchar *p = strPtr;
  std::wstring result;
  while (*p)
  {
    result += *(p++);
  }

  env->ReleaseStringChars(jstr, strPtr);

  return result;
}

std::string managedToString(String ^mstr)
{
  // Marshal the managed string to unmanaged memory.
  IntPtr hGlobal = Marshal::StringToHGlobalAnsi(mstr);

  char *stringPointer = static_cast(hGlobal.ToPointer());

  // Create the java string...
  std::string result = stringPointer;

  // Always free the unmanaged string.
  Marshal::FreeHGlobal(hGlobal);

  return result;
}

Finally, as said earlier, we need to kick start the whole mechanism. Hence the managed C++ code has currently no idea where to find an implementation of the HelloWorld class in the call to: IHelloWorld ^support = gcnew HelloWorld();
By the way, the import of the namespace has sneaked into the C++ code quite silently with: using namespace HelloWorldDotNet; Without setting a reference to the .Net assembly in the projects’ properties the compiler will not be able to compile this C++ class. Setting such a reference can be done with:

In the Java implementation we have seen that each instantiation of the java bridge class results in a call to the initialize function of the DotNetBridge component. Finally it is time to dive into the details of its implementation. We could have done without it if we deploy all dlls to the java executable directories. From a administration perspective this is not very neat. System administrators normally require that a solution is delivered to one location and the software should take care of its own path settings.

In essence the initialize function of the DotNetBridge is always called once and exactly once with the full path specification of the actual executable directory. So put all dlls into this directory and intercept the name resolver. Because if the runtime tries to load the assembly with the implementation of the HelloWorld class it should look into one specific dll to find this implementation. So this will happen:

  • On the initialize the directory path of the HelloWorld.dll is stored in a static variable
  • The AssemblyResolve event is augmented with its own handler
  • In the handler the directory path is used to load the HelloWorld class from the correct dll
std::wstring assemblyLoadPath;

System::Reflection::Assembly ^ MyResolveEventHandler( Object^ sender, ResolveEventArgs^ args )
{
  String ^name = args->Name;
  int idx = name->IndexOf(L',');
  if (idx >= 0) {
    name = name->Substring(0, idx);
  }
  name = String::Concat(name, gcnew String(L".dll"));

  String ^basePath = gcnew String(assemblyLoadPath.c_str());
  name = System::IO::Path::Combine(basePath, name);

  System::Reflection::Assembly ^ result = System::Reflection::Assembly::LoadFile(name);

  return result;
}

JNIEXPORT void JNICALL Java_DotNetBridge_Initialize (JNIEnv *env, jobject thisobject, jstring localPath)
{
    assemblyLoadPath = jstringToWString(env, localPath);

    AppDomain ^currentDomain = AppDomain::CurrentDomain;
    currentDomain->AssemblyResolve += gcnew ResolveEventHandler( MyResolveEventHandler );
}

This will do the trick on the .Net bridge side. The final implementation of the C# code is straightforward.

C# code

It is a C# class library project. It consists of two classes: the interface and its implementation. The code of the interface definition looks like:

using System;

namespace HelloWorldDotNet
{
    public interface IHelloWorld
    {
        void SetString(string stringValue);
        string GetString();
    }
}

Finally its implementation can be anything. In this case it is simple as:

using System;

namespace HelloWorldDotNet
{
    public class HelloWorld : IHelloWorld
    {
        private static string lastString = "brave new world";
        public void SetString(string stringValue)
        {
            lastString = stringValue;
        }
        public string GetString()
        {
            return String.Format("Hello {0}", lastString);
        }
    }
}

Finally

On purpose, in the code examples details such as exception handling and comments were left out. If you’re interested you can download all the code (see below). On the java side DOS command scripts are used to compile and run the example. You’ll find them easy and straightforward. The C++ and C# code are organized in two separate .Net projects. They were made with Visual Studio 2008. Either migrate to Studio 2008 or use the source code files to build a solution yourself.
If you can’t find your way through all the source files, this is the rough organization:

These are the main steps, with their correct order, to get it to work:

  • Compile the HelloWorld C# project
  • From the DotNetBridge project set the reference to the HelloWorld.dll created in the previous step
  • (possibly) in the DotNetBridge project set the include directories to include the path to the correct JVM include directories (otherwise the jni.h can not be found)
  • Compile the DotNetBridge project
  • Change the compile.cmd and run.cmd scripts in the Java directory to reflect the correct paths.
  • Run compile.cmd
  • Run run.cmd
  • If unchanged you should see something like:





    When you want a full download of all the code in this sample, click here.

    And last but not least: have fun!

, , , , , , , ,

  1. #1 door Pavel Savara om 18 april 2010

    try http://jni4net.sf.net, opensource project which solves the same thing generic way and without C++

  2. #2 door LordChariot om 7 september 2010

    I need some advice on the best way to approach this task:

    I have a vendor-supplied JNI DLL that designed to be used by a Java program. This JNI SDK contains functions I need access to via an application in C#. I do not need to access any java components that call this JNI, I only need to get to the API the JNI provides, so I don’t need to bridge back into the java.

    I have the entry points and definitions of how a Java program would access these functions, but need to figure out how to get to them from C#.

    I’ve been trying create an interop wrapper class to be called from C#, but it’s providing a challenge with how parameters are passed, and frankly, I’m not that good.

    Would this DotNetBridge help in accessing only the methods exposed in the JNI.dll without needing the JVM?

  3. #3 door Summer Davis om 10 mei 2011

    Hi!

    This is a great idea! I have always want to upgrade my Java programs. I will definitely try this out. Thanks for giving a detailed information, this will really handy, since I’m new with this stuff. Looking forward for your updates!

    Summer Davis

(wordt niet gepubliceerd)
  1. Nog geen trackbacks.