Thursday, June 23, 2011

Running JavaScript from C# with a hint of dynamic

The Noesis JavaScript.NET run-time is probably the fastest run-time that you can use from C# on Windows so far. The reason why, it uses Google V8 2.2 engine from 09/2010 (remark:this project seems to have been abandoned by its author :<) .

The Jurassic JavaScript run time is not as fast, but because written in C# provides better integration with the .NET world, if you really need it.

But both run-times do not let you access JavaScript objects and arrays in the C# world using the dynamic syntax available in JavaScript.

Here is a sample from the Noesis codeplex web site
JavascriptContext context = new JavascriptContext();

context.SetParameter("console", new SystemConsole());
context.SetParameter("message", "Hello World !");
context.SetParameter("number", 1);

string script = @"
    var i;
    for (i = 0; i < 5; i++)
        console.Print(message + ' (' + i + ')');
    number += i;
";

context.Run(script);

Console.WriteLine("number: " + context.GetParameter("number"));

Using the dynamic feature of C# 4.0 and my library DynamicJavaScriptRunTimes.net,  You can write the same code this way
dynamic jsContext = new DynamicJavascriptContext(
                          new JavascriptContext()
                    );                                               
jsContext.message = "Hello World !";
jsContext.number = 1;

string script = @"
    var i = 0;
    for (i = 0; i < 5; i++)
        console.log(message + ' (' + i + ')');
    number += i;
";

jsContext.Run(script);

Console.WriteLine("number: " + jsContext.number);


JavaScript array are translated into  .NET array and JavaScript object are translated into .NET Dictonary<string, object>. The method jsContext.Array() is a helper to create an array in a JavaScript like syntax, inspired by DynamicSugar.Net.

array:
How to create an array in C#, modify it in JavaScript and then read it back in C#.
dynamic jsContext   = new DynamicJavascriptContext(
                           new JavascriptContext()
                      );
jsContext.a = new object [] { 1, 2, 3 };  // Regular Syntax
jsContext.a = jsContext.Array( 1, 2, 3 ); // My Syntax

string script = @"
    a.push(4);
";
            
jsContext.Run(script);

Assert.AreEqual(4, jsContext.a.Length);

for(var i=0; i < jsContext.a.Length; i++)
    Assert.AreEqual(i+1, jsContext.a[i]);


Objects:
As you can see nested objects and arrays are supported. The [ ] syntax to access a property is also supported.
dynamic jsContext = new DynamicJavascriptContext(
                          new JavascriptContext()
                    );
string script = @"
    Configuration =  {
        Server   :    'TOTO',
        Database :    'Rene', 
        Debug    :    true, 
        MaxUser  :    3, 
        Users    :    [
            { UserName:'rdescartes'    ,FirstName:'rene'      ,LastName:'descartes'     }, 
            { UserName:'bpascal'       ,FirstName:'blaise'    ,LastName:'pascal'        }, 
            { UserName:'cmontesquieu'  ,FirstName:'charles'   ,LastName:'montesquieu'   } 
        ]
    }
";

jsContext.Run(script);

Assert.AreEqual("TOTO"      , jsContext.Configuration.Server);
Assert.AreEqual("Rene"      , jsContext.Configuration.Database);
Assert.AreEqual(true        , jsContext.Configuration.Debug);
Assert.AreEqual(3           , jsContext.Configuration.MaxUser);
Assert.AreEqual(3           , jsContext.Configuration.Users.Length);
Assert.AreEqual("rdescartes", jsContext.Configuration.Users[0].UserName);

Assert.AreEqual("TOTO"      , jsContext["Configuration"]["Server"]);
Assert.AreEqual("Rene"      , jsContext["Configuration"]["Database"]);
Assert.AreEqual(true        , jsContext["Configuration"]["Debug"]);
Assert.AreEqual(3           , jsContext["Configuration"]["MaxUser"]);
Assert.AreEqual(3           , jsContext["Configuration"]["Users"].Length);
Assert.AreEqual("rdescartes", jsContext["Configuration"]["Users"][0].UserName);


More Objects
We can also create objects from C#, pass them to the JavaScript run-time and then access the objects again. The method jsContext.Object() is a helper method inspired by DynamicSugar.Net to create object in a JavaScript like syntax or pass a POCO.

Note: Objects and arrays are not passed by reference. The data is copied from one world to the other.

dynamic jsContext   = new DynamicJavascriptContext(
                            new JavascriptContext()
                      );
                      
jsContext.i = jsContext.Object(
    new {
        a = jsContext.Object( new { LastName="Torres", Age=46 } ),
        b = jsContext.Object( new Person("Ferry", 47) ),
    }
);
string script = @"
    var p1 = {
        Name : i.a.LastName,
        Age  : i.a.Age
    }
    var p2 = {
        Name : i.b.LastName,
        Age  : i.b.Age
    }
";

jsContext.Run(script);

Assert.AreEqual("Torres", jsContext.p1.Name);
Assert.AreEqual("Torres", jsContext.p1["Name"]);
Assert.AreEqual("Torres", jsContext["p1"]["Name"]);

Assert.AreEqual(46, jsContext.p1.Age);
Assert.AreEqual(46, jsContext.p1["Age"]);
Assert.AreEqual(46, jsContext["p1"]["Age"]);

Assert.AreEqual("Ferry", jsContext.p2.Name);
Assert.AreEqual("Ferry", jsContext.p2["Name"]);
Assert.AreEqual("Ferry", jsContext["p2"]["Name"]);

Assert.AreEqual(47, jsContext.p2.Age);
Assert.AreEqual(47, jsContext.p2["Age"]);
Assert.AreEqual(47, jsContext["p2"]["Age"]);            

More Samples
Assert.AreEqual(
    "Fred",
    jsContext.Run(@"
        function Person(firstName){ this.FirstName = firstName; }
        (new Person('Fred'))"
    ).FirstName
);

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

DateTime refDate = new DateTime(1964, 12, 11, 01, 02, 03);

string script = @"
    var O2 = {
                F2: function(pInt,pDouble,pString,pBool,pDate) { 
                        return ''+this.Internal+'-'+pInt+'-'+pDouble+'-'+pString+'-'+pBool+'-'+formatDateUS(pDate);
                },
                Internal:1
                }
";

jsContext.Load("format", Assembly.GetExecutingAssembly());

jsContext.Run(script);

var expectedF2 = "1-1-123.456-hello-true-12/11/1964 1:2:3";
var f2Result   = jsContext.Call("O2.F2", 1, 123.456, "hello", true, refDate);

Assert.AreEqual(expectedF2, f2Result);



The DynamicJavascriptContext class
/// <summary>
/// Run the script and return the last value evaluated. Executing a declaration function
/// or a global object literal, will load the function or object in the JavaScript context.
/// </summary>
/// <param name="script"></param>
/// <returns>
/// </returns>
public object Run(string script);

/// <summary>
/// Load a JavaScript text file or text ressource in the JavaScript context
/// </summary>
/// <param name="name">The name without the .js extension</param>
/// <param name="assembly">The assembly is loading a ressource</param>
public void Load(string name, Assembly assembly = null);

/// <summary>
/// Execute a javascript global function or method
/// </summary>
/// <param name="functionName">the function name</param>
/// <param name="parameters">The parameters</param>
/// <returns></returns>
public object Call(string functionName, params object[] parameters);

/// <summary>
/// Helper function to make date compatible with the JavaScript
/// run time. Jurassic date are not .net datetime and need a convertion
/// </summary>
/// <param name="o"></param>
/// <returns></returns>
public object Date(DateTime d){);

/// <summary>
/// Helper function to make array compatible with the JavaScript run time.
/// </summary>
/// <param name="array"></param>
/// <returns></returns>
public object array(params object [] array);

/// <summary>
/// Helper function to make a JavaScript object compatible with the JavaScript run-time
/// </summary>
/// <param name="netObject"></param>
/// <returns></returns>
public object Object(object netObject);



Conclusion

DynamicJavaScriptRunTimes.net

1 comment:

  1. public class BaseJavaScriptContextImplementation:

    >> var a = new StringBuilder(1024);
    Why did you decide that the parameters will be no longer than 1024 bytes?

    >> return this.Run("{0}();".format(functionName));
    Why not use: functionName + "();"? It much faster.

    >> var parameterType = parameters[i].GetType().FullName.ToLower();
    >> if (parameterType == "system.string")
    Why not use: parameters[i] as string? It much faster too.

    >> a.AppendFormat("'{0}'", parameters[i]);
    You use this for string parameter, but string can contains ' character, you need to replace it to \'
    String can be null - '' no need for null.

    And so on...

    ReplyDelete