microsoft / ClearScript

A library for adding scripting to .NET applications. Supports V8 (Windows, Linux, macOS) and JScript/VBScript (Windows).
https://microsoft.github.io/ClearScript/
MIT License
1.77k stars 148 forks source link

Cannot update elements of .NET arrays from Javascript code (V8 engine) #526

Closed pitakp closed 1 year ago

pitakp commented 1 year ago

Hi,

I tried to create .NET arrays from Javascript code (V8 engine) and change element values, however it always fails. It seems that the byte value is always converted to Int32 value but such a value is refused when being assigned into a byte array element (even if I use explicit cast, see example). I tested it with byte arrays, but I suppose for other types shorter than Int32 it demonstartes the same behaviour. Interestingly, my custom method for updating an array (SetByteElement) requires byte value, but it accepts Javascript integer values without any conversion problem. Here is the code (failing code is commented-out):

using System;
using Microsoft.ClearScript.V8;
using Microsoft.ClearScript;

public class ScriptTest {
    public static void Run() {
        using (var engine = new V8ScriptEngine()) {
            engine.AddHostType("ScriptTest", typeof(ScriptTest));
            engine.AddHostObject("host", new ExtendedHostFunctions());
            engine.Evaluate(@"
                var ByteT = host.type('System.Byte');
                var elems = host.newArr(ByteT, 3);
                //elems[0] = 32; //this fails
                //elems[0] = host.cast(ByteT, 33);  //even explicit conversion doesn't work
                ScriptTest.SetByteElement(elems, 0, 35); //but this works
                ScriptTest.WriteLine('Element value: {0}', elems[0]);
            ");
        }
    }

    public static void SetByteElement(byte[] bytes, int index, byte value) {
        bytes[index] = value;
    }

    public static void WriteLine(string format, params object[] parameters) {
        Console.WriteLine(format, parameters);
    }
}

Here is stack trace for eror on line elems[0] = 32:

Unhandled Exception: Microsoft.ClearScript.ScriptEngineException: Error: Cannot widen from source type to target type either because the source type is a not a primitive type or the conversion cannot be accomplished. ---> Microsoft.ClearScript.ScriptEngineException: Error: Cannot widen from source type to target type either because the source type is a not a primitive type or the conversion cannot be accomplished. ---> System.ArgumentException: Cannot widen from source type to target type either because the source type is a not a primitive type or the conversion cannot be accomplished.
   at System.Array.InternalSetValue(Void* target, Object value)
   at System.Array.SetValue(Object value, Int32 index)
   at System.Array.System.Collections.IList.set_Item(Int32 index, Object value)
   at Microsoft.ClearScript.HostList.set_Item(Int32 index, Object value)
   at Microsoft.ClearScript.HostItem.InvokeListElement(Int32 index, BindingFlags invokeFlags, Object[] args, Object[] bindArgs)
   at Microsoft.ClearScript.HostItem.InvokeMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, CultureInfo culture, Boolean bypassTunneling, Boolean& isCacheable)
   at Microsoft.ClearScript.HostItem.<>c__DisplayClass147_0.<InvokeReflectMember>b__0()
   at Microsoft.ClearScript.ScriptEngine.HostInvoke[T](Func`1 func)
   at Microsoft.ClearScript.HostItem.HostInvoke[T](Func`1 func)
   at Microsoft.ClearScript.HostItem.InvokeReflectMember(String name, BindingFlags invokeFlags, Object[] wrappedArgs, CultureInfo culture, String[] namedParams, Boolean& isCacheable)
   at Microsoft.ClearScript.HostItem.System.Reflection.IReflect.InvokeMember(String name, BindingFlags invokeFlags, Binder binder, Object invokeTarget, Object[] wrappedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at Microsoft.ClearScript.HostItem.Microsoft.ClearScript.Util.IDynamic.SetProperty(String name, Object[] args)
   at Microsoft.ClearScript.HostItem.Microsoft.ClearScript.Util.IDynamic.SetProperty(Int32 index, Object value)
   at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.SetHostObjectIndexedProperty(IntPtr pObject, Int32 index, Ptr pValue)
   --- End of inner exception stack trace ---
   at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.Invoke[T](Func`2 func)
   at Microsoft.ClearScript.V8.SplitProxy.V8ContextProxyImpl.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate)
   at Microsoft.ClearScript.V8.V8ScriptEngine.ExecuteRaw(UniqueDocumentInfo documentInfo, String code, Boolean evaluate)
   at Microsoft.ClearScript.V8.V8ScriptEngine.ExecuteInternal(UniqueDocumentInfo documentInfo, String code, Boolean evaluate)
   at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass120_0.<Execute>b__0()
   at Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal[T](Func`1 func)
   at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass127_0`1.<ScriptInvoke>b__0()
   at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.InvokeHostAction(IntPtr pAction)
   --- End of inner exception stack trace ---
   at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.Invoke(Action`1 action)
   at Microsoft.ClearScript.V8.SplitProxy.V8ContextProxyImpl.InvokeWithLock(Action action)
   at Microsoft.ClearScript.V8.V8ScriptEngine.ScriptInvoke[T](Func`1 func)
   at Microsoft.ClearScript.V8.V8ScriptEngine.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate)
   at Microsoft.ClearScript.ScriptEngine.Evaluate(String code)
   at ScriptTest.Run() in ....\ScriptTest.cs:line 10
ClearScriptLib commented 1 year ago

Hi @pitakp,

I tested it with byte arrays, but I suppose for other types shorter than Int32 it demonstartes the same behaviour.

That's correct. This issue has less to do with arrays than with numbers passed from the script engine to the host.

ClearScript narrows each JavaScript number to the smallest signed .NET numeric type that retains full precision, but it doesn't go narrower than Int32 for reasons related to convenience and the complex behavior of C# method binding.

Unfortunately, as you've noticed, this solution can result in unexpected behavior. Some numeric parameter types require special handling in some cases, and HostFunctions provides helpers for that:

elems[0] = host.toByte(32);

Note that this isn't quite the same as casting. The cast method is meant to return a value that is of use to script code. It provides access to .NET features such as interfaces and user-defined conversions, whereas toByte et al are specifically for passing numbers to the host. The objects they return are not otherwise useful.

Good luck!

pitakp commented 1 year ago

Thank you for your explanation and showing a solution! I completely overlooked the host.toByte(...) method, and I didn't know the difference between cast and toByte methods. Your comment explained the intended behaviour very well.