Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generic methods under interop #1103

Merged
merged 6 commits into from Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
121 changes: 121 additions & 0 deletions Jint.Tests/Runtime/GenericMethodTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
40 changes: 38 additions & 2 deletions Jint/Runtime/Interop/MethodInfoFunctionInstance.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -27,6 +27,12 @@ public MethodInfoFunctionInstance(Engine engine, MethodDescriptor[] methods, Clr
_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)
{
JsValue[] ArgumentProvider(MethodDescriptor method)
Expand All @@ -40,6 +46,7 @@ JsValue[] ArgumentProvider(MethodDescriptor method)
? ProcessParamsArrays(jsArgumentsTemp, method)
: jsArgumentsTemp;
}

return method.HasParams
? ProcessParamsArrays(jsArguments, method)
: jsArguments;
Expand All @@ -56,7 +63,14 @@ JsValue[] ArgumentProvider(MethodDescriptor method)
{
parameters = new object[methodParameters.Length];
}

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++)
{
Expand All @@ -68,6 +82,16 @@ JsValue[] ArgumentProvider(MethodDescriptor method)
{
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)
{
// optional
Expand All @@ -84,6 +108,7 @@ JsValue[] ArgumentProvider(MethodDescriptor method)
{
result[k] = arrayInstance.TryGetValue(k, out var value) ? value : Undefined;
}

parameters[i] = result;
}
else
Expand All @@ -110,7 +135,18 @@ JsValue[] ArgumentProvider(MethodDescriptor method)
// todo: cache method info
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)
{
Expand Down
65 changes: 64 additions & 1 deletion Jint/Runtime/TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ public static ushort ToUint16(JsValue o)
{
intValue *= -1;
}

var int16Bit = intValue % 65_536; // 2^16
return (ushort) int16Bit;
}
Expand Down Expand Up @@ -713,6 +714,7 @@ internal static bool TryStringToBigInt(string str, out BigInteger result)
{
return false;
}

bigInteger = bigInteger * 8 + c - '0';
}

Expand Down Expand Up @@ -755,6 +757,7 @@ internal static long ToBigInt64(BigInteger value)
{
return (long) (int64bit - BigInteger.Pow(2, 64));
}

return (long) int64bit;
}

Expand Down Expand Up @@ -1114,12 +1117,62 @@ private static int CalculateMethodScore(Engine engine, MethodDescriptor method,
{
return parameterScore;
}

score += parameterScore;
}

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>
/// Determines how well parameter type matches target method's type.
/// </summary>
Expand Down Expand Up @@ -1191,6 +1244,16 @@ private static int CalculateMethodScore(Engine engine, MethodDescriptor method,
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))
{
// forcing conversion isn't ideal, but works, especially for int -> double for example
Expand Down Expand Up @@ -1245,4 +1308,4 @@ internal static bool TypeIsNullable(Type type)
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
}
}
}
}