Browse Source

Add support for generic methods under interop (#1103)

Co-authored-by: Tim Cassidy <[email protected]>
Co-authored-by: Marko Lahma <[email protected]>
source-transformer 3 years ago
parent
commit
d1e69aeb2f

+ 121 - 0
Jint.Tests/Runtime/GenericMethodTests.cs

@@ -0,0 +1,121 @@
+using System;
+using Xunit;
+
+namespace Jint.Tests.Runtime;
+
+public class GenericMethodTests
+{
+    [Fact]
+    public void TestGeneric()
+    {
+        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        engine.SetValue("TestGenericBaseClass", typeof(TestGenericBaseClass<>));
+        engine.SetValue("TestGenericClass", typeof(TestGenericClass));
+
+        engine.Execute(@"
+                var testGeneric = new TestGenericClass();
+                testGeneric.Bar('testing testing 1 2 3');
+                testGeneric.Foo('hello world');
+                testGeneric.Add('blah');
+            ");
+
+        Assert.Equal(true, TestGenericClass.BarInvoked);
+        Assert.Equal(true, TestGenericClass.FooInvoked);
+    }
+
+    [Fact]
+    public void TestGeneric2()
+    {
+        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var testGenericObj = new TestGenericClass();
+        engine.SetValue("testGenericObj", testGenericObj);
+
+        engine.Execute(@"
+                testGenericObj.Bar('testing testing 1 2 3');
+                testGenericObj.Foo('hello world');
+                testGenericObj.Add('blah');
+            ");
+
+        Assert.Equal(1, testGenericObj.Count);
+    }
+
+    [Fact]
+    public void TestFancyGenericPass()
+    {
+        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var testGenericObj = new TestGenericClass();
+        engine.SetValue("testGenericObj", testGenericObj);
+
+        engine.Execute(@"
+                testGenericObj.Fancy('test', 42, 'foo');
+            ");
+
+        Assert.Equal(true, testGenericObj.FancyInvoked);
+    }
+
+    [Fact]
+    public void TestFancyGenericFail()
+    {
+        var engine = new Engine(cfg => cfg.AllowOperatorOverloading());
+        var testGenericObj = new TestGenericClass();
+        engine.SetValue("testGenericObj", testGenericObj);
+
+        var argException = Assert.Throws<ArgumentException>(() =>
+        {
+            engine.Execute(@"
+                    testGenericObj.Fancy('test', 'foo', 42);
+                ");
+        });
+
+        Assert.Equal("Object of type 'System.String' cannot be converted to type 'System.Double'.", argException.Message);
+    }
+
+    public class TestGenericBaseClass<T>
+    {
+        private System.Collections.Generic.List<T> _list = new System.Collections.Generic.List<T>();
+
+        public int Count
+        {
+            get { return _list.Count; }
+        }
+
+        public void Add(T t)
+        {
+            _list.Add(t);
+        }
+    }
+
+    public class TestGenericClass : TestGenericBaseClass<string>
+    {
+        public static bool BarInvoked { get; private set; }
+
+        public static bool FooInvoked { get; private set; }
+
+        public bool FancyInvoked { get; private set; }
+
+        public TestGenericClass()
+        {
+            BarInvoked = false;
+            FooInvoked = false;
+            FancyInvoked = false;
+        }
+
+        public void Bar(string text)
+        {
+            Console.WriteLine("TestGenericClass: Bar: text: " + text);
+            BarInvoked = true;
+        }
+
+        public void Foo<T>(T t)
+        {
+            Console.WriteLine("TestGenericClass: Foo: t: " + t);
+            FooInvoked = true;
+        }
+
+        public void Fancy<T, U>(T t1, U u, T t2)
+        {
+            Console.WriteLine("TestGenericClass: FancyInvoked: t1: " + t1 + "u: " + u + " t2: " + t2);
+            FancyInvoked = true;
+        }
+    }
+}

+ 38 - 2
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Globalization;
 using System.Globalization;
 using System.Linq.Expressions;
 using System.Linq.Expressions;
 using System.Reflection;
 using System.Reflection;
@@ -27,6 +27,12 @@ namespace Jint.Runtime.Interop
             _fallbackClrFunctionInstance = fallbackClrFunctionInstance;
             _fallbackClrFunctionInstance = fallbackClrFunctionInstance;
         }
         }
 
 
+        private static bool IsAssignableToGenericType(Type givenType, Type genericType)
+        {
+            var result = TypeConverter.IsAssignableToGenericType(givenType, genericType);
+            return (result >= 0);
+        }
+
         public override JsValue Call(JsValue thisObject, JsValue[] jsArguments)
         public override JsValue Call(JsValue thisObject, JsValue[] jsArguments)
         {
         {
             JsValue[] ArgumentProvider(MethodDescriptor method)
             JsValue[] ArgumentProvider(MethodDescriptor method)
@@ -40,6 +46,7 @@ namespace Jint.Runtime.Interop
                         ? ProcessParamsArrays(jsArgumentsTemp, method)
                         ? ProcessParamsArrays(jsArgumentsTemp, method)
                         : jsArgumentsTemp;
                         : jsArgumentsTemp;
                 }
                 }
+
                 return method.HasParams
                 return method.HasParams
                     ? ProcessParamsArrays(jsArguments, method)
                     ? ProcessParamsArrays(jsArguments, method)
                     : jsArguments;
                     : jsArguments;
@@ -56,7 +63,14 @@ namespace Jint.Runtime.Interop
                 {
                 {
                     parameters = new object[methodParameters.Length];
                     parameters = new object[methodParameters.Length];
                 }
                 }
+
                 var argumentsMatch = true;
                 var argumentsMatch = true;
+                Type[] genericArgTypes = null;
+                if (method.Method.IsGenericMethod)
+                {
+                    var methodGenericArgs = method.Method.GetGenericArguments();
+                    genericArgTypes = new Type[methodGenericArgs.Length];
+                }
 
 
                 for (var i = 0; i < parameters.Length; i++)
                 for (var i = 0; i < parameters.Length; i++)
                 {
                 {
@@ -68,6 +82,16 @@ namespace Jint.Runtime.Interop
                     {
                     {
                         parameters[i] = argument;
                         parameters[i] = argument;
                     }
                     }
+                    else if ((parameterType.IsGenericParameter) && (IsAssignableToGenericType(argument.ToObject()?.GetType(), parameterType)))
+                    {
+                        var argObj = argument.ToObject();
+                        if (parameterType.GenericParameterPosition >= 0)
+                        {
+                            genericArgTypes[parameterType.GenericParameterPosition] = argObj.GetType();
+                        }
+
+                        parameters[i] = argObj;
+                    }
                     else if (argument is null)
                     else if (argument is null)
                     {
                     {
                         // optional
                         // optional
@@ -84,6 +108,7 @@ namespace Jint.Runtime.Interop
                         {
                         {
                             result[k] = arrayInstance.TryGetValue(k, out var value) ? value : Undefined;
                             result[k] = arrayInstance.TryGetValue(k, out var value) ? value : Undefined;
                         }
                         }
+
                         parameters[i] = result;
                         parameters[i] = result;
                     }
                     }
                     else
                     else
@@ -110,7 +135,18 @@ namespace Jint.Runtime.Interop
                 // todo: cache method info
                 // todo: cache method info
                 try
                 try
                 {
                 {
-                    return FromObject(Engine, method.Method.Invoke(thisObject.ToObject(), parameters));
+                    if ((method.Method.IsGenericMethodDefinition) && (method.Method is MethodInfo methodInfo))
+                    {
+                        var declaringType = methodInfo.DeclaringType;
+                        var genericMethodInfo = methodInfo.MakeGenericMethod(genericArgTypes);
+                        var thisObj = thisObject.ToObject();
+                        var result = genericMethodInfo.Invoke(thisObj, parameters);
+                        return FromObject(Engine, result);
+                    }
+                    else
+                    {
+                        return FromObject(Engine, method.Method.Invoke(thisObject.ToObject(), parameters));
+                    }
                 }
                 }
                 catch (TargetInvocationException exception)
                 catch (TargetInvocationException exception)
                 {
                 {

+ 64 - 1
Jint/Runtime/TypeConverter.cs

@@ -510,6 +510,7 @@ namespace Jint.Runtime
             {
             {
                 intValue *= -1;
                 intValue *= -1;
             }
             }
+
             var int16Bit = intValue % 65_536; // 2^16
             var int16Bit = intValue % 65_536; // 2^16
             return (ushort) int16Bit;
             return (ushort) int16Bit;
         }
         }
@@ -713,6 +714,7 @@ namespace Jint.Runtime
                         {
                         {
                             return false;
                             return false;
                         }
                         }
+
                         bigInteger = bigInteger * 8 + c - '0';
                         bigInteger = bigInteger * 8 + c - '0';
                     }
                     }
 
 
@@ -755,6 +757,7 @@ namespace Jint.Runtime
             {
             {
                 return (long) (int64bit - BigInteger.Pow(2, 64));
                 return (long) (int64bit - BigInteger.Pow(2, 64));
             }
             }
+
             return (long) int64bit;
             return (long) int64bit;
         }
         }
 
 
@@ -1114,12 +1117,62 @@ namespace Jint.Runtime
                 {
                 {
                     return parameterScore;
                     return parameterScore;
                 }
                 }
+
                 score += parameterScore;
                 score += parameterScore;
             }
             }
 
 
             return score;
             return score;
         }
         }
 
 
+        /// <summary>
+        /// resources:
+        /// https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection
+        /// https://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059
+        /// https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0
+        /// This can be improved upon - specifically as mentioned in the above MS document:
+        /// GetGenericParameterConstraints()
+        /// and array handling - i.e.
+        /// GetElementType()
+        /// </summary>
+        /// <param name="givenType"></param>
+        /// <param name="genericType"></param>
+        /// <returns></returns>
+        internal static int IsAssignableToGenericType(Type givenType, Type genericType)
+        {
+            if (!genericType.IsConstructedGenericType)
+            {
+                // as mentioned here:
+                // https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0
+                // this effectively means this generic type is open (i.e. not closed) - so any type is "possible" - without looking at the code in the method we don't know
+                // whether any operations are being applied that "don't work"
+                return 2;
+            }
+
+            var interfaceTypes = givenType.GetInterfaces();
+
+            foreach (var it in interfaceTypes)
+            {
+                if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType)
+                {
+                    return 0;
+                }
+            }
+
+            if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
+            {
+                return 0;
+            }
+
+            Type baseType = givenType.BaseType;
+            if (baseType == null)
+            {
+                return -1;
+            }
+
+            var result = IsAssignableToGenericType(baseType, genericType);
+            return result;
+        }
+
         /// <summary>
         /// <summary>
         /// Determines how well parameter type matches target method's type.
         /// Determines how well parameter type matches target method's type.
         /// </summary>
         /// </summary>
@@ -1191,6 +1244,16 @@ namespace Jint.Runtime
                 return 2;
                 return 2;
             }
             }
 
 
+            // not sure the best point to start generic type tests
+            if (paramType.IsGenericParameter)
+            {
+                var genericTypeAssignmentScore = IsAssignableToGenericType(objectValueType, paramType);
+                if (genericTypeAssignmentScore != -1)
+                {
+                    return genericTypeAssignmentScore;
+                }
+            }
+
             if (CanChangeType(objectValue, paramType))
             if (CanChangeType(objectValue, paramType))
             {
             {
                 // forcing conversion isn't ideal, but works, especially for int -> double for example
                 // forcing conversion isn't ideal, but works, especially for int -> double for example
@@ -1245,4 +1308,4 @@ namespace Jint.Runtime
             return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
             return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
         }
         }
     }
     }
-}
+}