InteropHelper.cs 12 KB

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