InteropHelper.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Globalization;
  3. using System.Reflection;
  4. using Jint.Extensions;
  5. using Jint.Native;
  6. namespace Jint.Runtime.Interop;
  7. #pragma warning disable IL2072
  8. internal sealed class InteropHelper
  9. {
  10. internal const DynamicallyAccessedMemberTypes DefaultDynamicallyAccessedMemberTypes = DynamicallyAccessedMemberTypes.PublicConstructors
  11. | DynamicallyAccessedMemberTypes.PublicProperties
  12. | DynamicallyAccessedMemberTypes.PublicMethods
  13. | DynamicallyAccessedMemberTypes.PublicFields
  14. | DynamicallyAccessedMemberTypes.PublicEvents;
  15. internal readonly record struct AssignableResult(int Score, Type MatchingGivenType)
  16. {
  17. public bool IsAssignable => Score >= 0;
  18. }
  19. /// <summary>
  20. /// resources:
  21. /// https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection
  22. /// https://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059
  23. /// https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0
  24. /// This can be improved upon - specifically as mentioned in the above MS document:
  25. /// GetGenericParameterConstraints()
  26. /// and array handling - i.e.
  27. /// GetElementType()
  28. /// </summary>
  29. internal static AssignableResult IsAssignableToGenericType(
  30. [DynamicallyAccessedMembers(DefaultDynamicallyAccessedMemberTypes | DynamicallyAccessedMemberTypes.Interfaces)]
  31. Type givenType,
  32. [DynamicallyAccessedMembers(DefaultDynamicallyAccessedMemberTypes | DynamicallyAccessedMemberTypes.Interfaces)]
  33. Type genericType)
  34. {
  35. if (givenType is null)
  36. {
  37. return new AssignableResult(-1, typeof(void));
  38. }
  39. if (!genericType.IsConstructedGenericType)
  40. {
  41. // as mentioned here:
  42. // https://docs.microsoft.com/en-us/dotnet/api/system.type.isconstructedgenerictype?view=net-6.0
  43. // 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
  44. // whether any operations are being applied that "don't work"
  45. return new AssignableResult(2, givenType);
  46. }
  47. var interfaceTypes = givenType.GetInterfaces();
  48. foreach (var it in interfaceTypes)
  49. {
  50. if (it.IsGenericType)
  51. {
  52. var givenTypeGenericDef = it.GetGenericTypeDefinition();
  53. if (givenTypeGenericDef == genericType)
  54. {
  55. return new AssignableResult(0, it);
  56. }
  57. else if (genericType.IsGenericType && (givenTypeGenericDef == genericType.GetGenericTypeDefinition()))
  58. {
  59. return new AssignableResult(0, it);
  60. }
  61. // TPC: we could also add a loop to recurse and iterate thru the iterfaces of generic type - because of covariance/contravariance
  62. }
  63. }
  64. if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
  65. {
  66. return new AssignableResult(0, givenType);
  67. }
  68. var baseType = givenType.BaseType;
  69. if (baseType == null)
  70. {
  71. return new AssignableResult(-1, givenType);
  72. }
  73. return IsAssignableToGenericType(baseType, genericType);
  74. }
  75. /// <summary>
  76. /// Determines how well parameter type matches target method's type.
  77. /// </summary>
  78. private static int CalculateMethodParameterScore(Engine engine, ParameterInfo parameter, JsValue parameterValue)
  79. {
  80. var paramType = parameter.ParameterType;
  81. var objectValue = parameterValue.ToObject();
  82. var objectValueType = objectValue?.GetType();
  83. if (objectValueType == paramType)
  84. {
  85. return 0;
  86. }
  87. if (objectValue is null)
  88. {
  89. if (!parameter.IsOptional && !TypeIsNullable(paramType))
  90. {
  91. // this is bad
  92. return -1;
  93. }
  94. return 0;
  95. }
  96. if (paramType == typeof(JsValue))
  97. {
  98. // JsValue is convertible to. But it is still not a perfect match
  99. return 1;
  100. }
  101. if (paramType == typeof(object))
  102. {
  103. // a catch-all, prefer others over it
  104. return 5;
  105. }
  106. if (paramType == typeof(int) && parameterValue.IsInteger())
  107. {
  108. return 0;
  109. }
  110. if (paramType == typeof(float) && objectValueType == typeof(double))
  111. {
  112. return parameterValue.IsInteger() ? 1 : 2;
  113. }
  114. if (paramType.IsEnum &&
  115. parameterValue is JsNumber jsNumber
  116. && jsNumber.IsInteger()
  117. && paramType.GetEnumUnderlyingType() == typeof(int)
  118. && Enum.IsDefined(paramType, jsNumber.AsInteger()))
  119. {
  120. // we can do conversion from int value to enum
  121. return 0;
  122. }
  123. if (paramType.IsAssignableFrom(objectValueType))
  124. {
  125. // is-a-relation
  126. return 1;
  127. }
  128. if (parameterValue.IsArray() && paramType.IsArray)
  129. {
  130. // we have potential, TODO if we'd know JS array's internal type we could have exact match
  131. return 2;
  132. }
  133. // not sure the best point to start generic type tests
  134. if (paramType.IsGenericParameter)
  135. {
  136. var genericTypeAssignmentScore = IsAssignableToGenericType(objectValueType!, paramType);
  137. if (genericTypeAssignmentScore.Score != -1)
  138. {
  139. return genericTypeAssignmentScore.Score;
  140. }
  141. }
  142. if (CanChangeType(objectValue, paramType))
  143. {
  144. // forcing conversion isn't ideal, but works, especially for int -> double for example
  145. return 3;
  146. }
  147. foreach (var m in objectValueType!.GetOperatorOverloadMethods())
  148. {
  149. if (paramType.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit")
  150. {
  151. // implicit/explicit operator conversion is OK, but not ideal
  152. return 3;
  153. }
  154. }
  155. if (ReflectionExtensions.TryConvertViaTypeCoercion(paramType, engine.Options.Interop.ValueCoercion, parameterValue, out _))
  156. {
  157. // gray JS zone where we start to do odd things
  158. return 10;
  159. }
  160. // will rarely succeed
  161. return 100;
  162. }
  163. /// <summary>
  164. /// Method's match score tells how far away it's from ideal candidate. 0 = ideal, bigger the the number,
  165. /// the farther away the candidate is from ideal match. Negative signals impossible match.
  166. /// </summary>
  167. private static int CalculateMethodScore(Engine engine, MethodDescriptor method, JsValue[] arguments)
  168. {
  169. if (method.Parameters.Length == 0 && arguments.Length == 0)
  170. {
  171. // perfect
  172. return 0;
  173. }
  174. var score = 0;
  175. for (var i = 0; i < arguments.Length; i++)
  176. {
  177. var jsValue = arguments[i];
  178. var parameterScore = CalculateMethodParameterScore(engine, method.Parameters[i], jsValue);
  179. if (parameterScore < 0)
  180. {
  181. return parameterScore;
  182. }
  183. score += parameterScore;
  184. }
  185. return score;
  186. }
  187. private static bool CanChangeType(object value, Type targetType)
  188. {
  189. if (value is null && !targetType.IsValueType)
  190. {
  191. return true;
  192. }
  193. if (value is not IConvertible)
  194. {
  195. return false;
  196. }
  197. try
  198. {
  199. Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
  200. return true;
  201. }
  202. catch
  203. {
  204. // nope
  205. return false;
  206. }
  207. }
  208. internal static bool TypeIsNullable(Type type)
  209. {
  210. return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
  211. }
  212. internal readonly record struct MethodMatch(MethodDescriptor Method, JsValue[] Arguments, int Score = 0) : IComparable<MethodMatch>
  213. {
  214. public int CompareTo(MethodMatch other) => Score.CompareTo(other.Score);
  215. }
  216. internal static IEnumerable<MethodMatch> FindBestMatch(
  217. Engine engine,
  218. MethodDescriptor[] methods,
  219. Func<MethodDescriptor, JsValue[]> argumentProvider)
  220. {
  221. List<MethodMatch>? matchingByParameterCount = null;
  222. foreach (var method in methods)
  223. {
  224. var parameterInfos = method.Parameters;
  225. var arguments = argumentProvider(method);
  226. if (arguments.Length <= parameterInfos.Length
  227. && arguments.Length >= parameterInfos.Length - method.ParameterDefaultValuesCount)
  228. {
  229. var score = CalculateMethodScore(engine, method, arguments);
  230. if (score == 0)
  231. {
  232. // perfect match
  233. yield return new MethodMatch(method, arguments);
  234. yield break;
  235. }
  236. if (score < 0)
  237. {
  238. // discard
  239. continue;
  240. }
  241. matchingByParameterCount ??= new List<MethodMatch>();
  242. matchingByParameterCount.Add(new MethodMatch(method, arguments, score));
  243. }
  244. }
  245. if (matchingByParameterCount == null)
  246. {
  247. yield break;
  248. }
  249. if (matchingByParameterCount.Count > 1)
  250. {
  251. matchingByParameterCount.Sort();
  252. }
  253. foreach (var match in matchingByParameterCount)
  254. {
  255. yield return match;
  256. }
  257. }
  258. }