65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Rhino 和 IKVM 将 JavaScript 嵌入 C#。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (18投票s)

2009年8月26日

BSD

8分钟阅读

viewsIcon

70741

介绍了一种从 C# 调用 JavaScript 的技术,然后允许 JavaScript 回调 C#。

Sample Image

引言

网络上充斥着关于如何将 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 是首选,原因如下:

  1. 我不想让我的用户学习一门新语言。JavaScript 是广为人知的。
  2. 大多数时候,我调用 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;
    }
}

如果一切顺利,控制台应如下所示:

Windows

Mac

Linux

总结与结论

使用 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 解释器
© . All rights reserved.