Browse Source

Add support for object return values in Func callbacks (#2053)

If a Func defines a none value-type return type that does
not implement IConvertible a InvalidCastException was
thrown. This update skips Convert.ChangeType if the returned
value is not IConvertible and just casts the result.

Co-authored-by: Martin Burtscher <[email protected]>
Martin Burtscher 5 months ago
parent
commit
dd4e681519
2 changed files with 77 additions and 3 deletions
  1. 65 1
      Jint.Tests/Runtime/InteropTests.cs
  2. 12 2
      Jint/Runtime/Interop/DefaultTypeConverter.cs

+ 65 - 1
Jint.Tests/Runtime/InteropTests.cs

@@ -263,6 +263,70 @@ public partial class InteropTests : IDisposable
             ");
     }
 
+    class Example()
+    {
+        public T ExchangeGenericViaFunc<T>(Func<T> objViaFunc)
+        {
+            return objViaFunc();
+        }
+
+        public object ExchangeObjectViaFunc(Func<object> objViaFunc)
+        {
+            return objViaFunc();
+        }
+
+        public int ExchangeValueViaFunc(Func<int> objViaFunc)
+        {
+            return objViaFunc();
+        }
+    }
+
+    [Fact]
+    public void ExchangeGenericViaFunc()
+    {
+        _engine.SetValue("Example", new Example());
+
+        RunTest(@"
+            const result = Example.ExchangeGenericViaFunc(() => {
+                return {
+                    value: 42
+                };
+            });
+
+            assert(result.value === 42);
+        ");
+    }
+
+    [Fact]
+    public void ExchangeObjectViaFunc()
+    {
+        _engine.SetValue("Example", new Example());
+
+        RunTest(@"
+            const result = Example.ExchangeObjectViaFunc(() => {
+                return {
+                    value: 42
+                };
+            });
+
+            assert(result.value === 42);
+        ");
+    }
+
+    [Fact]
+    public void ExchangeValueViaFunc()
+    {
+        _engine.SetValue("Example", new Example());
+
+        RunTest(@"
+            const result = Example.ExchangeValueViaFunc(() => {
+                return 42;
+            });
+
+            assert(result === 42);
+        ");
+    }
+
     private delegate string callParams(params object[] values);
 
     private delegate string callArgumentAndParams(string firstParam, params object[] values);
@@ -2925,7 +2989,7 @@ public partial class InteropTests : IDisposable
     {
         var engine = new Jint.Engine();
         var list = new List<string> { "A", "B", "C" };
- 
+
         engine.SetValue("list", list);
 
         Assert.Equal(1, engine.Evaluate("list.findIndex((x) => x === 'B')"));

+ 12 - 2
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -34,7 +34,8 @@ public class DefaultTypeConverter : ITypeConverter
     private static readonly Type engineType = typeof(Engine);
     private static readonly Type typeType = typeof(Type);
 
-    private static readonly MethodInfo convertChangeType = typeof(Convert).GetMethod("ChangeType", new[] { objectType, typeType, typeof(IFormatProvider) })!;
+    private static readonly MethodInfo changeTypeIfConvertible = typeof(DefaultTypeConverter).GetMethod(
+        "ChangeTypeOnlyIfConvertible", BindingFlags.NonPublic | BindingFlags.Static)!;
     private static readonly MethodInfo jsValueFromObject = jsValueType.GetMethod(nameof(JsValue.FromObject))!;
     private static readonly MethodInfo jsValueToObject = jsValueType.GetMethod(nameof(JsValue.ToObject))!;
 
@@ -323,7 +324,7 @@ public class DefaultTypeConverter : ITypeConverter
                 Expression.Convert(
                     Expression.Call(
                         null,
-                        convertChangeType,
+                        changeTypeIfConvertible,
                         Expression.Call(callExpression, jsValueToObject),
                         Expression.Constant(method.ReturnType),
                         Expression.Constant(System.Globalization.CultureInfo.InvariantCulture, typeof(IFormatProvider))
@@ -339,6 +340,15 @@ public class DefaultTypeConverter : ITypeConverter
             new ReadOnlyCollection<ParameterExpression>(parameters)).Compile();
     }
 
+    [return: NotNullIfNotNull(nameof(value))]
+    private static object? ChangeTypeOnlyIfConvertible(object? value, Type conversionType, IFormatProvider? provider)
+    {
+        if (value == null || value is IConvertible)
+            return System.Convert.ChangeType(value, conversionType, provider);
+
+        return value;
+    }
+
     private static bool TryCastWithOperators(object value, Type type, Type valueType, [NotNullWhen(true)] out object? converted)
     {
         var key = new TypeConversionKey(valueType, type);