Click here to Skip to main content
15,884,629 members
Articles / Programming Languages / Javascript

Embedding JavaScript into C# with Rhino and IKVM

Rate me:
Please Sign up or sign in to vote.
4.83/5 (19 votes)
11 Nov 2009BSD8 min read 69K   59   10
Describes a technique to call JavaScript from C#, and then to allow the JavaScript to call back into C#.

Sample Image

Introduction

The web is full of various discussions on how to embed C# into JavaScript. Most of these approaches are flawed because they rely on the deprecated Microsoft.JScript APIs. Other approaches, like hosting JavaScript in a WebBrowser control, aren't portable. In my particular situation, I need an embedded JavaScript engine that will work on Windows, Mac, and Linux. It has to work equally well in the .NET and Mono runtimes, and ideally, shouldn't require recompiling for each Operating System. I ended up using the IKVM tool to convert Rhino, a JavaScript interpreter written in Java, into a CLR DLL.

I use this technique to allow server-side Javascript in ObjectCloud, a web server that I've designed to host web applications that have minimal server-side needs. ObjectCloud uses server-side Javascript when an application needs to enforce business logic that can't be securely performed in the web browser.

Background

Various searches for C# and JavaScript often come up with examples that use the Microsoft.JScript namespace and the VSA API. After experimenting with these on Mono on Mac, I decided to move on. These APIs were deprecated a long time ago, and support seemed somewhat flakey. There are also some rumors of JScript support re-emerging with some fancy new scripting framework related to Silverlight; but it seems that the project is canned. I concluded that using the JScript APIs built into .NET and Mono would not meet my needs.

There are a few well-established JavaScript libraries written in C and C++. These are mature, but using them in C# would present a few problems. The primary problem is that I would have to include a version of the library for each Operating System that I wish to target, which would make my deployment scenario too complicated. In addition, PInvoke introduces its own set of complexities that can become a time sink. It is possible to compile C and C++ into a .NET DLL; however, in order to make the DLL work on Mac and Linux, they would need to be adapted to compile with the /clr:pure option. While this would yield the best performance, it's just too time consuming for now. (Anyone wants to figure out how to get V8 to compile with /clr:pure?)

I got my final clue from Joshua Tauberer's blog: Embedding a JavaScript interpreter with Mono. He describes the process of introducing Rhino into a C application by using embedded Mono and the ikvmc Java bytecode to CLR converter. According to him, it is a quick and painless process!

Why Not Use Some Other .NET Scripting Language?

In my search for a reliable approach to call JavaScript from C#, I came across many plugs for various .NET scripting technologies, like Lua, Boo, IronPython, and IronRuby. In my situation, JavaScript is preferable for the following reasons:

  1. I don't want my users to have to learn a new language. JavaScript is well-known.
  2. Most of the time that I'm calling into JavaScript, it will contain data originating from a browser's AJAX call. The results of my JavaScript will return to the AJAX callback. Keeping with JavaScript simplifies JSON serialization and type mapping issues.

Preparing and Referencing Rhino

Converting from Java Bytecode to CLR

The first thing I did was obtain the latest release of Rhino. As of this writing, it is version 1.7R2.

A version of IKVM is included with Mono, but I had to download IKVM in order to get it to work correctly in .NET / Visual Studio. This is because DLLs generated by IKVM depend on additional DLLs to provide Java classes. I used version 0.40.0.1.

Converting Rhino to work under .NET is very painless. I opened a shell into a folder with the ikvmc.exe executable. (It's part of the PATH on Mono on Mac.) Next, I put js.jar, from Rhino, into the same directory as ikvmc.exe. The command to convert is:

ikvmc.exe -target:library js.jar

I got a bunch of warnings related to XML, but I ignored them because I don't anticipate using XML functionality in JavaScript.

Referencing js.dll

In Mono, if I used the ikvmc.exe that was part of the PATH, I could import js.dll directly without problems. In .NET, I had to also include IKVM.OpenJDK.Core.dll. This is because Mono includes IKVM's DLLs in the GAC and .NET doesn't.

Using the Code

For my test, I decided to call a function declared in JavaScript from C#, call a static method declared in C# from JavaScript, and then call a non-static method declared in C# from JavaScript. In order to satisfy my needs, I had to successfully pass primitive values between C# and JavaScript. I'm not concerned with more complicated types because my application will pass structured data as JSON into and out of JavaScript.

To write this code, I followed instructions from Rhino's quickstart guide.

The first step was to add a "using" statement. Notice the Java-style namespace:

C#
using org.mozilla.javascript;

I then declared my JavaScript and C# functions:

C#
private const string script =
    @"
    function calledWithState()
    {
       return _CalledWithState(me);
    }
    
    function foo(x)
    {
       return calledWithState() + fromCSharp(321.9) + x + ""!!!         "";
    }
    ";

public static string FromCSharp(java.lang.Double i) 
{
    return string.Format("  {0}   the ", i);
}

private string State = "the state";

public string CalledWithState()
{
    return State + "\n";
}

public static string CalledWithState(object fromJS)
{
    if (fromJS is MainClass)
        return ((MainClass)fromJS).CalledWithState();
    else
        throw new Exception("Wrong class");
}

Notice that FromCSharp uses a java.lang.Double. This is because Rhino still works with Java types, and is incapable of converting JavaScript values into the equivalent C# values. IKVM handles some translation, but it doesn't yet allow casting between double? and java.lang.Double.

There are both static and instance methods. Rhino has functionality that can automatically expose a Java object's methods to JavaScript. I couldn't get this to work under IKVM. My technique for exposing C# methods and objects to JavaScript is discussed in a few paragraphs.

Using Rhino requires that I declare a Context and scope:

C#
Context cx = Context.enter();
            
try
{
    cx.setClassShutter(new ClassShutter());
    Scriptable scope = cx.initStandardObjects();
    ...
}
finally
{
    Context.exit();
}

The ClassShutter is described at the end.

The Context must always be exited. The most reliable way to do this is to wrap all use of the Context in a try block, and close it in a finally clause. The Context is only intended to be used on a single thread. See Rhino's documentation for more information.

Adding the static C# method requires using IKVM to go through Java's Reflection API. Static methods are relatively painless.

C#
java.lang.Class myJClass = typeof(MainClass);
java.lang.reflect.Member method = 
    myJClass.getMethod("FromCSharp", typeof(java.lang.Double));

Scriptable function = new FunctionObject("fromCSharp", method, scope);

scope.put("fromCSharp", scope, function);

Non-static methods, on the other hand, require a bit of work. As I stated earlier, I could not get Rhino's ability to expose a Java object to JavaScript to work under IKVM. Fortunately, IKVM and Rhino allow C# objects to be opaquely passed into JavaScript and then back to a static C# method. In this case, I want JavaScript to be able to call the non-static method CalledWithState(). To do so, I created a static method in the same class that takes an object as its argument. The static method casts the object to the desired type, and then calls the non-static CalledWithState(). I also had to create a wrapped function in JavaScript that passes the opaque object "me" to the static CalledWithState().

The JavaScript is shown above, and the code to add the opaque object and the static wrapper method is shown below:

C#
// Me
ScriptableObject.putProperty(scope, "me", new MainClass()); //wrappedMe);

// CalledWithState
method = myJClass.getMethod("CalledWithState", typeof(object));
function = new FunctionObject("_CalledWithState", method, scope);
scope.put("_CalledWithState", scope, function);

There is probably a fancier way to allow JavaScript to access methods on a C# object, but I'll leave that as an exercise for the reader. ;)

Adding the actual JavaScript only requires a single line (note that the script is declared above):

C#
cx.evaluateString(scope, script, "<cmd>", 1, null);

To make the call into JavaScript, I get the function "foo" and call it. (Remember that foo calls FromCSharp.)

C#
object fooFunctionObj = scope.get("foo", scope);

if (!(fooFunctionObj is Function))
    Console.WriteLine("Foo isn't a function");
else
{
    Function fooFunction = (Function)fooFunctionObj;
    object result = fooFunction.call(cx, scope, scope, new object[] { "bar" });

    Console.WriteLine(result);
}

The final part is a simple security test. Rhino, by default, allows unrestricted access to all Java APIs from within JavaScript. It appears that their intention is that Java's security APIs should be used to enforce security constraints. A different approach is to explicitly lock down JavaScript to only use functions and objects directly passed into it.

This block of code attempts to prove that a Java API cannot be used. Something that I do not understand is that evaluateString has some kind of user-unhandled exception. While the program does not crash, Visual Studio mysteriously breaks into the debugger even though there is a catch-all clause:

C#
try
{
    cx.evaluateString(scope, 
      "java.lang.System.out.println(\"Security Error!!!\")", 
      "<cmd>", 1, null);
}
catch (Exception e)
{
    Console.WriteLine("Couldn't call a Java method");
    Console.WriteLine(e.ToString());
}

This block of code is the class filter assigned when entering the context. It explicitly denies use of any Java class from JavaScript. Note that interfaces imported from IKVM don't follow the .NET "I" convention:

C#
/// <summary>
/// Implements security by restricting which classes can be used in JavaScript
/// </summary>
private class ClassShutter : org.mozilla.javascript.ClassShutter
{
    public bool visibleToScripts(string str)
    {
        Console.WriteLine("Class used in JavaScript: {0}", str);
        return false;
    }
}

If all goes well, the console should look like the following:

Windows:

Image 2

Mac:

Image 3

Linux:

Image 4

Summary and Conclusion

Using Rhino and IKVM allows calling JavaScript from C#. The performance is okay. The process of importing a Java library into C# is mostly painless, except for some issues with exposing C# objects to JavaScript. Using Rhino and IKVM allows a C# program to work on Windows, Linux, and Mac without needing to compile a library for each platform, thus resulting in the simplest deployment scenario possible. Finally, it is my opinion that using IKVM to call a Java library is a lot easier than using PInvoke to call into a C library.

Source Code

The entire source code is given below:

C#
using System;
using System.Reflection;
using System.Text;

using org.mozilla.javascript;

namespace TestRhino
{
    class MainClass
    {
        private const string script =
            @"
            function calledWithState()
            {
               return _CalledWithState(me);
            }
            
            function foo(x)
            {
               return calledWithState() + 
                      fromCSharp(321.9) + x + ""!!!         "";
            }
            ";

        public static string FromCSharp(java.lang.Double i) 
        {
            return string.Format("  {0}   the ", i);
        }

        private string State = "the state";

        public string CalledWithState()
        {
            return State + "\n";
        }

        public static string CalledWithState(object fromJS)
        {
            if (fromJS is MainClass)
                return ((MainClass)fromJS).CalledWithState();
            else
                throw new Exception("Wrong class");
        }

        public static void Main(string[] args)
        {
            Context cx = Context.enter();
            
            try
            {
                cx.setClassShutter(new ClassShutter());
                Scriptable scope = cx.initStandardObjects();

                java.lang.Class myJClass = typeof(MainClass);

                // FromCSharp
                java.lang.reflect.Member method = 
                   myJClass.getMethod("FromCSharp", typeof(java.lang.Double));
                Scriptable function = new FunctionObject("fromCSharp", method, scope);
                scope.put("fromCSharp", scope, function);

                // Me
                ScriptableObject.putProperty(scope, "me", new MainClass()); //wrappedMe);

                // CalledWithState
                method = myJClass.getMethod("CalledWithState", typeof(object));
                function = new FunctionObject("_CalledWithState", method, scope);
                scope.put("_CalledWithState", scope, function);

                cx.evaluateString(scope, script, "<cmd>", 1, null);
                
                object fooFunctionObj = scope.get("foo", scope);

                if (!(fooFunctionObj is Function))
                    Console.WriteLine("Foo isn't a function");
                else
                {
                    Function fooFunction = (Function)fooFunctionObj;
                    object result = fooFunction.call(cx, scope, 
                                         scope, new object[] { "bar" });

                    Console.WriteLine(result);
                }

                try
                {
                    cx.evaluateString(scope, 
                      "java.lang.System.out.println(\"Security Error!!!\")", 
                      "<cmd>", 1, null);
                }
                catch (Exception e)
                {
                    Console.WriteLine("Couldn't call a Java method");
                    Console.WriteLine(e.ToString());
                }
            }
            finally
            {
                Context.exit();
            }

            Console.ReadKey();
        }

        /// <summary>

        /// Implements security by restricting which classes can be used in JavaScript
        /// </summary>
        private class ClassShutter : org.mozilla.javascript.ClassShutter
        {
            public bool visibleToScripts(string str)
            {
                Console.WriteLine("Class used in JavaScript: {0}", str);
                return false;
            }
        }
    }
}

History

  • 26th August, 2009: Initial post
  • 27th August, 2009: Fixed a mistake in the arguments for ikvmc.exe
  • 10th November, 2009: Added link to ObjectCloud, my C# project that uses Rhino and IKVM to host a JavaScript interpreter

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralJint Pin
Sebastien Ros11-Nov-09 19:48
Sebastien Ros11-Nov-09 19:48 
GeneralRe: Jint Pin
Dewey11-Nov-09 21:28
Dewey11-Nov-09 21:28 
GeneralRe: Jint Pin
GWBas1c11-Nov-09 22:07
GWBas1c11-Nov-09 22:07 
GeneralRe: Jint Pin
GWBas1c12-Nov-09 15:51
GWBas1c12-Nov-09 15:51 
GeneralKeep it up Pin
friendly_coder1-Nov-09 1:32
friendly_coder1-Nov-09 1:32 
GeneralRe: Keep it up Pin
GWBas1c10-Nov-09 13:05
GWBas1c10-Nov-09 13:05 
GeneralMy vote of 1 Pin
wallstreetdeveloper3-Sep-09 7:22
wallstreetdeveloper3-Sep-09 7:22 
This article is for sceintists and not for developers
Questionfascinating, but how would you use this in the "real world" ? Pin
BillWoodruff26-Aug-09 19:31
professionalBillWoodruff26-Aug-09 19:31 
AnswerRe: fascinating, but how would you use this in the "real world" ? Pin
GWBas1c26-Aug-09 20:05
GWBas1c26-Aug-09 20:05 
AnswerRe: fascinating, but how would you use this in the "real world" ? Pin
GWBas1c10-Nov-09 13:03
GWBas1c10-Nov-09 13:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.