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

Different behaviour between Linux and Windows when calling host methods with params and default arguments #534

Closed SomebodyOdd closed 1 year ago

SomebodyOdd commented 1 year ago

Hello! I've found some code examples, that work on Windows but throw errors on Linux environments. This was caught during unit tests in our CI\CD environment and reproduces with dotnetfiddle. Didn't try to reproduce on Linux in my own VM yet, but I bet it will. Here are some code that illustrate this (it seems, that dotnetfiddle uses Linux runtime for .NET 7, because Environment.OSVersion returns Unix 5.15.0.1042). BOTH of those snippets run to completion on Windows without errors.

Calling a params method fails: https://dotnetfiddle.net/82teQe

using System;
using Microsoft.ClearScript.V8;

public class Program
{
    public static void Main()
    {
        var engine = new V8ScriptEngine();

        engine.AddHostObject("paramsMethod", ParamsMethod);

        engine.Execute("paramsMethod('123', '321', '231')");
    }

    public static void ParamsMethod(params string[] str) 
    {

    }
}
Exception stacktrace ````` Unhandled exception. Microsoft.ClearScript.ScriptEngineException: Error: Invalid argument specified for parameter 'obj' ---> Microsoft.ClearScript.ScriptEngineException: Error: Invalid argument specified for parameter 'obj' ---> System.ArgumentException: Invalid argument specified for parameter 'obj' at Microsoft.ClearScript.Util.InvokeHelpers.GetCompatibleArg(String paramName, Type type, Object value) at Microsoft.ClearScript.Util.InvokeHelpers.GetCompatibleArg(ParameterInfo param, Object value) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeMethodInternal[T](IHostContext context, T method, Object target, Object[] args, Func`4 invoker, Type returnType, ScriptMemberFlags flags) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeMethod(IHostContext context, MethodInfo method, Object target, Object[] args, ScriptMemberFlags flags) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeDelegate(IHostContext context, Delegate del, Object[] args) at Microsoft.ClearScript.Util.InvokeHelpers.TryInvokeObject(IHostContext context, Object target, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Boolean tryDynamic, Object& result) at Microsoft.ClearScript.HostItem.InvokeHostMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Boolean& isCacheable) 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.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.InvokeReflectMember(String name, BindingFlags invokeFlags, Object[] wrappedArgs, CultureInfo culture, String[] namedParams) 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.Invoke(Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.V8ProxyHelpers.InvokeHostObject(Object obj, Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.V8ProxyHelpers.InvokeHostObject(IntPtr pObject, Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.InvokeHostObject(IntPtr pObject, Boolean asConstructor, Int32 argCount, Ptr pArgs, Ptr pResult) --- End of inner exception stack trace --- at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.ThrowScheduledException() 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__DisplayClass130_0.b__0() at Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal[T](Func`1 func) at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass137_0`1.b__0() at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.InvokeHostAction(IntPtr pAction) --- Script error details follow --- Error: Invalid argument specified for parameter 'obj' at Script:1:1 -> paramsMethod('123', '321', '231') --- End of inner exception stack trace --- at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.ThrowScheduledException() 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.Execute(DocumentInfo documentInfo, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, Boolean discard, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String code) at Program.Main() --- Script error details follow --- Error: Invalid argument specified for parameter 'obj' at Script:1:1 -> paramsMethod('123', '321', '231') Command terminated by signal 6 `````


Calling method without giving default params fails: https://dotnetfiddle.net/HFyH97

using System;
using Microsoft.ClearScript.V8;

public class Program
{
    public static void Main()
    {
        var engine = new V8ScriptEngine();

        engine.AddHostObject("paramsMethod", ParamsMethod);

        engine.Execute("paramsMethod()");
    }

    public static void ParamsMethod(string? str = null) 
    {

    }
}
Exception stacktrace ````` Unhandled exception. Microsoft.ClearScript.ScriptEngineException: Error: Parameter count mismatch. ---> Microsoft.ClearScript.ScriptEngineException: Error: Parameter count mismatch. ---> System.Reflection.TargetParameterCountException: Parameter count mismatch. at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at Microsoft.ClearScript.Util.InvokeHelpers.<>c.b__0_0(MethodInfo invokeMethod, Object invokeTarget, Object[] invokeArgs) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeMethodInternal[T](IHostContext context, T method, Object target, Object[] args, Func`4 invoker, Type returnType, ScriptMemberFlags flags) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeMethod(IHostContext context, MethodInfo method, Object target, Object[] args, ScriptMemberFlags flags) at Microsoft.ClearScript.Util.InvokeHelpers.InvokeDelegate(IHostContext context, Delegate del, Object[] args) at Microsoft.ClearScript.Util.InvokeHelpers.TryInvokeObject(IHostContext context, Object target, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Boolean tryDynamic, Object& result) at Microsoft.ClearScript.HostItem.InvokeHostMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Boolean& isCacheable) 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.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.InvokeReflectMember(String name, BindingFlags invokeFlags, Object[] wrappedArgs, CultureInfo culture, String[] namedParams) 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.Invoke(Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.V8ProxyHelpers.InvokeHostObject(Object obj, Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.V8ProxyHelpers.InvokeHostObject(IntPtr pObject, Boolean asConstructor, Object[] args) at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.InvokeHostObject(IntPtr pObject, Boolean asConstructor, Int32 argCount, Ptr pArgs, Ptr pResult) --- End of inner exception stack trace --- at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.ThrowScheduledException() 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__DisplayClass130_0.b__0() at Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal[T](Func`1 func) at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass137_0`1.b__0() at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyManaged.InvokeHostAction(IntPtr pAction) --- Script error details follow --- Error: Parameter count mismatch. at Script:1:1 -> defaultMethod() --- End of inner exception stack trace --- at Microsoft.ClearScript.V8.SplitProxy.V8SplitProxyNative.ThrowScheduledException() 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.Execute(DocumentInfo documentInfo, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, Boolean discard, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String documentName, String code) at Microsoft.ClearScript.ScriptEngine.Execute(String code) at Program.Main() --- Script error details follow --- Error: Parameter count mismatch. at Script:1:1 -> defaultMethod() Command terminated by signal 6 `````

ClearScript version: 7.4.3 Windows version: Win 10 Enterprise 22H2 Runtime version (Windows): 6.0.21 Runtime version (Dotnetfiddle): 7.0.5 (from System.Environment.Version) Runtime version (CI/CD): 6.0.20 (from System.Environment.Version)

ClearScriptLib commented 1 year ago

Hi @SomebodyOdd,

Well, that's interesting.

There seems to be a difference in the Windows and Linux versions of the C# compiler. Specifically, the platforms differ in the intrinsic delegate types associated with methods. On Windows you get a custom delegate that captures params and default argument semantics. On Linux you just get Action or Func.

As a result, the following works on Windows but fails to compile on Linux:

public class Program {
    public static void Main() {
        var paramsMethod = ParamsMethod;
        paramsMethod("123", "321", "231");
    }
    public static void ParamsMethod(params string[] args) => Array.ForEach(args, Console.WriteLine);
}

The compilation error on Linux is "error CS1593: Delegate 'Action<string[]>' does not take 3 arguments". Surprisingly, when built on Windows, the code runs on Linux without any problems.

Anyway, that's the root cause of the issue here. It seems to be a C# compiler bug, and we'll report it internally. In the meantime, you can work around it by specifying the delegate type explicitly. Here's a ClearScript sample that works on both platforms:

public class Program {
    public delegate void ParamsDelegate(params string[] args);
    public delegate void DefaultArgDelegate(string? arg = null);
    public static void Main() {
        using var engine = new V8ScriptEngine();
        engine.AddHostObject("paramsMethod", new ParamsDelegate(ParamsMethod));
        engine.Execute("paramsMethod('123', '321', '231')");
        engine.AddHostObject("defaultArgMethod", new DefaultArgDelegate(DefaultArgMethod));
        engine.Execute("defaultArgMethod()");
    }
    public static void ParamsMethod(params string[] args) => Array.ForEach(args, Console.WriteLine);
    public static void DefaultArgMethod(string? arg = null) => Console.WriteLine(arg ?? "[null]");
}

Thanks, and good luck!

SomebodyOdd commented 1 year ago

Workaround did the trick, thank you! Any issues on other Github repos I could follow about this? Or should I report this somewhere else?

ClearScriptLib commented 1 year ago

Any issues on other Github repos I could follow about this?

We reported the issue here.

ClearScriptLib commented 1 year ago

This appears to have been fixed in .NET 7.0.200.