Preface
This article is first in a series of articles, that will show you how to fully integrate Java and .NET.
The articles are as follows:
- Part I: Introduction to Java & .NET interoperability and the suggested solution
- Part II: Implement .NET proxy to the Java classes
- Part III: Using Attributes to extend the Java API solution
- Part IV: Java to .NET API calls
- Part V: Implement Java proxy to the .NET classes
- Part VI: Adding Annotations to extend the .NET API solution
Introduction
Java has been with us for more than ten years now and has made phenomenal inroads into the world of system, business, Internet and educational programming. In the last few years, a new and competing technology was introduced by Microsoft; this technology is known today as the .NET Framework. This article is not intended to deal with each of the platforms, the purpose of this article is to showcase my experience for making C# co-exist with Java and vice versa in a native manner.
This article is based on a project with the code name Espresso, which was developed by Reflective Software. On their Web site, you will be able to find updated information and build for this project.
Background
As mentioned before, Java and .NET have been around for several years. During this time, there were many articles and solutions attempting to solve the interoperability issues between these two frameworks. The major problem with these solutions were that they tried to integrate the two frameworks using external communication. This means that each framework simultaneously runs as a separate process on the same machine, or as a process on different machines. The communication used employs several technologies, such as Web Services, .NET remoting etc. The cardinal problem with this type of communication is that it's very slow and network dependent.
The suggested solution will show how the two frameworks can live together in the same process and communicate seamlessly with each other.
This article describes a high-performing interoperability solution between the Java platform and the .NET Framework. The suggested solution does not replace the Java Virtual Machine or the .NET Framework runtime, instead, your JVM or .NET are each hosted within the opposing runtime environment, ensuring that vendor-specific VM optimizations are preserved.
"Espresso" objectives are:
- Run on any combination of .NET runtime (1.0+) and Java Virtual Machine (1.2+).
- Allow full reuse of any Java library from a .NET environment and vice versa, working exclusively at the API level and avoiding bytecode translation of the actual implementation.
- Provide optimal performance, running the JVM and .NET under the same process and avoiding network or IPC costs.
The initial code was taken from the Caffeine open source, which is a one-way API invocation from .NET to Java. This article and the following ones, describe a full solution, that will allow invocation of APIs from .NET to Java and vice versa and seamless integration between the two platforms.
In one of the projects we developed for my company, Reflective Software, we had an interoperability requirement between Java and .NET. We tried using Web Services, but it was just not good enough. We used Caffeine and extended it, so we will be able to make Java call .NET and vice versa. These articles contain the phases to accomplish the project and the source code.
Part I
In this first article, I will explain the Espresso solution which will be the base for the other articles.
Must of the information of this first part article is based on the Caffeine project, so if you are familiar with this problem you can just jump to the next part, which is about extending the Caffeine project to new levels.
Runtime Architecture
The Java Native Interface (JNI) is a public interface that all implementors of the Java Virtual Machine must provide. JNI is a set of native functions contained in the JVM native library (jvm.dll, or libjvm.so depending on the architecture) that allows calling native functions from Java as well as native code creating a JVM and invoking Java classes.
Espresso employs JNI technology to host a Java Virtual Machine (JVM) under a .NET runtime. As the JVM runs under the same OS process as the .NET runtime, there are no IPC costs associated with this solution. The next figure illustrates the runtime architecture of Espresso. The blocks in grey are provided by Espresso and the blocks in yellow are provided by the JVM.
The bridge.dll is C++ code that exposes the function that will be used by the JNI.NET.Bridge.dll. The JNI.NET.Bridge.dll is a group of classes that enable the .NET Framework to call Java API using P/Invoke. The .NET classes are some classes that wrap the Caffeine
API in order to give it a more attractive and OO look. In these articles, we will not display this library because I took a different approach, the proxy one, which is different from the Caffeine
approach.
bridge.dll
If you know JNI, you will know that the JNIEnv
interface pointer is at its core. JNIEnv
contains a table of JNI functions passed as an argument to each native method. The main advantage of the JNIEnv
interface pointer design is that JNI implementations do not need to link to a particular version of the Java Virtual Machine. Binary compatibility is the main design criteria for JNI, and JNIEnv
obeys this.
Accessing an interface pointer from C#, although possible, is very cumbersome. For this reason, Espresso provides the bridge library. Another reason for the bridge library is that JNIEnv
uses thread-local storage, which means that each native thread must be attached to a Java thread in order to ensure correctness. The Espresso bridge library flattens the JNIEnv
interface and ensures that each native thread is correctly attached to a Java thread.
FindClass
function is as follows:
jclass
FindClass(const char *name)
{
JNIEnv *env = GetEnv();
return (*env)->FindClass(env, name);
}
For those who know the C++ bindings of JNI, the construct above will be familiar. We are flattening the structure interface, removing JNIEnv
from the function signature. Instead, the first line in the FindClass
function calls the GetEnv()
function. GetEnv()
returns the JNIEnv
interface pointer attached to the current thread, and if the current native thread is not attached to a thread-local storage, it attaches the thread and returns a JNIEnv
interface pointer. The second line makes the actual call to the JNI FindClass
function, via the JNIEnv
interface pointer.
The bridge library contains 1:1 flattened versions of all the functions contained in the JNIEnv
interface pointer structure. Additionally, the bridge library contains the GetEnv()
function described above, as well as functions to create and destroy a Java VM.
Creating the JVM
The bridge library provides two convenience functions to create a Java VM:
int CreateJavaVMDLL(const char *dllName,
JavaVMOption * options,
int nOptions);
int CreateJavaVMAnon(JavaVMOption * options,
int nOptions);
CreateJavaVMDLL
allows to specify the shared library containing the Java VM which should be loaded at runtime. The bridge library is not linked to any particular Java VM implementation. Instead, it uses dynamic library location and loading (LoadLibrary
/GetProcAddress
), based on the dllName
passed to the CreateJavaVMDLL
function. CreateJavaVMAnon
does not take a dllName
, and defaults dllName
to jvm.dll on Win32. This seems to be a valid default for 90% of the setups. Most applications will not need to specify the JVM library, but in case the default does not work, JNI.NET.Bridge
provides the ability to configure the name of the DLL which contains the JVM (see Section Configuration).
Passing Arguments
Most of the JNIEnv
functions have three forms:
jchar (JNICALL *CallNonvirtualCharMethod)
(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...);
jchar (JNICALL *CallNonvirtualCharMethodV)
(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID,
va_list args);
jchar (JNICALL *CallNonvirtualCharMethodA)
(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID,
jvalue *args);
JNIEnv
functions can take either an ellipsis, a variable argument list, or a jvalue
union. P/Invoke does not allow ellipsis nor variable argument lists, as type marshalling would not be possible, so the bridge library only exhibits the function form using the jvalue
union.
JNI.NET.Bridge.dll
This library is the .NET counterpart of bridge.dll. In this library, you will find a class that wraps the JNI calls in a more OO manner and enables the .NET developer to use the Java API in a much more elegant way than just calling JNI API.
Binding
All the JNI types have a .NET counterpart:
JObject
, encapsulates jobject
, and is the base type for all .NET types JClass
, encapsulates the JNI jclass
typeJMethod
, encapsulates jmethodID
JField
, encapsulates jfieldID
JArray
, JBooleanArray
, JByteArray
, JCharArray
, JDoubleArray
, JFloatArray
, JIntArray
, JLongArray
, JObjectArray
, and JShortArray
, which encapsulate, respectively, jarray
, jbooleanarray
, jbytearray
, jchararray
, jdoublearray
, jfloatarray
, jintarray
, jlongarray
, jobjectarray
and jshortarray
JThrowable
, encapsulates jthrowable
JString
, encapsulates jstring
JNI.NET.Bridge
provides a number of additional types, to simplify the usage of the bindings:
JMember
, base class for JConstructor
, JMethod
, and JField
JConstructor
, special version of JMethod
All JNIEnv
functions have been encapsulated in the corresponding .NET type. For example, the bridge function:
jint MonitorExit(jobject obj);
is encapsulated by:
public class JObject
{
[DllImport(JNIEnv.DLL_JAVA)]
static extern int MonitorExit (IntPtr obj);
public void MonitorExit () {
}
Passing Arguments
As mentioned earlier, the bridge library only exposes the functions using a jvalue
union, but not the ones using a variable argument list, nor ellipsed parameters. JValue
is a C# struct that can be marshalled to a C union, by using the FieldOffset
attribute. The original jvalue
union, defined in jni.h, looks like:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
The C# struct that simulates this union looks like:
[StructLayout (LayoutKind.Explicit)]
public struct JValue
{
[FieldOffset (0)] bool z;
[FieldOffset (0)] byte b;
[FieldOffset (0)] char c;
[FieldOffset (0)] short s;
[FieldOffset (0)] int i;
[FieldOffset (0)] long j;
[FieldOffset (0)] float f;
[FieldOffset (0)] double d;
[FieldOffset (0)] IntPtr l;
}
Instantiating a Java Object
All Java objects are wrapped by JObject
. JObject
is a .NET type that keeps a reference (Handle
property) to the Java object. In other words, to instantiate a Java object, we must create an instance of a JObject
.
There are two ways to create an instance of a JObject
: by calling the JObject
constructor and providing a JConstructor
, or by calling the instance method NewInstance
in JClass
. Let's concentrate on the first option, which is the generic way to create an instance. The steps we must follow to create an object are:
- Get an instance of the
JClass
object corresponding to the Java type we are interested in. - Out of the
JClass
instance, find a suitable constructor, and obtain an instance of JConstructor
. - Finally, call the constructor of
JObject
and pass the JConstructor
instance obtained previously from JClass
.
In order to find the class, we invoke the static
method ForName
in JClass
, providing the fully qualified name of the Java type we require to be loaded. Under the scenes, ForName
calls the FindClass
function in the JNIEnv
interface pointer.
Once we have a JClass
object, which corresponds to a Java type, we must obtain a constructor for that type. We do this by calling the GetConstructor(string sig)
method in JClass
. Note that the signature follows the JNI naming convention (you can use the Java tool javap
for obtaining signatures). An example of use would be:
JClass clazz = JClass.ForName ("Prog");
JConstructor ctr = clazz.GetConstructor ("()V");
In this example, we are obtaining the no args constructor for the Java class, Prog
. For the no-args constructor, we could have also called the convenience method GetConstructor()
, equivalent to calling GetConstructor("()V");
.
Once we have a constructor, we can call the JObject
constructor:
JObject obj = new JObject (ctr);
You should know that JObject
exposes three constructors, none of them public
. The first constructor is the one we have just shown, and is used by derived classes from JObject
. For example, if we were writing Prog.cs, a C# wrapper to a Java class Prog.class, we would use that constructor. This constructor's signature is:
protected JObject (JConstructor ctr, params object[] args);
This constructor takes a variable number of System.Object
arguments (this is what the C# params
keyword does). This allows derived classes to obtain the constructor (JConstructor
) during the static
class constructor, and call the base constructor with the reference to JConstructor
and the arguments passed to it.
Invoking a Method
JConstructor
is just a special type of JMember
. We can query JClass
for any JMember
, be it a field, a constructor or a method. Invoking a method or accessing a field is not very different from creating a new object instance. We still require a JClass
, out of which we will obtain a JMethod
or a JField
using its signature.
Going back to our example, Prog
, if we had a Java method:
public class Prog {
public int max (int a, int b) {
return (a > b) ? a : b;
}
}
We could obtain the method from JNI.NET.Bridge
by calling the GetMethod()
in JClass
:
JClass clazz = JClass.ForName ("Prog");
JMethod _max = clazz.GetMethod ("max", "(II)I");
GetMethod
takes two arguments: the name of the method, and the internal method signature. To obtain the internal method signature, you can either learn the specifications, and use Sun's Java SDK javap
tool.
Once we have obtained a method, we can invoke it. In our Prog
example, we would call:
int result = _max.CallInt (obj, a, b);
CallInt
is used for calling methods that return an int
. Based on the return type, there are other variants in JMethod
to invoke a Java method:
CallBoolean
, CallByte
, CallChar
, CallShort
, CallInt
, CallLong
, CallFloat
, CallDouble
, CallVoid
, used for calling methods that return a boolean
, byte
, char
, short
, int
, long
, float
, double
and no return, respectivelyCallObject
, used for calling methods that return an object or an array (regardless of whether it is an object array or a primitive type array)
All CallXXX methods take two versions: one that takes a JValue
array, and one that takes a variable number of arguments. For example, for CallInt
:
public int CallInt (JObject obj, JValue[] args);
public int CallInt (JObject obj, params object[] args);
Whenever possible, the JValue[]
version should be preferred for performance reasons. When a variable argument list is used, boxing/unboxing overhead, as well as a conversion routing overhead is incurred.
Additionally, JMethod
provides a convenience method, Invoke
, that avoids having to know which version of CallXXX to use. The signatures of Invoke
are:
public object Invoke (JObject obj, JValue[] args);
public object Invoke (JObject obj, params object[] args);
Invoke
will determine the right CallXXX to call based on the method signature provided when the instance of JMethod
was created. Invoke
returns an object, exploiting .NET's boxing. Note therefore that although convenient, this method provides less performance than calling directly the right CallXXX version, where there is no boxing involved.
All versions of CallXXX, as well as Invoke
, take as their first argument a reference to an instance of JObject
. Obviously, an object reference is only required for instance methods, and for class (static
) methods, the first argument is ignored. It is therefore legal to pass a null
reference to class (static
) methods.
Arrays
All arrays in Java (and .NET) are objects. JNI exposes all arrays as a jarray
, which is encapsulated in JNI.NET.Bridge
as JArray
. JNI also has specialized versions of jarray
, such as jintarray
, which JNI.NET.Bridge
encapsulates as JIntArray
.
JNI.NET JArray
specialized versions, such as JIntArray
or JObjectArray
, provide three constructors:
public JObjectArray (JObject other);
A copy constructor public JObjectArray (int length, JClass clazz);
Creates a JObjectArray
with the specified length and using the Java class referenced by the JClass
instance public JObjectArray (JObject[] source, JClass clazz);
Creates a JObjectArray
out of the array of JObject
, using the type specified by JClass
An example will clarify when to use each constructor. If we were calling a Java method fooB
:
public int fooB (Prog[] arg);
The method fooB
needs to get an instance of a JObjectArray
, so the first step in .NET will be to convert the .NET Prog[]
array to a JObjectArray
. We do this by calling the third constructor:
Prog[] args = ...
JObjectArray array = new JObjectArray (args, Prog.JClass);
int r = _fooB.CallInt (this, array);
The JObjectArray
constructor first allocates a jobjectarray
using the JNI function NewObjectArray
, and providing the jclass referenced by Prog.JClass
. The constructor of JObjectArray
then copies the elements contained in Prog[]
args into the newly created object using the JNI function SetObjectArrayElement
.
We have seen how to call a Java method taking an array. A similar pattern is used for returned arrays. Let's say we wanted to call a Java method fooA
:
public Prog[] fooA ();
As we know, the call to fooA
is done as:
JObject o = _fooA.CallObject (this);
In this case, we must cast a JObject
to a .NET array. We will use the copy constructor:
JObjectArray array = new JObjectArray (o);
And obtain the elements in the array as an array of JObject[]
and copy one to one to a newly allocated Prog[]
:
JObject[] oArray = (JObject[]) array.Elements;
int l = array.Length;
Prog[] r = new Prog[l];
for (int i = 0; i < l; i++) {
r[i] = new Prog (oArray[i]);
}
Note that ideally, we would have liked to write something like:
Prog[] r = (Prog[]) array.Elements;
The current implementation does not do this because of performance reasons. The same as with casting, commented in the previous section, we could have implemented array casting using reflection, as JMethod
knows the type of o
, and JObjectArray
could have used the static JClass
property in JObject
to instantiate the right object array.
Configuration
The configuration mechanism allows you to specify the library load path, the classpath
, and the command line arguments you would like to pass to the Java runtime. This is especially interesting when either you have to setup a complex CLASSPATH
(usually the case in J2EE applications), or because your JVM is not called jvm.dll or libjvm.so.
="1.0"="utf-8"
<configuration>
<configSections>
<sectionGroup name="jvm">
<section name="settings" type="Jni.Net.Bridge.JNISectionHandler, Jni.Net.Bridge"/>
</sectionGroup>
</configSections>
<jvm>
<settings>
<jvm.dll value="C:\Program Files\Java\jre1.5.0_07\bin\client\jvm.dll"/>
<java.class.path value="minijavart.jar"/>
<java.class.path value="."/>
<java.library.path value="C:\Program Files\Java\jre1.5.0_07\lib"/>
<java.option value="-verbose:gc,class"/>
</settings>
</jvm>
</configuration>
In this example, we are specifying that the JVM is contained in a DLL called jvm.dll; that the classpath
should contain the minijavart.jar file and "." directory (in the current directory since no relative nor absolute path is given in this example); that native libraries (e.g. System.loadLibrary()
) should be searched under C:\Program Files\Java\jre1.5.0_07\lib, and that class garbage collection should be traced at verbose level.
The jvm.dll needs to be specified in the settings section if it is not found in the PATH
or in the registry under HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment.
Sample Source and Build
The project was developed in .NET Framework 2.0, using Visual C# 2005 Express and Visual C++ 2005 Express.
The source zip file contains two Visual Express projects, the first one is the bridge
project, which is the C++ project of bridge.dll. First, open this project and build it. The second project is the JNI.NET.Bridge
project which is C# project, it includes all the source code of JNI.NET.Bridge.dll and a sample test program.
Open it and build it using Visual C# 2005 Express or Visual Studio 2005.
Run the Test project to check if everything is working.
To Be Continued...
In the next part, I will explain how to implement .NET proxy in order to call the Java classes in a more OO manner, and how to call .NET from Java.
History
- 8th June, 2006: Initial post