Passing javascript objects to c# native code

Dec 18, 2013 at 11:59 PM
Edited Dec 19, 2013 at 12:00 AM
I am currently trying to pass a js array to a C# method.


Engine.AddHostObject("plotUtility", plotUtility);

Engine.Evaluate("myData = [ [1,2], [3,4],[5,8] ]" );
....
Engine.Evaluate("plotUtility.plot( myData )");

Where plotUtility is a c# object with method plot.

What would be the proper method for converting myData to a C# array? It comes in as a Microsoft.ClearScript.V8.V8ScriptItem which is protected, meaning i can't use it it my PlotUtlity.Plot signature. (e.g. Public void Plot(V8ScriptItem scriptItem) does not work.)

I can do the following:

Engine.Evaluate("plotUtility.plot( JSON.stringify(myData ) )");

and use a c# signature: Public void Plot(string scriptItem), but that seems a bit superfluous.

Is there a better way to do this where i can preserve the syntax:

Engine.Evaluate("plotUtility.plot( myData )");
Coordinator
Dec 19, 2013 at 2:56 AM
Edited Dec 19, 2013 at 3:16 AM
Greetings!

ClearScript was designed to provide direct access to script objects, without data conversion. For example, you could write your C# class as follows:
public class ConsolePlotUtility {
    public void plot(dynamic data) {
        for (var i = 0; i < data.length; i++)
            Console.WriteLine("({0},{1})", data[i][0], data[i][1]);
    }
}
and then use it like this:
engine.AddHostObject("plotUtility", new ConsolePlotUtility());
engine.Evaluate("plotUtility.plot([[1,2], [3,4], [5,8]])");
Keep in mind, however, that host-to-script and script-to-host calls are relatively expensive. Data conversion can actually yield better performance in many cases. With JSON, for example, you can package your data and move it across the boundary in one step, but of course you have to weigh that against the costs of JSON serialization and parsing.

Good luck, and thanks for your question!
Jan 9, 2014 at 11:51 AM
I'm trying to create a command-prompt with a functionality like the console.log in my engine.
    public class Konsoll
    {
        public void log(dynamic o)
        {
            Console.WriteLine("Type:" + o.GetType());
            Console.WriteLine(o.ToString());
            // var p = (Microsoft.ClearScript.V8.V8ScriptItem)o;

            // foreach (o.GetType().)
        }

    }
used like this
            using (var engine = new V8ScriptEngine())
            {

                engine.AddHostObject("console", new Konsoll());
                string str;
                while ((str = Console.ReadLine()) != "end")
                {
                    engine.Execute(str);
                }
            }
            Console.ReadLine();
        }
How would you recommend to do this? It would be a very nice start if you made V8ScriptItem public. Maybe this object should rather be of type ExpandoObject?
Jan 9, 2014 at 1:07 PM
I found a solution. The V8ScriptItem is a DynamicObject - so this works:
    public class Konsoll
    {
        private void logg(int indent, dynamic o)
        {
            foreach (string egenskap in o.GetDynamicMemberNames())
            {
                Console.Write(new String(' ', indent));
                Console.Write(egenskap);
                Console.Write(':');
                
                if (o[egenskap] is System.Dynamic.DynamicObject)
                {
                    Console.WriteLine();
                    logg(indent + 1, o[egenskap]);
                }
                else
                {
                    Console.WriteLine(o[egenskap].ToString());
                }
            }
        }
        public void log(dynamic o)
        {
            logg(0, o);
        }

    }
so
engine.Execute("console.log({v1:4,v2:['a','bc', 'de', {f:'g'}],h:1,i:2,j:3,k:4});");
will output
v1:4
v2:
 0:a
 1:bc
 2:de
 3:
  f:g
h:1
i:2
j:3
k:4
Clearscript is really cool stuff.
Coordinator
Jan 9, 2014 at 3:11 PM
Glad that you found a solution!

V8ScriptItem is just a concrete implementation of DynamicObject. It doesn't have any additional members that would be useful to the host. The idea is that you either use its DynamicObject members, or you use it via the dynamic keyword.

By the way, it would probably be best to minimize your use of the dynamic machinery. Declaring a variable as dynamic causes the compiler to emit a dynamic callsite everywhere that variable is used, and that could get very expensive, especially when combined with the overhead of invoking V8. Consider rewriting your class something like this:
public class Konsoll {
    private void logg(int indent, DynamicObject od) {
        foreach (string egenskap in od.GetDynamicMemberNames()) {
            Console.Write(new String(' ', indent) + egenskap + ':');
            object m = ((dynamic)od)[egenskap]; // this is the only dynamic callsite
            var md = m as DynamicObject;
            if (md != null) {
                Console.WriteLine();
                logg(indent + 1, md);
            }
            else {
                Console.WriteLine(m);
            }
        }
    }
    public void log(DynamicObject od) {
        logg(0, od);
    }
}
Another option might be to expose only System.Console and implement console entirely in script code.

Good luck!
Jan 10, 2014 at 1:33 PM
Thanks for the warning. I wanted to know just how expensive the dynamic statement was - so I ran a test. The Console.Write was 3-4 times as fast, but to be fair, I added a third way "console.logs" deserializes a JavaScript literal and uses the same recursive printing style as console.log.
public class Konsoll
    {
        public int Indent = 0;
        public JavaScriptSerializer jss = new JavaScriptSerializer();
        public void logs(string s)
        {
            object o = jss.DeserializeObject(s);
            loggs(0, o);
        }
        private void loggs(int indent, object o)
        {
            if (o is Dictionary<string, object>)
            {
                foreach (KeyValuePair<string, object> kvp in (Dictionary<string, object>)o )
                {
                    Console.Write(kvp.Key);
                    Console.Write(':');
                    loggs(indent + Indent, kvp.Value);
                }
            }
            else if (o is Object[])
            {
                for (int i = 0; i < ((Object[])o).Length; i++)
                {
                    Console.Write(i);
                    Console.Write(':');
                    loggs(indent + Indent, ((Object[])o)[i]);
                }
            }
            else if (o == null)
            {
                Console.Write("null");
            }
            else
            {
                Console.Write(o.ToString());
            }

        }
        private void logg(int indent, object o)
        {
            
            if (o is System.Dynamic.DynamicObject)
            { 
                
                foreach (string egenskap in ((System.Dynamic.DynamicObject)o).GetDynamicMemberNames())
                {
                    // Console.Write(new String(' ', indent));
                    Console.Write(egenskap);
                    Console.Write(':');
                    logg(indent + Indent, ((dynamic)o)[egenskap]);
                }
            }
            else if (o == null)
            {
                Console.Write("null");
            }
            else
            {
                Console.Write(o.ToString());
            }
        }
        public void log(object o)
        {
            logg(0, o);
        }

    }
Writing a test:
            using (var engine = new V8ScriptEngine())
            {
                engine.AddHostObject("console", new Konsoll());
                engine.AddHostType("Console", typeof(System.Console));
                var start3 = DateTime.Now;
                for (int i = 0; i < 1000; i++)
                {
                    engine.Execute("Console.Write(JSON.stringify({v1:4,v2:['a','bc', 'de', {f:'g'}],h:1,i:2,j:3,k:4}));");
                }
                var tid3 = DateTime.Now.Subtract(start3);

                var start = DateTime.Now;
                for (int i = 0; i < 1000; i++ )
                {
                    engine.Execute("console.logs(JSON.stringify({v1:4,v2:['a','bc', 'de', {f:'g'}],h:1,i:2,j:3,k:4}));");
                }
                var tid = DateTime.Now.Subtract(start);
                var start2 = DateTime.Now;
                for (int i = 0; i < 1000; i++)
                {
                    engine.Execute("console.log({v1:4,v2:['a','bc', 'de', {f:'g'}],h:1,i:2,j:3,k:4});");
                }
                var tid2 = DateTime.Now.Subtract(start2);


                
                Console.WriteLine("static tok : " + tid.ToString());
                Console.WriteLine("dynamic tok : " + tid2.ToString());
                Console.WriteLine("Console.Write tok: " + tid3.ToString());
                string str;
                while ((str = Console.ReadLine()) != "end")
                {
                    engine.Execute(str);
                }
            }
Getting the result:
static tok : 00:00:05.3335333
dynamic tok : 00:00:05.6545654
Console.Write tok: 00:00:02.4172417
So the "static" console.logs alternative to dynamic only slightly cheaper.

In the logging example - I should just pass the jsonstring to .net, but generally, if I need to send a literal to .net in a predefined structure, it's ok to use dynamic. I think, however, there is one weakness in GetDynamicMemberNames: that you don't know if its an array or object. Is there another way of knowing?
Coordinator
Jan 10, 2014 at 3:51 PM
Edited Jan 10, 2014 at 3:52 PM
So the "static" console.logs alternative to dynamic only slightly cheaper.

Right, but that's after you minimized your usage of the dynamic infrastructure. In your original implementation you declared o as a dynamic parameter, which led to unnecessary dynamic calls to GetDynamicMemberNames() and ToString(). You also redundantly evaluated o[egenskap] several times; each of those indexing operations is a dynamic call into the underlying script engine.

​​I think, however, there is one weakness in GetDynamicMemberNames: that you don't know if its an array or object. Is there another way of knowing?

For arrays, GetDynamicMemberNames() returns the valid indices as strings. If that's not what you're looking for, remember that examining script objects is often easiest to do in script code:
dynamic isScriptArray = engine.Evaluate("(function (x) { return x instanceof Array; })");

var result = engine.Evaluate("[1,2,3,4,5]");
if (isScriptArray(result)) {
    // do something
}
Cheers!