Simplicity – Native Java bridge for .NET Developers

Introduction

Today’s world is fast paced and agile, this is more evident when we talk about technology, and particularly software development. In order to be competitive, organizations and developers must produce high quality software that aims to leverage existing libraries, thus reducing development, testing time and efforts. In the past, developers had to port or re-write functionality available in libraries, mainly to be used and consumed from their technology stack, a good example, a scientific library with its routines written in C++ that needs to be ported across to C# or any other managed language. This process in itself many times is not productive, but it’s the only option available. After the previously mentioned, wouldn’t it be nice and useful to have some software smart enough to take care of the intrinsic and plumbing (e.g.: interop, marshalling, etc.) when using different libraries and let developer focus on the business requirements? Having this in mind, Simplicity came to be from an idea to realization. Simplicity is a .NET based solution implemented as a Windows Service that implements a native library responsible for interacting with JNI (Java Native Interface) that allows developers run Java code natively from .NET by creating .NET assemblies that act as proxies exposed over HTTP in a simple way. So, developers they would be calling Java code under the scenes, to them they’ll be interacting with .NET types in a similar way they call web services. The abstraction layer provided by Simplicity takes care of the boilerplate code and subtleties around JNI. JNI development hasn’t ever been straightforward to do and it’s always been very “strict” in terms of having code written to meet certain requirements, thus making it inflexible and rigid. Simplicity changes that, because Java methods can be anything yet Simplicity can process it. Developers just drag and drop a Jar that then it’s processed by Windows service and JNI information is extracted and cached in a SQLite database; a .NET assembly is created and acts as a proxy that in turn calls the JNIBridge who does all the interaction with Java and its JVM. To the eyes of the developer, they’ll be calling a .NET Web Service. Simplicity implements Automatic Programming that’s code writing code, in this case .NET assemblies are created from Jar files. Simplicity can host N number of libraries, because every library is exposed as an endpoint. This article is organized in three main sections, which are:

  • · Part I: Overview of the JNIBridge Library (Native)
  • · Part II: Integration of JNIBridge with managed languages (Native & Managed)
  • · Part III: Putting all the pieces together
  • · Part IV: Notes on deployment

 

Part I: Overview of the JNIBridge Library

The core of Simplicity is a Dynamic Link Library called JNIBridge that exposes the JVM  and operations available to external clients, being a daemon (Windows service) the client implemented in this case. JNIBridge’s responsibilities are:

  • Loading and unloading of JVM into Windows service’s  AppDomain, hence both virtual machines (including the CLR) coexist and interact with one another in the same process.
  • Exposure of functionality (execution of methods) in Jar files to call from .NET (RunAsJavaPassThrough)
  • Marshalling of data (Native – Managed and viceversa)
  • Extract and surfacing of exceptions in JVM that are bubbled up to .NET
  • Dynamic load and inspection of Jar files
  • Loading of specific JVM and its run parameters as per user’s preference (via configuration)

In a nutshell, JNIBridge is responsible for everything that requires Java and its JNI.

 

Part II: Integration of JNIBridge with Managed Languages

There are two managed components in Simplicity:

  • Inspector.jar – Java library responsible for extracting metadata from Jar files. This information is serialized as an XML file and later used in the generation of the C# proxies that are exposed as a Web Service. The inspector also has a method (extractExceptionDetails) that allows returning exceptions occurred in JVM back to the CLR.  The way it works is simple, from JNIBridge (Native) I pass a pointer of jthrowable that represents an exception and in Java I downcast it to java.lang.Exception. The following code snippets describe this functionality

    /////////////////////////////////////////

    /// extractExceptionDetails (Java code)

    ///////////////////////////////////////

    
    

    public static String extractExceptionDetails(Object ex) {

           String retval = "";

    
    

           try {

               StringWriter sw = new StringWriter();

               PrintWriter pw = new PrintWriter(sw);

               Exception jniEx =  new Exception((Throwable) ex);

               jniEx.printStackTrace(pw);

               String stackTrace =  sw.toString();

               retval = String.format("Error: %s\nStackTrace: %s", jniEx.getMessage(), stackTrace);

               pw.close();

               sw.close();

           } catch(Exception e) {

               retval = "Unable to cast from JNI pointer - " + e.getCause().toString();

           }

           return retval;

       }

    
    

    
    

    ////////////////////////////////////////////////////////////////

    /// GetJniExceptionDetails & CheckForExceptionsInJvm (C++ Code)

    ///////////////////////////////////////////////////////////////

    
    

    /// <summary>

    /// Gets the jni expection details.

    /// </summary>

    /// <param name="e">The e.</param>

    /// <param name="env">The env.</param>

    /// <returns></returns>

    std::string JNIBridge::Core::Interop::GetJniExpectionDetails(const jthrowable& e, JNIEnv** env) {

        jobject res;

        jclass clazz;

        jmethodID method;

        std::string retval;

    
    

        if (env != nullptr) {

            clazz = (*env)->FindClass("com/simplicity/Inspector");

            method = (*env)->GetStaticMethodID(clazz, "extractExceptionDetails", "(Ljava/lang/Object;)Ljava/lang/String;");

            res = (*env)->CallStaticObjectMethod(clazz, method, e);

            const char* val = (*env)->GetStringUTFChars((jstring)res, nullptr);

            retval = std::string(val);

    
    

            // Release JNI resources

            (*env)->ReleaseStringUTFChars((jstring)res, val);

            (*env)->DeleteLocalRef(res);

            (*env)->DeleteLocalRef(clazz);

        }

    
    

        return retval;

    }

    
    

    /// <summary>

    /// Checks for exceptions in JVM.

    /// </summary>

    /// <param name="env">The env.</param>

    /// <param name="exceptionsThrown">The exceptions thrown.</param>

    void JNIBridge::Core::Interop::CheckForExceptionsInJvm(JNIEnv* env, std::vector<std::string>& exceptionsThrown) {

        jthrowable ex = nullptr;

    
    

        if (env->ExceptionCheck() && (ex = env->ExceptionOccurred()) != nullptr) {

            exceptionsThrown.push_back(JNIBridge::Core::Interop::GetJniExpectionDetails(ex, &env));

            env->ExceptionClear();

            env->DeleteLocalRef(ex);

        }

    }

     

  • Simplicity Daemon – Responsible for processing Jar files, emitting C# files and related assemblies (Proxies)  (via CodeDOM) that are then exposed as a Web Service endpoint. The image below depicts .NET components and JNIBridge (including tests).
    image

    The way it works is simple, because it monitors activity on a specific folder on the filesystem such as copying of files. If a Jar is copied into the folder specified in jarDropFolder (JvmOptions element in configuration file) then it is processed by Simplicity that extracts metadata, storing it in a SQLite database, writes C# code that’s parsed and “translated” from Java to C# syntax based on an XML file (ConversionMaster.xml) and it Is merged with another file ServiceTemplate.xml that describes the skeleton or structure of the proxy code to be emitted. The resulting files are compiled and registered by the daemon as an HTTP endpoint with the name of the class (as specified in Java class in processed Jar file). The proxy assembly then resolves the  IJniBridgeManager type that allows calling the underlying Jar file through RunAsJavaPassThrough.

    The resulting auto-generated code can be seen below

    public float Divide(float a, float b) {

        float tag = 0f;

        List<object> list = new List<object> {a, b};

    
    

        ExecutionResult result = GlobalContainer.Current.TypeContainer.Resolve<IJniBridgeManager>()

                                .RunAsJavaPassThrough(new StackFrame(), base.GetType(), list.ToArray());

    
    

       if ((result != null) && result.IsSuccess)  {

            tag = (float) result.Tag;

        }

    
    

        return tag;

    }

    
    

    
    

    
    

    
    

    
    

 

 

Part III: Putting all the pieces together

So far, we’ve described Simplicity’s main components and the way they interact with one another. Simplicity will automatically host and start listening to requests on the endpoints that have been defined as per the SQLite database. The SQLite database is important because when Simplicity daemon starts it will dynamically and automatically load the proxy assemblies using the information in the database. The database also implements cascade deletion, in other words it is very likely that a Jar with the same name has been registered already, if another jar with same name is processed then all the previous information in the database is deleted through a trigger. The database also holds information on the native JNI function prototypes for the classes/methods in the Jar file. In the past, most JNI solutions had this hardcoded and that’s one of the reasons why JNI development has such a bad reputation of being inflexible and hard to do. Simplicity as its name implies makes this easy, because when the file was processed the signature or prototype of those methods was extracted and stored in the database.

Simplicity as a service (daemon) can be started and stopped.

image

When Simplicity is running, we can confirm that both virtual machines (JVM & CLR) are coexisting as depicted below

 

image

Since Simplicity does not provide any user interface, I’ve added some performance counters that allow users to check and monitor the status and health of the service. The performance counters available are

 

image

A sample Perfmon screen capture with Simplicity’s performance counters is shown

image

 

As mentioned earlier, .NET developers can interact with Simplicity completely unaware that they’re effectively calling a Java library

private void btnRandomCalculate_Click(object sender, EventArgs e) {

            try {

                var random = new Random();

                var randomOp = new Random();

                var operators = new string[] { "+", "-", "x", "÷" };

                var methods = new List<Func<float, float, float>>();

                var values = new List<KeyValuePair<float, float>>();

                var operationCount = int.Parse(txtOperationCount.Text);

                Enumerable.Range(1, operationCount).Select(p => p * p).ToList().ForEach(z => {

                    values.Add(new KeyValuePair<float, float>(random.Next(1, 150),

                               random.Next(150, 300)));

                });


                using (var proxy = new Simplicity.Services.CalculatorClient()) { // This proxy points to endpoint in Simplicity

                    methods.Add(proxy.Sum);

                    methods.Add(proxy.Subtract);

                    methods.Add(proxy.Multiply);

                    methods.Add(proxy.Divide);


                    if (!chkRunInParallel.Checked) {

                        values.ForEach(r => {

                            var selected = randomOp.Next(0, 3);

                            var result = methods[selected](r.Key, r.Value);


                            new Thread(() => {

                                _sbOutputRandom.AppendLine($"{r.Key} {operators[selected]} {r.Value} = {result}");

                                txtOutputRandom.Invoke(new MethodInvoker(delegate {

                                    txtOutputRandom.Text = _sbOutputRandom.ToString();

                                }));

                            }).Start();

                        });

                    } else {

                        Parallel.ForEach(values, (r) => {

                            var selected = randomOp.Next(0, 3);

                            var result = methods[selected](r.Key, r.Value);


                            new Thread(() => {

                                _sbOutputRandom.AppendLine($"{r.Key} {operators[selected]} {r.Value} = {result}");

                                txtOutputRandom.Invoke(new MethodInvoker(delegate {

                                    txtOutputRandom.Text = _sbOutputRandom.ToString();

                                }));

                            }).Start();

                        });

                    }

                }

            } catch (Exception ex) {

                System.Diagnostics.Trace.WriteLine(ex.ToString());

            }



        }

 

Part IV: Notes on deployment

  • Simplicity daemon must be installed as a Windows service with InstallUtil (In my case, it’s 64-Bit)
  • Ensure that JAVA_HOME and JRE_HOME environment variables exist (for both, user and system)
  • Add JDK’s bin folder to PATH environment variable
  • Ensure that files and folders specified in configuration files exist
  • Ensure that symbolic links are created (as specified in configuration)

 

Project source code – https://github.com/angelhernandezm/Simplicity

Leave a Reply

Your email address will not be published. Required fields are marked *