This project is read-only.

Calling a .Net method with a params specifier

Sep 20, 2014 at 5:21 AM
Hi, V8.Net looks really good, though the lack of documentation is making integration difficult, even with the sample source code to go on. I have started to get a handle on how to do basic things, but I'm having trouble calling a method that uses the params keyword. I'm also having trouble exposing functions globally, as opposed to as properties of some other object.

Two examples below:
using(var engine = new V8Engine())
{
    engine.GlobalObject.SetProperty(typeof(GlobalUtilities), recursive: true, memberSecurity: ScriptMemberSecurity.Permanent);
    engine.GlobalObject.SetProperty("GlobalUtilities", new GlobalUtilities());
    engine.DynamicGlobalObject.add = engine.DynamicGlobalObject.GlobalUtilities.Add;
    engine.DynamicGlobalObject.square = engine.DynamicGlobalObject.GlobalUtilities.Square;
    var value1 = engine.Execute("square(3.5);");
    var value2 = engine.Execute("add(3, 5);");
    Console.WriteLine("Value 1: {0}", value1);
    Console.WriteLine("Value 2: {0}", value2);
}
using(var engine = new V8Engine())
{
    engine.GlobalObject.SetProperty(typeof(GlobalUtilities), recursive: true, memberSecurity: ScriptMemberSecurity.Permanent);
    engine.GlobalObject.SetProperty("foo", new GlobalUtilities());
    var value1 = engine.Execute("foo.Square(3.5);");
    var value2 = engine.Execute("foo.Add(3, 5);");
    Console.WriteLine("Value 1: {0}", value1);
    Console.WriteLine("Value 2: {0}", value2);
}

internal class GlobalUtilities
{
    public double Square(double d)
    {
        return d*d;
    }

    public double Add(params double[] values)
    {
        return values.Aggregate(0d, (m, n) => m + n);
    }
}
The first example was an attempt to expose the add and square methods as globals, but it doesn't work at all. It complains about a missing ObjectBinder, though I'm not sure what that is.

The second example works with the methods as properties of a global "foo" object, but the second method call fails with an InvalidCastException - "Invalid cast from 'System.Int32' to 'System.Double[]'". Clearly I'm missing an important step because in your console sample I saw that you had exposed a method in a test class that took a params argument, though you weren't calling it anywhere...

Essentially all I need to do is expose a console function which can accept a variable number of arguments (I'll be logging these externally), and most importantly, I need a function which takes a single string argument representing the ID of some data, and which makes a blocking call into my .Net code where I resolve a value and return it to the JavaScript code for further processing. The return type will usually be a simple type, or a DateTime, but can also be an array of integers or an array of strings.

Any help here would be much appreciated. V8.Net is my last port of call considering Jint and Jurassic both failed to meet my performance requirements.
Sep 20, 2014 at 8:41 AM
Edited Sep 20, 2014 at 8:56 AM
Hi, my apologies for the lack of docs. Having so many projects I'm swamped in is taking most of my time these days (v8.net is part of a much bigger vision).

First off, for these lines:
engine.GlobalObject.SetProperty(typeof(GlobalUtilities), recursive: true, memberSecurity: ScriptMemberSecurity.Permanent);
engine.GlobalObject.SetProperty("GlobalUtilities", new GlobalUtilities());
you need to select one or the other - not both. The first line creates a property of the type, and the second one is creating a new object of the type and overwriting the existing type-based property (with an instance based object). If you want to register the type (optional - only for more control), you should call this first:
engine.RegisterType<GlobalUtilities>(null, recursive: true, memberSecurity: ScriptMemberSecurity.Permanent);
... OR, add attributes (see code below).

Next, you need to think of the instance object "GlobalUtilities" in terms of the CLR side. You can't copy properties that NEED an object reference; hence, you cannot just copy the properties to the root and expect them to work. ;) Calling bound instance properties on the root causes the GLOBAL object (just a simple native object) to become the context of the call, and thus there is no binder. If the method on the class was static, then this wouldn't be an issue.

In regards to "rest" parameters, you cannot think in terms of the C# code. You must think of how the code looks within the type system (behind the scenes). The "add()" function will actually show the parameter as a simple array of double, and that is what is expected. This might be an oversight on my part (perhaps I should map that case somehow), but currently as is, passing arguments in the JS function like "(3,5)" to a "rest" parameter will produce the error "Failed to invoke method Add(» Double[] values «)" - because it expects a double array. Try the following as a work around for now:
    internal class GlobalUtilities
    {
        [ScriptMember(inScriptName: "square", security: ScriptMemberSecurity.Permanent)]
        public double Square(double d)
        {
            return d * d;
        }

        [ScriptMember(inScriptName: "add", security: ScriptMemberSecurity.Permanent)]
        public double Add(params double[] values)
        {
            return values.Aggregate(0d, (m, n) => m + n);
        }

        // Convert the javascript array (which can be a mix of value types!) to a double array.
        // Note: The method signature is designed specially for callbacks from JavaScript.
        public static InternalHandle JSArrayToDoubleArray(V8Engine engine, bool isConstructCall, InternalHandle _this, params InternalHandle[] args)
        {
            if (args.Length == 0) return engine.CreateError("No array specified!", JSValueType.ExecutionError);
            var array = args[0];
            if (!array.IsArray) return engine.CreateError("Not an array!", JSValueType.ExecutionError);
            double[] values = new double[array.ArrayLength];
            for (var i = 0; i < array.ArrayLength; ++i)
                values[i] = (Handle)array.GetProperty(0); // (assumes all source values are valid;  convert to 'handle' to prevent memory leaks [only needed here, not for given function arguments])
            return engine.GetTypeBinder(typeof(double[])).CreateObject(values);
        }
    }

    using (var engine = new V8Engine())
    {
        engine.RegisterType<double[]>();
        engine.RegisterType<GlobalUtilities>(null, recursive: true);
        engine.DynamicGlobalObject.jSArrayToDoubleArray = engine.CreateFunctionTemplate().GetFunctionObject(GlobalUtilities.JSArrayToDoubleArray);
        // Note: Why "CreateFunctionTemplate"? Because that's how V8 works for callbacks. ;)
        //       (V8.Net exposes V8 to CLR, not CLR to V8, which was the original focus)
        engine.GlobalObject.SetProperty("globalUtilities", new GlobalUtilities());
        //engine.DynamicGlobalObject.add = engine.DynamicGlobalObject.GlobalUtilities.Add;
        //engine.DynamicGlobalObject.square = engine.DynamicGlobalObject.GlobalUtilities.Square;
        var value1 = engine.Execute("globalUtilities.square(3.5);");
        var value2 = engine.Execute("globalUtilities.add(jSArrayToDoubleArray([3, 5]));");
        Console.WriteLine("Value 1: {0}", value1);
        Console.WriteLine("Value 2: {0}", value2);
    }
(see comments)

Bottom line, the JavaScript world only sees the reflected types, which does not contain the "sugar syntax" that does magic stuff (like rest parameters). I find the best approach is to map a single CLR type to the global scope and use the console to inspect it. For instance, in the demo console I can type:
> Int32

(CLR Type: System.Int32)

> dump(Int32)

* function Int32() { [native code] }.MaxValue = (2147483647)
* function Int32() { [native code] }.MinValue = (-2147483648)
* function Int32() { [native code] }.Parse = (function Parse() { [native code] }
)
* function Int32() { [native code] }.TryParse = (function TryParse() { [native c
ode] })


> Int32.Parse

<object: Function (V8Function [122])>

> dump(Int32.Parse)

* function Parse() { [native code] }.$__Signature1 = (Parse(String s))
* function Parse() { [native code] }.$__Signature2 = (Parse(String s, NumberStyl
es style))
* function Parse() { [native code] }.$__Signature3 = (Parse(String s, NumberStyl
es style, IFormatProvider provider))
* function Parse() { [native code] }.$__Signature4 = (Parse(String s, IFormatPro
vider provider))

>
I designed the system to write the signatures to the functions for easy inspection of what is expected.

With V8.Net, low level power means higher control and flexibility to what you can accomplish - but may take a bit more work than other libraries that may do all the magic for you, with less flexibility. With great power comes ... ;)
Sep 22, 2014 at 1:03 PM
Thanks for the detailed reply, that was really helpful!