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:
- I don't want my users to have to learn a new language. JavaScript is well-known.
- 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:
using org.mozilla.javascript;
I then declared my JavaScript and C# functions:
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:
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.
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:
ScriptableObject.putProperty(scope, "me", new MainClass());
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):
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
.)
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:
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:
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:
Mac:
Linux:
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:
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);
java.lang.reflect.Member method =
myJClass.getMethod("FromCSharp", typeof(java.lang.Double));
Scriptable function = new FunctionObject("fromCSharp", method, scope);
scope.put("fromCSharp", scope, function);
ScriptableObject.putProperty(scope, "me", new MainClass());
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();
}
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