使用 Rhino 和 IKVM 将 JavaScript 嵌入 C#。
介绍了一种从 C# 调用 JavaScript 的技术,然后允许 JavaScript 回调 C#。
引言
网络上充斥着关于如何将 C# 嵌入 JavaScript 的各种讨论。其中大多数方法都有缺陷,因为它们依赖于已弃用的 Microsoft.JScript
API。其他方法,例如在 WebBrowser 控件中托管 JavaScript,则不具备可移植性。在我的特定情况下,我需要一个嵌入式的 JavaScript 引擎,它可以在 Windows、Mac 和 Linux 上运行。它必须在 .NET 和 Mono 运行时中同样表现良好,并且理想情况下,不应该为每个操作系统重新编译。我最终使用了 IKVM 工具将用 Java 编写的 JavaScript 解释器 Rhino 转换为 CLR DLL。
我使用这项技术来实现服务器端 JavaScript 在 ObjectCloud 中的应用,这是一个我设计的 Web 服务器,用于托管对服务器端需求很少的 Web 应用程序。 当应用程序需要强制执行无法在 Web 浏览器中安全执行的业务逻辑时,ObjectCloud 会使用服务器端 JavaScript。
背景
对 C# 和 JavaScript 的各种搜索通常会得到使用 Microsoft.JScript
命名空间和 VSA API 的示例。在 Mac 上的 Mono 上试验了这些之后,我决定继续前进。这些 API 早已弃用,并且支持似乎有些不稳定。还有一些传言称 JScript 支持将随着一个与 Silverlight 相关的新型脚本框架重新出现;但似乎该项目已被取消。我得出结论,使用 .NET 和 Mono 内置的 JScript API 无法满足我的需求。
有一些用 C 和 C++ 编写的成熟的 JavaScript 库。这些库很成熟,但在 C# 中使用它们会带来一些问题。主要问题是我必须为我想支持的每个操作系统包含一个库版本,这将使我的部署方案过于复杂。此外,PInvoke 本身也带来了一些复杂性,这些复杂性可能会耗费大量时间。虽然可以将 C 和 C++ 编译为 .NET DLL,但为了使 DLL 可以在 Mac 和 Linux 上运行,需要使用 /clr:pure 选项对其进行改编。虽然这将产生最佳性能,但目前来说耗时过长。(有没有人想出如何让 V8 编译为 /clr:pure?)
我从 Joshua Tauberer 的博客中获得了最后的线索:使用 Mono 嵌入 JavaScript 解释器。他描述了通过使用嵌入式 Mono 和 ikvmc Java 字节码到 CLR 转换器来将 Rhino 引入 C++ 应用程序的过程。据他所说,这是一个快速而无痛的过程!
为什么不使用其他 .NET 脚本语言?
在我寻找一种可靠的方法来从 C# 调用 JavaScript 的过程中,我遇到了许多关于各种 .NET 脚本技术的插件,例如 Lua、Boo、IronPython 和 IronRuby。在我的情况下,JavaScript 是首选,原因如下:
- 我不想让我的用户学习一门新语言。JavaScript 是广为人知的。
- 大多数时候,我调用 JavaScript,它将包含来自浏览器 AJAX 调用的数据。我的 JavaScript 的结果将返回到 AJAX 回调。保留 JavaScript 可以简化 JSON 序列化和类型映射问题。
准备和引用 Rhino
将 Java 字节码转换为 CLR
我做的第一件事是获取最新版本的 Rhino。在撰写本文时,它是 1.7R2 版本。
Mono 中包含一个版本的IKVM,但我必须下载 IKVM 才能使其在 .NET / Visual Studio 中正常工作。这是因为 IKVM 生成的 DLL 依赖于其他 DLL 来提供 Java 类。我使用的是 0.40.0.1 版本。
将 Rhino 转换为在 .NET 中工作非常简单。我打开了一个外壳,进入了一个包含 ikvmc.exe 可执行文件的文件夹。(在 Mac 上的 Mono 上,它已包含在 PATH 中。)接下来,我将 Rhino 中的 js.jar 放入与 ikvmc.exe 相同的目录中。转换命令是
ikvmc.exe -target:library js.jar
我收到了一堆与 XML 相关的警告,但我忽略了它们,因为我不打算在 JavaScript 中使用 XML 功能。
引用 js.dll
在 Mono 中,如果我使用 PATH 中的 ikvmc.exe,我可以直接导入 js.dll 而没有问题。在 .NET 中,我还需要包含 IKVM.OpenJDK.Core.dll。这是因为 Mono 将 IKVM 的 DLL 包含在 GAC 中,而 .NET 没有。
Using the Code
为了进行测试,我决定从 C# 调用 JavaScript 中声明的函数,从 JavaScript 调用 C# 中声明的 static
方法,然后从 JavaScript 调用 C# 中声明的非静态方法。为了满足我的需求,我必须成功地在 C# 和 JavaScript 之间传递基本值。我不关心更复杂的类型,因为我的应用程序将以 JSON 格式在 JavaScript 的输入和输出之间传递结构化数据。
为了编写这段代码,我遵循了Rhino 的快速入门指南。
第一步是添加一个“using
”语句。请注意 Java 风格的命名空间
using org.mozilla.javascript;
然后我声明了我的 JavaScript 和 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");
}
请注意,FromCSharp
使用 java.lang.Double
。这是因为 Rhino 仍然使用 Java 类型,并且无法将 JavaScript 值转换为等效的 C# 值。IKVM 处理了一些转换,但它还不允许在 double
? 和 java.lang.Double
之间进行转换。
既有 static
方法,也有 instance
方法。Rhino 具有自动将 Java 对象的方法公开给 JavaScript 的功能。我无法在 IKVM 下使其工作。我将 C# 方法和对象公开给 JavaScript 的技术将在几段中进行讨论。
使用 Rhino 需要我声明一个 Context
和作用域
Context cx = Context.enter();
try
{
cx.setClassShutter(new ClassShutter());
Scriptable scope = cx.initStandardObjects();
...
}
finally
{
Context.exit();
}
ClassShutter
在最后进行了描述。
Context
必须始终退出。最可靠的方法是将所有 Context
的使用都包装在 try
块中,并在 finally
子句中关闭它。Context
仅供单个线程使用。有关更多信息,请参阅 Rhino 的文档。
添加静态 C# 方法需要使用 IKVM 来访问 Java 的 Reflection API。Static
方法相对比较简单。
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);
另一方面,非静态方法需要一些工作。正如我之前所说,我无法让 Rhino 将 Java 对象公开给 JavaScript 的功能在 IKVM 下工作。幸运的是,IKVM 和 Rhino 允许将 C# 对象不透明地传递到 JavaScript,然后传回静态 C# 方法。在这种情况下,我希望 JavaScript 能够调用非静态方法 CalledWithState()
。为此,我在同一类中创建了一个 static
方法,该方法接受一个 object
作为参数。静态方法将该对象强制转换为所需的类型,然后调用非静态 CalledWithState()
。我还必须在 JavaScript 中创建一个包装函数,该函数将不透明对象“me
”传递给 static CalledWithState()
。
上面的 JavaScript 已经显示,并且添加不透明对象和 static
包装方法的代码如下所示:
// 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);
可能还有一种更花哨的方法可以让 JavaScript 访问 C# 对象上的方法,但我将把它留给读者作为练习。 ;)
添加实际的 JavaScript 只需一行(请注意,脚本已在上方声明)
cx.evaluateString(scope, script, "<cmd>", 1, null);
为了调用 JavaScript,我获取函数“foo
”并调用它。(请记住,foo
调用 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);
}
最后一部分是一个简单的安全测试。Rhino 默认允许从 JavaScript 无限制地访问所有 Java API。看来他们的意图是使用 Java 的安全 API 来强制执行安全限制。另一种方法是明确地将 JavaScript 限制为仅使用直接传递给它的函数和对象。
此代码块试图证明无法使用 Java API。我不理解的一件事是 evaluateString
有某种用户未处理的异常。虽然程序不会崩溃,但 Visual Studio 仍然会神奇地中断到调试器,即使有一个捕获所有子句。
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());
}
此代码块是在进入上下文时分配的类过滤器。它明确拒绝 JavaScript 使用任何 Java 类。请注意,从 IKVM 导入的接口不遵循 .NET 的“I”约定。
/// <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;
}
}
如果一切顺利,控制台应如下所示:
总结与结论
使用 Rhino 和 IKVM 允许从 C# 调用 JavaScript。性能尚可。将 Java 库导入 C# 的过程大部分都很简单,除了在将 C# 对象公开给 JavaScript 时遇到的一些问题。使用 Rhino 和 IKVM,C# 程序可以在 Windows、Linux 和 Mac 上运行,而无需为每个平台编译库,从而实现最简单的部署场景。最后,我认为使用 IKVM 调用 Java 库比使用 PInvoke 调用 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;
}
}
}
}
历史
- 2009 年 8 月 26 日:初始帖子
- 2009 年 8 月 27 日:修复了 ikvmc.exe 参数中的一个错误
- 2009 年 11 月 10 日:添加了指向
ObjectCloud
的链接,这是我的 C# 项目,它使用 Rhino 和 IKVM 来托管 JavaScript 解释器